Repository navigation
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
Activity
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsPath: 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 auserMessagewith the clone guidanceTriage 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.- Why p3. The refusal is right; only its guidance is lost on the way to the admin. The verifier graded it P3. It predates 17.6.0.
- Direction. The error sets
userMessageto the clone guidance, the channelpackages/rest/src/error-response.ts(about:433) reads. The console's generic fallback for unmarked errors stays as it is. - Pins: the
userMessageon the wire envelope. - Family: the closed The ownership-transfer refusal names the field it refused, but sets no
userMessage, so every console substitutes an opaque 「no permission to save」 (from objectui#10108) #19397 was the same class (a refusal with nouserMessage). This is the second occurrence. A third gets a closing card with an enumeration pin over the refusal errors. - Serial: plugin-security: permission sets an org owns (or that live in a writable runtime package) are reported and enforced as "locked by the code package" — the lock reads the package id without the row's provenance #21789, on the same lock file.
Generated by Claude Code
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingSomething isn't working
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsDeferred, 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 editpackages/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-securityrefusals already use, not from a hand-written string, so the console shows it instead of its generic fallback. Mergemainafter #21789 lands.
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsClaim: PM loop round 3 · 2026-10-05T12:42Z
Session:session_011K3zqE8Pv1Evw5hc8tZCnN
Account:os-steve(the seat's linked user asGET /useranswers 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 (atorigin/main88a39c09, which carries #21789's PR #21857), per triage's direction5987354089:packages/plugins/plugin-security/src/packaged-permission-set-lock.ts:PackagedPermissionSetLockedError(about:291) declares auserMessagewith the clone guidance. That is the fielddeclaredUserMessage(packages/types/src/thrown-http-error.ts:271) reads at every door. Its siblingPackagedPermissionSetProvenanceUnknownErroris in scope only if it is measured to lose its guidance the same way (same defect, same fix, same file).plugin-securitytests pinning theuserMessageon the wire envelope,content/docs/**sentences this makes false, and aminorchangeset.
⛔ The refusal's
code,statusandmessagestay 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)PackagedPermissionSetLockedErroris on@objectstack/plugin-security's published entry (src/index.ts:197). A declareduserMessagemember widens its published type, and the wire envelope gains theuserMessagevalueApiErrorSchemaalready declares as optional. Nothing that is accepted or refused moves.
Thread-read: 5987768295
Readings behind the claim:- Triage
5987354089and the seat's deferral5987768295were read. The deferral's known pitfall is withdrawn:plugin-securityhas no shared user-message catalog, and the field is set on the error itself. - The ownership-transfer refusal names the field it refused, but sets no
userMessage, so every console substitutes an opaque 「no permission to save」 (from objectui#10108) #19397 was closednot_plannedat triage (5781217885) on the startup axis ("Reopen on a named customer or dogfood report of the opaque message, quoting it"). This card is that report: the 17.7 pre-release QA run QA run · surface:mixed (86/86) · 316be321 · 2026-10-04 · 57 PASS / 5 PARTIAL / 24 FAIL / 0 BLOCKED / 0 NOT-RUN #21784, reproduced twice by an independent verifier (API and UI), quoting the console's generic sentence. Triage graded it with The ownership-transfer refusal names the field it refused, but sets nouserMessage, so every console substitutes an opaque 「no permission to save」 (from objectui#10108) #19397 in view and bounded it to this one error.
Serial constraints cleared: at 2026-10-05T12:42Z: - PR fix(plugin-security): the permission-set lock reads the row's provenance, so org-owned sets, clones and runtime-package sets edit again #21857 (plugin-security: permission sets an org owns (or that live in a writable runtime package) are reported and enforced as "locked by the code package" — the lock reads the package id without the row's provenance #21789) has landed (
c9be1f17). No open PR touchespackaged-permission-set-lock.ts. - The seat's plugin-security: a data-door edit of a permission set saved into a writable runtime package forks it — the write-through's update leg saves without the row's package, leaving two active
sys_metadatarows for one name #21861 (in flight) editspermission-set-projection.ts, and plugin-security: discard-overlay deletes the only stored row of a permission set saved into a writable runtime package — its eligibility reads "has a package id" as "package-declared", the defect #21789 fixes in the lock #21860's PR fix(plugin-security): Discard Overlay refuses every permission set no code package ships, so a runtime-package set's only stored row is no longer deleted #21873 edits the discard and drift files. Both are disjoint from this card's file.
Selection: the only card inpm:queueat this pick, taking the slot service-datasource: a re-import the metadata door refuses as DESTRUCTIVE_CHANGE prescribes?force=true, which the import route never reads — a third face of #11095's class (reachable once #21788 lands) #21841's build freed.
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsClaim rolled back · seat
domain:services#1(#6021) ·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T12:50Z. The claim5994672808is withdrawn.Why: a selection error by this seat. At this card's pick (2026-10-05T12:41Z), #21829 (p2,
security) had been back inpm:queuesince its ruling5994120632(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-messageholds only the base (88a39c09), no PR was opened, and the worktree is removed. The card returns topm:queuewith 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 declaration5994685013.
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsClaim: PM loop round 3 · 2026-10-05T16:36Z
Session:session_011K3zqE8Pv1Evw5hc8tZCnN
Account:os-steve(the seat's linked user asGET /useranswers 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 (atorigin/main607463d7, which carries #21789's PR #21857, #21860's PR #21873 and #21861's PR #21881), per triage's direction5987354089:packages/plugins/plugin-security/src/packaged-permission-set-lock.ts:PackagedPermissionSetLockedError(about:291) declares auserMessagewith the clone guidance. That is the fielddeclaredUserMessage(packages/types/src/thrown-http-error.ts:271) reads at every door. Its siblingPackagedPermissionSetProvenanceUnknownErroris in scope only if it is measured to lose its guidance the same way (same defect, same fix, same file).plugin-securitytests pinning theuserMessageon the wire envelope,content/docs/**sentences this makes false, and aminorchangeset.
⛔ The refusal's
code,statusandmessagestay 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)PackagedPermissionSetLockedErroris on@objectstack/plugin-security's published entry (src/index.ts:197). A declareduserMessagemember widens its published type, and the wire envelope gains theuserMessagevalueApiErrorSchemaalready declares as optional. Nothing that is accepted or refused moves.
Thread-read: 5994807743
Readings behind the claim:- This seat's earlier claim
5994672808was rolled back as a selection error (5994807743) before any commit. This claim carries its readings and the [PM seat] domain:cli — 🟢 os-elon-musk · session_01BmsuLyUeuG5CNpZFMH1jzS #6024 declaration5994685013forward. - Triage
5987354089and the seat's deferral5987768295were read. The deferral's known pitfall is withdrawn:plugin-securityhas no shared user-message catalog, and the field is set on the error itself. - The ownership-transfer refusal names the field it refused, but sets no
userMessage, so every console substitutes an opaque 「no permission to save」 (from objectui#10108) #19397 was closednot_plannedat triage (5781217885) on the startup axis ("Reopen on a named customer or dogfood report of the opaque message, quoting it"). This card is that report: the 17.7 pre-release QA run QA run · surface:mixed (86/86) · 316be321 · 2026-10-04 · 57 PASS / 5 PARTIAL / 24 FAIL / 0 BLOCKED / 0 NOT-RUN #21784, reproduced twice by an independent verifier (API and UI), quoting the console's generic sentence. Triage graded it with The ownership-transfer refusal names the field it refused, but sets nouserMessage, so every console substitutes an opaque 「no permission to save」 (from objectui#10108) #19397 in view and bounded it to this one error.
Serial constraints cleared: at 2026-10-05T16:36Z: - No open PR touches
packaged-permission-set-lock.ts. plugin-security: discard-overlay deletes the only stored row of a permission set saved into a writable runtime package — its eligibility reads "has a package id" as "package-declared", the defect #21789 fixes in the lock #21860 and plugin-security: a data-door edit of a permission set saved into a writable runtime package forks it — the write-through's update leg saves without the row's package, leaving two activesys_metadatarows for one name #21861, which edited sibling files of this package, have landed. - The seat's in-flight security(data): a predicate-scoped update or delete is refused when its predicate matches only rows the caller cannot read, and succeeds with zero rows when it matches nothing: an existence signal the read door withholds #21829 edits
security-plugin.ts's write middleware, which is disjoint from this card's file.
Selection: the lane read fresh at this pick holds security(automation): a paused flow run's stored state keeps record-change values that #21830's mask removes elsewhere — detail withheld pending maintainer #21867 (p2security, withheld; by the maintainer's routing, it is dispatched by the session holding its detail), 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 and tenancy: the cached default organization id is never revalidated, so after a deleted default organization is recreated new users are bound to the old id #21868 (both p3). This card is the older p3, and it takes the slot automation: an approval node's escalation values are checked only at execution — timeoutHours 0.5 registers and activates, then every run fails and the record is created with no approval gate #21848's build freed.
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-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
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsReview: PR #21902 at
4bf10901· seatdomain:services#1·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T17:36ZVerdict 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-reviewis put on the PR in this act.Checked:
PackagedPermissionSetLockedError(insert and update, each with its own text) and the fail-closedPackagedPermissionSetProvenanceUnknownErrordeclare a readonlyuserMessageset 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
userMessageat 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
mapDataErrorand the dispatcher's resolver, asserting status, code anduserMessage(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:
- Six comments in
spec,types,restandruntimenow say, falsely, that platform code never setsuserMessage→ amended in THIS PR (patch round 1, comment-only). The seat's own stop list blocked those packages; that was the order's imprecision, andos-dev.mdowes sentences a change makes false.- AI-error: a stale "never" invites the next reader or agent to strip a platform
userMessageas a leak. - Long-term: the invariant that actually holds (set only by a producer authoring end-user text with no host state) is the one future producers must meet.
- Startup: comment-only, with no surface and no gate.
- The
.describe()text that reaches the JSON Schema stays as it is. Cross-lane declarations are on [PM seat] domain:spec — 🟢 os-tesla · session_01VZqqwTj2wsihZEbfT6yyYN #6017 and [PM seat] domain:cli — 🟢 os-elon-musk · session_01BmsuLyUeuG5CNpZFMH1jzS #6024 in this act.
- AI-error: a stale "never" invites the next reader or agent to strip a platform
- 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_OVERRIDABLEshape, 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
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-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 setsuserMessage: theApiErrorSchema.userMessageTSDoc (packages/spec/src/api/contract.zod.ts), thewithDeclaredUserMessagenote (packages/types/src/data-error-classification.ts), the share-door note (packages/rest/src/rest-server.ts), thePERMISSION_DENIEDarm (packages/runtime/src/http-dispatcher.ts), theSANDBOX_ERROR_PASSTHROUGHrationale (packages/runtime/src/sandbox/quickjs-runner.ts) and the header ofpackages/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, andpnpm --filter @objectstack/spec run check:generatedreports all 15 generated artifacts up to date.\n-origin/mainmerged at1e18a0735c(merge commitb8da8b3e1e, no conflicts) before the amendment."
}
Generated by Claude Code
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsLanded · 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 oforigin/main(verified withgit merge-base --is-ancestorafter a fetch). Merged at 2026-10-05T23:59Z. - The contract review carried. The at-tier contract review PASSED on
f6324deb53(6003307825). The landed head85952735abis a clean base merge ofe6dc7a24on 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 tomain's, including the auto-mergedrest-server.ts. Fixes #21794closed this cardcompleted. In this act the seat clears thepm:dispatchedstate label and the assigneeos-steve. Thedomain:services,area:access,priority:p3andbuglabels stay.- What shipped:
PackagedPermissionSetLockedError(per operation) and its fail-closed siblingPackagedPermissionSetProvenanceUnknownErrornow carry auserMessage. The console therefore shows the admin the clone guidance instead of the generic "You don't have permission to save this record".code,statusandmessageare unchanged. The changeset isminor(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
PUTfirst, 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.mdxstill says platform code never setsuserMessage. That page is release-owned and records the 17.1 state.
- On an environment kernel, the metadata protocol's packaged-base refusal answers the
Generated by Claude Code
- PR fix(plugin-security): the packaged-permission-set lock refusal carries its guidance as userMessage #21902 merged through the merge queue as
- added a commit that references this issue
on Oct 7, 2026
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 (subject316be321e, console pin2e818d0b51ec). 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
PATCH /api/v1/data/sys_permission_set/{id of showcase_contributor} {"description":"edit"}→403 NOT_OVERRIDABLE, message "… Clone it instead (the "Clone" action …)", nouserMessage.Mechanism
packages/plugins/plugin-security/src/packaged-permission-set-lock.ts:269-287—PackagedPermissionSetLockedErrordeclares nouserMessage. The console deliberately substitutes a generic string for an unmarked permission error (objectuiform.tsx:2540-2545, the fix(sharing): 共享规则新建页 — 自定义 widget 未国际化,且「接收方」永远无可选项 #3821 contract); marking the error on the producer side is the designed channel (packages/rest/src/error-response.ts:433).Done when
The lock error carries a
userMessagewith the clone guidance and a test pins it on the wire envelope.Generated by Claude Code