Skip to content

fix(rest): the record-share family carries a producer-marked userMessage on its two non-classified exits (#12693) - #12707

Merged
os-litant merged 2 commits into
mainfrom
claude/issue-12693-share-usermessage-bypass-exits
Aug 27, 2026
Merged

fix(rest): the record-share family carries a producer-marked userMessage on its two non-classified exits (#12693)#12707
os-litant merged 2 commits into
mainfrom
claude/issue-12693-share-usermessage-bypass-exits

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#12693

respondSharingError has carried the producer's caller-facing sentence at its
classified re-dress since #12669 fork (a) / b853cf303. The record-share
family has two other exits that never reach that classification, and both
still dropped it:

  • the 500 fault terminal (SHARES_LIST_FAILED / SHARE_GRANT_FAILED /
    SHARE_REVOKE_FAILED) — classifiedRefusalAnswer deliberately hands a
    declared or resolved 5xx back to "the catching route's own terminal", so a
    marked fault never had a classification to ride;
  • the ADR-0111 message-prefix arm — it runs precisely when the
    classification answered undefined.

Reproduced before it was repaired, on 15bf9e859

One marked producer per exit, driven through the real routes on both doors
(no in-tree producer sets userMessage at this seam, so the tests build their
own — see the severity qualifier below):

throw { code: 'SHARE_STORE_DOWN', status: 503, userMessage: '…recovers…' }
share GET/POST/DELETE : 500 {"code":"SHARES_LIST_FAILED",
"message":"share store connection reset"}
/data door : 503 {"code":"SERVICE_UNAVAILABLE",
"declaredCode":"SHARE_STORE_DOWN",
"userMessage":"…recovers…"}
throw Error('NOT_FOUND: no such record account/a1') + userMessage
share GET/POST/DELETE : 404 {"code":"NOT_FOUND",
"message":"no such record account/a1"}
/data door : 500 {"code":"INTERNAL_ERROR",
"userMessage":"Check the record link…"}

Nothing invalid shipped — every body parsed as ApiErrorSchema — which is what
made the loss silent and one-directional: a console told by ADR-0112 to render
userMessage verbatim found nothing at these two exits and fell back to its
generic substitution, for the same throw the twin door rendered.

⭐ The prefix exit was an OPEN question, and it was measured rather than assumed

The two exits do not rest on the same evidence, and the card said so. The
fault terminal has a precedent — #9934 deliberately rides the mark onto fault
terminals, and /data does carry it there. The prefix arm had none, so the
question was: what does /data answer for that identical shape?

Measured: /data carries the mark on all five prefixes. The two doors
therefore disagree and the exit is in. Had /data omitted it there, the doors
would have agreed and there would have been nothing to repair — that is
what §3 of the new test asserts, so the answer stays checkable rather than
remembered.

The value source is measured, not borrowed

Neither exit holds a refusal.body, so the classified arm's one line is not
reusable at either — the shape is copied, the source is not. error-response.ts
now exports boundedDeclaredUserMessage: declaredUserMessage's presence
answer with #5423's bound applied, lifted out of the private
withDeclaredUserMessage wrapper so a caller with no body to merge into asks
the same rule instead of open-coding it beside it. The flat /data door is
unchanged and goes on reaching it through that wrapper.

⛔ The userMessage / declaredCode asymmetry is not restated here.
b853cf303 wrote it down once, beside the classified re-dress, and this change
cites it.

⛔ Only the mark is added

Measured across 32 route/door answers before and after the repair: 0
statuses moved, 0 existing keys moved or changed value, 24 gained
exactly one key (userMessage) — the 24 share-door rows; the 8 /data rows
already had it. That is Clause ②'s falsifiable criterion, tested rather than
asserted.

The three deliberate share-vs-/data differences visible in the same
measurement stay exactly as they are, and §4b pins all three as still present so
a later tidy-up reds here:

  1. the family still folds a declared 503 into its own 500 terminal;
  2. it still interpolates the caught message where /data withholds 5xx prose
    unconditionally (sendError 的显式状态直通覆盖 400–599,5xx 的原始驱动报错绕过全部泄漏启发式直达客户端(metadata-protocol 有活体产出方) #5437);
  3. /data is still not taught this service's local prefix idiom (ADR-0111).

All three are argued in respondSharingError's own #11683 docblock. Reporting a
deliberate difference as a defect costs as much as missing a real one.

⚠️ Severity qualifier — unreachable in-tree today

git grep -c userMessage -- packages/plugins/plugin-sharing is 0 (positive
control: throw hits 25 files), so no in-tree producer reaches these
paths today
. This wires a declared channel that was not connected at two
exits. ⛔ It is not a claim that users are losing sentences right now, and
severity is left unjudged.

Tests

packages/rest/src/rest-share-user-message-bypass-exits.test.ts — 21 tests, six
sections: the fault terminal (all three routes, both status spellings, an
unclassified Error, and a sandboxed body whose prose is withheld while the
mark travels); the prefix arm (all five prefixes, all three routes, plus the
startsWith-not-includes boundary); the door-to-door comparison that IS the
reproduction; the absences with a positive control inside each body; #5423's
bound and door-to-door equality of the bounded value; and the scope guards.

Ablation — deleting the repair's four call-site arguments: 16 of 21 red,
every failure naming the route, the body it got and the exact string expected.
Mutation confirmed on disk before any verdict was read (anchored count
4 → 0, blob 0112c6b7b079cac4), restored with git checkout HEAD -- at an
absolute path and proven by the blob hash returning to 0112c6b7 plus an empty
git diff HEAD.

⚠️ Predicted first, then measured, and the miss is recorded in the file rather
than quietly corrected: the prediction said 12 of 20. Every section's
direction was right — including §4b staying green and §4a reding through
its positive control rather than through its absences, which is #12669's
lesson — and the one wrong number was §2, predicted "2/2" by counting the
section's two it shapes instead of the six cases its table expands to.

Gates run locally, at 0a1a1e467 (the final commit)

pnpm --filter @objectstack/rest test152 files / 2502 tests passed;
pnpm --filter @objectstack/rest typecheck — clean, and
check:test-typecheck confirms the new test file compiles (verified in the tsc
program with --listFiles, 1 hit).
pnpm lint (repo-wide, eslint . --no-inline-config) — clean in 69s, so no
narrowing was needed.
The families node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
derived from the real change set — check:route-envelope,
check:dispatcher-error-vocabulary, check:published-files,
check:test-source-alias, check:type-source-resolution,
check:cross-package-test-inputs, check:engine-double-contract,
check:where-matcher, check:query-options-erasure,
check:type-check-coverage, check:type-check-debt (on a built closure),
check:nul-bytes, the changeset family and the docs-drift pair — all green.
scripts/pm/check-half-states.mjs exits 3 = NOT MEASURED (no GitHub
credential in this container, its own words), not a red.

Changeset: patch on @objectstack/rest — published source, wire output
changes, same call as #12510 and #12669 made for the same door.

Generated by Claude Code


Generated by Claude Code

…sage` on its two non-classified exits (#12693)
`respondSharingError` carries the producer's caller-facing sentence at its
classified re-dress since #12669 fork (a) / `b853cf303`. The family has two
OTHER exits that never reach that classification, and both still dropped it:
the 500 fault terminal (`SHARE_*_FAILED`) and the ADR-0111 message-prefix arm.
Measured on `15bf9e859` before the repair, one marked producer per exit driven
through the real routes on both doors:
throw { code: 'SHARE_STORE_DOWN', status: 503, userMessage: '…' }
share door : 500 SHARES_LIST_FAILED — no mark
/data door : 503 SERVICE_UNAVAILABLE — mark carried
throw Error('NOT_FOUND: no such record …') + userMessage
share door : 404 NOT_FOUND — no mark
/data door : 500 INTERNAL_ERROR — mark carried
⭐ The second row is the card's OPEN question, measured rather than assumed.
The fault terminal had a precedent — #9934 deliberately rides the mark onto
fault terminals — and the prefix exit had none. `/data` carries the mark for
that identical throw on all five prefixes, so the two doors disagree and the
exit is in; had `/data` omitted it there, the doors would have AGREED and there
would have been nothing to repair.
Neither exit holds a `refusal.body`, so the classified arm's expression is not
reusable at either. `boundedDeclaredUserMessage` is `declaredUserMessage`'s
presence answer with #5423's bound applied, lifted out of the private
`withDeclaredUserMessage` wrapper so a caller with no body to merge into asks
the same rule instead of open-coding it; the flat `/data` door is unchanged and
goes on reaching it through that wrapper.
⛔ Only the mark is added. Measured across 32 route/door answers before and
after: 0 statuses moved, 0 existing keys moved or changed value, 24 gaining
exactly `userMessage`. The three deliberate share-vs-`/data` differences in the
same measurement are pinned as STILL PRESENT rather than tidied up — the family
still folds a declared 503 into its own 500, still interpolates the caught
message where `/data` withholds 5xx prose (#5437), and `/data` is still not
taught this service's local prefix idiom (ADR-0111).
No in-tree producer sets `userMessage` at this seam today (`plugin-sharing` = 0
hits; positive control `throw ` = 25 files), so this wires a declared channel
rather than repairing a live loss. The tests therefore build their own
producers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
…ass exits (#12693)
Replaces the written-first prediction with what the run produced: 16 of 21 red,
not the predicted 12 of 20. Every section's DIRECTION was predicted correctly —
including §4b staying green, and §4a reding through its positive control rather
than through its absences — and the one wrong number is recorded rather than
quietly corrected: §2 was predicted "2/2" by counting the section's two `it`
shapes instead of the six cases its table expands to.
Mutation confirmed on disk before any verdict was read (anchored count 4 -> 0,
blob 0112c6b -> b079cac4) and restored by `git checkout HEAD -- <abs path>`,
proven by the blob hash returning to 0112c6b and an empty `git diff HEAD`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️1 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 15bf9e859e56862e6ebe7b5c42404de103362457packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4b06f80284da41468896e87ab66ea612a7a942fc — the merge of head 0a1a1e467e14c565c33f732295c05e52d7d1cee2 into base 15bf9e859e56862e6ebe7b5c42404de103362457, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4b06f80284da41468896e87ab66ea612a7a942fc && git checkout 4b06f80284da41468896e87ab66ea612a7a942fc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 15bf9e859e56862e6ebe7b5c42404de103362457 0a1a1e467e14c565c33f732295c05e52d7d1cee2 && git checkout -B drift-repro 15bf9e859e56862e6ebe7b5c42404de103362457 && git merge --no-ff 0a1a1e467e14c565c33f732295c05e52d7d1cee2
node scripts/docs-audit/affected-docs.mjs --json 15bf9e859e56862e6ebe7b5c42404de103362457

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@os-litant
os-litant marked this pull request as ready for review August 27, 2026 12:52
@os-litant
os-litant added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 4af6c44Aug 27, 2026
34 checks passed
@os-litant
os-litant deleted the claude/issue-12693-share-usermessage-bypass-exits branch August 27, 2026 13:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] share 家族在两个绕过分类 re-dress 的出口上仍丢弃 producer 的 userMessage —— 500 故障终端与 ADR-0111 前缀臂

2 participants

@os-litant@claude