Skip to content

fix(security): platform default permission sets are stamped managed_by: 'platform' so os meta resync reconciles them (#8692) - #9129

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-8692-platform-owned-seed-provenance
Aug 16, 2026
Merged

fix(security): platform default permission sets are stamped managed_by: 'platform' so os meta resync reconciles them (#8692)#9129
os-project-manager merged 1 commit into
mainfrom
claude/issue-8692-platform-owned-seed-provenance

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#8692

Implements the maintainer ruling of 2026-08-15 (Option A): the platform default permission sets are platform-owned and resyncable. The decision is not re-opened here and the measurement is not re-run — PR #8804 delivered it (a seeded row stored 'admin'; a real resync returned resynced 0 / resyncSkipped 8) and it is the ruling's confirmed basis.

The defect

bootstrapPlatformAdmin seeded the default sets without writing managed_by, so the value fell to the declared defaultValue: 'admin' on sys_permission_set. os meta resync reconciles only rows the platform still owns (managed_by absent or 'platform'), so the platform's own defaults took the skip branch — every shipped set — and each skip was logged as an "intentional override" for a row no admin had ever touched. That is the inverse of what #2705 built the flag for.

The ruling's four constraints, each implemented

1. Forward-stamp only. The seed insert now stamps managed_by: 'platform', matching bootstrap-builtin-positions.ts and bootstrap-system-capabilities.ts, which have always stamped it. ⛔ No migration, no restamp of existing 'admin' rows — a stored 'admin' is indistinguishable between "the old seeder's field default" and "an administrator took this set over in Setup", so rewriting it could silently overwrite real admin edits on the next resync. Report-don't-rewrite.

A detail worth naming, because it is what keeps constraint 1 true at runtime: the stamp is written at the insert call site, deliberately not added to platformOwnedFields(). That helper is shared by the insert and the resync UPDATE, so putting managed_by in it would make every resync restamp the row it reconciles — converting a legacy 'admin' row into a platform-owned one and clobbering it on the same pass, which is exactly the data loss the ruling forbids. A comment on the helper now says so.

2. The warn stops lying. It read … row is admin-owned (intentional override); it now reads … row is admin-owned — provenance and action, no claim about intent. For a pre-ruling row that claim was false: the only writer may have been this same seeder one call earlier.

3. Both pins, not one. The #8804 suite now asserts both sides of the line the ruling drew — a fresh install (stores 'platform', resyncs every set, warns about nothing) and a legacy install (a pre-ruling 'admin' row is still skipped, with its content intact and its provenance unrestamped).

4. Recorded in the docblock. The seeder's module docblock states what pre-ruling installs carry, why they keep the skip permanently, and that re-owning the rows or --fresh is an operator's choice rather than one a boot makes for them — so the next reader finds a decision, not a mystery.

The endorsed side-finding, removed with the change

The comment claiming the posture "keeps the platform defaults env-authored — the posture bootstrapDeclaredPermissions relies on" was measured false and is gone. bootstrap-declared-permissions.ts special-cases onlymanaged_by === 'package' (its line 124); every other value — 'platform' included — falls to the same skippedEnvAuthored branch, so its behaviour is identical before and after this change. The same claim appeared twice in the file (the inline comment above the resync branch and the resync? option docblock); both were removed, since leaving either would keep the file asserting a reason the ruling measured false.

Tests

bootstrap-platform-admin-seeded-provenance.test.ts keeps driving a realObjectQL engine over better-sqlite3 with the real shipped defaultPermissionSets — 8 cases, up from 5:

sectioncaseasserts
forwardseeded row provenancestored 'platform', both at the driver and through the engine read the resync branch uses
forwardresync reconciles allresynced == the shipped set, resyncSkipped 0
forwardresync of a fresh installemits no warn at all
forwarda stale platform rowreconciled by content, and provenance NOT restamped
legacypre-ruling row still skippedresyncSkipped 1 / resynced n-1 on an upgraded install, stale payload still stale
legacythe skip logs neutrallyexact reworded sentence, and intentional override absent
attributionthe stamp beats the declared defaultsee below
anti-vacuitythe shipped defaults are non-empty and really seedguards every count above

The legacy pre-image is built through its original mechanism — inserted the way the old seeder wrote it, i.e. without managed_by, so defaultValue: 'admin' supplies the value — rather than hand-writing 'admin'. Each case asserts the resulting value before relying on it, so a moved default surfaces as a failure instead of quietly turning the legacy pin into a test of something else.

The ATTRIBUTION case now runs the opposite way round from the pre-ruling version, deliberately. Before, the seeder wrote no managed_by, so respelling the declared default flipped the stored value — which is what proved the default was the source. Now the seeder stamps explicitly, so the declared default must no longer be able to move the value at all: a clone of the declaration defaulting to 'package' still stores 'platform'. Without that case, respelling defaultValue: 'admin' to 'platform' in the object file would make every forward assertion pass with the stamp deleted.

The three hand-seeded cases in bootstrap-platform-admin.test.ts are untouched and still green. The shipped defaultValue: 'admin' on the declaration is also untouched — it is the right default for a set an admin creates in Setup, and the ruling changed the seeder, not the declaration.

Reverse verification

Run from the committed state, direction predicted before running: delete the stamp and the forward pins go red while ANTI-VACUITY stays green. Observed exactly that — 7 failed, 1 passed:

× a seeder-created row is stored `managed_by: "platform"` -> expected 'admin' to be 'platform'
× so `os meta resync` now reconciles EVERY platform default set -> expected +0 to be 8
× a resync of a fresh install warns about nothing -> expected [ ...(8) ] to deeply equal []
× LEGACY: a pre-ruling `admin`-stamped row is still skipped -> expected 8 to be 1
× ATTRIBUTION: the seed stamp beats the declared default -> expected 'package' to be 'platform'
✓ ANTI-VACUITY: the shipped defaults are non-empty and really get seeded

The ATTRIBUTION line is the informative one: with the stamp gone the row falls to the clone's 'package' default, which is the direct proof that the stamp — not a declaration that happens to agree — is what supplies the value. Restored by checking the file back out of the branch (the fix was committed first, so a restore point existed); the numbers below are from the restored tree.

Verification — all at 6e744ce70, the head this PR proposes

  • pnpm --filter @objectstack/plugin-security test -- --maxWorkers=265 files, 1246 tests passed
  • pnpm --filter @objectstack/plugin-security typecheck — clean, exit 0
  • Gates re-derived against the actual changed paths with node scripts/pm/dispatch-gates.mjs (the dispatch prompt named no families, so this derivation is the list): 9 path-matched + 6 convention-triggered, all run and green — check:changeset-gate-self-tests, check:cross-package-test-inputs, check:objectui-changeset, check:test-source-alias, check:type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check:query-options-erasure, check:type-check-coverage, check:engine-double-contract, check:where-matcher, check:i18n, check:type-check-debt, plus check:nul-bytes.

Two gates first refused to run, and a refusal was not taken as a pass. check:i18n refused pending an @objectstack/cli build; check:type-check-debt --re-measure refused pending the full workspace closure (@objectstack/service-knowledge unbuilt). Both were built exactly as each gate's message prescribes and then ran for real: check-i18n-bundles: OK (9 package(s) — all bundles in sync) and check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured, none above its recorded number. Exit codes were read with echo "EXIT=$?" into a log rather than through a pipe, since both scripts warn that piping reports the pipe's status.

Changeset: .changeset/platform-default-permission-sets-platform-owned.md (patch, @objectstack/plugin-security) — this changes what a fresh install stores and what os meta resync does.


Generated by Claude Code

…latform'` so `os meta resync` reconciles them (#8692)
`bootstrapPlatformAdmin` seeded the default permission sets without writing
`managed_by`, so the value fell to the declaration's `defaultValue: 'admin'`.
`os meta resync` reconciles only platform-owned rows, so every shipped default
set took the skip branch (measured: resynced 0 / resyncSkipped 8) and each skip
was logged as an "intentional override" for a row no admin had touched.
Implements the maintainer ruling of 2026-08-15 (Option A):
- the seed insert stamps `managed_by: 'platform'` explicitly, matching
`bootstrap-builtin-positions.ts` and `bootstrap-system-capabilities.ts`;
- FORWARD ONLY -- no migration, no restamp of existing 'admin' rows, since a
stored 'admin' cannot be told apart from a real Setup takeover and rewriting
it could silently overwrite admin edits on the next resync;
- the skip warn drops its "(intentional override)" claim of intent;
- the seeder docblock records what pre-ruling installs carry and why they keep
the skip;
- two comments claiming the posture "keeps the platform defaults env-authored --
the posture bootstrapDeclaredPermissions relies on" are removed: measured
false, since bootstrapDeclaredPermissions special-cases only 'package'.
The #8804 pin suite now asserts both sides: a fresh install stores 'platform'
and resyncs every set, and a pre-ruling 'admin' row is still skipped with its
content intact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y26DJEHSBhhAQ6wwfsHNza
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security.

12 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx(via @objectstack/plugin-security)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/plugin-security)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/plugin-security)
  • content/docs/permissions/access-recipes.mdx(via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx(via @objectstack/plugin-security)
  • content/docs/permissions/explain.mdx(via @objectstack/plugin-security)
  • content/docs/permissions/permissions-matrix.mdx(via packages/plugins/plugin-security)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/plugin-security)
  • content/docs/plugins/index.mdx(via @objectstack/plugin-security)
  • content/docs/plugins/packages.mdx(via @objectstack/plugin-security)
  • content/docs/ui/audience-based-interfaces.mdx(via packages/plugins/plugin-security)
  • content/docs/ui/dashboards.mdx(via @objectstack/plugin-security)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/plugin-security)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 16, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 16, 2026 13:09
@os-project-manager
os-project-manager added this pull request to the merge queueAug 16, 2026
Merged via the queue into main with commit 712e185Aug 16, 2026
26 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-8692-platform-owned-seed-provenance branch August 16, 2026 13:28
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-project-manager@claude