Skip to content

Support generation-bound Gmail and Calendar accounts - #87

Merged
jerryfane merged 2 commits into
mainfrom
feat/workspace-multiple-accounts
Oct 17, 2026
Merged

jerryfane merged 2 commits into
mainfrom
feat/workspace-multiple-accounts

Conversation

@jerryfane

@jerryfane jerryfane commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Assignment and scope

Implements #86. Assignment: Phobos327316/327332/327349/327360; contract frozen by Phobos327457 and Keyring327466, with TTL clarification327520 acknowledged327621. Implementer: keyring seat, not the independent reviewer. No automatic fix target.

Adds multiple Gmail/Calendar connection slots inside each existing personal instance. No deployment, live migration/hydration, new Google calls, public enrollment, or Drive/SDK migration is authorized by this PR.

Behavior

  • Sealed immutable account IDs, bounded aliases, separate account grant ceilings, cached verified identity and per-service status. Explicit metadata-only default migration preserves the original key, role grants and independent Drive state.
  • Generation-qualified admin consent/identity/disconnect and /_accounts/<id>/<service>/... proxy routes. Missing/stale/unverified bindings fail closed before provider work; role/service quotas stay aggregate.
  • Per-account dispatch/commit leases, post-renewal generation checks, atomic identity/credential publication, stale-flow invalidation and shared Gmail+Calendar disconnect. No automatic write replay or fallback to another account.
  • Existing unqualified routes remain pinned to the original default. Access-token refresh preserves generation; replacement/disconnect/client changes invalidate it.
  • Worker routes checkpoint sealed account changes and preserve them across DO eviction/container restart. Consent links retain existing bounded TTL support, including900.
  • Documents the schema-capable rollback boundary; an old image cannot read the persisted account registry.

Verification

  • Eight TestWorkspaceAccount* real-instance HTTP regressions pass with a synthetic TLS Google provider. Coverage: account/default isolation, sealed restart, generation/dispatch/disconnect ordering, failed remote revocation, grant ceilings/aggregate quotas, identity caching/binding invalidation, legacy diagnostics, immutable default key references, cache retention, and audit/HTTP status agreement.
  • Full GOTOOLCHAIN=go1.24.13 go test -race ./... and go vet ./... passed in the initial implementation run. After the review corrections, go test -race ./internal/instance ./internal/server ./internal/consent and go vet ./... passed again. Worker bun run check passed; its TypeScript is unchanged by the corrections.
  • test-check prove --test "env GOTOOLCHAIN=go1.24.13 go test ./internal/instance -v -run '^{name}$' -count=1": the original four feature tests are proven red/green against the base. An additional run with --base a38bd39c6eaa858d0802cd34f541601aa9abc600 proves all four review-fix regressions fail on the previously reviewed implementation and pass now—not merely because the old API is absent. Receipts: /root/fleet-tools/state/keyring-seat/multiple-google-accounts-20261017/test-check-proof.json and test-check-review-fixes.json.
  • Actual compiled CLI smoke passed in non-root/read-only Docker with external networking disabled. Positive-control loopback transport proves provider-free discovery/migration, explicit hydration, legacy revoke diagnostics/local disconnect after remote failure, service-key rebinding rejection, and actual audit status428. Receipt: account-cli-review-audit-result.json in the same durable directory.
  • Actual Worker/DO smoke against the corrected CLI passed, including account persistence, caller-proof enforcement, generation-bound links, stale disconnect refusal, and durable shared disconnect without changing the default. Output: worker-review-fix-smoke.log. Cloudflare runtime is replaced by the existing local harness.
  • The pinned actual Worker+CLI fixture for head f178bbea6ab5ea1d86dd41987e11cb0ab2069b7e verifies work-account cache retention across router-token rotation: one provider refresh, unchanged account generation. Alice personal/work and Bob identities are isolated. The current fixture uses actual HTTPS with a loopback-SAN test CA and the deployed canonical Calendar base https://www.googleapis.com. Manifest: fixture-manifest-1792273233581-d42adb0d.json; delivered to Phobos327815. Native Python with process-local SSL_CERT_FILE verifies three correct Calendar organizers, doubled-prefix404, and actual HTTPS consent page200 (fixture-tls-smoke-result.json). This corrects two fixture-only incompatibilities reported by Phobos; production/consumer URL validation and provider routes are unchanged.
  • GitHub CI is green at f178bbea6ab5ea1d86dd41987e11cb0ab2069b7e: format/vet/build/test and native macOS private-pipe import.

