The custody transition could not run against a real host - #159
Conversation
|
Merged as I verified the two structural claims on Mutation-verified two myself. Restoring the bare Defect 4 is the one I would have missed in review and it is the one I am most glad you caught. The other three fail loudly. That one serves traffic perfectly while a number routing depends on quietly stops moving, and sticky placement keeps weighing a figure that was true an hour ago. There is no symptom to notice until an account hits a limit the plugin believed was a third used. Your rule from defect 3 is the one worth carrying beyond this repo: any resource a transition needs must be acquirable from the state before the transition. Gating on "am I already there" rather than "do I have a handle and a reason to try" makes the mode unreachable, and committing the flag last makes it more likely rather than less, since the whole preflight runs while the flag still reads the old value. The logging stays, and I would have asked for it if you had not included it. An unconditional first-line hook log whose absence proves the host never dispatched is the difference between a one-line diagnosis and the three wrong guesses you made first. An aborted barrier writes nothing to disk, so without the finish line a refusal and a no-op are indistinguishable afterwards — that is not debug leftovers, it is the only witness. Gate on the merge: core 147, opencode 1364, pi 14, typecheck and biome clean. |
Four defects found by running
/openai-account claustrumon a live machine tonight. Custody is now serving from the vault with all four applied; every one of them was invisible to the suite.What happened
The command did nothing. Four attempts, no dialog, and — the part that cost the most — nothing in the log at all. I misdiagnosed that silence three separate ways (missing command registration, stale config, wrong verb form) before adding one unconditional log line at the top of
command.execute.before. The next run gave the answer outright.Silence was never evidence of where it stopped. It was evidence that nothing recorded anything.
The defects
1.
client.auth.getdoes not exist.index.tsdoesinput.client.auth as unknown as { all(); get(); set() }. That cast is an assertion, not a check. Captured live:Only
setis real. The transition crashed 4ms in withU.get is not a functionat the main-slot fingerprint capture. Fixed by preferringhostAuth.getwhen callable and falling back to the loader-capturedgetAuth, which is the plugin's actual read path for the main slot everywhere else.2.
client.auth.alldoes not exist either. Same cast, surfaced once fix 1 let the transition reach the host-slot write.all()backs only the torn-read guard, so it now reads$XDG_DATA_HOME/opencode/auth.jsondirectly. That is more faithful than an SDK call, not less: the hazard being guarded is a partial read of that exact file, andAuth.setdoes unlocked read-modify-write on it. Any read or parse failure returns{}, which makes the guard refuse the write.3. The mode was unreachable from
local.custody-runtime.tsboot()returns early unlessclaustrum.mode === 'claustrum', so the cache is never connected — but the transition into claustrum has to probe the vault before any mode is written. Every participant preflightedvault-cold, forever. You could not enter the mode because entering required already being in it. Fixed with anensureCache()on the runtime interface that uses the existing single-flightedconnectCache().Worth stating as a rule, because a sibling plugin shipped the same bug three weeks ago and fixed it only after a live incident: any resource a transition needs must be acquirable from the pre-transition state. Gate on "do I have a handle and a reason to try", never on "am I already there". Committing the mode flag last does not help — it makes this more likely, since the whole preflight runs while the flag still reads
local.4. Main quota dies under custody. Every poll cycle after the flip:
The quota path had no vault resolver for main — it reads the local token, finds the tombstone, calls
refreshMainWithLease, and throws. Fallbacks were fine becauseresolveFallbackAccessis injected for them. Main's quota froze at its last pre-flip reading and would never have updated again.This one is the worst of the four despite being the least dramatic. The other three crash or refuse — loud, immediate, blocking. This one serves traffic perfectly and silently stops updating one number that routing reads. Sticky-balanced placement weighs window pressure, so it would keep sending cold sessions against a stale figure until main hit a limit the plugin believed was 32% used.
Fixed with an optional
resolveMainAccesson the quota deps, wired to the same resolver the request path uses. A refusal records a failed outcome and skips; it does not fall through to a local refresh, which would either throw on the tombstone or resurrect material the flip retired.Why the suite passed through all four
Every existing test injects
deps.authandresolveFallbackVaultState. A suite that injects a seam cannot discover the seam does not exist. The tests proved the transition works against a host that was never there, and would have kept proving it.The new tests deliberately do not inject those. They construct a client stub shaped like the real object —
{ _client: {}, set() {} }, noget, noall— and drive the real adapter. Mutation proofs, each reverting one fix alone:I re-ran the first mutation by hand against the merged branch to confirm it is not self-satisfying: restoring the bare cast call reddens three tests, including the malformed-auth-file case.
The logging
Included deliberately rather than as debug leftovers. It is what turned four silent failures into a one-line diagnosis: an unconditional first-line hook log whose absence proves the host never dispatched; the parsed command tokens; the typeof of every injected dep; the participant list and per-participant handle presence; and the transition's finish line carrying
status,reasonand the per-accountoutcomesmap — an aborted barrier writes nothing to disk, so without that line a refusal and a no-op are indistinguishable afterwards.Gates from the repo root:
build,format:check,lint,types,testall pass — core, 1364 OpenCode, 14 Pi.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Fixes custody transition so it runs against a real host by replacing assumed auth client methods with fallbacks, connecting the cache before preflight, and routing main quota reads through custody. Adds logging that surfaces silent failures and tests that drive the real adapter without injecting seams.
Bug Fixes
client.auth.getandalldo not exist on the host; fallbacks to the loader-captured auth and a directauth.jsonread replace the casted calls.ensureCache()when the mode is stilllocal, so entering claustrum no longer blocks on the wrong mode.resolveMainAccessdep, preventing tombstoned main from freezing at a stale reading.Testing
setand drive the real adapter, proving the transition works against a host missing assumed methods.Written for commit 0024a51. Summary will update on new commits.