Skip to content

fix(plugin-security): the packaged-permission-set lock refusal carries its guidance as userMessage - #21902

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21794-lock-refusal-user-message
Oct 5, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21794-lock-refusal-user-message

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21794
Clause-②: yes (widening)

The packaged-permission-set lock refusal now carries its guidance as userMessage. The console renders that field verbatim and keeps its generic "You don't have permission to save this record." only for refusals that carry none, so an admin who edits a packaged set in Setup is now told to clone it.

What changed

packages/plugins/plugin-security/src/packaged-permission-set-lock.ts only.

  • PackagedPermissionSetLockedError declares readonly userMessage: string, set per operation. An edit (update) is told to clone the set with the Clone action and edit the clone. A new set named like a packaged one (insert) is told to choose a different name, or clone.
  • PackagedPermissionSetProvenanceUnknownError, the fail-closed sibling in the same file, gets the same member: try again, or clone. It was measured to lose its guidance the same way (see Tests). The unreadable source's reason stays in message.
  • code (NOT_OVERRIDABLE), status (403) and message are byte-identical for both classes. Nothing the lock refuses or accepts moves. No other refusal class gains a userMessage, and no package entry gains an export: the three texts are module-private constants.
  • The texts carry no set name, package id, id or API path. message keeps that diagnostic for logs and developers. The texts are English, like every platform refusal: no producer in this repo localizes a thrown userMessage, and none is invented here.

A minor changeset for @objectstack/plugin-security declares the widened published type.

Measured on a booted showcase (scratch probe, not committed)

bootStack(showcase), admin token, the set showcase_contributor that com.example.showcase ships.

request before (base 607463d7) after (4bf10901)
PATCH /api/v1/data/sys_permission_set/:id {description} 403, body keys error, code, object, code NOT_OVERRIDABLE, no userMessage 403, same three keys byte-identical, plus userMessage (edit guidance)
POST /api/v1/data/sys_permission_set {name: showcase_contributor} 403, NOT_OVERRIDABLE, no userMessage 403, same body plus userMessage (insert guidance)
PUT /api/v1/meta/permission/showcase_contributor 403, keys error, code, the lock's own sentence, no userMessage 403, same body plus userMessage (edit guidance)
  • The data door's body is the REST /data flat dialect (object sibling), so mapDataError in the PATCH and POST handlers' catch is the mapping that serves it.
  • The metadata door on this kernel (no environment id) answers through the lock's registered authoring gate: the body is the lock's own sentence, the same class.

Tests