Independent review

Approved at exact head f178bbea6ab5ea1d86dd41987e11cb0ab2069b7e. Reviewer g7-review, model devin/swe-2, job local-review-review-router-18df6e7cd7a96f7b-1, review-only with --no-fix-target. The reviewer did not implement. Implementation receipts are session-implement-keyring-18df6cee78b91330 and session-implement-keyring-18df6e7938db0a3f, acting role keyring.

The initial whole-diff review approved a38bd39; the final delta review confirms all four P3 findings fixed, with no new defect. Final reviewer checks: build and actual binary usage, changed-package vet, all eight Workspace regressions, consent tests (30 passing), and server tests. Reviewer race execution was sandbox-blocked and is not counted as passed; the implementing seat's changed-package race run passed independently. Full verdicts: review-a38bd39.json and review-f178bbe.json in the durable evidence directory above.

Cross-service acceptance

Passed: Phobos 95edc3dfcebba54ae4831ccbef3341d05625a8db with Keyring f178bbea6ab5ea1d86dd41987e11cb0ab2069b7e, reported327848 and read back by the Keyring seat. Actual OMP/pinned MCP image, HTTPS Worker and CLI exercised three identities, overlapping IDs, approved Calendar creation and correct Gmail sender, stale/dispatch-raced approval refusal, legacy-default non-fallback, migration invariant and ordinary refresh. The exact compiled router's signed-webhook command smoke also passed HTTPS consent and durable shared disconnect, preserving Bob and non-Workspace status. Receipts: /root/fleet-tools/state/phobos-dev/multi-account/keyring-artifact-smoke/receipt-f178bbe.json and /root/fleet-tools/state/phobos-dev/multi-account/keyring-binary-command-smoke-f178bbea-1792273924439426599.json.

Google and WhatsApp transports were synthetic; no live provider requests or messages. No consumer tests were duplicated by Keyring. Superseded HTTP fixtures were stopped after Phobos confirmed them unused.

Bounded pilot cutover

Phobos-dev released its contract-first coordination hold in327880 (record) only for the existing-pilot plan327848/327861. This was not an owner stop. No new consent, account, enrollment, scope, Drive permissions, public rollout, paid work or sends are authorized. Live mutations still require explicit Phobos quiescence.

PR87 merged as 13c266c57dbf932d4f57cae34a49740462b8ecca after final exact-head level3/CI gates. Reviewed and merged commits have the same tree db8ad64be79de6eea3b7b9e2a3e440ff236b59e3. Ordinary production image keyring:workspace-accounts-f178bbe (index sha256:335486bb3b2b63a0c429a4ce0d586f594cd4c1dd46a92c27ef0b14a1bfdb83cc) was built from the committed tree without any test overlay and passed the actual non-root/read-only/network-disabled CLI smoke. Receipt: production-image-smoke.json.

Bounded backend cutover passed. Phobos-dev executed through its existing authorized operator path (split327937), after explicit quiescence328025. Keyring independently read back and compared the receipts; no role borrowing or permission change.

Deployed Worker 8be4d941-70c0-4efa-a09c-bafad6640fdd; rollout 06a1eaea-11a4-4fae-97c4-9bd2424f1ac7 completed with7 healthy instances on the exact ordinary image above. Application configuration changed only in image; Worker settings/bindings stayed equal except its deployment-message annotation. Code rollout is application-wide, but metadata migration and identity hydration were each issued once for the existing pilot only. The verified identity matches prior acceptance; all seven cached service statuses and caller token mapping are preserved. Phobos's cached-only policy/account checker passed without provider paths or mutations.

Independent preservation receipt: /root/fleet-tools/state/keyring-seat/multiple-google-accounts-20261017/live-preservation-review.json; operator receipts: /root/fleet-tools/state/phobos-dev/multi-account/live-cutover/. Approval328046. Final caller activation passed at2026-10-17T22:49:09Z: active/running Phobos executable matches the reviewed release SHA256 a3313eb4e950d6bf076da5d68acd0e0e82b977119357b4e5796972b0ebec16c3. Final receipt operator-final-receipt.json, operator confirmation328070, and Phobos deployed acceptance. All synthetic fixture processes are now stopped.

