Skip to content

flows: onboard write phase reaches done — project pin, adopt auth, first-push auto-resolve, json purity - #348

Merged
cvince merged 11 commits into
feat/portabilityfrom
feat/portability-refactor
Aug 29, 2026
Merged

flows: onboard write phase reaches done — project pin, adopt auth, first-push auto-resolve, json purity#348
cvince merged 11 commits into
feat/portabilityfrom
feat/portability-refactor

Conversation

@cvince

@cvince cvince commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Closes the gap between a completed sandbox ceremony and a finished onboard flow. All four fixes were proven live on a real broker-ceremony flow driven to kind:done, with .env ciphertext at rest and a decrypt round-trip through capy run.

  1. Project pin: create_org ceremonies now carry result.project_id (mint-ceremony response → worker marker → driver step report), so the service pins the project the mint provisioned and write_keep_lock adopts it instead of dead-ending on the adopt-vs-create picker under --broker-ceremony.
  2. Adopt auth: bootstrapProjectForFlow settles the command's own auth service first — the adopt path previously issued co-decrypt with no Authorization header.
  3. First-push auto-resolve: the sync menu's sole-action fast path also fires under brokerCeremony (a menu with one real action is not a decision). TTY and --web prompts are byte-identical to before.
  4. --json purity: the adopt stub-project lines route through human() so stdout stays exactly one JSON envelope.

Includes the let-free refactor of applyFirstRun/runCeremonyWorker per house immutability rules. Suites: sandboxCeremony 42/0, ceremonyWorker 16/0, driver 42/0, executors 33/0, orgCreationFromEnvelope 11/0, capyCommand 69/0; tsc clean.

cvince added 11 commits August 28, 2026 14:32
…reate

A resumed onboard run (--flow-id/--flow-secret --broker-ceremony) never
calls create, so its own minted keypair had no way to reach the service.
Send client_pubkey on every /next report while a broker-ceremony keypair
is held; the create-path resend is the documented idempotent no-op there.

Surfaces the service's coded 409 (client pubkey conflict) as a fatal,
coded failure on the --broker-ceremony --json path instead of collapsing
it to a generic service error.
… leaked across files in one bun process

runOnboardCommand sets brokerCeremonyMode/onboardJsonMode as process
globals and never unsets them (a real CLI is one process per run). Under
bun test's shared process, tests exercising the broker-ceremony path left
brokerCeremonyMode=true, and errorScreen's isWebMode() &&
!isBrokerCeremonyMode() gate then silently refused to serve loopback pages
for every later --web test in the same run. Reset both flags
unconditionally in this file's shared restore(), which every test already
calls. No test expectations changed. Full suite: 1851 pass / 0 fail.
A --flow-id/--flow-secret resume skips create entirely, so a flow minted
remotely (no local directory at mint time) reached its confirm/onboard_plan
step with target_dir/variable_count/plan_hash all empty: the service only
ever gets plan facts from a client-reported body.plan, and the resume path
never sent one.

The driver now sends its own already-computed plan (the same value the
self-mint path already puts in the create body) once, on a resumed run's
first next report, falling back to buildPlan() when no plan was
pre-computed. The service's existing plan-replacement path treats this the
same way it treats any plan report: a no-op when the hash already matches,
so later reports on the same run send nothing more.
…emony

create_org now mints personal orgs through the mint-ceremony rail
(ServiceClient.mintPersonalOrgCeremony) instead of /auth/create-org:
the endpoint creates the org and claims this device's key-mint lease
in one round trip, name-collision suffixing moves server-side (the
CLI-side retry loop is gone), auth is re-scoped to the new org via
authenticateSilent, and the lease is finalized (finalizeKeyMintOrThrow)
only after the local key is saved — a finalize conflict is now a coded
step failure, never swallowed into a false success. 409
ALREADY_PROVISIONED arrives as a typed CapyError.

New first_run.kind:'mint_key' sealed-answer kind covers an org that
exists but never had its first master key minted: pin org, claim the
lease, derive+wrap M, run the same canned Case A device-key enrollment
create_org uses, then finalize with the same never-swallow semantics.

sandbox_session steps gain an optional mint_org_id param, threaded
through prepareCeremonyScreen -> the detached ceremony worker's stdin
payload -> its own buildCeremonyUrl call, so the fragment jumps
straight to the mint rail for an already-known org. Absent params stay
byte-identical to the prior fragment shape.

Additive to the vendored flow contract (sandbox_session.params_schema
gains mint_org_id, optional) — sync-flow-contract.ts --check will flag
this vendored copy as hand-edited until shared/flows/steps.json in the
monorepo (outside this submodule's scope) gets the same property and
the sync script is re-run for real.
…tderr capture

parse-failure debug() logs SHAPE only (type/keys/kind, never content);
CAPY_CEREMONY_LOG routes the detached worker's stderr to a file so the
breadcrumbs survive the parent's exit. Unset env = byte-identical.
… moves forward

WorkOS refresh tokens are single-use. save() persisted the whole
in-memory session, so an instance that never rotated could write its
stale loaded token over a newer rotation another instance persisted —
next invocation died on invalid_grant with no recovery. Observed live
twice in the mint ceremony worker chain.

Guard: track the loaded baseline; a non-rotating save re-reads the
store and preserves its token (adopting it as the new baseline), while
a genuine rotation still advances. Three tests pin clobber-prevention,
adopt-then-rotate, and rotate-wins.
…-push auto-resolve, json-mode purity

Four fixes proven live on a real broker-ceremony flow (validated to
kind:done with an encrypted .env and a decrypt round-trip):

1. create_org ceremonies now report result.project_id (threaded from
   mint-ceremony through the worker marker and driver report), so the
   service pins the project the mint provisioned and write_keep_lock
   adopts it instead of dead-ending on the project picker.
2. bootstrapProjectForFlow settles the command's own auth service before
   bootstrapExistingProject — the adopt path's co-decrypt previously went
   out with no Authorization header at all.
3. The sync menu's sole-action fast path now also fires under
   brokerCeremony (State 6 first push: one real action is not a
   decision); TTY and --web keep their prompts byte-identical.
4. The adopt stub-project lines route through human() so --json stdout
   stays exactly one JSON envelope.

Also refactors applyFirstRun/runCeremonyWorker to let-free early-return
form per house immutability rules; behavior preserved, suites green.
…marker path is organically unreachable

Headless journey runs 6 and 7 both hit the same wall: pinned_org_id set,
pinned_project_id empty, write_keep_lock dead-ended on FLOW_STOP_UNREACHABLE.
Root cause (proven from the run-7 service log): the marker→driver-report leg
requires a later driver run to RE-ENCOUNTER the sandbox_session step, and
organically it never does — the worker's own session-store write flips the
next derivation's sessionLive observation, the service (correctly) skips
authenticate and derives past the satisfied ceremony screen, and the settled
marker is never consumed. The service pins the org off the bearer's claim
but never learns the project the mint provisioned.

The worker now POSTs the settled step to /flows/:id/next itself, right after
writing the marker: real flow/step ids ride the spawn payload, the report
carries result.{org_id, project_id}, the flow secret, the client pubkey, and
the org-scoped bearer the ceremony just settled (exactly what
projectPinnableBy requires). Best-effort — on any failure the marker remains
and the driver's consume-and-report path (unchanged) still covers every
sequencing where the step is re-encountered. Payload ids are optional for
back-compat; absent, behavior is byte-identical to before.

ceremonyWorker suite asserts the report wire shape (ids, result, secret,
bearer); 16/0, adjacent suites green, tsc clean.
@cvince
cvince merged commit 45b02f1 into feat/portability Aug 29, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant