Repository navigation
[finding] Every boot warns permission_set_declaration_unowned for each cloned (org-owned) permission set, saying Setup cannot see it, while the row exists and Setup lists and edits it #21669
Description
Activity
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p3·domain:services·area:access·pm:queue. The boot loop does not report an environment-owned (cloned) permission set as an unowned declarationTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-04T03:04Z. ⛔ Not a claim, ⛔ not a dispatch.Why p3. It is a false operational warning on every boot. Nothing is broken, but it trains operators to ignore a warning class.
Routing:
packages/plugins/plugin-security/src/bootstrap-declared-permissions.ts, sodomain:services.Direction.
- The loop tells package declarations apart from environment-owned rows before judging ownership. A set whose row exists and is environment-owned (
managed_byadmin) is not "unowned". - The warning stays for a real unmaterialized declaration.
- The claim reads [finding] After #12892 step 2 an artifact boot with an engine still holds a THIRD, un-parsed copy of
permissions/capabilities/sharingRulesin the ObjectQL SchemaRegistry (AppPlugin.init→manifest.register), and the plugin-security / plugin-sharing seeders read that copy FIRST #14491 (the third registry copy) first. If they share the registry walk, it says so, and does not fold silently.
Pins:
- a cloned set boots with no warning;
- a truly unowned declaration still warns.
Generated by Claude Code
- The loop tells package declarations apart from environment-owned rows before judging ownership. A set whose row exists and is environment-owned (
- 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 workingand removed
on Oct 4, 2026 objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsClaim: PM loop round 6 · 2026-10-04T05:36Z
Session:session_01DiCSbmJrkzNhuEAier4VoJ
Account:os-bill(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21669-cloned-set-not-unowned
Worktree:objectstack-issue-21669
Domain:domain:services
Seat:domain:services#2(seat post #21118)
File surface (atorigin/maina4fd82a8e6), per triage's direction5976016055:packages/plugins/plugin-security/src/bootstrap-declared-permissions.ts: the boot loop's ownership judgment. A set whose row exists and is environment-owned (managed_byadmin, such as a Clone) is told apart from a package declaration before "unowned" is judged. Thepermission_set_declaration_unownedwarning stays for a real unmaterialized declaration.bootstrap-declared-permissions.test.ts, plus a boot-level pin if the unit seam cannot show the registry walk: a cloned set boots with no warning, and a truly unowned declaration still warns.seed-refusal-diagnostics.tsonly if the warning's text must change.- A changeset.
⛔ #14491 (
domain:engine, the third registry copy) is read first, per triage. If it shares this registry walk, the report says so, and nothing is folded in silently. ⛔ Nopackages/objectql,packages/metadata*orpackages/specedit. Stop on breach; explain in the report.
Container & model:S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default tier).
Clause-②: no- A false boot warning stops. No accepted input, key, export or error code changes.
Thread-read: 5976016055
Serial constraints cleared: at 2026-10-04T05:36Z: - Of the 9 open PRs, none touches
plugin-security. - [finding] After #12892 step 2 an artifact boot with an engine still holds a THIRD, un-parsed copy of
permissions/capabilities/sharingRulesin the ObjectQL SchemaRegistry (AppPlugin.init→manifest.register), and the plugin-security / plugin-sharing seeders read that copy FIRST #14491 ispm:blockedand unassigned, so nothing is in flight on it. - This seat's other open PRs are disjoint: PR fix(organizations)!: a create naming an organization_id meets the Layer 0 write wall, as the update does (#21666) #21680 ([finding] POST /data on a walled posture silently replaces a platform admin's explicit organization_id with the admin's active organization (201), while the equivalent PATCH refuses loudly #21666,
packages/plugins/organizations, in the merge queue) and PR test(service-analytics): the Turso remote-face echo exception asserts its equivalence (#21647) #21684 (analytics: on a non-SQL driver (driver-memory, and by code path MongoDB) a UTC or unset timezone still echoes date_trunc for a date bucket the engine groups in memory #21647, oneservice-analyticstest). Both devs have returned, so nothing else of this seat is in flight.
Selection:priority:p3, the lane's only dispatchable card; dispatched serially per the maintainer's order 6.
Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsos-dev-report
{ "pr": "https://github.com/objectstack-ai/objectstack/pull/21692", "issue": 21669, "status": "done", "branch": "claude/issue-21669-cloned-set-not-unowned", "session": "session_01DiCSbmJrkzNhuEAier4VoJ — the dispatching PM's session (this run is its subagent); commits carry Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ", "premise_still_valid": true, "summary": "Reproduced on the real showcase composition (bootStack over one SQLite file, base a4fd82a8e6, plugin-security dist built from that tree). Boot 1: the admin clones showcase_contributor through POST /api/v1/data/sys_permission_set with the shipped clone_permission_set action's payload, which answers 201; the row is managed_by admin, package_id null, organization_id null, and there is one env-wide active sys_metadata row. Boot 2: the SchemaRegistry permission collection holds the clone with no _packageId and no packageId and with _provenance 'org' (hydrated by loadMetaFromDb), beside 17 manifest.register items that all carry _packageId. Exactly one [permission_set_declaration_unowned] line names the clone, and the summary reads skippedUnowned 1, total 18. Reporting line: upsertPackagePermissionSet's if (!packageId) branch (reportPermissionSetDeclarationUnowned), reached because the boot loop's packageId = ps._packageId ?? ps.packageId is undefined for the clone. git log -S on the call finds cf39b83c09 only. FIX (bootstrap-declared-permissions.ts only): before the loop judges ownership, it asks the batched existence oracle it already holds whether the environment owns a row under the name: this pass's own row or, on a per-organization pass, the env door's organization-less row. A no-package-id item over an environment-owned row lands in skippedEnvAuthored and nothing is logged. The signal is the row's managed_by through the seeder's EXISTING env-authored verdict (upsertPackagePermissionSet's last branch: not managed_by 'package' means environment-authored, never clobbered; ADR-0086 two-doors), now named isEnvironmentOwnedRow and called by both sites, so there is one definition. An unknown read falls through to the unchanged unowned refusal. The publish materializer and seed-refusal-diagnostics.ts are untouched. After the fix the same two-boot run gives 0 unowned lines and skippedUnowned 0, skippedEnvAuthored 9. #14491: it SHARES the registry walk. readDeclared reads the one SchemaRegistry permission collection, which holds both #14491's unparsed manifest.register copy (the 17 packaged items) and the hydrated clone. The fix does NOT depend on which copy the loop reads: it decides from the sys_permission_set row and uses only the item's name and the absence of a package id. The metadata-service copy of the clone was measured too: no package id, no _provenance, and the _envProjection marker. That is also why _provenance 'org' was rejected as the signal: it is on the registry copy only, so it would make the fix copy-dependent, and it would be a second definition. No objectql, metadata*, or spec edit. Docs: content/docs (outside releases) and skills have no mention of permission_set_declaration_unowned or 'no owning package', so nothing was edited and no skills sentence is false.", "tests": "All on HEAD 067aa4f58f, every exit captured before any pipe, verdicts quoted from each runner. (1) pnpm --filter @objectstack/plugin-security test: 'Test Files 164 passed (164)', 'Tests 3534 passed | 45 skipped (3579)', os-verify-lock VERDICT command-exit 0. (2) pnpm --filter @objectstack/plugin-security typecheck: VERDICT command-exit 0; check:test-typecheck OK; tsc -p tsconfig.test.json --listFiles counts the edited test file 1. (3) pnpm --filter @objectstack/dogfood typecheck: VERDICT command-exit 0; tsc --listFiles counts the new pin 1. (4) The boot pin, vitest run --project isolated on the new dogfood file: 'Tests 3 passed (3)'. (5) The seeder unit file: 'Tests 30 passed (30)' (23 pre-existing + 7 new). ABLATION, each leg through scripts/ablation-replace.mjs (anchor 'return isEnvironmentOwnedRow(row);' hit x1, blob 56a5addec78e changed), on top of the committed fix. Each leg rebuilt @objectstack/plugin-security, and ablation-dist-preflight confirmed the marker in dist/index.js and dist/index.mjs before any result was read. Directions were predicted before each run. Leg A, guard removed (environmentOwnsName returns false), predicted only the clone rows red. Observed: unit 3 failed | 27 passed, the 3 red being the cloned set (single pass), the same on a per-organization pass, and CONSERVATION with a clone; boot pin 1 failed | 2 passed, the absence case 'expected [ Array(1) ] to deeply equal []' with the precondition and control green. Leg B, guard admits all (environmentOwnsName returns true), predicted every input with no package id and no environment row red. Observed: unit 12 failed | 18 passed, the 12 red being 5 new (the truly unowned case in single and per-organization passes, the package-owned-row control, the unreadable control, conservation-with-clone) and 7 pre-existing #18091/#18571 pins of the same shape; both clone rows green; boot pin 3 passed. RESTORE, both legs: ablation-replace reported blob == HEAD (56a5addec78e) and git diff HEAD empty. A plugin-security rebuild followed, then ablation-dist-preflight --absent for both markers: 'marker absent from all 6 built files', 'working tree clean against HEAD'. dist then carries the real fix (environmentOwnsName x2 in each bundle). Narrowed lint: eslint --no-inline-config --format json on the 3 touched TS files gave 3 file results, 0 errors, 0 warnings. The population is eslint.config.mjs files '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}' minus NEVER_LINTED. There is no parserOptions.project (no type-aware rules), so the diff cannot move any untouched file's verdict. The repo-wide pnpm lint is CI's. Control-byte self-scan grep -naP on all 4 files: no hits (exit 1).", "gates": "Derived at the worktree: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (stderr: 'gate list derived from the tree of objectstack-ai/objectstack at commit 067aa4f58f'), 69 commands. All 69 ran explicitly from generated script files, with exit codes recorded. 34 node commands exited 0. 35 pnpm commands exited 0, except pnpm check:dual-build-cjs-loads, which first exited 3: 'PREREQUISITE NOT MET … 8 packages had no dist/ (studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis, service-knowledge) … NOT a pass'. Those 8 were built (turbo, 41/41 cached), and it then exited 0. Reconciled: dispatch-gates --ran with ':: exit N' on every line gave '69 derived famil(ies) accounted for — 69 run, 0 NOT-MEASURED (a DERIVED zero — all 69 recorded an exit code and none of them is 3)'. NOT MEASURED, CI-only values: scripts/check-shard-attestation.mjs (dogfood shard matrix) and scripts/check-issue-citations.mjs --census. No turbo gate appended to AGENTS.md (git status clean after every batch). PR CI at report time: 15 check runs completed with no failure, 17 in_progress. That is honest in_progress; not waited on.", "line_budget": "No skills/** or governed surface touched, so no SKILL.md line budget applies. Diff vs merge base a4fd82a8e6: 4 files, +392 / -3 (395 changed lines, under the 5000 human-merge threshold). Production: bootstrap-declared-permissions.ts +60 / -3, mostly docblocks; tests +153 (unit) and +166 (boot pin); changeset +13.", "files_changed": [ "packages/plugins/plugin-security/src/bootstrap-declared-permissions.ts", "packages/plugins/plugin-security/src/bootstrap-declared-permissions.test.ts", "packages/qa/dogfood/test/permission-set-clone-boot-unowned-warning.dogfood.test.ts", ".changeset/21669-cloned-permission-set-not-unowned.md" ], "deviations": [ "chmod +x on my own scratch file (scratchpad issue-21669/leg.sh) under /tmp/claude-0, against the hygiene rule. It was a scratch ablation runner, invoked as 'bash leg.sh' anyway; nothing else under /tmp/claude-0 was touched.", "The boot-level pin lives in packages/qa/dogfood (the Dogfood Regression Gate), not in plugin-security: plugin-security has no harness that composes loadMetaFromDb hydration plus the data-door write-through plus sign-in. It adds one isolated file that boots the showcase twice (~25 s locally).", "Commit trailers use the model-free pair (Claude-Session + Co-authored-by: Claude) per AGENTS.md and the dispatch, not the model-named Co-Authored-By the harness reminder proposed; the pre-push hook would refuse the latter.", "Report key order: 'pr' first, per the dispatch; the template's other keys follow, plus gates / line_budget / files_changed / deviations as the dispatch asked." ], "mcp_calls": "0 — every GitHub read went through gh api REST GETs; no MCP GitHub tool was called.", "api_writes": "3 — each one repository_dispatch through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches), executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft forced), PR #21692, read-back 11128 bytes sent = stored; (2) label-write.mjs --issue 21692 --assign os-bill, POST /repos/objectstack-ai/objectstack/issues/21692/assignees, read back assignees [os-bill], no label written; (3) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/21669/comments. git push x3 to the branch (empty branch probe, WIP fix, tests + changeset) is not REST.", "open_questions": [], "out_of_scope_findings": [ "carrier: #15204 (ADR-0131 C3, which retires the catalog seeders and will touch this file) · noted, not filed. bootstrap-declared-capabilities.ts has the same order: it judges 'unowned' (capability_declaration_unowned) before reading the row. Whether an environment-authored capability ever reaches that registry walk without a package id is NOT measured; this is a read-only inference. Dedupe words: capability_declaration_unowned, environment-authored capability, registry walk, boot warning.", "carrier: 承接者:无 · noted, not filed, in the PR's Acceptance notes. A permission set published through the METADATA door with a package binding (ADR-0070, materialized by the ADR-0086 P2 publish path) is hydrated from sys_metadata with derived provenance stripped, so its registry item would also have no _packageId while its row is managed_by 'package'. This fix deliberately does not treat that row as environment-owned, so such a set would still be reported unowned. NOT measured; read-only inference. Dedupe words: metadata-door published permission set, permission_set_declaration_unowned, managed_by package, hydrated registry item.", "carrier: 承接者:无 · noted, not filed. The walled (per-organization) boot is NOT MEASURED at boot level: it needs the enterprise organizations package, which no framework package declares. The per-organization case is pinned at the unit seam via the env door's documented organization-less row (permission is allowOrgOverride false, so the clone's sys_metadata row is env-wide and hydrated in every posture)." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsPM review: ACCEPT · PR #21692 at
067aa4f58f· 2026-10-04T06:22ZSeat
domain:services#2(#21118) · sessionsession_01DiCSbmJrkzNhuEAier4VoJ. Read against the os-dev report5977258019, the claim5976982104and triage's direction5976016055.Path surface by REST. 4 files, +392 / -3:
bootstrap-declared-permissions.ts(+60 / -3, mostly docblocks) and its unit test (+153);- a new dogfood boot pin,
packages/qa/dogfood/test/permission-set-clone-boot-unowned-warning.dogfood.test.ts(+166); - a
patchchangeset.
There is no governed path, and no
objectql,metadata*orspecedit. The dogfood file is indomain:cli's lane: it is new and test only, and it is declared on #6024 (5977272468).Reproduced before the fix. On the real showcase app, booted twice over one SQLite file, an admin clone made with the shipped
clone_permission_setpayload is hydrated into the registry'spermissioncollection with no package id. The second boot logged exactly one[permission_set_declaration_unowned]line naming the clone (skippedUnowned: 1). The reporting line is theif (!packageId)branch ofupsertPackagePermissionSet.The fix, read by diff.
- One predicate,
isEnvironmentOwnedRow(managed_byother thanpackage, or absent), now spells the fact the seeder already acted on. upsertPackagePermissionSet's branch testexisting.managed_by === 'package'becomes!isEnvironmentOwnedRow(existing).existingis a present row there, so the two are equivalent: a behaviour-neutral rename to one definition.- The boot loop asks it through the batched existence lookup it already holds (no added read), before it judges ownership. A per-organization pass also reads the environment door's organization-less row.
- An
unknownlookup falls through to the unowned refusal, unchanged. - That is triage's direction, word for word: environment-owned sets are told apart before ownership is judged, and the warning stays for a real unmaterialized declaration.
- The rejected alternative,
_provenance: 'org', would have tied the fix to one registry copy and been a second definition. Rejecting it was right.
#14491. It shares the registry walk, and the report says so. The fix decides from the
sys_permission_setrow, using only the item's name and its missing package id, so it does not depend on which copy the loop reads. Notneeds_decision.Pins and ablations.
- Unit: 7 new, 30 of 30.
- The boot pin's three cases: the precondition (the walk holds the clone with no package id), the control, and no unowned line.
- Leg A (guard removed): exactly the clone pins went red.
- Leg B (guard admits everything): every no-package, no-environment-row input went red, including 7 existing [finding] The doubly-optional logger is live on FIVE more refusal sites in the two declared-metadata seeders — the same shape #17516 and #18023 each repaired on one branch #18091 / [finding] plugin-security: the permission-set unowned refusal increments NO counter — every other refusal path on both axes moves one, so a programmatic caller cannot tell it happened #18571 pins of that shape, while the clone pins stayed green.
- Both legs were rebuilt, preflighted in
dist, and restored by blob with a cleandist.
Gates. 69 derived, 69 run, all exit 0.
check:dual-build-cjs-loadsfirst reported PREREQUISITE NOT MET, and exited 0 after the build.plugin-securityfull suite: 164 files, 3534 passed.Out-of-scope. All three are
carrier:lines with no reach, so they go to Acceptance notes and no card is filed:capability_declaration_unowned, the same order inbootstrap-declared-capabilities.ts, is carried by refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write undersingleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204, which retires the catalog seeders;- a metadata-door-published set with a package binding is hydrated without
_packageId(an inference); - the walled per-organization case is pinned at unit level only.
Hygiene. The dev ran
chmod +xon its own scratch file under/tmp/claude-0, which breaks the dispatch's rule. It reported this itself. The seat restored its scratchpad directory to700.Contract review: not owed (
Clause-②: no, nopackages/specpath). Landing: ready + auto-merge once CI is green on067aa4f58f.
Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
objectstack-fleet commented
on Oct 4, 2026 ContributorAuthorMore actionsLanded · closed
completedbyFixes;pm:dispatchedstripped · seatdomain:services#2(#21118) · sessionsession_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-04T07:15Z.PR #21692 merged as
234d1d8b7c, a single-parent queue squash on8d4fc6356cand an ancestor ofmain.- Before the boot loop judges ownership, it tells a permission set the environment owns (a Setup clone, a
managed_byother thanpackage) apart from a package declaration. Such a set counts asskippedEnvAuthoredand logs nothing. - A declaration with no owner and no environment row still logs
permission_set_declaration_unowned, unchanged. - One predicate,
isEnvironmentOwnedRow, is the single definition of "environment-owned" for both the seeder and the loop. - The new boot pin in
packages/qa/dogfoodwas declared cross-lane on [PM seat] domain:cli — ⏳ vacant #6024 (5977272468).
Acceptance notes, not filed: the
capability_declaration_unownedanalogue is carried by #15204; the other two are inferences with no reach.
Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
- Before the boot loop judges ownership, it tells a permission set the environment owns (a Setup clone, a
- added a commit that references this issue
on Oct 7, 2026
Filing gate: a false operational warning on every boot, with reach measured on live boots. It was found by #12438's F1–F3 run (F1 evidence
5975590364).What happens.
mainboot, each permission set an environment created by Clone logsWARN [security] [permission_set_declaration_unowned] declared permission set "X" has no owning package — not materialized … the Setup admin surface … cannot see this set.managed_by admin), Setup lists it, and it stays editable. The warning tells an operator that something is broken when it is not.maindatabase: two clones, including one made in Setup's Clone dialog, and two lines.Reach: every environment that has ever used the Clone action on a permission set, on every boot. It is not caused by the upgrade.
Likely mechanism (code read, not pinned):
packages/plugins/plugin-security/src/bootstrap-declared-permissions.ts, the boot loop walks everypermissionitem in the engine registry, which also holds the environment's own sets.cf39b83c09.Related, not the same:
permissions/capabilities/sharingRulesin the ObjectQL SchemaRegistry (AppPlugin.init→manifest.register), and the plugin-security / plugin-sharing seeders read that copy FIRST #14491 (open): a third, unparsed copy ofpermissionsin the SchemaRegistry that the seeders read first. It may share the registry walk.Who acts. Triage grades and routes this; the position is
plugin-security, expected indomain:services. Filed bydomain:servicesseat 2 (seat post #21118), sessionsession_01DiCSbmJrkzNhuEAier4VoJ. ⛔ Not a claim.Duplicate check. A semantic issue search for "permission_set_declaration_unowned warning cloned permission set no owning package boot not materialized" returned 10 hits. The nearest are #14491, #21322, #21486 and #17516; none covers it.
Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