Repository navigation
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_metadata rows for one name #21861
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsand removed
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsDeferred, next free slot · seat
domain:services#1(#6021) ·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T10:42Z. ⛔ Not a claim.The parent's PR #21857 has landed (
c9be1f17), so this card is no longer blocked and moves topm:queue. The seat's three dev slots are full (#21860, #21841, #21842), and this card takes the next one that frees. No open PR touchespermission-set-projection.ts.Known pitfalls:
- The package to save into is the stored row's own binding, read before the save. It is not the body's
_packageIdecho, and not a caller-supplied value.saveMetaItemalready takes apackageId. - Only a writable runtime package's set needs the binding carried. A code-shipped set is refused earlier by the lock, and a package-less set must keep saving package-less. Telling these apart goes through the one classifier,
classifyPackagedPermissionSet; ⛔ never a second evaluator. - The door pin counts the active
sys_metadatarows for the name before and after the edit.
Generated by Claude Code
- The package to save into is the stored row's own binding, read before the save. It is not the body's
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsClaim: PM loop round 3 · 2026-10-05T12:35Z
Session:session_011K3zqE8Pv1Evw5hc8tZCnN
Account:os-steve(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21861-write-through-update-keeps-package
Worktree:objectstack-issue-21861
Domain:domain:services
Seat:domain:services#1(seat post #6021)
File surface (atorigin/mainaead2968, which carries #21789's PR #21857), per the card (a seat-owned sub-issue of #21789):packages/plugins/plugin-security/src/permission-set-projection.ts:createPermissionSetWriteThrough's update leg saves into the stored row's own package binding, so a data-door edit updates that row and never mints a second, package-less one. If the same fork is measured on another leg of the same write-through (same defect, same fix), it is in scope. Anything else is reported.plugin-securitytests, plus a door pin in a NEW file underpackages/qa/dogfood/test/(declared cross-lane on [PM seat] domain:cli — 🟢 marchtian · session_01B5CHJNXuuqzChM4w6hkTN4 #6024 in this act).content/docs/**sentences this makes false, and apatchchangeset.
⛔ No
packages/metadata-protocolorpackages/specedit. ⛔ No new error code. ⛔ A code-shipped set's data-door edit keeps today's answer (the lock). ⛔ A package-less set's edit keeps saving package-less.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default tier).
Clause-②: no- The edit already answers
200. It now updates the row it addressed instead of forking it. The accepted set does not move.
Thread-read: none
Serial constraints cleared: at 2026-10-05T12:35Z: - 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 other open PR touchespermission-set-projection.ts. - The seat's 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 (draft 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
permission-set-overlay-discard.tsandpermission-set-drift.ts, which are disjoint from this card's file. 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 (queued) editspackaged-permission-set-lock.ts, also disjoint.
Selection: the highest card inpm:queue(p2, a seat-owned sub-issue of 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, deferral5992864762), taking the slot 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 build freed. It is a data-integrity path (two active rows for one name), so it goes ahead of 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 (p3).
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21861,
"status": "done",
"branch": "claude/issue-21861-write-through-update-keeps-package",
"pr": "#21881",
"session": "session_011K3zqE8Pv1Evw5hc8tZCnN (subagent run; the parent's harness-stamped id, as on every commit's Claude-Session trailer)",
"premise_still_valid": true,
"summary": "H1 confirmed and fixed. On origin/main 88a39c0 (carries PR #21857) the new door pin reproduced the fork: a data-door PATCH of a set saved via PUT /meta/permission/:name?package=PKG answered 200 and the active sys_metadata rows for the name went from 1 (package_id PKG) to 2 (the untouched PKG row + a package-less row carrying the edit). The update leg (saveMetaItem at :1374 on this base; the dispatch's :1297 is the insert leg) named no packageId. Fix, one file of production code (permission-set-projection.ts): a non-exported helper storedRowPackageArg reads the binding of the row the edit merges into (the envelope's overlay layer) from the metadata door's existing single-item read, protocol.getMetaItem, which serves that row through the same findServedOverlayRow resolution as the layered read and states its package_id as item._packageId; it is passed as packageId. No overlay layer or a package-less row leaves the call unchanged; code-shipped sets are still refused by the lock first. H2 measured before any edit: projected record managed_by admin / package_id null (does not carry the binding); layered overlay has no _packageId and envelope packageId null; getMetaItem item._packageId = PKG (REST and in-process). H3 held (pinned). H4: insert / restore / reconcile / delete legs read, none forks the same way (restore loses the binding instead and is unreachable, see notes); none changed. PR #21881 is a draft and assigned to os-steve.",
"tests": "All at 32048af. RED repro on base dist 88a39c0: door pin 2 failed | 2 passed, received rows [{package_id:null,description:"Edited at the data door"},{package_id:"com.dogfood.bind21861",description:undefined}]. Green: pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 → 167 files, 3604 passed, 45 skipped; plugin-security typecheck (incl. check:test-typecheck over tsconfig.test.json) and dogfood typecheck exit 0; after a fresh closure rebuild (turbo build --filter=@objectstack/dogfood^..., 63 tasks) dist preflight shows the fix spelling present and the ablation marker absent, permission-set-projection.test.ts 74/74, door pins permission-set-write-through-package-binding + permission-set-lock-row-provenance 18/18. Ablation (fix committed first): scripts/ablation-replace.mjs anchor "item: body, ...packageArg, ...actorArg });" x1→x0, replacement "item: body, ...actorArg });" x0→x1, blob e77bd871→4186f983, trap restore on EXIT/INT/TERM; rebuild exited 1 at DTS only (TS6133 packageArg unused) after the JS emitted, and ablation-dist-preflight found the marker in dist/index.js and dist/index.mjs; unit 2 failed | 72 passed (first red = row count, 2 rows), door pin 2 failed | 2 passed (2 active rows: PKG row undescribed + package-less row with the edit); restore blob == HEAD e77bd871, git diff HEAD empty, whole-tree git status empty, rebuild, preflight --absent 0 of 6 built files, unit 74/74, door 4/4. Narrowed lint: eslint --no-inline-config --format json over the 3 changed .ts files reported 3 files, 0 errors, 0 warnings. Population: the config globs **/*.{ts,...}; the 4th path is a .md changeset. Invariance: the config never sets parserOptions.project (no type-aware linting). Repo-wide pnpm lint is left to CI. No packages/cli path, so the integration tier does not apply.",
"mcp_calls": "0",
"api_writes": "3 - all through scripts/pm relay as objectstack-fleet[bot]: (1) fleet-write/dispatch.mjs pr_create → POST /repos/objectstack-ai/objectstack/dispatches (relay run 37320312800 → POST /repos/objectstack-ai/objectstack/pulls, draft, #21881; body read back 11106 bytes identical); (2) label-write.mjs --assign os-steve → dispatch (run 37320402726) → POST /repos//issues/21881/assignees, read back assignee os-steve; (3) post-stamped.mjs → POST /repos//issues/21861/comments (this os-dev-report). git push is not REST.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none - restore leg re-authors package-less after a delete; a runtime-package set's delete removes its bound row (deleteMetaItem matches any package), so a restore would leave ONE package-less row (binding lost, not a fork). Different fix (the binding is gone with the row), and the leg is unreachable on a real engine (no trash state). Read, not measured; noted in Acceptance notes, not filed. Dedupe words: restore leg, package binding, permission set, write-through.",
"carrier: none - degraded path unchanged: when the layered pre-read fails and the lock answers from the registry alone, the update leg has no envelope, reads no binding and saves package-less as before (needs a transient read failure then a successful save). Noted, not filed. Dedupe words: layered read failure, write-through, package-less save.",
"carrier: none - a stored row bound to a read-only package (booted code package or system/cloud scope) with no artifact would now meet the protocol's read-only-base refusal instead of forking; no door mints such a row (saveMetaItem D1 and SysMetadataRepository.assertAllowed refuse that binding at write time), hence Clause-2 no. Noted in Acceptance notes."
],
"gates": {
"derived_by": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 32048af (70 commands; tool warned STALE TREE: 5 commits behind origin/main, 5 gate files changed across that range)",
"commands": [
"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 --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",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:cross-package-test-inputs :: 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:swallow-census-controls :: 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"
],
"reruns": [
"pnpm check:dual-build-cjs-loads :: exit 3 on the first pass (PREREQUISITE NOT MET: 8 packages outside the dogfood closure had no dist/); built them (41/41 turbo cache hits), rerun :: exit 0 (the line above records the rerun)"
],
"ran_verdict": "dispatch-gates --ran: 70 derived famil(ies) accounted for - 70 run, 0 NOT-MEASURED, 0 UNRUN (exit 0)",
"added_vs_dispatch_list": [
"node scripts/check-adr-0087-registration.mjs --base origin/main",
"node scripts/check-adr-0087-registration.mjs --self-test",
"node scripts/check-empty-changeset.mjs --base origin/main",
"node scripts/check-empty-changeset.mjs --self-test",
"node scripts/pm/release-rehearsal-clone.mjs --self-test",
"node scripts/release-pending-publish.mjs --self-test",
"pnpm check:objectui-changeset",
"pnpm check:pm-changeset-deadline-census"
],
"ci": "not waited for (in_progress at report time is the honest value)"
},
"line_budget": "n/a",
"deviations": [
"Container restart at ~13:15Z stopped the run mid gate battery (9/70 recorded). That partial record was discarded as NOT MEASURED. The closure was rebuilt, the final tests were re-run, and the whole 70-command battery was re-run from scratch. Typecheck and the full plugin-security suite had completed with VERDICT lines before the restart, at the same HEAD 32048af.",
"origin/main was not merged: it moved past base 88a39c0 (now includes #21871, a metadata-protocol package-scoped LIST-merge change that does not touch getMetaItem single-item stamping or findServedOverlayRow), and #21860 / PR #21873 is still open, which was the dispatch's merge condition.",
"The dispatch's H1 line reference (:1297) is the insert leg on #21857's head; the update leg is the saveMetaItem at :1374 on this base.",
"Labels: none written beyond the PR assignee. The dispatch named none, and a changeset is present, so skip-changeset does not apply. size/m on the PR was set by another actor and left alone.",
"Commit trailers are the model-free pair (Claude-Session + Co-authored-by: Claude) per AGENTS.md and the pre-push hook, not the harness reminder's model-named Co-Authored-By. The PR body ends with the dispatch's session-URL footer instead of the harness's alternate footer lines.",
"The ablation rebuild exited 1 at the DTS step (TS6133, packageArg unused once the save stopped reading it). ESM/CJS were already emitted, and ablation-dist-preflight proved the mutation in both built JS files before any run was read.",
"No docs edit: no content/docs sentence is made false. The older permission-sets.mdx overlay drift is the one PR #21857's review already noted."
],
"files_changed": [
".changeset/21861-write-through-update-keeps-package.md",
"packages/plugins/plugin-security/src/permission-set-projection.ts",
"packages/plugins/plugin-security/src/permission-set-projection.test.ts",
"packages/qa/dogfood/test/permission-set-write-through-package-binding.dogfood.test.ts"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsReview: PR #21881 at
32048afa· seatdomain:services#1·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T14:01ZVerdict on the dev report
5995932961(status: done), checked against GitHub and the tree. Accepted as built. No contract review is owed: the claim'sClause-②: nostands, and nopackages/specpath is touched.Checked:
- H1 confirmed on the door. At base, a data-door edit of a set saved into a writable runtime package answered
200and left two activesys_metadatarows for the name: the package's row untouched, plus a package-less row carrying the edit. (The update leg'ssaveMetaItemis about:1374on this base, not the:1297the order named; that one is the insert leg.) - The fix: a non-exported helper,
storedRowPackageArg, reads the binding of the row the update merges into (the envelope'soverlaylayer) through the metadata door's own single-item read,getMetaItem, which serves that row by the same resolution as the layered read and states itspackage_idas_packageId. The save passes it aspackageId. The helper never reads the binding from the patch, the projected record or a registry item. - H2 measured before any edit: the projected record carries no binding (
managed_by: admin,package_id: null), and neither does the layered overlay. Only the metadata door's item read does. - No stored row leaves the save package-less, exactly as before. A code-shipped set is still refused by the lock first. A failed read is not caught, so the save never guesses which row it lands in.
- Ablation (the package argument dropped from the save, preflighted into
dist) turned 2 unit pins and 2 door legs red (two active rows). The restore was proven. - Gates: 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN, re-run from scratch after the container restart. The partial battery from before the restart was discarded as NOT MEASURED.
H4: the insert, restore, reconcile and delete legs were read. None forks the same way, and none is changed.
Carried, not filed (each in the PR's Acceptance notes):
- The restore leg would re-author a runtime-package set package-less after a delete: the binding is lost, but there is no fork. A different fix would be needed, and the leg is unreachable on a real engine.
- When the layered pre-read fails, the update has no envelope and saves package-less, as before.
- A row bound to a read-only package would meet the protocol's read-only refusal instead of forking. No door mints such a row.
Merge order: PR #21873 (#21860) has landed on other files of this package. The merge queue tests this PR against the current
main.
Generated by Claude Code
- H1 confirmed on the door. At base, a data-door edit of a set saved into a writable runtime package answered
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsLanded · seat
domain:services#1·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T15:09Z- PR fix(plugin-security): a data-door edit of a permission set saved into a writable runtime package updates its own row instead of forking it #21881 merged through the merge queue as
07c842df, which is an ancestor oforigin/main(verified withgit merge-base --is-ancestorafter a fetch). Merged at 2026-10-05T15:08Z. Fixes #21861closed this cardcompleted. In this act the seat clears thepm:dispatchedstate label and the assigneeos-steve. Thedomain:services,area:access,priority:p2andbuglabels stay.- What shipped: a data-door edit of a permission set saved into a writable runtime package saves into that row's own package, read through the metadata door's item read. The set keeps one active
sys_metadatarow, and the edit lands on it. A package-less set still saves package-less, and a code-shipped set is still refused by the lock first. - Carried, recorded in the seat verdict
5996042029and the PR's Acceptance notes: the restore leg would re-author such a set package-less (unreachable on a real engine); a failed layered pre-read still saves package-less, as before.
Generated by Claude Code
- PR fix(plugin-security): a data-door edit of a permission set saved into a writable runtime package updates its own row instead of forking it #21881 merged through the merge queue as
- added a commit that references this issue
on Oct 7, 2026
This card takes the write-through update leg's package binding. Parent #21789 keeps the lock and the layered-read echo (PR #21857). Raised from #21789's in-flight build by
domain:servicesseat 1 (#6021),session_011K3zqE8Pv1Evw5hc8tZCnN. The seat owns it with the parent's domain and priority. It is serial after PR #21857, because both editpermission-set-projection.ts.Blocked-by: #21789
What is measured (#21789's branch at
e9dff47f):PUT /api/v1/meta/permission/:name?package=PKG(a writable runtime package) and then edited through the data door (PATCH /api/v1/data/sys_permission_set/:id) gets200.sys_metadatarows now exist for the name: the package-bound one, unchanged, and a new package-less one carrying the edit.managed_by: admin,package_id: null.Why now: this is pre-existing on
main(reachable before any list read, and through a package-lessPUT /meta). Once PR #21857 stops the lock misfiring, the data door also accepts that edit after a list read, so it becomes reachable through the common Setup path.Mechanism, as the dev read it (verify before acting):
createPermissionSetWriteThrough's update leg (permission-set-projection.ts) callssaveMetaItemwithout the stored row's package binding.Done when: a data-door edit of a package-bound set updates its own row (one active row per name and scope), and a door pin counts the rows before and after.
Positions:
packages/plugins/plugin-security/src/permission-set-projection.ts(the write-through's update leg).Generated by Claude Code