Limits: raw live credential/client/role/SDK-state byte equality is not exposed by the supported API and is not claimed. Exact synthetic preservation is covered by the ordinary-image/regression proofs; live preservation uses observable configuration and cached status. Ordinary refresh telemetry is unknown. Drive diagnostics report no_attempt, not a new Drive probe. No new consent/account, extra provider test or send was authorized. Retain schema-capable Keyring and current sealed state on any caller rollback; never restore stale credentials.

@jerryfane

Copy link
Copy Markdown
Contributor Author

Agent: g7-review
Runtime: omp
Job: local-review-review-router-18df6cfa7d8484e9-1

Decision: approved
Summary: Reviewed PR #87 at a38bd39 against base fd4d9b4 (12 files, +1901). Verified by execution: CGO_ENABLED=0 go build and go vet clean; the four workspace regression tests pass (PreserveDefaultAndIsolateCredentials, GenerationGatesDispatchAndGroupDisconnect, QuotasAndSelectorsFailClosed, HydrationCachesByBinding); full go test ./... passes including internal/server 73.9s and cmd/keyring. Race detector is sandbox-blocked (-race requires cgo; CGO_ENABLED=1 fails reading /usr/include), so lock ordering was audited statically: consent s.mu -> workspaceMu -> account gate -> in.mu is consistent across disconnectWorkspace/CommitConsent/changeBindings/Detach/hydrateAccount with no cycle; hydrateAccount releases gate.RLock before workspaceMu; Acquire's gate.RLock is held across token renewal, Revalidate and the upstream Do, so no mutation publishes a replacement across an admitted dispatch; Revalidate is a correct defensive recheck. reconcileWorkspace bumps binding_generation on any key/client-secret/gmail-calendar service drift for every change() caller including putNetwork/putConsent (no-op when unchanged). recordLocked enforces 428 missing / 409 stale generation; account grant snapshots (all roles at migrate, router-only at create) are always intersected with the live role ceiling so aggregate per-role/service daily quota is preserved; VerifiedEmail uses the gmail profile endpoint with a redirect-disabled client; boundService revalidates slot+generation+OAuth client at every browser transition; duplicate connected identities are refused before publication without revoking the active grant. Worker routes cover all frozen admin routes, forward the generation header into /_proxy/, and sync sealed state after mutations; smoke.ts exercises DO-eviction durability. Generation header is deleted in outboundHeader. Legacy unqualified gmail/calendar dispatch and consent pin to the default slot post-migration and fall through unchanged pre-migration. No prior verdicts exist for PR #87 (pr
[truncated]
Severity: P3

Findings

  • Legacy service-key delete loses revoke_error detail (P3)
    • detail: DELETE /keys/<gmail|calendar> (consent API) now routes through disconnectWorkspace(legacy=true), whose response is {deleted,key,revoked}. The pre-existing deleteOAuthKey also returned revoke_error with the bounded provider failure text; that diagnostic is dropped for workspace services, so an operator can no longer see why remote revocation failed on the legacy path. Local disconnect semantics are unchanged and correct.
    • file: internal/consent/workspace.go
    • id: f1
    • locator: internal/consent/workspace.go:55-96 (legacy writeJSON) vs internal/consent/consent.go:497-506 (revoke_error)
  • Changing gmail/calendar service key name after migration wedges all account operations (P3)
    • detail: workspaceServices() returns 409 account_binding_changed whenever s.Services[gmail].Key != WorkspaceAccounts[default].Key. If an admin PUTs /services/gmail with a different key name post-migration, every workspace operation - including DELETE /accounts/ via Detach - fails until the original key name is restored, and the recorded a.Key is immutable so no in-API repair path exists short of reverting the service config. Fail-closed (arguably intended), but a single admin key rename creates a total account-admin lockout with no stated recovery except config rollback.
    • file: internal/instance/workspace.go
    • id: f2
    • locator: internal/instance/workspace.go:138-150 (workspaceServices default check), 454-481 (Detach calls workspaceServices before deleting the key)
  • Per-account OAuth cache entries evicted on every state swap (P3)
    • detail: retainOAuth keeps only entries for config service keys; account selections use per-slot key names (GOOGLE_WORKSPACE_ACCOUNT_*) that are never in keep[], so every commit (e.g. unrelated admin PUT) deletes idle account access-token cache entries, forcing extra provider refreshes. Refresh-in-flight entries survive via e.refreshing. Performance nit only, no correctness impact.
    • file: internal/server/oauth2.go
    • id: f3
    • locator: internal/server/oauth2.go:169-185; selection uses a.Key at internal/instance/workspace.go:197-203
  • Account selection refusals collapse to one audit status/note (P3)
    • detail: WriteError emits {error:} for workspace failures, but ServeHTTP records rec.Status as http.StatusConflict and note 'account selection refused' for every Acquire refusal including 428 and 404; audit/usage tooling must parse response bodies, not recorded status, to distinguish missing vs stale vs forbidden. Consistent with the frozen contract per README; informational only.
    • file: internal/workspace/accounts.go
    • id: f4
    • locator: internal/server/server.go:411-416, internal/workspace/accounts.go:96-110

Tests Run

  • CGO_ENABLED=0 go build ./... -> PASS (go1.26.4; CGO_ENABLED=1 blocked by sandbox: /usr/include/stdc-predef.h permission denied)
  • CGO_ENABLED=0 go vet ./... -> PASS
  • CGO_ENABLED=0 go test ./internal/instance/ -run TestWorkspace -v -> PASS 4/4 in 1.8s
  • CGO_ENABLED=0 go test ./... -> PASS all packages (cmd/keyring 0.9s, internal/consent 0.02s, internal/instance 2.9s, internal/server 73.9s, all others ok)
  • go test -race ./internal/instance -run TestWorkspace -> NOT RUN: -race requires cgo; CGO_ENABLED=1 is sandbox-blocked, so race claims rest on static lock-order audit only

Raw runtime output was retained in local Gitmoot state and is not posted here.

@jerryfane

Copy link
Copy Markdown
Contributor Author

Independent level3 review: g7-review / devin-swe-2 APPROVED exact a38bd39. Job local-review-review-router-18df6cfa7d8484e9-1; implementing seat receipt session-implement-keyring-18df6cee78b91330 is separate. Reviewer executed CGO_ENABLED=0 build, vet, all Go tests, and the four Workspace regressions. Reviewer race execution was sandbox-blocked, not counted as passed. Durable full verdict: /root/fleet-tools/state/keyring-seat/multiple-google-accounts-20261017/review-a38bd39.json. Four nonblocking P3 observations are being corrected before final acceptance: legacy revoke_error preservation; refusing service-key rebinding of a migrated immutable default; per-slot OAuth-cache retention across unrelated swaps; actual refusal status in audit/usage. Fresh exact-head review will follow those changes. Actual frozen Worker + CLI synthetic-provider fixture at this approved head was delivered to Phobos327723; its smoke verified Alice personal/work and Bob identities plus cross-instance token rejection. No deployment or live Google calls.

@jerryfane

Copy link
Copy Markdown
Contributor Author

Agent: g7-review
Runtime: omp
Job: local-review-review-router-18df6e7cd7a96f7b-1

Decision: approved
Summary: Reviewed the a38bd39..f178bbe delta only (7 files). All four open P3s are fixed at f178bbe and their interactions hold: legacy DELETE /keys/gmail now matches deleteOAuthKey field presence (revoked and bounded revoke_error only when revocation is attempted); post-migration gmail/calendar key renames are rejected in validateWorkspace before commit/Swap, so account admin is not wedged and caches are not touched; retainOAuth keeps GOOGLE_WORKSPACE_ACCOUNT_ entries (and their refresh-failure state) across unrelated swaps while a replaced refresh is still dropped; Acquire/Revalidate/invalid-path refusals record WriteError's actual status, which usage.record persists as LastStatus. Default slot stays on the immutable service key, so the existing service-key keep path still covers it. No new defect in the delta. Race detector could not be built (cgo denied /usr/include/stdc-predef.h).

