Skip to content

plugin-security: the packaged-permission-set lock refusal carries no userMessage, so the console replaces it with "You don't have permission to save this record" and the "Clone it instead" guidance never reaches the admin #21794

Description

@objectstack-fleet

QA-source: #21784 · access-security.packaged-permission-set-lifecycle · acceptance[5]

Clause A6 of access-security.packaged-permission-set-lifecycle (rev 1) fails in the 17.7 pre-release run #21784 (subject 316be321e, console pin 2e818d0b51ec). An independent verifier (RUNNER rule 7) reproduced it twice (API and UI): CONFIRMED, P3. Predates 17.6.0. Same class as the closed #19397.

Reproduction

  1. Boot the showcase. Admin PATCH /api/v1/data/sys_permission_set/{id of showcase_contributor} {"description":"edit"} → 403 NOT_OVERRIDABLE, message "… Clone it instead (the "Clone" action …)", no userMessage.
  2. In the console, open the same record in Setup → Edit → change Description → Save.
  • Expected: the form shows the refusal's guidance (clone the set).
  • Actual: the same 403 on the wire, and the form shows the generic "You don't have permission to save this record."; "Clone it instead" is absent from the page.

Mechanism

Done when

The lock error carries a userMessage with the clone guidance and a test pins it on the wire envelope.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: permissions that actually hold | access-security.packaged-permission-set-lifecycle | P2

    Triage: first grade — bug · priority:p3 · domain:services · area:access · pm:queue. The lock error carries a userMessage with the clone guidance

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-05T03:01Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in packages/plugins/plugin-security/src/packaged-permission-set-lock.ts (PackagedPermissionSetLockedError, about :269–:287) ⇒ domain:services; rationale: the console shows only marked permission errors by design, and marking is the producer's job.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Deferred, serial · seat domain:services#1 (#6021) · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-05T03:49Z. ⛔ Not a claim.

    This card stays in pm:queue, serial behind #21789. Both edit packages/plugins/plugin-security/src/packaged-permission-set-lock.ts, but they are different defects with different fixes, so fold gate ① fails. #21789 also waits behind the seat's in-flight #21771 (area:access).

    Known pitfall: the refusal's user message must come from the shared user-facing message catalog, the one the plugin-security refusals already use, not from a hand-written string, so the console shows it instead of its generic fallback. Merge main after #21789 lands.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-05T12:42Z
    Session: session_011K3zqE8Pv1Evw5hc8tZCnN
    Account: os-steve (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21794-lock-refusal-user-message
    Worktree: objectstack-issue-21794
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (at origin/main 88a39c09, which carries #21789's PR #21857), per triage's direction 5987354089:

    • packages/plugins/plugin-security/src/packaged-permission-set-lock.ts: PackagedPermissionSetLockedError (about :291) declares a userMessage with the clone guidance. That is the field declaredUserMessage (packages/types/src/thrown-http-error.ts:271) reads at every door. Its sibling PackagedPermissionSetProvenanceUnknownError is in scope only if it is measured to lose its guidance the same way (same defect, same fix, same file).
    • plugin-security tests pinning the userMessage on the wire envelope, content/docs/** sentences this makes false, and a minor changeset.

    ⛔ The refusal's code, status and message stay as they are. ⛔ No other refusal class moves: triage routes a third occurrence of this class to its own closing card. ⛔ The console's generic fallback for unmarked errors stays.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier). The at-tier contract review is owed and runs before landing.
    Clause-②: yes (widening)


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim rolled back · seat domain:services#1 (#6021) · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-05T12:50Z. The claim 5994672808 is withdrawn.

    Why: a selection error by this seat. At this card's pick (2026-10-05T12:41Z), #21829 (p2, security) had been back in pm:queue since its ruling 5994120632 (2026-10-05T12:07Z). The seat picked from an older lane reading instead of reading the lane fresh. A higher card in the queue outranks a lower claim, so this claim is rolled back and #21829 takes the slot.

    State: the dev was stopped about six minutes after dispatch, before any commit. Its branch claude/issue-21794-lock-refusal-user-message holds only the base (88a39c09), no PR was opened, and the worktree is removed. The card returns to pm:queue with no assignee.

    Carried to the next claim, unchanged: the readings in 5994672808 (the deferral's pitfall is withdrawn; #19397's reopen condition is met by the 17.7 QA run), Clause-②: yes (widening), and the #6024 declaration 5994685013.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-05T16:36Z
    Session: session_011K3zqE8Pv1Evw5hc8tZCnN
    Account: os-steve (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21794-lock-refusal-user-message
    Worktree: objectstack-issue-21794
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    File surface (at origin/main 607463d7, which carries #21789's PR #21857, #21860's PR #21873 and #21861's PR #21881), per triage's direction 5987354089:

    • packages/plugins/plugin-security/src/packaged-permission-set-lock.ts: PackagedPermissionSetLockedError (about :291) declares a userMessage with the clone guidance. That is the field declaredUserMessage (packages/types/src/thrown-http-error.ts:271) reads at every door. Its sibling PackagedPermissionSetProvenanceUnknownError is in scope only if it is measured to lose its guidance the same way (same defect, same fix, same file).
    • plugin-security tests pinning the userMessage on the wire envelope, content/docs/** sentences this makes false, and a minor changeset.

    ⛔ The refusal's code, status and message stay as they are. ⛔ No other refusal class moves: triage routes a third occurrence of this class to its own closing card. ⛔ The console's generic fallback for unmarked errors stays.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier). The at-tier contract review is owed and runs before landing.
    Clause-②: yes (widening)


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 21794,
    "status": "done",
    "branch": "claude/issue-21794-lock-refusal-user-message",
    "pr": "#21902",
    "session": "session_011K3zqE8Pv1Evw5hc8tZCnN (subagent; the parent PM session's id, also the container's CLAUDE_CODE_REMOTE_SESSION_ID)",
    "premise_still_valid": true,
    "summary": "PackagedPermissionSetLockedError (insert and update, each its own text) and the fail-closed PackagedPermissionSetProvenanceUnknownError now declare a readonly userMessage set in the constructor; code NOT_OVERRIDABLE, status 403 and message are byte-identical and nothing the lock accepts or refuses moved. The texts are module-private constants carrying the guidance only (clone it; insert: choose another name or clone; sibling: try again or clone) with no set name, package id, object name or API path, in English (no producer in the repo localizes a thrown userMessage; none invented). H1 reproduced at the wire on a booted showcase at base 607463d (PATCH and POST /data/sys_permission_set 403 flat {error,code,object} without userMessage; PUT /meta/permission/showcase_contributor 403 {error,code} with the lock's own sentence, i.e. the same class through the host-config authoring gate = H5 measured); after the fix all three carry userMessage and their bodies are otherwise byte-identical. H2 confirmed: no plugin-security source set userMessage and no catalog exists. H4: the sibling was measured to lose its guidance the same way (red pin, same envelope) so it is in scope. Not done by design: no other refusal class, no spec/types/rest/metadata-protocol edit, no content/docs edit (zero sentences made false).",
    "tests": "HEAD 4bf1090. Pin: new describe block in packages/plugins/plugin-security/src/packaged-permission-set-lock.test.ts (5 cases) drives the real data-door write-through, the metadata door's registered lock gate and the fail-closed path, and maps each thrown error through mapDataError (the REST /data door's own catch call) and resolveThrownHttpError (dispatcher); every case asserts status 403 + code NOT_OVERRIDABLE + userMessage (guidance present; set name, package id, object name, /api/ path and the sibling's reason absent). RED before the fix: '5 failed | 22 skipped', each 'userMessage must be present on the wire: expected undefined to be string' (a first draft passed 2 cases vacuously, undefined===undefined; fixed before commit by asserting presence first). GREEN: 4 lock suites '45 passed'; whole package 'pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2' -> 'Test Files 167 passed (167) / Tests 3620 passed | 45 skipped', exit 0; 'pnpm --filter @objectstack/plugin-security typecheck' exit 0 incl. check:test-typecheck 'test layer compiles ... 0 error(s)'. Ablations via scripts/ablation-replace.mjs under the lock (anchor hit x1 -> x0, blob changed on disk, restore proven blob == HEAD 483e5e60 and git diff HEAD empty, git status clean); subject imported from source (relative import, @objectstack/types aliased to src), so no dist leg: A1 delete locked userMessage assignment -> '4 failed | 23 passed' (locked-class cases red on presence, sibling green); A2 delete sibling assignment -> '1 failed | 26 passed'; A3 locked userMessage = this.message -> '4 failed | 23 passed' (must not name ehr_quality_inspector); A4 same on sibling -> '1 failed | 26 passed' (must not name unknown_provenance). End to end (scratch dogfood probe on bootStack(showcase), never committed, deleted): before = no userMessage on PATCH/POST/PUT; after (plugin-security rebuilt at 4bf1090) = userMessage on all three, rest of each body identical. Lint narrowed: eslint --no-inline-config --format json over the 2 changed .ts files -> 2 files, 0 errors, 0 warnings; population from eslint.config.mjs 'packages/**/.{ts,tsx,mts,cts}'; invariance: config never enables type-aware linting (no parserOptions.project) so untouched files cannot move; full pnpm lint left to CI. Integration/dogfood tiers declared to CI.",
    "mcp_calls": "0",
    "api_writes": "3 — all through the fleet relay as objectstack-fleet[bot] (each one POST /repos/objectstack-ai/objectstack/dispatches executing one action): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (#21902, run 37348770035, body read back identical 8223 bytes); (2) label-write assign os-steve -> POST /repos//issues/21902/assignees (run 37348887857, output says 'via the relay run'; zero labels written); (3) os-dev-report comment -> POST /repos//issues/21794/comments via post-stamped. git push x2 (base probe, commit 4bf1090), not REST.",
    "open_questions": [
    {
    "question": "Six sentences state that platform code never sets userMessage, and this change makes them literally false: packages/spec/src/api/contract.zod.ts (ApiErrorSchema.userMessage TSDoc, ships in spec d.ts), packages/types/src/data-error-classification.ts (withDeclaredUserMessage note), packages/rest/src/rest-server.ts (share door note), packages/runtime/src/http-dispatcher.ts (PERMISSION_DENIED arm), packages/runtime/src/sandbox/quickjs-runner.ts (SANDBOX_ERROR_PASSTHROUGH rationale), header of packages/runtime/src/http-dispatcher.permission-denied-user-message.test.ts. os-dev.md rule 3 says sentences a change makes false are owed; this dispatch puts spec/types/rest edits on its stop list. I followed the dispatch and did not edit them (my reading: rationale comments, not a published defect — the .describe() text that reaches JSON Schema stays true). Who carries the amendment?",
    "options": [
    "A: a small follow-up card for the services seat amending the six sentences to the invariant that actually holds ('set only by a producer that authors end-user text: application hooks, and platform refusals carrying static guidance with no host state')",
    "B: leave them; the property they protect still holds and is pinned for these texts"
    ],
    "recommendation": "A. Business need: the sentences are the stated reason the sandbox passes userMessage into the VM and why the REST/dispatcher doors may forward it across fault terminals; a reader auditing that reasoning against the tree now finds a false premise. Long-term: the true invariant (end-user-authored, no host state) is what a future platform producer must meet, so it should be the written one. AI-error axis: a stale 'never' invites an agent to treat any platform-set userMessage as a leak and strip it, or to assume no platform text ever needs the no-host-state discipline. Startup axis: comment-only, no new surface, no gate."
    },
    {
    "question": "Localization trade-off: the console renders userMessage verbatim, so a non-English admin who previously saw the localized generic form.noPermissionToSave now sees English guidance. No i18n path exists for a thrown userMessage and none was invented (H3). Accept as is?",
    "options": [
    "A: accept; English guidance beats a localized but wrong 'no permission' sentence",
    "B: add a message-key channel for platform-authored userMessage (a spec/contract change, new capability)"
    ],
    "recommendation": "A. Business need: the reported pull (QA run #21784) is about the missing guidance, not language; no non-English admin report is named. Long-term and AI-error: a key channel is a new contract surface with two spellings of user text; startup axis says no unpulled capability. Revisit only on a named non-English admin report."
    }
    ],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted in PR Acceptance notes, not filed · On an environment kernel (environmentId set) saveMetaItem runs metadata-protocol's package door refusePackagedBaseOverride before the authoring gate, so PUT /api/v1/meta/permission/:name on a code-shipped set is answered by metadata-protocol's own NOT_OVERRIDABLE (packagedBaseRegimeSentence) with no userMessage — the console shows the generic sentence there. Read-only inference from packages/metadata-protocol/src/protocol.ts (saveMetaItem, refusePackagedBaseOverride), NOT MEASURED at a booted environment kernel, so no reach: and not filed. The same unmarked shape covers the ADR-0126 flow/action packaged-base refusals and ITEM_LOCKED; it is the natural population for triage's third-occurrence closing card with an enumeration pin over refusal errors. Dedupe words: packaged base refusal userMessage environment kernel · refusePackagedBaseOverride NOT_OVERRIDABLE console generic · ITEM_LOCKED userMessage · refusal errors without userMessage enumeration",
    "carrier: 承接者:无 · noted in PR Acceptance notes, not filed · the six 'platform and driver code never sets userMessage' sentences listed in open_questions[0] (stale after this change; no behaviour, no consumer parses them). Dedupe words: platform never sets userMessage · declaredUserMessage rationale stale · SANDBOX_ERROR_PASSTHROUGH userMessage host state"
    ],
    "gates": {
    "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 4bf1090: 66 commands from 3 changed paths; also ran the dispatch list's 4 packages/spec audits that this derivation does not name (70 total). Ran unlocked per os-dev (check:
    family), sequentially, after the locked test run finished.",
    "results": [
    "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
    "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
    "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
    "node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
    "node scripts/check-ci-filter-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-corpus.mjs :: exit 0",
    "node scripts/check-dts-emitted.mjs --self-test :: exit 0",
    "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
    "node scripts/check-empty-changeset.mjs --self-test :: exit 0",
    "node scripts/check-issue-citations.mjs :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
    "node scripts/check-registry-log-declared.mjs :: exit 0",
    "node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
    "node scripts/check-system-context-census.mjs :: exit 0",
    "node scripts/check-system-context-census.mjs --self-test :: exit 0",
    "node scripts/check-tenant-audit-census.mjs :: exit 0",
    "node scripts/check-tenant-audit-census.mjs --self-test :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
    "node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
    "node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
    "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
    "node scripts/release-pending-publish.mjs --self-test :: exit 0",
    "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
    "pnpm check:changeset-gate-self-tests :: exit 0",
    "pnpm check:cross-package-test-inputs :: exit 0",
    "pnpm check:dispatcher-error-vocabulary :: exit 0",
    "pnpm check:doc-authoring :: exit 0",
    "pnpm check:driver-memory-census :: exit 0",
    "pnpm check:dts-closure :: exit 0",
    "pnpm check:dual-build-cjs-loads :: exit 0",
    "pnpm check:engine-double-contract :: exit 0",
    "pnpm check:gitlink-declared :: exit 0",
    "pnpm check:i18n :: exit 0",
    "pnpm check:i18n-stale-fill :: exit 0",
    "pnpm check:issue-citations :: exit 0",
    "pnpm check:lean-entry-closure :: exit 0",
    "pnpm check:logger-receiver-detach :: exit 0",
    "pnpm check:nul-bytes :: exit 0",
    "pnpm check:objectql-double-limit :: exit 0",
    "pnpm check:objectui-changeset :: exit 0",
    "pnpm check:org-identifier :: exit 0",
    "pnpm check:page-declaration-shape :: exit 0",
    "pnpm check:pm-changeset-deadline-census :: exit 0",
    "pnpm check:published-files :: exit 0",
    "pnpm check:query-options-erasure :: exit 0",
    "pnpm check:refd-timer-probe :: exit 0",
    "pnpm check:slot-lookup :: exit 0",
    "pnpm check:sourcemap-no-sources-content :: exit 0",
    "pnpm check:test-source-alias :: exit 0",
    "pnpm check:tier-file-adoption :: exit 0",
    "pnpm check:type-check-coverage :: exit 0",
    "pnpm check:type-check-debt :: exit 0",
    "pnpm check:watch-hint-literal :: exit 0",
    "pnpm check:where-matcher :: exit 0",
    "pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
    "pnpm --filter @objectstack/spec run check:liveness :: exit 0",
    "pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
    "pnpm --filter @objectstack/spec run check:variant-docs :: exit 0"
    ],
    "notes": "check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET: no dist for studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis, service-knowledge); built those under the lock (turbo 41/41 tasks, 41 cached) and reran: exit 0, recorded as the single code. The --ran derivation warns the tree is 3 commits behind origin/main 1e18a07 with one input changed (scripts/engine-double-contract.pinned.json gained a pin for packages/metadata-protocol/src/protocol.search-skip-unreadable.test.ts); none of the 3 commits touches this diff's files and this diff adds no engine double, so no merge of main was made.",
    "ran_verdict": "Run reconciliation — 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN. ✓ dispatch-gates --ran: 66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED (a DERIVED zero — all 66 recorded an exit code and none of them is 3).",
    "ci": "PR #21902 head 4bf1090, one read at report time: 14 check runs completed (0 failed), 17 in_progress. Not awaited (CI convergence is the PM's)."
    },
    "line_budget": "n/a",
    "deviations": [
    "Commit trailers use AGENTS.md's model-free pair (Co-authored-by: Claude, Claude-Session) instead of the harness reminder's model-named Co-Authored-By line; the PR body ends with the dispatch's session-URL footer instead of the reminder's alternative footer (repo instructions take precedence over the reminder).",
    "A scratch dogfood probe (packages/qa/dogfood/test/zz-scratch-21794-measure.dogfood.test.ts) existed uncommitted in the worktree for the before/after showcase measurement and was deleted; no packages/qa file is in the diff.",
    "Gate battery ran 70 commands: the 66 derived plus 4 packages/spec audits from the dispatch-time list that the current derivation no longer names (the dispatch list predated the changeset and lacked 6 changeset-family commands the derivation adds).",
    "The data-door PATCH is served by the REST /data door (flat body with the object sibling = mapDataError), measured on the booted showcase; the unit pin therefore maps through mapDataError and additionally through resolveThrownHttpError for the dispatcher. No dogfood file was added because the pin did not need a booted stack.",
    "Conflict noted, not silently resolved: os-dev.md rule 3 (sentences a change makes false are owed) vs the dispatch's stop list (no spec/types/rest edits) — see open_questions[0]."
    ],
    "files_changed": [
    "packages/plugins/plugin-security/src/packaged-permission-set-lock.ts (+53/-1)",
    "packages/plugins/plugin-security/src/packaged-permission-set-lock.test.ts (+152/-1)",
    ".changeset/21794-lock-refusal-user-message.md (+13, @objectstack/plugin-security: minor, body line 'Clause-②: yes (widening)')"
    ]
    }


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: PR #21902 at 4bf10901 · seat domain:services#1 · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-05T17:36Z

    Verdict on the dev report 5999709704 (status: done), checked against GitHub and the tree. The build is accepted, and one patch round follows (comment-only, below). Clause-②: yes (widening) stands, so an at-tier contract review is owed; it runs on the patch round's green head. needs:contract-review is put on the PR in this act.

    Checked:

    • PackagedPermissionSetLockedError (insert and update, each with its own text) and the fail-closed PackagedPermissionSetProvenanceUnknownError declare a readonly userMessage set in the constructor. Code, status and message are byte-identical, and nothing the lock accepts or refuses moves.
    • The texts carry guidance only. There is no set name, package id, object name or API path (the console's friendly-copy rule). They are in English, since no producer in the repo localizes a thrown userMessage.
    • Measured at the wire on the booted showcase: the data door's PATCH and POST, and the metadata door's PUT (H5: the same class, through the host-config authoring gate), had no userMessage at base and carry it after the fix, with the rest of each body identical.
    • H4: the sibling was measured to lose its guidance the same way, so it is in scope.
    • Pins: 5 cases map each error through the REST door's mapDataError and the dispatcher's resolver, asserting status, code and userMessage (guidance present; names, ids and paths absent).
    • Ablations: four legs, covering removal and leaking the diagnostic, on both classes, each turned exactly its pins red. Every restore was proven.
    • Gates: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN.

    The dev's two open questions, decided with the four-axis frame:

    1. Six comments in spec, types, rest and runtime now say, falsely, that platform code never sets userMessage → amended in THIS PR (patch round 1, comment-only). The seat's own stop list blocked those packages; that was the order's imprecision, and os-dev.md owes sentences a change makes false.
    2. English-only guidance → A, accepted as built. The reported pull (QA run QA run · surface:mixed (86/86) · 316be321 · 2026-10-04 · 57 PASS / 5 PARTIAL / 24 FAIL / 0 BLOCKED / 0 NOT-RUN #21784) is about the missing guidance, not its language. No non-English admin report is named. A message-key channel would be a new, unpulled contract surface with two spellings of user text. Revisit only on a named report.

    Carried, not filed: on an environment kernel, the metadata protocol's own packaged-base refusal (the same NOT_OVERRIDABLE shape, and the flow and action packaged-base refusals) would still reach the console unmarked. This is inferred from source and not measured, so it is not filed. It is the natural population for triage's "third occurrence" closing card, with an enumeration pin over refusal errors.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 21794,
    "status": "done",
    "branch": "claude/issue-21794-lock-refusal-user-message",
    "pr": "#21902",
    "session": "session_011K3zqE8Pv1Evw5hc8tZCnN (subagent; the parent PM session's id)",
    "premise_still_valid": true,
    "round": "patch round 1 (seat verdict 5999760387): comment-only; new head 7c2b636 (b8da8b3 merges origin/main 1e18a07, 7c2b636 amends the six sentences)",
    "summary": "Verified live state first: PR #21902 open, draft, head 4bf1090. Re-created the worktree from the branch, merged origin/main 1e18a07 (no rebase, no force; clean), and amended exactly the six sentences, comment-only, to the invariant that holds: a userMessage is set only by a producer that authors end-user text with no host state (an application hook, or a platform refusal carrying static guidance such as the packaged-permission-set lock's); platform and driver diagnostics never set it. No code, behaviour, export, schema, describe() text or test assertion moved: for all six files the comment-free TypeScript print (parser + printer with removeComments) is byte-identical HEAD vs tree. The changeset is unchanged (@objectstack/plugin-security: minor, Clause-②: yes (widening)); check:empty-changeset and check-changeset-no-major exited 0 and asked for nothing.",
    "tests": "At 7c2b636, under the verify lock, 3 GB heap, turbo --concurrency=1, vitest --maxWorkers=2: turbo build of spec + runtime^... + plugin-security^... (29 tasks) exit 0; 'pnpm --filter @objectstack/spec run check:generated' exit 0, 'All 15 generated artifacts are up to date' against the dist that run built (the TSDoc moved no artifact; nothing regenerated); typecheck spec 0, types 0, rest 0, runtime 0; plugin-security lock suite (4 files) 'Tests 45 passed (45)'; runtime src/http-dispatcher.permission-denied-user-message.test.ts (header amended) '17 passed'. Then a full 'turbo run build --filter=!@objectstack/docs' (72 tasks, 29 cached) exit 0 so no dist-reading gate would refuse its prerequisite. Comment-only proof: scratch script printing each file with TypeScript's printer (removeComments) at HEAD and in the tree: IDENTICAL for all six (rest-server 821599aabacc, http-dispatcher 0cf90f8dd614, quickjs-runner 3fa5ed66836a, contract.zod 16166622dd60, data-error-classification e8d892ab036e, the runtime test 4bf9b83e4493). No ablation owed: no pin or behaviour moved.",
    "mcp_calls": "0",
    "api_writes": "1 this round — the os-dev-report comment through scripts/pm/post-stamped.mjs (relay: POST /repos/objectstack-ai/objectstack/dispatches executing POST /repos//issues/21794/comments). PR body not edited. git push x1 (4bf1090..7c2b636), not REST.",
    "open_questions": [],
    "out_of_scope_findings": [],
    "gates": {
    "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 7c2b636: 91 commands from 9 changed paths (80 by path, 9 by change kind, 8 whole-tree); 25 more than round 0 for the new spec/types/rest/runtime paths. All 91 run unlocked per os-dev, sequentially, after the full build.",
    "results": [
    "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
    "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
    "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
    "node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
    "node scripts/check-ci-filter-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-corpus.mjs :: exit 0",
    "node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
    "node scripts/check-dts-emitted.mjs --self-test :: exit 0",
    "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
    "node scripts/check-empty-changeset.mjs --self-test :: exit 0",
    "node scripts/check-issue-citations.mjs :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
    "node scripts/check-registry-log-declared.mjs :: exit 0",
    "node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
    "node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0",
    "node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0",
    "node scripts/check-system-context-census.mjs :: exit 0",
    "node scripts/check-system-context-census.mjs --self-test :: exit 0",
    "node scripts/check-tenant-audit-census.mjs :: exit 0",
    "node scripts/check-tenant-audit-census.mjs --self-test :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
    "node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
    "node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
    "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
    "node scripts/release-pending-publish.mjs --self-test :: exit 0",
    "pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
    "pnpm --filter @objectstack/spec run check:api-surface :: exit 0",
    "pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0",
    "pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0",
    "pnpm --filter @objectstack/spec run check:docs :: exit 0",
    "pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0",
    "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
    "pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
    "pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0",
    "pnpm --filter @objectstack/spec run check:export-origins :: exit 0",
    "pnpm --filter @objectstack/spec run check:exported-any :: exit 0",
    "pnpm --filter @objectstack/spec run check:liveness :: exit 0",
    "pnpm --filter @objectstack/spec run check:llms-txt :: exit 0",
    "pnpm --filter @objectstack/spec run check:objectui-pin-citations :: exit 0",
    "pnpm --filter @objectstack/spec run check:skill-refs :: exit 0",
    "pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
    "pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
    "pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
    "pnpm check:authz-resolver :: exit 0",
    "pnpm check:changeset-gate-self-tests :: exit 0",
    "pnpm check:cross-package-test-inputs :: exit 0",
    "pnpm check:dispatcher-error-vocabulary :: exit 0",
    "pnpm check:doc-authoring :: exit 0",
    "pnpm check:driver-memory-census :: exit 0",
    "pnpm check:dts-closure :: exit 0",
    "pnpm check:dual-build-cjs-loads :: exit 0",
    "pnpm check:engine-double-contract :: exit 0",
    "pnpm check:gitlink-declared :: exit 0",
    "pnpm check:i18n :: exit 0",
    "pnpm check:i18n-stale-fill :: exit 0",
    "pnpm check:issue-citations :: exit 0",
    "pnpm check:lean-entry-closure :: exit 0",
    "pnpm check:logger-receiver-detach :: exit 0",
    "pnpm check:merge-driver :: exit 0",
    "pnpm check:nul-bytes :: exit 0",
    "pnpm check:objectql-double-limit :: exit 0",
    "pnpm check:objectui-changeset :: exit 0",
    "pnpm check:org-identifier :: exit 0",
    "pnpm check:page-declaration-shape :: exit 0",
    "pnpm check:pm-changeset-deadline-census :: exit 0",
    "pnpm check:pm-prior-rulings :: exit 0",
    "pnpm check:published-files :: exit 0",
    "pnpm check:query-options-erasure :: exit 0",
    "pnpm check:refd-timer-probe :: exit 0",
    "pnpm check:route-envelope :: exit 0",
    "pnpm check:slot-lookup :: exit 0",
    "pnpm check:sourcemap-no-sources-content :: exit 0",
    "pnpm check:spec-parsed-alias :: exit 0",
    "pnpm check:test-source-alias :: exit 0",
    "pnpm check:tier-file-adoption :: exit 0",
    "pnpm check:type-check-coverage :: exit 0",
    "pnpm check:type-check-debt :: exit 0",
    "pnpm check:watch-hint-literal :: exit 0",
    "pnpm check:where-matcher :: exit 0"
    ],
    "ran_verdict": "Run reconciliation — 91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN. ✓ dispatch-gates --ran: 91 derived famil(ies) accounted for — 91 run, 0 NOT-MEASURED (a DERIVED zero — all 91 recorded an exit code and none of them is 3).",
    "notes": "The derivation also prints, outside the 91, families that take a value only CI has (check-issue-citations --census, three check-shard-attestation emits, two check-test-completeness reads) and the CI-only job steps (Test Core, Temporal Conformance, Dogfood Regression Gate, Dogfood Verify CLI, Build Core, the lint.yml type-check lanes): NOT MEASURED locally, CI's. --ran reports origin/main moved again to 866683f after the merge, and that none of the commits it can see touched what the answer derives from; no second merge.",
    "ci": "One read at report time, head 7c2b636: 35 check runs completed; 1 failure, 'TypeScript Type Check' (step 'Verify every type-check lane succeeded'), because run 37350207059's 'Type Check · source gates' lane concluded cancelled. That lane has timeout-minutes: 10 (lint.yml:5174) and ran 10m06s (17:41:21 to 17:51:27) with a 7m16s 'Checkout repository' step; every step it ran, including 'Type check (@objectstack/spec)' and the generated-artifact checks, concluded success. Read as a runner timeout on a slow checkout, not a defect of this diff. 'Lint & Repo Gates' success. Not rerun: no rerun write is in this round's budget; the seat's call."
    },
    "line_budget": "n/a",
    "deviations": [
    "Merge commit b8da8b3 brings origin/main 1e18a07 into the branch (ordered: merge first, no rebase, no force-push).",
    "A full build of all 72 packages ran before the gate battery (not named in the order) so dist-reading gates measured instead of refusing with PREREQUISITE NOT MET.",
    "Comment-only proof by a scratch parser/printer script; a first scratch attempt with TypeScript's raw scanner reported 2 files DIFFERENT at equal token counts, a scanner artifact (a backtick inside a comment read as template context without parser re-scan); superseded by the parser-based print, which is the cited proof.",
    "Also ran the runtime test file whose header was amended (17 passed), beyond the ordered measurements.",
    "CI 'TypeScript Type Check' is red at 7c2b636 from the source-gates lane's 10-minute timeout (see gates.ci); reported, not rerun."
    ],
    "files_changed": [
    "packages/spec/src/api/contract.zod.ts (+6/-3, TSDoc only; describe() byte-identical)",
    "packages/types/src/data-error-classification.ts (+7/-5, comment only)",
    "packages/rest/src/rest-server.ts (+5/-3, comment only)",
    "packages/runtime/src/http-dispatcher.ts (+4/-1, comment only)",
    "packages/runtime/src/sandbox/quickjs-runner.ts (+6/-3, comment only)",
    "packages/runtime/src/http-dispatcher.permission-denied-user-message.test.ts (+5/-3, header comment only)",
    "merge b8da8b3 (origin/main 1e18a07); round-0 files unchanged: packaged-permission-set-lock.ts, packaged-permission-set-lock.test.ts, .changeset/21794-lock-refusal-user-message.md"
    ],
    "acceptance_notes_block": "- 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.\n- origin/main merged at 1e18a0735c (merge commit b8da8b3e1e, no conflicts) before the amendment."
    }


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · seat domain:services#1 · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-06T00:00Z

    • PR fix(plugin-security): the packaged-permission-set lock refusal carries its guidance as userMessage #21902 merged through the merge queue as 833d57c9, which is an ancestor of origin/main (verified with git merge-base --is-ancestor after a fetch). Merged at 2026-10-05T23:59Z.
    • The contract review carried. The at-tier contract review PASSED on f6324deb53 (6003307825). The landed head 85952735ab is a clean base merge of e6dc7a24 on top of it, and the PR's own net diff is unchanged: the same 9 files, with comment-only lines in the six amended ones. That was proven by a comment-free print equal to main's, including the auto-merged rest-server.ts.
    • Fixes #21794 closed this card completed. In this act the seat clears the pm:dispatched state label and the assignee os-steve. The domain:services, area:access, priority:p3 and bug labels stay.
    • What shipped: PackagedPermissionSetLockedError (per operation) and its fail-closed sibling PackagedPermissionSetProvenanceUnknownError now carry a userMessage. The console therefore shows the admin the clone guidance instead of the generic "You don't have permission to save this record". code, status and message are unchanged. The changeset is minor (Clause-②: yes (widening)) for @objectstack/plugin-security. Six comments the change made false are amended in the same PR, comment-only.
    • Carried, not filed:
      • On an environment kernel, the metadata protocol's packaged-base refusal answers the PUT first, unmarked. This is source inference, not measured. By triage's bound, a third occurrence of this class gets its own closing card with an enumeration pin.
      • content/docs/releases/v17/17-1.mdx still says platform code never sets userMessage. That page is release-owned and records the 17.1 state.

    Generated by Claude Code

  10. added a commit that references this issue on Oct 7, 2026
    833d57c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions