Skip to content

feat(core): recognize the Claustrum custody tombstone and fail loud before refresh - #182

Merged
ualtinok merged 16 commits into
cortexkit:mainfrom
iceteaSA:feat/custody-tombstone-guard
Sep 2, 2026
Merged

feat(core): recognize the Claustrum custody tombstone and fail loud before refresh#182
ualtinok merged 16 commits into
cortexkit:mainfrom
iceteaSA:feat/custody-tombstone-guard

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Stacked on #175 (base is main; the diff includes #175's commit until it merges — review git diff e5ae3aa..HEAD, 11 files, +474). Prepares the main slot for vault custody without implementing vault-served main.

Why

Claustrum's custody plugin replaces a main-slot auth.json entry with a non-secret tombstone:

"anthropic": { "type":"oauth", "refresh":"claustrum-tombstone:v1:anthropic", "access":"claustrum-tombstone:v1:anthropic", "expires": 0 }

expires: 0 is deliberate: any reader that doesn't recognize the shape sees an expired token and takes the refresh path. Today that path sends the sentinel to the OAuth token endpoint → 400 invalid_grantpermanent: true → the main account is latched dead. That is the failure that froze main-slot custody.

What

  • isCustodyTombstoneOAuth(auth, provider) — exact, provider-bound match; readCustodyHandles (tolerates superseded, serves handle only).
  • CustodyTombstoneRefreshError carries none of the ProviderHttpError markers (no status, isRefreshError, permanent), so it structurally cannot arm refresh backoff or latch an account. Refresh-error persistence additionally skips it.
  • One guard at the shared token-refresh function (auth.ts) covers every refresh entrypoint: background refresh, 401 retry, per-request refresh, prime refresh paths, FallbackAccountManager, Pi OAuth + fallback refresh. The loader additionally branches on the tombstone before the expiry check and throws (this branch becomes the vault path in the takeover PR).
  • Golden pin: fixtures vendored from legion-works/claustrum@7cf41ca (SOURCE.json records repo/ref/paths); the test derives every expectation from the fixture. bun run check:claustrum-golden fetches the pinned ref and byte-diffs (a script, not a test — the test network guard blocks it). biome.json excludes the fixture dir so the formatter can't perturb the byte-identical copies.

Proof

  • Tombstone on main → loader throws typed error, /v1/oauth/token called 0 times, no permanent/needsReauth/backoff persisted.
  • Mutation (guard removed): token endpoint called 1×, stubbed invalid_grant latches → test reddens (4/1); restored 5/0.
  • Fallback account with tombstone refresh → typed error, no network, no lastRefreshError.permanent.
  • Root 1461/0 (second run: known Miniflare load timeout, solo 4/4), e2e 29/0, typecheck/format/biome clean. Golden check: both files IDENTICAL at the pinned SHA.

Out of scope: vault-served main (the loader branch is the seam), the shared handle-file reader wiring, serve: ownership conjunction — takeover PR.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Adds optional Claustrum custody for fallback OAuth accounts and rejects custody tombstones before OAuth refresh. Previously, tombstones reached Anthropic and could persist permanent refresh failures; now refresh fails locally without a token request or persisted refresh error. Main-account vault serving remains out of scope.

Features

  • Connects opted-in fallback accounts to Claustrum over authenticated local IPC with cached reads, detached refresh, shared reconnects, deduplicated 401 reporting, and sidecar fallback.
  • Resolves fallback quota from the vault-first cache, avoids expired sidecar tokens, and persists 429 backoff across processes.
  • Validates provider-bound handles and tombstones without exposing handles in dumps, sidebar state, or RPC payloads.
  • Shows Claustrum, vault, and vault reauthentication status in account commands, dialogs, and the sidebar.
  • Preserves scoped account state when configuration is missing, malformed, whitespace-padded, or changed by a stale writer.

Tests

  • Adds a fail-closed network guard, deterministic provider stubs, and cleanup checks for leaked fetch mocks, timers, temporary directories, and log sinks.
  • Covers custody failures, fallback behavior, tombstone refresh paths, typed errors, zero token requests, quota backoff, and refresh-error persistence.
  • Pins the golden fixtures to the upstream Claustrum PR head; bun run check:claustrum-golden byte-verifies them, including golden-only updates.

Written for commit d0256c8. Summary will update on new commits.

Review in cubic

@socket-security

socket-security Bot commented Sep 2, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​cortexkit/​subc-client@​0.8.18810010093100

View full report

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 37 files

Not reviewed (too large): packages/opencode/src/tests/index.test.ts (~4,414 lines) - if these are generated or fixture files, add them to ignored paths to exclude them from future reviews.

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread packages/opencode/src/transform.ts
Comment thread packages/core/src/auth.ts
Comment thread packages/opencode/src/tui.tsx
Comment thread packages/core/src/claustrum.ts
Comment thread scripts/check-claustrum-golden.ts Outdated
Comment thread packages/opencode/src/tui/command-dialogs.tsx Outdated
Comment thread packages/opencode/src/tests/accounts.test.ts
Comment thread packages/opencode/src/tests/credential-handle-blindness.test.ts
Comment thread scripts/check-claustrum-golden.ts
Comment thread packages/opencode/src/tests/claustrum-client.test.ts
Fail-loud preload network guard for the test suite: any non-loopback
HTTP(S) fetch throws, including across redirects (manual redirect
following, 20-hop cap, unparseable URLs fail closed). Replaces 44 live
vendor requests per run with deterministic local stubs, isolates
background intervals and temp dirs, and asserts no test leaves
globalThis.fetch or an interval behind.

Production fixes found by the guard: account-state membership treats a
missing or unparseable config as UNKNOWN (no pruning) rather than empty,
loader/membership agree on whitespace-normalized ids, and rotated
credentials persist under the canonical key.

Loopback classification tests call assertLoopback() directly instead of
opening sockets (127.0.0.2:1 hangs to timeout on Darwin), and the
upstream Fable 5.1 effort test restores globalThis.fetch after itself.
A fallback OAuth account can keep its credential in Claustrum, a local
credential vault reached over an IPC daemon, instead of in the sidecar.
Per-account opt-in, off by default: with no vault present, or no account
opted in, nothing here runs. Main-account custody stays out of scope —
a vault-managed main would send a placeholder to Anthropic's token
endpoint, and our classifier marks that failure permanent with no way back.

The vault's slow path is kept off the response path structurally.
credential.get is bimodal — microseconds when resident, seconds when it
lands in the refresh window with callers queued behind one upstream
exchange — so serving only ever peeks synchronously, a bounded startup warm
primes the cache, and refreshes run detached. A cold peek falls through to
the sidecar rather than waiting.

The vault owns refresh for an account it serves: that account's sidecar
refresh token is a lineage the vault may already have rotated, and spending
it returns invalid_grant, which is permanent. But a vault outage must never
be worse than having no vault, so an unavailable vault still permits
ordinary local recovery, and a resident credential clears whatever error
that recovery persisted.

Identity is explicit in both directions. consumerIdentity is forced to null
because the client otherwise reads SUBC_MODULE_ID and SUBC_LAUNCH_NONCE
from the environment and would authenticate as whichever supervised module
spawned the host. A BindIdentity is supplied because the client requires
one for every managed call; its session is derived from the account-store
fingerprint, so it is stable across restarts without new persisted state.

A 401 is reported only for a credential the vault actually served, carried
on the response rather than looked up again at report time. Invalidation is
version-gated and the version in hand is usually still current, so a report
raised from a locally served token would pass that gate and invalidate a
healthy credential — version-correct and provenance-wrong.

Failures classify so the vault's shared rate limiter stays protected: a
transport failure or a terminal route failure backs off per handle instead
of issuing one credential.get per request, and an unrecognised error class
retries boundedly and loudly rather than blocking the account, which is the
most operator-visible action available and needs positive evidence.

Credential handles never reach dump artifacts, sidebar state, or RPC
responses; a compile-time contract keeps them out of dump inputs.
…reports

Also normalize the main TUI gate placeholder, clean test log sinks with finally, and make fake-daemon goodbye waits event-driven.
…an expired token

1. Resolve fallback quota credentials from the vault-first cache seam.
2. Skip vault-cold expired credentials and refresh non-vault expired credentials before polling.
3. Persist and seed cross-process fallback 429 backoff.
4. Fence auth-failure reports monotonically by credential record version.
5. Cancel the current response only when an eligible fallback actually sends.

Anthropic returns 429, not 401, when the expired sidecar token reaches the usage endpoint.
Bytes unchanged (verified IDENTICAL x2); the previous pin named a commit
off cortexkit/claustrum#28's history after its squash.

Also: biome check in the pre-commit hook errored when every staged path
was ignored, so any golden-only bump commit failed the hook. Pass
--no-errors-on-unmatched.
Bytes unchanged (IDENTICAL x2); the prior pin named a pre-squash commit.
Bytes unchanged (IDENTICAL x2); PR cortexkit#28 rebased onto its upstream master.
@iceteaSA
iceteaSA force-pushed the feat/custody-tombstone-guard branch from 62dcff5 to 25e1456 Compare September 2, 2026 12:14
Bytes unchanged (IDENTICAL x2); ref verified via ls-remote.
Bytes unchanged (IDENTICAL x2); ref verified via ls-remote.
Bytes unchanged (IDENTICAL x2); ref verified via ls-remote.
@ualtinok
ualtinok merged commit b454f0c into cortexkit:main Sep 2, 2026
4 of 5 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.

2 participants