Keep agent connector profiles safe across server upgrades

Merged
RO
2d ago
+1299-264

Summary

Make connector state per profile, and connection guidance safe to release before the companion server change.

  • Separate each profile's ledger, cursor, durable hold and instance lock. Two profiles for one agent can run together; a second run of the same profile is refused.
  • Adopt the whole legacy state directory only after the credential proves the saved agent identity, under the legacy lock. Preserve checkpoints, held work and operator decisions; log both migration paths and warn fresh siblings where earlier state lives. Canonical account aliases meet one new-profile lock without losing older builds' literal account spelling.
  • Keep status/doctor/offline decisions on legacy state until adoption. Refuse unverified legacy shadow promotion: first run the selected profile with connect --shadow, verify, stop, then promote.
  • Name the profile in refusals and say disconnected in Basecamp, or replaced by a newer connection. An approval may add or replace: an agent of either kind, personal or team, adds a connection once Basecamp enables adding for that kind; until then a new approval replaces the existing connection. The auth agent connect and connect help, README, rollout doc and connect skill all say so. No unconditional "each approval adds" promise, including in the skill published on merge.
  • Warn about project overlap at guided project selection and in help: requests admitted by both connections can be answered twice. Overlap remains allowed; no lease/deduplication redesign.
  • Point lost-handover cleanup at the newest connection, not Disconnect all. Do not read/delete external credential files; on an add-capable server an old file or overwritten profile credential can remain live until its connection is disconnected.

Companion server change: bc3 #13840, which merges after this CLI is released.

Known issue before adding goes on

With two connections of one agent, a Ping can be answered twice, or boosted twice by the acknowledgement (#918), because Pings (#914) belong to no project: serving each project from one connection doesn't keep them apart. A design is in progress. Adding is off, so it can't happen yet. Raised in the rebase note.

CLI-first rollout / rollback

The secret remains opaque; no new server endpoint or feature flag is required by this CLI. Upgrade without reconnecting:

  1. Stop older connectors/supervisors for this account/agent on the host.
  2. Restart its existing profile on this build before adding another profile. Check the adoption WARN, resumed cursor, hold and status.
  3. Add another connection only once Basecamp enables adding for the agent's kind, personal or team. Until then a second approval replaces the first.

Operator upgrade, mixed-version and downgrade runbook: an older build restarted after adoption can recreate legacy state and miss the migrated hold/cursor. Downgrade offline, choosing one profile and restoring its whole normal/shadow directories to the old names; never overwrite existing ledgers or delete other profiles' state/credentials. Server switch-off disables adding, not existing added connections; server code downgrade after own-secret issuance is unsupported.

The consumer namespace intentionally stays per agent: it is a key in each isolated local profile ledger, never a server consumer. Renaming it would orphan migrated checkpoints.

Head and validation

Head 396da982, rebased onto main at 2dbca87a (v0.14.0) from cd5c2955; what conflicted and how it was resolved is in the rebase note. Six commits:

CommitAuthor
786820a2Rob ZolkosIsolate connector state by profile and verify legacy adoption
01c24632Rob ZolkosMake connection guidance safe for CLI-first release
3a3c901cRob ZolkosProve held work survives adoption and remains operable
3602f086Rob ZolkosPreserve legacy lock errors through diagnostics
25918356Rob ZolkosKeep legacy account spelling while canonicalizing profile locks
396da982Jeremy DaerSay approvals add or replace by what Basecamp enables for the agent's kind
  • GitHub checks on 396da982: 27 successful, 4 skipped, 1 neutral / 32 total, none failing or pending. Skipped: Auto-merge, Benchmarks, Refresh the Nix vendorHash, Skill Evals. Neutral: cubic, which didn't review this head. Skipped and neutral checks are not test proof.
  • make check and go test ./... pass at 396da982, and go vet passes at each of the six commits (rebase note).
  • The copy commit's contract test pins the auth agent connect and connect help's add-or-replace wording, and that "always for shared" is gone.
Earlier validation, at 13109a9 (before the copy commit and the rebase onto v0.14.0)

bin/ci exit 0, fresh temporary HOME/XDG, BASECAMP_NO_KEYRING=1. The script is only exec make check: a local CI pass, not a recorded GitHub signoff.

CheckResult
Go top-level4,930 passed / 4,935 total, 0 failures, 5 skips
Go including subtests9,265 passed / 9,271 total, 0 failures, 6 skips
E2E/BATS490 passed / 498 total, 0 failures, 8 skips
Focused migration/copy race suite36/36 passed, no skips, exit 0
Deliberate regression probes5/5 caught, each mutant exit 1; clean suite exit 0
Lint / surface / skill drift / provenance / tidyPassed; lint 0 issues; 19,565 surface entries unchanged

Go skips: opt-in callback preview; existing SDK redirect-classification skip pending re-pin; three subprocess entrypoints (credential-lock helper, fake MCP server, crash helper); one additional nested foreign-ownership test requiring root/chown. The helper entrypoints are invoked by their parent tests, but skips themselves are not proof. BATS skips: six PowerShell installer cases because pwsh is absent, and two unimplemented URL breadcrumb cases.

Source review at 13109a9: 8/8 state-reader classes rechecked, no actionable CLI finding. This is source review, not executed lifecycle/CI.

Isolated lifecycle evidence โ€” limited matrix

The lifecycle report records 28 passed / 34 planned atomic observations, 6 explicitly unexecuted. Synthetic-only real processes, HTTP, browser approvals and WebSockets; isolated HOME/XDG/keyring and loopback network. No customer credentials. These are observations from real lifecycle runs, not fake-server unit tests, and not a claim of 34/34 runtime scenarios. The table below is the CLI-relevant subset, not another aggregate denominator. These runs predate team-agent adding behind its own switch in bc3 #13840, so the rows where shared approvals replaced describe a server without that switch.

Scenario actually exercisedObservation
Released v0.13.2 on the old serverReal browser approval, mint/me/projects, six-check setup and live feed subscription; 2/2 synthetic assignments admitted under a durable hold
Released v0.13.1Existing A credential me on old server exit 0; real shared-agent approval/mint/me on integrated server. Not a separate old-server new-approval/feed proof
Earlier integrated CLI before server upgradeNo re-pair; cursor, hold and 2/2 admitted records migrated; status/release exit 0; same-profile second connector exit 5; fresh event emitted one request. This baseline observation preceded 13109a9
Integrated server adding OFFA freshly minted without re-pair; a second personal approval replaced the first as intended
Adding ON / shared agentsSame-host new profile and separate-HOME connections; 3/3 disjoint project events each emitted one request. Same-name personal reapproval added; shared v0.13.1/new/v0.13.2 approvals still replaced
Own-secret authentication / scopeValid full-scope mint HTTP 200; cross-agent/tampered secrets rejected; refresh/introspection/revocation denied. Read-only write HTTP 403 while A retained full write access
#1 and own-row disconnectA stopped after original #1 disconnect; 13109a9 B/C reconnected and emitted 2/2 fresh post-revocation requests. Later B-only disconnect stopped its socket with disconnected-or-replaced copy while other connections survived
Deliberate project overlapSame event emitted once on each connector, 2/2, accepted policy. JSON setup prints no human warning; built help warning verified
Adding-only rollback4/4 existing own-secret profiles freshly minted; legacy reapproval preserved all four; fresh event admission continued
Disconnect all5/5 legacy/own connections stopped/refused

The two older admitted requests were retained after migration but excluded from handoff after release by the existing before_this_run age guard. Migration does not promise historical replay. Explicitly held records remaining held and accessible to release/discard are separately covered by automated tests.

Limits: guided selection warning is source/unit-covered, not real-TTY exercised. Genuine runtime full shadow adoption/promote/import/discard/all-operator-command coverage was not executed; automated CLI tests cover these paths. Runtime status/release/adoption were exercised. The six unexecuted runtime groups are shared Adminland rotation, stale browser forms across replacement races, concurrent HTTP mint/revoke/scope interleavings, actual replica lag/roles, full shadow/operator commands, and ONCE browser/HTTP/schema downgrade. Automated server/CLI coverage is separate, not counted as lifecycle proof.

Attribution / source consolidation

Preserve the original work, not its rollout-unsafe promises:

Rob Zolkos's authorship and source provenance are preserved in the first five commits; the sixth, the copy change, is Jeremy Daer's. Existing source PRs/branches are not closed or rewritten by this consolidation.

Original-review replies

Point-by-point replacement-only replies to the latest review bodies, with pinned source/test links and explicit retained declines/human decisions:

The cross-repository comparison covered 36/36 Jeremy top-level points and their nested subfindings, plus older bot/body findings: 81 pre-reply source records (7 PR bodies, 40 reviews, 6 issue comments, 28 inline comments) and all 14 GraphQL threads (the same 28 inline comments, not extra findings). Empty bodies and a Copilot quota failure were accounted for, not counted as approvals. Rechecking those records found no further in-scope source defect. This is a disposition map, not a claim that every literal proposal was implemented or accepted.

The old switch-on CLI release prerequisite is superseded by the CLI-first plan: refusal, help, README and the skill published on merge say an approval replaces the existing connection wherever Basecamp hasn't enabled adding for the agent's kind. Unsupported mixed builds and the stop/whole-directory downgrade procedure remain explicit.