Findings

  • Legacy service-key delete loses revoke_error detail (P3)
    • continues_uid: Support generation-bound Gmail and Calendar accounts #87-f1
    • detail: Fixed at f178bbe. disconnectWorkspace legacy responses now match deleteOAuthKey (consent.go:497-506): revoked and revoke_error are present only when a refresh and RevokeURL made revocation attempted; revoke_error is revoke()'s bounded text. Local delete still commits before the provider call. Checked by reading workspace.go:79-98 and consent.go:497-506. Executed TestWorkspaceAccountLegacyRevokeDiagnostics: pre-migration DELETE /keys/gmail and post-migration legacy delete DeepEqual under provider refusal, and the slot stays disconnected.
    • file: internal/consent/workspace.go
    • state: answered
    • uid: f1-check
  • Changing gmail/calendar service key name after migration wedges all account operations (P3)
    • continues_uid: Support generation-bound Gmail and Calendar accounts #87-f2
    • detail: Fixed at f178bbe. validateWorkspace (workspace.go:108-112) rejects any commit whose gmail or calendar Key differs from the default account key; commit maps that to 400 and returns before seal/Swap (instance.go:260-262), so live state, a.Key, and token caches stay put. workspaceServices 409 remains only as a guard for an already-divergent loaded state; restoring the original key name is a matching commit and still succeeds. Credential rotation remains PUT of the same key name. Executed TestWorkspaceAccountDefaultRejectsServiceKeyRebinding: both service rebinds returned 400, the default view was unchanged, and identity still resolved.
    • file: internal/instance/workspace.go
    • state: answered
    • uid: f2-check
  • Per-account OAuth cache entries evicted on every state swap (P3)
    • continues_uid: Support generation-bound Gmail and Calendar accounts #87-f3
    • detail: Fixed at f178bbe. retainOAuth (oauth2.go:178-187) keeps oauthKey(gmail OAuth2, refresh, client secret) for every GOOGLE_WORKSPACE_ACCOUNT_<32 hex> key when the handler has a workspace boundary. That is the same secret and, under workspaceServices' Equal constraint, the same TokenURL/ClientID Acquire uses, so idle tokens and cached refresh failures survive unrelated swaps. In-flight entries are still kept by e.refreshing. A replaced refresh is not in keep and is evicted. The default slot is not prefix-named; immutability keeps it on the service key, which the existing service loop already retains. SetWorkspace runs before the post-create Swap, so the nil-workspace first Swap cannot drop live account entries. Executed TestWorkspaceAccountCacheSurvivesUnrelatedChanges: an unrelated PUT caused only a profile call (no token refresh); replacing the account refresh minted a new identity and the old generation returned 409.
    • file: internal/server/oauth2.go
    • state: answered
    • uid: f3-check
  • Account selection refusals collapse to one audit status/note (P3)
    • continues_uid: Support generation-bound Gmail and Calendar accounts #87-f4
    • detail: Fixed at f178bbe. WriteError now returns the emitted status (accounts.go:68-78). ServeHTTP assigns that to rec.Status for invalid account paths, Acquire refusals, and Revalidate failures (server.go:275-277, 412-414, 553-556) before the defer writes audit and usage.record. rec.Service is set at server.go:302, so usage LastStatus is the real HTTP status (428/409/404/403), not a collapsed 409. The note string is still shared; status is what usage and audit tooling read. Context errors that are not *workspace.Error still surface as 500, now recorded as 500 instead of 409. Executed TestWorkspaceAccountAuditMatchesRefusalStatus: missing generation returned 428 and the gmail audit line recorded 428; the provider was not called.
    • file: internal/server/server.go
    • state: answered
    • uid: f4-check

Tests Run

  • CGO_ENABLED=0 go build -o $TMPDIR/keyring-f178bbe ./cmd/keyring → exit 0; ELF static go1.26.4 binary; running it printed usage
  • CGO_ENABLED=0 go vet ./internal/consent/ ./internal/instance/ ./internal/server/ ./internal/workspace/ → exit 0
  • CGO_ENABLED=0 go test -count=1 -timeout 240s ./internal/instance/ -run TestWorkspaceAccount → PASS 8/8 in 2.524s, including TestWorkspaceAccountLegacyRevokeDiagnostics, TestWorkspaceAccountDefaultRejectsServiceKeyRebinding, TestWorkspaceAccountCacheSurvivesUnrelatedChanges, TestWorkspaceAccountAuditMatchesRefusalStatus
  • CGO_ENABLED=0 go test -count=1 -timeout 180s ./internal/consent/ → PASS 30 fail 0 (0.023s)
  • CGO_ENABLED=0 go test -count=1 -timeout 240s ./internal/server/ → PASS (72.104s)
  • CGO_ENABLED=0 go test -count=1 ./internal/workspace/ → no test files
  • CGO_ENABLED=1 go test -race -count=1 -timeout 180s ./internal/instance/ -run TestWorkspaceAccountLegacyRevokeDiagnostics|TestWorkspaceAccountDefaultRejectsServiceKeyRebinding|TestWorkspaceAccountCacheSurvivesUnrelatedChanges|TestWorkspaceAccountAuditMatchesRefusalStatus → build failed: /usr/include/stdc-predef.h Permission denied (cgo/race sandbox-blocked; not executed)

Raw runtime output was retained in local Gitmoot state and is not posted here.

@jerryfane
jerryfane merged commit 13c266c into main Oct 17, 2026
3 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