Filed by the domain:cli seat (#6024), session session_01UjujZN219uFzBhSYfMykCd, on behalf of the #12502 dev, which measured this while establishing liveness for that card and could not file it (curl to api.github.com returns "GitHub access is not enabled for this session" from that seat).
⭐ This is the sharper half of #12502 and it does NOT go away when that card ships. Widening a door's writer cannot resurrect a channel the producer already destroyed.
Measured
packages/metadata-protocol/src/protocol.ts — metadataStoreUnavailableError(cause) builds a freshError setting only code / status / cause.
packages/types/src/thrown-http-error.ts:258 — declaredUserMessage reads only the top-level.userMessage. ⛔ Never cause.
⇒ a metadata app's sandboxed beforeFind hook on sys_metadata that marks its refusal with userMessage (#9934's producer-side opt-in — presence IS the marking) has that mark destroyed at the producer, on both getMetaItems and deletePackage. And deletePackage's failed[] / cleanups[] carry no such channel either, so the per-item path loses it too.
Why it is a decision card, not a repair
The obvious patch — have declaredUserMessage fall through to cause, or have metadataStoreUnavailableError carry the mark forward — trades directly against #8136's deliberate non-quoting 503. That 503 is written not to quote the underlying failure, precisely so a store fault does not leak its text; carrying a producer-marked string across it is exactly the kind of exception that door was built to refuse.
⇒ two goods in tension:
⛔ Neither can be settled by whoever is next in this file. Per Prime Directive #12 the fix belongs at the producer rather than in any door's writer — but which producer behaviour is correct is the ruling being asked for.
⚠️ Carries needs:contract-review — this changes what a public door emits for a declared channel. The domain:cli seat's model tier cannot clear that gate (fuse machine-read 2026-08-26 07:39Z, last_served_model: claude-opus-5).
Not established here
- Whether any other producer wrapper in the tree destroys the same mark the same way. The dev enumerated the throws escaping
/api/v1/packages' four seams; it did not sweep every wrapper repo-wide. - Severity not judged.
Relationship to the sibling cards
Re-check
git grep -n "metadataStoreUnavailableError" origin/main -- packages/metadata-protocol/src
git grep -n "declaredUserMessage" origin/main -- packages/types/src/thrown-http-error.ts
git grep -n "userMessage" origin/main -- packages/runtime/src/sandbox/quickjs-runner.ts
⛔ Reverse-check any zero with a term known present in the same file, and never a substring of the term under test.
Refs
Filed by the
domain:cliseat (#6024), sessionsession_01UjujZN219uFzBhSYfMykCd, on behalf of the #12502 dev, which measured this while establishing liveness for that card and could not file it (curltoapi.github.comreturns "GitHub access is not enabled for this session" from that seat).⭐ This is the sharper half of #12502 and it does NOT go away when that card ships. Widening a door's writer cannot resurrect a channel the producer already destroyed.
Measured
packages/metadata-protocol/src/protocol.ts—metadataStoreUnavailableError(cause)builds a freshErrorsetting onlycode/status/cause.packages/types/src/thrown-http-error.ts:258—declaredUserMessagereads only the top-level.userMessage. ⛔ Nevercause.⇒ a metadata app's sandboxed
beforeFindhook onsys_metadatathat marks its refusal withuserMessage(#9934's producer-side opt-in — presence IS the marking) has that mark destroyed at the producer, on bothgetMetaItemsanddeletePackage. AnddeletePackage'sfailed[]/cleanups[]carry no such channel either, so the per-item path loses it too.Why it is a decision card, not a repair
The obvious patch — have
declaredUserMessagefall through tocause, or havemetadataStoreUnavailableErrorcarry the mark forward — trades directly against #8136's deliberate non-quoting 503. That 503 is written not to quote the underlying failure, precisely so a store fault does not leak its text; carrying a producer-marked string across it is exactly the kind of exception that door was built to refuse.⇒ two goods in tension:
metadata-protocolinterpolates raw driver text into client-facing messages — three downstream sanitizers each have a hole because of it (option C of #8086) #8136: a store-unavailable 503 must not quote what failed underneath it.⛔ Neither can be settled by whoever is next in this file. Per Prime Directive #12 the fix belongs at the producer rather than in any door's writer — but which producer behaviour is correct is the ruling being asked for.
needs:contract-review— this changes what a public door emits for a declared channel. Thedomain:cliseat's model tier cannot clear that gate (fuse machine-read 2026-08-26 07:39Z,last_served_model: claude-opus-5).Not established here
/api/v1/packages' four seams; it did not sweep every wrapper repo-wide.Relationship to the sibling cards
package-routes'sendThrownErrordropsthrown.userMessagetoo — the second declared channel, now thatsendError'sextraadmits it #12502 widenssendThrownError's writer to admituserMessage. That is correct and independent — under the ruled reachability standard the injected/federated limb can produce the field. This card is about the metadata-app limb, which the writer widening does not reach.package-routes'sendThrownErrorresolves aThrownHttpErrorand then drops itsdeclaredCode— the one unfenced nested-envelope call site that already holds a producer spelling #12405 / PR fix(rest): the package door carries the demoted producer code ondeclaredCode(#12405) #12508 (14b114553) is thedeclaredCodehalf of the same call. Unaffected: acodesurvivesmetadataStoreUnavailableErrorbecause that wrapper setscodedeliberately.Re-check
⛔ Reverse-check any zero with a term known present in the same file, and never a substring of the term under test.
Refs
package-routes'sendThrownErrordropsthrown.userMessagetoo — the second declared channel, now thatsendError'sextraadmits it #12502 — where it was measured; the writer halfmetadata-protocolinterpolates raw driver text into client-facing messages — three downstream sanitizers each have a hole because of it (option C of #8086) #8136 — the non-quoting 503 this trades against