New describe block in packaged-permission-set-lock.test.ts, 5 cases. Each drives the real write door and maps the thrown error through the producer's own mapping: mapDataError (the REST /data door's call) and resolveThrownHttpError (the dispatcher's resolution). Every case asserts status 403, code NOT_OVERRIDABLE and the userMessage. The text is not pinned word for word. What is pinned is the guidance (clone; for insert, a different name; for the sibling, try again) and the absence of the set name, package id, object name, API path and the sibling's diagnostic reason.

  • Data door, update and insert; each operation's text differs.
  • Dispatcher door, the same throw: same status, code and userMessage.
  • Metadata door's registered lock gate: same class, same userMessage as the data door's update.
  • Fail-closed sibling at the data door.

Runs, all through scripts/pm/os-verify-lock.sh:

  • Red before the fix (base plus the new block): 5 failed | 22 skipped, each userMessage must be present on the wire: expected 'undefined' to be 'string'. A first draft passed two cases vacuously (undefined equal to undefined); they now assert presence first.
  • Green at 4bf10901af: the four lock suites (packaged-permission-set-lock, -lock-gate, -restore-leg, permission-set-duplicate-name-refusal) 45 passed. Whole package: 167 passed (167) files, 3620 passed | 45 skipped. pnpm --filter @objectstack/plugin-security typecheck exit 0, including check:test-typecheck (test layer compiles, 0 errors).
  • Ablations at 4bf10901af, each through scripts/ablation-replace.mjs (anchor hit once, blob changed on disk, restore proven blob == HEAD 483e5e60 and git diff HEAD empty). The suite imports the module from source, so no dist is involved.
    • A1, delete the locked error's userMessage assignment: 4 failed | 23 passed. The four locked-class cases go red on presence; the sibling stays green.
    • A2, delete the sibling's assignment: 1 failed | 26 passed, only the sibling case.
    • A3, set the locked error's userMessage to its message: 4 failed | 23 passed, userMessage must not name 'ehr_quality_inspector'.
    • A4, the same on the sibling: 1 failed | 26 passed, userMessage must not name 'unknown_provenance'.

Gates, at 4bf10901af

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 66 commands from the 3 changed paths. All 66 ran, plus the dispatch list's 4 packages/spec audits that fall outside this derivation: 70 of 70 exit 0. check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET: 8 packages had no dist/). After those 8 were built (41 of 41 turbo tasks, all cache hits) it exited 0, and that is the recorded code.
  • --ran: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN.
  • The derivation warns that the tree is 3 commits behind origin/main (1e18a0735c) and that one of its inputs changed there: scripts/engine-double-contract.pinned.json gained a pin for a metadata-protocol test file. None of those 3 commits touches a file this diff touches, and this diff adds no engine double.
  • Lint, narrowed: eslint --no-inline-config --format json over the 2 changed source files reports 2 files, 0 errors, 0 warnings. Population: eslint.config.mjs matches packages/**/*.{ts,tsx,mts,cts}. Invariance: the config never enables type-aware linting (no parserOptions.project), so this diff cannot move the verdict on a file it does not touch. The full pnpm lint is CI's.

Acceptance notes

  • Sentences this makes stale, outside this PR's surface. Six places state that platform code never sets userMessage: packages/spec/src/api/contract.zod.ts (the ApiErrorSchema.userMessage TSDoc), packages/types/src/data-error-classification.ts (withDeclaredUserMessage's note), packages/rest/src/rest-server.ts (the share door's note), packages/runtime/src/http-dispatcher.ts (the PERMISSION_DENIED arm), packages/runtime/src/sandbox/quickjs-runner.ts (SANDBOX_ERROR_PASSTHROUGH's rationale) and the header of packages/runtime/src/http-dispatcher.permission-denied-user-message.test.ts. Platform code now sets it. The property those sentences protect still holds: the marked text is authored for the end user and carries no host state, so a sandboxed body that catches this refusal receives static guidance. The new pin holds that property for these texts. This PR's dispatch rules out spec, types and rest edits, so the six sentences are left for the owning seat; content/docs has no sentence this makes false (searched for userMessage, NOT_OVERRIDABLE, the clone path and the console's generic sentence).
  • The metadata door on an environment kernel is a different producer. With an environment id, saveMetaItem runs the metadata protocol's package door (refusePackagedBaseOverride) before the authoring gate, and that refusal (NOT_OVERRIDABLE, no userMessage) answers a PUT on a code-shipped permission set first. It is unchanged here, and it is a reading of the source, not a measurement at a booted environment kernel. The data door throws this lock's class on every kernel.
  • Localization. The texts are English. The console renders userMessage verbatim, so a non-English admin sees English guidance where they saw a localized generic sentence before. No localization path exists for a thrown userMessage.

