Skip to content

feat(plugin-security): the curated platform capabilities are declared capability metadata (ADR-0131 D3, #15204 stage 6b-1a) - #22669

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-15204-s6b1a-capability-declarations
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-15204-s6b1a-capability-declarations

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #15204
Clause-②: no

Stage 6b-1a of the cutover plan only (claim amendment 6095767104): the platform's nine curated capabilities (PLATFORM_CAPABILITIES, packages/spec/src/security/capabilities.ts) are declared capability metadata of plugin-security, so the security catalog's one home (ADR-0131 D3; #15196: "Positions, permission sets and capabilities have exactly one home — the registry") holds them. The rest of #15204 stays open: no seeder is edited or deleted (that is 6b-1b, after C9), no sys_capability row is written or deleted, and who may author a capability is unchanged (#22621 → A).

Premise, re-verified on origin/main d85615dd

The curated capabilities had no registry home. On a booted showcase, in all three postures, the engine registry, the metadata service, the catalog read and GET /api/v1/meta/capability each held exactly the stack's two capabilities (showcase.export_data, showcase.restricted_ops), and GET /api/v1/meta/capability/manage_users answered 404 RESOURCE_NOT_FOUND. The nine curated names existed only as sys_capability rows. No open PR and no claude/* branch declared them.

Before / after (booted showcase, same worktree; before = d85615dd, after = this branch)

reading single single + Default Organization walled
createSecurityCatalogReader().list('capability') 2 → 11 2 → 11 2 → 11
GET /api/v1/meta/capability 2 → 11 2 → 11 2 → 11
engine registry capability items 2 → 11 2 → 11 2 → 11
metadata service capability items 2 → 2 2 → 2 2 → 2
GET /meta/capability/manage_users 404 → 200 (name, label, description, scope, package provenance) same same
PUT /meta/capability/manage_users (platform admin) 403 → 403 same same
PUT /meta/capability/zz_s6b1a_control (control) 403 → 403 same same
sys_capability rows (name, managed_by, package_id, label, scope) 11 → identical identical identical
capability_platform_name_refused lines in the boot log 0 → 0 0 → 0 0 → 0

The nine new entries answer from the registry with packageId: 'com.objectstack.plugin-security'. The PUT stays refused; only its message changes, from "the type is code-only" to "provided by a managed package and its type is code-only".

What changed

  • builtin-capabilities.ts (new). registerBuiltinCapabilities registers each PLATFORM_CAPABILITIES entry (name, label, description, scope) through registry.registerItem('capability', item, 'name', SECURITY_PLUGIN_ID). The list is the spec's own (securityBuiltinCapabilities === PLATFORM_CAPABILITIES), so there is one list, not a copy. CapabilityDeclarationSchema already declares every curated field, so the spec is not widened.
  • The route: the registry's item seam, as S2 (feat(plugin-security)!: the six built-in positions are declared position metadata (ADR-0131 C2 stage S2) #22139) did for the built-in positions. The engine does decompose a manifest capabilities collection, but the package door in front of it (SchemaRegistry.refuseSecurityCatalogNameConflicts, the Q4 A one-holder rule) refuses a package that declares a built-in name, and the curated names are exactly BUILT_IN_SECURITY_CATALOG_NAMES.capability. The item seam with a package id asks only whether another package holds the name. SecurityPlugin.start calls it beside registerBuiltinPositions, and says so once at warn if the engine has no registry to register into.
  • The one adapter (see Deviations). bootstrapDeclaredCapabilities reads the registry's capabilities as package declarations. Handed the curated ones, it would refuse each as a package claiming a curated name (nine false capability_platform_name_refused lines every boot), and it would stop falling back to the metadata service when the registry holds no package declaration. The seeder may not be edited (batch Release version 0.4.0 #310 item 4), so the call site in security-plugin.ts hands it an engine view whose registry lists every capability except this plugin's own curated declarations (withoutPlatformCapabilityDeclarations). readDeclaredCapabilityContext (declared-capability-context.ts, not a seeder) drops the same items. Both readers see exactly what they saw before. The view goes with the seeder in 6b-1b.

The one-holder rule, measured

  • Fresh database and a database the seeders already populated. builtin-capabilities.boot.test.ts boots the plugin twice on one SQLite file. The second boot registers cleanly, writes zero sys_capability rows, and leaves the same census. Existing rows are rows, not registry holders.
  • Showcase. Registration raised no SecurityCatalogNameConflictError in any posture.

The derived half (not declared here)

The seeder's derived half humanizes a systemPermissions string that no curated entry and no declaration names. Measured: every systemPermissions entry of the platform's default sets is a curated name. The showcase's only non-curated one, showcase.export_data, is the stack's own declared capability and already resolves through the catalog read. No humanized placeholder is declared.

Pins

  • builtin-capabilities.test.ts:
    • the declared list is the spec list itself, and each entry parses as a CapabilityDeclarationSchema;
    • registration: type, key field, owner, the four fields, and a copy per registration;
    • the declaration predicate and the engine view;
    • readDeclaredCapabilityContext still falls back to the metadata service.
  • builtin-capabilities.boot.test.ts: a real boot (init, start, every kernel:ready handler) over ObjectQL on SQLite, in single and walled, with the stack's capability in the registry or in the metadata service only, each on a fresh and on a re-opened database. It asserts:
    • the catalog read lists every curated name @registry, owned by this plugin, with the spec's fields;
    • the sys_capability census equals the one derived from the producers (the curated pass's rows plus the stack's package row), and the second boot writes nothing;
    • no warning about a capability.
  • packages/qa/dogfood/test/security-catalog-showcase.dogfood.test.ts: its expected capability set adds PLATFORM_CAPABILITY_NAMES, read off @objectstack/spec. Its door-parity rows stay as they were (the read and the door agree on 11).

Ablations. Each went through scripts/ablation-replace.mjs (anchor hit 1 → 0, blob changed). After each run the blob equalled HEAD and git diff HEAD was empty.

ablation file red
the seeder gets the raw engine (no view) security-plugin.ts 6 of 23: the no-warning pin in all four scenarios; the census in both metadata-service-only scenarios (the stack's capability is no longer seeded)
no registration (if (0 === 0)) security-plugin.ts 8 of 23: the catalog-read pin and the no-warning pin (the absence warn fires) in all four scenarios
the context read keeps the curated items declared-capability-context.ts 2 of 23: both fallback pins

The third ablation's first attempt was a no-op: its replacement text was a substring of the anchor, so ablation-replace refused it and restored the file. The second attempt used a distinct replacement.

Verification (at cca8fe4648)

  • plugin-security: build; typecheck exit 0 (tsc, scripts, check:test-typecheck); vitest run 197 files, 4124 passed, 45 skipped.
  • Dogfood: security-catalog-showcase, audit-log-audit-capability, me-apps-and-everyone-baseline and org-admin-affordance-reach (4 files, 64 passed). Runtime: standalone-stack-seeder-declaration-copy (13 passed).
  • Gates: dispatch-gates --commands derives 70 commands for this diff. All 70 exit 0, and --ran reconciles them: 70 derived, 70 run, 0 NOT-MEASURED (a derived zero, with exit codes recorded).
    • On the first pass, check:plugin-teardown-shape --self-test and check:dual-build-cjs-loads answered PREREQUISITE NOT MET: a shallow clone, and eight packages outside this diff's closure were unbuilt. Both passed once the clone was unshallowed and those packages were built.
    • check:query-options-erasure caught two as any find options in the new boot test (test surface 236 → 238). They are typed in cca8fe4648, and the surface is back at 236.

Deviations

  • The call-site engine view is a transitional adapter. The stage plan forbids projectors, dual writes, row fallbacks and id bridges; this is none of those, but it exists only because the seeder it feeds may not be edited. The alternatives were: S2's route (edit the seeder), forbidden by batch Release version 0.4.0 #310 item 4; registering after the seeding pass, which is order-dependent and leaves the declarations missing whenever bootstrap fails early; or stopping. The PM's report carries this as an open question.
  • One file outside the declared landing zone: packages/qa/dogfood/test/security-catalog-showcase.dogfood.test.ts (a domain:cli path). It pinned the capability set to the stack's names only, so it reds with the nine registered. S2 made the same edit for positions.

Acceptance notes

  • core/src/security/security-catalog.ts's module doc records a measured table (capability: 2 per reader) taken at 3d9188502e. It is a dated measurement, not a false claim; after this stage the engine registry holds 11 on the showcase. Noted for whoever next edits that seam (owner: none).
  • skip-changeset does not apply: @objectstack/plugin-security publishes dist, and the changeset is minor.

Generated by Claude Code

…apability metadata (ADR-0131 D3)

Claude-Session: https://claude.ai/code/session_014DUFKvmT2Vyzx42PyAAXV3
Co-authored-by: Claude <noreply@anthropic.com>
… beside the curated declarations

Claude-Session: https://claude.ai/code/session_014DUFKvmT2Vyzx42PyAAXV3
Co-authored-by: Claude <noreply@anthropic.com>
…ies beside the stack's; changeset

Claude-Session: https://claude.ai/code/session_014DUFKvmT2Vyzx42PyAAXV3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 10, 2026
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 12 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/capabilities.mdx (via /api/v1/meta/capability (route, a path literal in a comment on a changed line; a path literal in start), /api/v1/meta/capability/:name (route, a path literal in a comment on a changed line))
What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 16 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 243dd3c6256673dc1a56f9419be03c397129f825 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a93b2cd2ff4d31e651af2b135f6fc93b12bca441 — the merge of head 1c502d391a910d83d876d0e5cfbfe235bed3691f into base 243dd3c6256673dc1a56f9419be03c397129f825, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a93b2cd2ff4d31e651af2b135f6fc93b12bca441 && git checkout a93b2cd2ff4d31e651af2b135f6fc93b12bca441
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 243dd3c6256673dc1a56f9419be03c397129f825 1c502d391a910d83d876d0e5cfbfe235bed3691f && git checkout -B drift-repro 243dd3c6256673dc1a56f9419be03c397129f825 && git merge --no-ff 1c502d391a910d83d876d0e5cfbfe235bed3691f

node scripts/docs-audit/affected-docs.mjs --json 243dd3c6256673dc1a56f9419be03c397129f825

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 243dd3c6256673dc1a56f9419be03c397129f825 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Merge-queue kick-out: signature and first reading · epic PM session_01Rerax7QTjKMPCUZxQUtPFR · 2026-10-10T13:29Z

What happened. Queue build gh-readonly-queue/main/pr-22669-243dd3c6 (CI run 38053815441) was dequeued at 13:27Z with reason CI_FAILURE.

Signature:

  • Failed step: Dogfood Regression Gate (2/3) → step 10, "Boot example apps and exercise real user flows" → cancelled.
  • First error: the annotation "The job has exceeded the maximum execution time of 30m0s". The aggregate Dogfood Regression Gate then failed.
  • The job log could not be fetched: blob storage answered Forbidden through this container's proxy. The stall-diagnostics upload step was skipped.

The three facts for a blind re-queue:

  1. ✗ The failing step's import closure is not disjoint from this diff. SecurityPlugin.start now registers the curated capabilities, and every dogfood boot runs it.
  2. ✓ The queue base 243dd3c6 passed its own queue build: fix(service-automation)!: flow CEL record is the record the run was handed, or unbound #22674's, run 38049241741, success.
  3. ✓ The first error is a timeout, not an assertion.

Fact 1 fails, so per the landing rules this is treated as a new signature: no blind re-queue.

First reading (not a diagnosis):

Next: merge origin/main into this branch (relay pr_update_branch), so the PR's own CI runs the dogfood shards on the current base.

  • If shard 2 hangs again, the dev reproduces it on the merged tree (a patch round).
  • If it is green, re-queue once; a second identical stall is real.

Part of #15204. That card carries a pointer to this comment.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Re-queued once · epic PM session_01Rerax7QTjKMPCUZxQUtPFR · 2026-10-10T13:51Z

This follows the kick-out record 6098007229. Head 1c502d391a is the merge of origin/main. On it, CI has 32 success and 3 skips, and all 3 skips are on the roster: Build Docs, Console Pin Gate, and Packed-tarball smoke (opt-in). All three Dogfood Regression Gate shards passed on the current base. The PR was added to the merge queue at 2026-10-10T13:51:14Z.

This is the single re-queue. If shard 2 stalls the same way again, I will treat it as real and open a patch round to reproduce it on the merged tree.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants