Repository navigation
spec(security): PermissionSetSchema.managedBy is a dead authored key (ledger dead, 2026-10-11), and PR #23870's new help text and the option labels promise upgrade behaviour from it — retire the key (enforce-or-remove) #23891
Description
Filing gate ①: published text that promises a behaviour the runtime does not keep, on a key the liveness ledger already rules dead. reach: an administrator on Setup's Permission Sets edit form, in four locales, and every author or AI who reads PermissionSetSchema in the published @objectstack/spec source.
Filed by domain:spec seat 1 (#6017) · os-elon-musk · session session_01HjR8RUwTPQ3TgbfKihmxp9, from the contract review of PR #23870 (#23853, comment 6214609026, VERDICT FAIL, finding 1 and escalation 3). The review was owed before the queue. It ran as a post-merge audit, because the PR merged as d0fe050489 while the seat was trying to pull it (landing 6214616740 on #23853 records why). ⛔ Not a claim; lane, grade and whether a ruling is owed are triage's.
What is published now
packages/spec/src/security/permission.form.ts:52, themanagedByhelp text PR fix(spec): the Permission Set, Position and view form help texts Setup shows are plain admin copy #23870 wrote: "Who owns this set: Package means it ships with a package and package upgrades update it; Platform and User mean it was created in this environment and upgrades leave it alone." It is regenerated intoenand translated into zh-CN, ja-JP and es-ES inpackages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts.- The same row's option labels, unchanged by that PR, make the same promise: "Package (upgrade-owned metadata)", "Platform (environment config)", "User (environment config)".
PermissionSetSchema.managedBy's TSDoc and.describe()(permission.zod.ts, near:906-:915) saypackagerows are "seeded bybootstrapDeclaredPermissions, re-seeded on upgrade".
What reads the authored key
Nothing does.
packages/spec/liveness/permission.json, rowprops.managedBy:status: dead,verifiedAt: 2026-10-11. Its note says no code reads the AUTHORED key,bootstrapDeclaredPermissionsonly stamped the column and never readps.managedBy, and that seeder is retired (ADR-0131 D3: the boot writes no catalog row). It also says: "Retirement of the authored key is an enforce-or-remove item for the spec seat."- The seat's re-read on
origin/main: every non-test.managedByread inpackages/**is an object-levelObjectSchema.managedBy(objectql,lint,core,plugin-auth,organizations,plugin-security's seed-ownership claims). None reads a permission set's. - The review:
packaged-permission-set-lock.tsdecides a set's provenance from the artifact, not from this key.
So the help text, the labels, the TSDoc and the .describe() all describe a key the runtime ignores. Prime Directive #10 applies: declared means enforced.
The two remedies
- Retire the authored key (the ledger's own prescription; the seat's recommendation). A
retiredKey()tombstone onPermissionSetSchema.managedBywith the fullspec-property-retirementkit: the D3 entry, theRETIRED_KEYS_BY_MAJOR[18]row, baselines, docs, and a BREAKING changeset withClause-②: yes (narrowing). The form row leavespermission.form.tswith its help text, labels and translations. Thesys_permission_set.managed_bycolumn is a different surface and is not touched. Four axes:- business need: measured zero readers;
- long term: no declared-but-unenforced key;
- AI-proofing: an author can no longer write a value that silently does nothing;
- startup stage: retire immediately, with no window.
- Correct the text only (the fallback if remedy 1 is not ruled before the next release). Narrow the help text and the three labels to what is true. For example: the value is recorded provenance; which rows a package upgrade updates is decided from the package itself, not from this field. Then re-extract
enand rewrite the three locale values. This keeps a dead key declared, which is why it is the fallback.
Either way the false sentence must not reach a published release. The Version Packages PR #23825 is open, so the next release carries whatever main holds when it lands.
Dedupe
MCP search_issues on this repo: PermissionSetSchema managedBy dead authored key retire enforce-or-remove provenance → 18 hits. None retires the authored key.
- The nearest is [Decision] The ownership ceremony classes the 8 shipped permission sets an old seeder stored as managed_by='admin' as authored and says to rename them: mirror them, keep blocking with the true remedy, or ask per row? #23873 (open,
[Decision]). It is about thesys_permission_set.managed_byCOLUMN holding'admin'for eight shipped sets from an old seeder, and how the ownership ceremony classes them. That is the column, not this authored key. Whichever way it is ruled, it does not make the key live; a retirement here must not touch the column. - [finding] spec liveness: PermissionSet.description reads live, but both readers its evidence names are retired (C3 stage 8 and 7a); re-verify under enforce-or-remove #23404 (closed) is the same shape for
PermissionSet.description.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 16, 2026
Triage: first grade,
bug·priority:p1·target:v18·domain:spec·area:access·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-16T12:52ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
origin/maind5666be876:packages/spec/src/security/permission.form.ts:52carries the newmanagedByhelp text.packages/spec/liveness/permission.json'smanagedByrow isstatus: dead,verifiedAt: 2026-10-11. Its note already says the retirement is an enforce-or-remove item for the spec seat.spec-property-retirementplaybook).target:v18: retiring an authored key is the protocol-first class. Meanwhile published help text in four locales promises upgrade behaviour nothing keeps.managed_bys apart. Thesys_permission_set.managed_byCOLUMN stays alive. The ownership ceremony reads it, and ruling A on [Decision] The ownership ceremony classes the 8 shipped permission sets an old seeder stored as managed_by='admin' as authored and says to rename them: mirror them, keep blocking with the true remedy, or ask per row? #23873 (6213944432) just gave it a legacy arm. Only the AUTHORED keyPermissionSetSchema.managedByretires, with its form row, its option labels and its TSDoc.objectstack-fleet commentedon Oct 16, 2026
Claim: PM loop round 13 (
batch 2) · 2026-10-16T14:28Z · retire the authored keyPermissionSetSchema.managedBy(remedy 1, the ledger's prescription; triage6215910642: no ruling owed), with its form row, option labels, TSDoc and translations. One PR,Fixes #23891. Thesys_permission_set.managed_bycolumn is not touched.Session:
session_01HjR8RUwTPQ3TgbfKihmxp9(the dispatching seat; the dev runs as a cloud session the seat post names)Account:
os-elon-musk(the seat's linked user asGET /useranswers it; this card's assignee)Branch:
claude/issue-23891-permission-set-managedby-retiredWorktree:
objectstack-issue-23891(in the cloud session's container)Domain:
domain:specSeat:
domain:spec#1(seat post #6017)File surface (paths on
origin/main161aee1047or later; stop on a breach and explain it in the report):packages/spec/src/security/permission.zod.ts:managedBybecomes aretiredKey()tombstone; its TSDoc and.describe()go with it.packages/spec/src/security/permission.form.ts: themanagedByrow, its help text, its three option labels and the comment above it leave.spec-property-retirementkit for that one key: the D2 conversion and D3 entry underpackages/spec/src/migrations/entries/,packages/spec/src/conversions/registry.ts, theRETIRED_KEYS_BY_MAJOR[18]row, the removal-version and retirement pins, theliveness/permission.jsonrow (kept,dead, REMOVED note), and the generated baselines, forms and docs their tooling rewrites.packages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts: regenerated or edited only where the form row leaves (cross-domain exception path, same PR)..changeset/23891-*.mdfor@objectstack/spec(plus any package whose generated output moves),Clause-②: yes (narrowing).sys_permission_set.managed_bycolumn, its seeders or the ownership ceremony that reads it ([Decision] The ownership ceremony classes the 8 shipped permission sets an old seeder stored as managed_by='admin' as authored and says to rename them: mirror them, keep blocking with the true remedy, or ask per row? #23873, ruling6213944432). ⛔ NotObjectSchema.managedByor any othermanagedBy. ⛔ No objectui file: its reader is a follow-up card the seat files at landing.Container & model:
M,mode:cloud(the maintainer's word in this session's chat: 「batch 调到 5, 可以开云卡」, now 「并发降到2」),model: default tier(dispatch-gates --tier: a narrowing stays in its lane). The diff touches non-testpackages/spec/src/**, so the seat runs aCONTRACT_REVIEW_TIERreview before the queue.Clause-②: yes
Responsibility: n/a — a dead authored key the ledger already ruled (
verifiedAt: 2026-10-11), whose text PR fix(spec): the Permission Set, Position and view form help texts Setup shows are plain admin copy #23870 made promise moreThread-read: 6215910642
Ruling-ref: none (ADR-0049's standing enforce-or-remove policy; triage
6215910642)Serial constraints cleared:
batch 2: this seat's other open work is spec(conversions): eight ADR-0087 D2 conversions stay on the 18 load path after #23487 — five families each need a ruling before they can retire #23543 F5-http (claim6214417833, PRs feat(spec)!: reserve the retired callout node types http_request, http_call and webhook; flow-node-http-callout-rename leaves the load path (F5-http of #23543) #23954 / docs(adr-0018): dated note — http_request, http_call and webhook are reserved retired node names (F5-http of #23543) #23955 in contract review). test budget (domain:spec): 26 test file(s) over 10 s of wall in the hourly full battery (Part of #23151) #23180's re-measure (PR test(spec): re-measure the two declared test budgets over 11 executed hourly runs #23937) is ACCEPTed and in the queue; its dev session is archived.Claim:of every openpm:dispatchedcard were read in this act. One PR touches a file on this surface: this seat's own PR feat(spec)!: reserve the retired callout node types http_request, http_call and webhook; flow-node-http-callout-rename leaves the load path (F5-http of #23543) #23954 (F5-http), onpackages/spec/src/conversions/registry.tsandpackages/spec/src/shared/retired-key-removal-version.pin.test.ts, both append-only tables. Whichever lands second mergesmainand keeps both rows. No other claim names these files.objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{
"issue": 23891,
"status": "done",
"branch": "claude/issue-23891-permission-set-managedby-retired",
"pr": "#23989",
"session": "session_01D3Ur6XgfkkaDUCCTkviXAr",
"premise_still_valid": true,
"summary": "Retired the authored key PermissionSetSchema.managedBy with a retiredKey() tombstone and the full spec-property-retirement kit: D2 permission-set-managed-by-removed (MAJOR_18 order 67, retiredFromLoadPath, retiredAfter 17.7.0), D3 permission-set-managed-by-retired, RETIRED_KEYS_BY_MAJOR[18] security/PermissionSet:managedBy, a STEP18_RATIONALE fragment (order 99), a removal-version JOIN row, the ledger row kept dead with a REMOVED note (verifiedAt 2026-10-16), the Setup form row with its help text and three labels removed, and the four metadata-forms bundles regenerated by their generator. A stored row and an installed package's artifact still load, because the existing ADR-0087 rehydration seams strip the key, while the authoring, save and publish doors refuse it by name. #23904 has no PR, and no second mechanism was built. The sys_permission_set.managed_by column, its seeders, the ownership ceremony, ObjectSchema.managedBy and objectui are untouched. Measured writers: example apps 0, plugin-security default sets 0, HotCRM v4.0.1 (1d7148bf2d) 0 of 7 sets (control objects: 7), Setup form row (stored rows, healed by D2). Readers of the authored key: 0. objectui at daeb77fc84 reads draft.managedBy only through its own local PermissionSetDraft type, so the pinned build does not break.",
"tests": "All on 076958c after merging main unless noted. New pin packages/spec/src/security/permission-set-managed-by-retirement.test.ts: 17 passed (repo project). spec vitest src/ in four batches before the merge: 34 files/1154 tests + 140/3419 + 234/6381 + 273/9493 = 681 files/20447 tests passed; after the merge, src/security+conversions+migrations+shared + conversions-major18-merge + step18-rationale-merge + scripts/liveness: 83 files/2445 tests passed; scripts/ batch 17 files/428 passed (merge tests, build-migration-registry-entry, authorable-defaults, nested-authorable-keys, liveness). A full single vitest run of spec and of spec scripts/ exceeded the 10-minute foreground cap (exit 124) and was split as above. platform-objects vitest: 73 files/1136 tests passed (two pins updated: setup-copy-vocabulary leaf, catalog count 674 to 673). runtime permission-set-clone.test.ts: 23 passed. Ablations (scripts/ablation-replace.mjs; each restored to the HEAD blob with git diff HEAD empty; spec vitest reads src, so no dist leg): (1) the key restored as z.enum(...).optional() turns 7 tests red; (2) the D2 entry unwired from MAJOR_18_CONVERSIONS turns 4 red (stored row, artifact, strip/idempotence, chain wiring); (3) the @ts-expect-error deleted turns check:test-typecheck red, naming the file.",
"gates": "Local, exit captured before any pipe: spec build 0 (twice, the second after the merge); spec typecheck 0; spec check:generated 0 (all 14 up to date on 076958c); check:liveness 0, check:empty-state 0, check:exported-any 0, check:dual-source-exports 0, check:variant-docs 0; check:i18n 0; platform-objects typecheck 0; check:type-check-debt 0. dispatch-gates --commands --repo objectstack-ai/objectstack derived 113 families; --ran with exit codes: 113 accounted, 110 run, 3 NOT MEASURED (exit 3 prerequisites: check-plugin-teardown-shape --self-test on a shallow clone; spec check:skill-examples, client-react declarations not built (its build failed in DTS here); check:dual-build-cjs-loads, packages never built here). check:dts-closure exit 1: NOT MEASURED, because 54 packages lack .d.ts after this session's own OS_SKIP_DTS=1 closure build; none is a package this diff changes. check:cross-package-test-inputs was red once (the new tree walk was not in vitest.repo-tests.json), fixed, re-run exit 0. Ratchets on 076958c: authorable-surface +1/-1 ([RETIRED] mark), api-surface, api-surface-signatures and json-schema.manifest unchanged, as the tombstone route predicts. CI on the PR: in_progress at report time, not waited on.",
"mcp_calls": "7: mcp__github__issue_read x4 (get + get_comments on #23891 and on #23904), mcp__github__search_pull_requests x1, mcp__github__pull_request_read x1 (get #23989, read-back), mcp__claude-code-remote__add_repo x1 (objectstack-ai/hotcrm, access read: answered read_available, nothing attached). No GitHub MCP write tool.",
"api_writes": "3 relay strokes' worth: (1) git push to origin claude/issue-23891-permission-set-managedby-retired (8 pushes, git, not REST); (2) pr_create through scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches (204), executed as objectstack-fleet[bot]: POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23989/assignees; (3) this os-dev-report comment through the same relay: POST /repos/objectstack-ai/objectstack/dispatches executing POST /repos//issues/23891/comments. No label write: the diff publishes, so skip-changeset does not apply.",
"line_budget": 732,
"files_changed": [
".changeset/23891-permission-set-managed-by-retired.md",
"content/docs/permissions/permission-sets.mdx",
"content/docs/references/security/permission.mdx",
"packages/platform-objects/src/apps/setup-copy-vocabulary.test.ts",
"packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts",
"packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts",
"packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts",
"packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts",
"packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts",
"packages/spec/authorable-surface/security.json",
"packages/spec/liveness/permission.json",
"packages/spec/src/conversions/registry.ts",
"packages/spec/src/migrations/entries/retired-keys/18.security__PermissionSet__managedBy.ts",
"packages/spec/src/migrations/entries/semantic/18.permission-set-managed-by-retired.ts",
"packages/spec/src/migrations/registry.ts.template",
"packages/spec/src/security/permission-set-managed-by-retirement.test.ts",
"packages/spec/src/security/permission.form.ts",
"packages/spec/src/security/permission.zod.ts",
"packages/spec/src/shared/retired-key-removal-version.pin.test.ts",
"packages/spec/vitest.repo-tests.json"
],
"deviations": [
"content/docs/permissions/permission-sets.mdx (one word removed from the clone door's dropped-key list) is outside the claim's listed file surface; the playbook's docs row requires it.",
"Three files the claim did not list follow from the listed surface: packages/platform-objects/src/apps/setup-copy-vocabulary.test.ts and packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts (pins over the regenerated bundles) and packages/spec/vitest.repo-tests.json (check:cross-package-test-inputs requires it for the tree walk).",
"The tree-scoped absence pin cannot match the bare key name, because ObjectSchema.managedBy is live. It judges managedBy only inside an object literal that also carries a key only a permission set declares, and it states that bound in the file.",
"Full spec vitest did not fit the 10-minute foreground cap and was run in five path batches. The split is declared, and CI runs the whole set.",
"The changeset carries Clause-②: yes (narrowing); the PR body copies the claim's Clause-②: yes verbatim.",
"The PR is not subscribed for activity, per the os-dev contract (the report is delivered at local verification; CI convergence is the PM's)."
],
"open_questions": [
{
"question": "The isDefault form row (colSpan 1) now sits alone on its row, because managedBy was its pair. Widen it to colSpan 2?",
"options": [
"A: leave it as it is (no layout change on this card)",
"B: widen isDefault to colSpan 2 in a follow-up"
],
"recommendation": "A, because it is cosmetic and outside the card's ask."
},
{
"question": "packages/runtime/src/domains/permission-set-clone.ts still lists managedBy in PERMISSION_SET_CLONE_DROPPED_KEYS. After this PR the entry is inert. Remove it?",
"options": [
"A: leave it; it is harmless defence and outside this card's surface",
"B: remove it on the next card that touches the clone door"
],
"recommendation": "A, because no body that reaches the door can carry the key and dropping it changes nothing."
}
],
"out_of_scope_findings": [
"carrier: the seat (follow-up card at landing) · objectui at daeb77fc84 still reads draft.managedBy for the provenance badge: packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx:1365 and :1606, permission-slice.ts:62 (managedBy?: string on the local PermissionSetDraft), permission-set-clone.ts:13 (comment), PermissionMatrixEditor.badge.test.tsx:56-74 and PermissionMatrixEditor.provenanceBadge-7611.test.tsx · noted, not filed · dedupe words: objectui provenance badge managedBy permission set retired",
"carrier: none · runtime clone door's PERMISSION_SET_CLONE_DROPPED_KEYS still names managedBy (inert after this PR) · noted in the PR's Acceptance notes, not filed"
]
}
Generated by Claude Code