Seat's append: patch round 1 (written by domain:services seat 1 from the dev's report 6000532525; the dev does not edit this body)

  • Sentences this made stale, amended in this PR (patch round 1, 7c2b636d8b, comment-only). Six comments said platform code never sets userMessage: the ApiErrorSchema.userMessage TSDoc (packages/spec/src/api/contract.zod.ts), the withDeclaredUserMessage note (packages/types/src/data-error-classification.ts), the share-door note (packages/rest/src/rest-server.ts), the PERMISSION_DENIED arm (packages/runtime/src/http-dispatcher.ts), the SANDBOX_ERROR_PASSTHROUGH rationale (packages/runtime/src/sandbox/quickjs-runner.ts) and the header of packages/runtime/src/http-dispatcher.permission-denied-user-message.test.ts. Each now states the invariant that holds: only a producer that authors end-user text with no host state sets it (an application hook, or a platform refusal carrying static guidance such as the packaged-permission-set lock's); platform and driver diagnostics never do. For each file the comment-free TypeScript print is byte-identical before and after, the .describe() text is unchanged, and pnpm --filter @objectstack/spec run check:generated reports all 15 generated artifacts up to date. The seat decided this in its verdict 5999760387; the cross-lane declarations are [PM seat] domain:spec — 🟢 os-project-manager · session_01T9u38rswFp5Rw8DswRUReJ #6017 5999766898 and [PM seat] domain:cli — 🟢 os-warren · session_01RWZbGvPFcRKvUqASZtunCU #6024 5999776957.
  • origin/main merged at 1e18a0735c (merge commit b8da8b3e1e, no conflicts) before the amendment.
  • English-only guidance is accepted as built (the seat's verdict): no producer in the repo localizes a thrown userMessage, and none was invented.

Generated by Claude Code

…s its guidance as userMessage

PackagedPermissionSetLockedError (insert and update) and the fail-closed
PackagedPermissionSetProvenanceUnknownError now declare a readonly
userMessage: the guidance addressed to the end user, read at every HTTP
door through declaredUserMessage and rendered verbatim by the console in
place of its generic 403 sentence. code, status and message are unchanged;
the texts carry no set name, package id or API path.

The wire-envelope pin drives the real data-door write-through and maps the
thrown error through mapDataError (the REST /data door's own call) and
resolveThrownHttpError (the dispatcher's), plus the metadata door's
registered lock gate.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN
@github-actions github-actions Bot added the size/m label Oct 5, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 5, 2026
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/plugin-security, @objectstack/rest, @objectstack/runtime, @objectstack/spec, @objectstack/types, touching 7 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts, packages/runtime/src/sandbox/quickjs-runner.ts, packages/types/src/data-error-classification.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via ApiErrorSchema (symbol, a top-level const))
  • content/docs/api/index.mdx (via ApiErrorSchema (symbol, a top-level const))
  • content/docs/protocol/kernel/error-handling.mdx (via userMessage (symbol, a field of class PackagedPermissionSetLockedError; a field of class PackagedPermissionSetProvenanceUnknownError))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via ApiErrorSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-1.mdx (via ApiErrorSchema (symbol, a top-level const), userMessage (symbol, a field of class PackagedPermissionSetLockedError; a field of class PackagedPermissionSetProvenanceUnknownError))
  • content/docs/releases/v17/17-4.mdx (via userMessage (symbol, a field of class PackagedPermissionSetLockedError; a field of class PackagedPermissionSetProvenanceUnknownError))
  • content/docs/releases/v17/index.mdx (via userMessage (symbol, a field of class PackagedPermissionSetLockedError; a field of class PackagedPermissionSetProvenanceUnknownError))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts, packages/runtime/src/sandbox/quickjs-runner.ts, packages/types/src/data-error-classification.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 148 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 e6dc7a240617eaeef9a64e788bf6e5561c107f1b → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3bc96f852a74cf11f67b1914576a76c9529c51ac — the merge of head 85952735ab45baa14540650a7ee43a776e1d3336 into base e6dc7a240617eaeef9a64e788bf6e5561c107f1b, 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 3bc96f852a74cf11f67b1914576a76c9529c51ac && git checkout 3bc96f852a74cf11f67b1914576a76c9529c51ac
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e6dc7a240617eaeef9a64e788bf6e5561c107f1b 85952735ab45baa14540650a7ee43a776e1d3336 && git checkout -B drift-repro e6dc7a240617eaeef9a64e788bf6e5561c107f1b && git merge --no-ff 85952735ab45baa14540650a7ee43a776e1d3336

node scripts/docs-audit/affected-docs.mjs --json e6dc7a240617eaeef9a64e788bf6e5561c107f1b

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs e6dc7a240617eaeef9a64e788bf6e5561c107f1b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

claude added 2 commits October 5, 2026 17:38
…platform refusal does

Six comments said platform and driver code never set a thrown
userMessage. The packaged-permission-set lock's refusals now do, with
static guidance and no host state. Each comment now states the invariant
that holds: only a producer authoring end-user text with no host state
sets it (an application hook, or a platform refusal carrying static
guidance); platform and driver diagnostics never do.

Comment-only: each file's comment-free TypeScript print is byte-identical
before and after, and the ApiErrorSchema.userMessage describe() text is
unchanged.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K3zqE8Pv1Evw5hc8tZCnN
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI note from domain:services seat 1: the red TypeScript Type Check at 7c2b636d8b is not this PR's

  • Failing check. TypeScript Type Check (job 111906046465) is red only because its Type Check · source gates lane (job 111899125089, run 37350207059) was cancelled at the lane's 10-minute limit. Its checkout step alone took 7m16s, and every step that ran succeeded. The same lane completes in 2 to 5 minutes on main at 866683f96f and on this lane's other open heads. So the runner was slow before any gate body ran, and the diff did not cause it.
  • Fix. None exists in this repo for a slow runner checkout. The seat has no re-run channel. Instead, origin/main is 3 commits ahead of this branch, including feat(spec)!: the build doors judge an approval node config against its declared contract, whole — an undeclared key or a refused value is refused with a location #21893, which edits packages/spec (a package this PR also edits). The PR's dev merges origin/main (a merge commit, no rebase) and pushes, so the next CI run judges the real combined head.
  • If the lane fails again on the new head, that failure is treated as real.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI note from domain:services seat 1, follow-up to 6000655116: at f6324deb53, every check passed: all four type-check lanes, all six Test Core shards and all three dogfood shards. The exception is the required Test Core aggregator. It sat queued from 19:45Z and was cancelled at 20:00Z without a runner, so no test ran in it. This is not the lane failure the earlier note said would count as real.

  • The repo's hosted runners have been starved since about 19:20Z: 55 to 80 runs queued, with 1 to 3 in progress. Jobs that wait 15 minutes without a runner end cancelled with "The job was not acquired by Runner of type hosted even after multiple attempts". The seat has no re-run channel. This PR gets a fresh run with its next push (a merge of origin/main once main moves) or with a re-run by a maintainer. A failure on that fresh run is treated as real.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f6324deb5371c367bf4b8d10a6db1c98a3af1762
Local-runs: none

① Derived judgments

Inputs read: card #21794 (body and all eight comments: triage 5987354089, deferral 5987768295, claim 5994672808 and its rollback 5994807743, claim 5998800282, dev reports 5999709704 and 6000532525, seat verdict 5999760387); PR #21902 (body, the nine-file list, and the net three-dot diff against the merge base 866683f96f, which is the head's second parent — the two-dot and three-dot file lists are identical, and the one main commit since the merge base touches none of the nine files); the 35 check-runs on the head, read latest-run-per-name.

  1. PackagedPermissionSetLockedError.userMessage: string — a new readonly member, assigned in the constructor per operation (insert and update each get their own text). The class is re-exported from packages/plugins/plugin-security/src/index.ts, the package's one exports entry, so this WIDENS the published surface of @objectstack/plugin-security. RIGHT: it is exactly the producer-side mark declaredUserMessage (packages/types/src/thrown-http-error.ts) reads, ThrownHttpError.userMessage?: string admits it, and the card's "Done when" names this member.
  2. PackagedPermissionSetProvenanceUnknownError.userMessage: string — the same member on the fail-closed sibling, also re-exported. RIGHT: the claim put the sibling in scope only if it was measured to lose its guidance the same way; the dev measured it (red pin, same envelope, same door) and the fifth test case pins it.
  3. Accept set unchanged — RIGHT. At the head both class bodies differ from the merge base only by the userMessage declaration, its one-line docblock and the constructor assignment; code (NOT_OVERRIDABLE), status/statusCode (403) and every message template are byte-identical; classifyPackagedPermissionSet, the write-through and the authoring gate are untouched. Nothing accepted is refused now and nothing refused is accepted.
  4. Wire envelope — RIGHT. The REST /data door's mapDataError adds the mark through withDeclaredUserMessage and boundedDeclaredUserMessage, bounded by truncateClientMessage (CLIENT_MESSAGE_MAX = 500); the three texts are 244, 188 and 252 characters, so they ride whole. resolveThrownHttpError carries the same value at the dispatcher door, and the metadata door's registered gate throws the same class. The field lands as the optional ApiErrorSchema.userMessage that already exists: no schema, no JSON Schema and no .describe() text moves (the describe(...) call in contract.zod.ts is byte-identical between merge base and head).
  5. No new export and no host state — RIGHT. The three text constants are module-private (no reference outside packaged-permission-set-lock.ts anywhere at the head). The texts carry no set name, package id, object name, record id, /api/ path or tracker number, so the "static end-user text with no host state" property the amended comments now state holds for them. The new describe block (5 cases) pins presence first and then the negative-name assertions, computing the envelope through the producers' own mappings rather than restating a body; the message channel is pinned to still carry the diagnostic.
  6. The six comment-only amendments (contract.zod.ts TSDoc, data-error-classification.ts, rest-server.ts, http-dispatcher.ts, quickjs-runner.ts, the header of http-dispatcher.permission-denied-user-message.test.ts) — RIGHT and correctly scoped. Mechanically, every +/- line in those six files is a comment line (0 non-comment changed lines in each), so no behaviour, export, pin or generated artifact moves; the replacement invariant ("set only by a producer that authors end-user text with no host state: an application hook, or a platform refusal carrying static guidance; platform and driver diagnostics never set it") is the invariant the diff actually establishes, and the old "never" would have invited a reader to strip this mark as a leak.
  7. Governed surfaces: none of the nine paths is one (Governed Surface Queue Guard success). Head repo is the base repo, not a fork. Size 251/20 lines, far under the 5,000 ceiling.

Check-runs on the head, latest run per check name, as read at 2026-10-05T19:56Z:

  • Required set: Lint & Repo Gates success · TypeScript Type Check success (all four lanes success: source gates, consumer gates, debt ledger, workspace — the lane timeout seen at 7c2b636d8b did not recur) · Build Core success · Temporal Conformance (live PG + MySQL) success · Dogfood Regression Gate success (shards 1/3, 2/3, 3/3 success) · Governed Surface Queue Guard success · Test Core: the six shards (1/6) to (6/6) all success; the Test Core aggregator (check-run 111949322575) was still queued with no conclusion at 19:56Z, 11 minutes after its last shard completed. That aggregator is NOT a verdict yet; the six shard conclusions are the test family's measured result, and the aggregator's green is owed before Tier S landing.
  • Other gates: Check Changeset success · Spec property liveness success · Dogfood Verify CLI success · Flag docs affected by code changes success · Check Documentation Links success · Check PR Size success · Auto Label success · The card this PR closes must claim this branch success · No other open PR may claim the same issue success · No other open PR may claim the same single-writer path success · Part-of PR must not also close its card success · filter success.
  • Skipped by design: Console Pin Gate, Packed-tarball smoke (opt-in) (not opted in), Build Docs (path-filtered).

② Semver level

.changeset/21794-lock-refusal-user-message.md declares '@objectstack/plugin-security': minor. RIGHT. Two exported classes gain a public member, a widening of the published entry, and Post-Task Checklist 3 makes Clause-②: yes take at least minor; minor is both the floor and the correct level, since nothing is removed, renamed or narrowed (so no ADR-0087 disposition marker is owed and check-adr-0087-registration / check-changeset-no-major have nothing to read). No other package owes a changeset: spec, types, rest and runtime change comment lines only, which publishes nothing from any released package; skip-changeset is correctly absent. The changeset body states the behaviour per door, the environment-kernel carve-out, the "no set, package, id or API path" property and the unchanged code/status/message, which is what a CHANGELOG.md reader needs.

Clause-②: line: yes (widening) in the governing claim 5998800282, on line 2 of the PR body (the line Check Changeset reads), and in the changeset body. The three agree, the arm is one of the closed pair, and the stated reason (the published type widens by a member; the envelope gains a value ApiErrorSchema already declares optional; nothing accepted or refused moves) is true of the diff.

③ Boundary flags

  • open_questions[0] — six sentences stating platform code never sets userMessage made false, against the dispatch's stop list on spec/types/rest. ANSWERED by the seat's verdict 5999760387: amend in this PR, comment-only. Done at 7c2b636d8b; verified above as comment-only in all six files, .describe() unchanged, generated artifacts unaffected (TypeScript Type Check success on the head).
  • open_questions[1] — English-only guidance where a non-English admin saw a localised generic sentence. ANSWERED by the same verdict: option A, accept as built; no producer in the repo localises a thrown userMessage and none is invented. Consistent with the diff (literal texts, no key channel).
  • Round-0 deviations: (a) model-free commit trailers per AGENTS.md — right, the repo rule outranks the harness reminder; (b) scratch dogfood probe deleted — confirmed, no packages/qa path in the file list; (c) 70-command local battery — superseded by the head's check-runs; (d) PATCH served by the REST /data door, pins mapped through mapDataError and resolveThrownHttpError — matches the door code at the head; (e) the stop-list conflict — resolved through open_questions[0].
  • Round-1 deviations: merge of origin/main 1e18a0735c (no rebase, no force), full build before gates, parser-based comment-only proof, the extra runtime test run, and the red TypeScript Type Check at 7c2b636d8b (source-gates lane cancelled at its 10-minute limit) — the last is superseded by the head, where that check and all four lanes are success; the rest are procedural and raise no contract question.
  • out_of_scope_findings[0] — on an environment kernel the metadata protocol's refusePackagedBaseOverride answers the PUT first, unmarked (same shape on ITEM_LOCKED and the flow/action packaged-base refusals). Carried, not filed, by the seat: source inference, not measured at a booted environment kernel, and triage bounded this card to this one error with a third occurrence routed to its own closing card with an enumeration pin. Named in the PR body and the changeset. Not a defect of this PR; stands as escalated to the seat.
  • Reviewer's own observation, release-owned: content/docs/releases/v17/17-1.mdx:153 still reads "platform/driver code never sets it". That page is RELEASE-OWNED (read-only in a code PR per Documentation Guardrails) and records the 17.1 state; the hand-written pages the drift bot named (protocol/kernel/error-handling.mdx, api/error-catalog.mdx, api/index.mdx) say only that userMessage is text a producer marked and is rendered verbatim, which stays true. Nothing is owed in this PR; the seat may route the release-note sentence to a docs-only PR or an issue if it reads it as a factual error rather than history.
  • Landing mechanics (PR is draft, mergeable_state: blocked, needs:contract-review on the PR, the Test Core aggregator still queued) are the owning seat's; this record judges the contract and reports the gate readings above.

Implemented-by: claude/issue-21794-lock-refusal-user-message
Reviewed-by: session_011K3zqE8Pv1Evw5hc8tZCnN

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21794-lock-refusal-user-message branch October 5, 2026 23:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants