Skip to content

fix(metadata-protocol): a packaged action or permission set's refusal names its own sanctioned path (ADR-0126 Regime C rows) - #21026

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-20910-regime-c-action-permission-paths
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-20910-regime-c-action-permission-paths

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20910
Clause-②: no

What this changes

ADR-0126 §3 puts flow, action and permission in Regime C, and §2 requires the Regime C refusal to name the sanctioned path. Before this PR, only flow's package-less refusal at the environment-scoped package door did that. Every other refusal of an in-place write onto one of these packaged items prescribed the OS_METADATA_WRITABLE hatch or a redeploy, and a packaged action's removal named no path at all.

This PR follows triage's direction on the card (one regime, per-type sanctioned routes, the sentence built from the row) and the seat answer on Q1 and Q2 (comment 5923536089):

  • One table, one new module. packages/metadata-protocol/src/packaged-base-regime.ts holds PACKAGED_BASE_REGIME. Each row is { regime, routes }, and routes declares only the primitives the type has: clone, switchOff, or both. A row that names neither does not compile. The module builds the prescription (the row's paths in a fixed order, clone then switch, then the ADR-0126 citation) and the whole package-door sentence. Nothing in it reads the type.
    • The module is not exported from the package entry.
    • It lives in its own module because protocol.ts imports sys-metadata-repository.ts, so the table could not live in either file without an import cycle.
  • Rows:
    • flow: clone (POST /api/v1/automation/:name/clone) and switch (POST /api/v1/automation/:name/toggle).
    • action: the switch only, POST /api/v1/actions/_activation/:object/:action with {enabled: false}, where :object is global for an object-less action. No clone, because the action-clone half is not chartered. Its operator-only clause matches the flow toggle's, since both routes run the one ADR-0126 §5 gate.
    • permission: the clone only, spelled as plugin-security's own lock refusal spells it.
  • Three doors read it. No door has a per-type prose branch.
    1. The protocol's package door (refusePackagedBaseOverride / refusePackagedBaseRemoval), on an environment-scoped kernel, answers 403 NOT_OVERRIDABLE. flow's sentence is byte-identical, held by a literal pin.
    2. The repository's type door (SysMetadataRepository.assertAllowed) is where a write lands when the /meta protocol is not environment-scoped, which includes the default pnpm dev boot. For an override-artifact write with the hatch closed, on a type with a row, it now answers the same row-built sentence, 403 NOT_OVERRIDABLE, on save and on removal. Every other type keeps the overlay-allowed-list sentence and its pins.
    3. The named-base ITEM_LOCKED limb (readOnlyBaseOverrideError) applies with the hatch closed, on a type with a row. Its opener is shortened to "Cannot overlay 'TYPE' in package 'ID': that package is read-only, and its packaged base is locked against in-place edits.", followed by the row's prescription and the ADR-0126 citation.
      • Unchanged: the hatch-OPEN remedy, code, status, lockSource, packageId and docs.
      • Unchanged: NARROW. A package-less hatch write still lands, and that is pinned.
  • The [Decision] OS_METADATA_WRITABLE unlocks a write on a read-only package while Studio renders a "Read-only" badge — which one is telling the truth? #8146 hatch-closed pin moved and was not deleted. It now sits on page, a type with no regime row, and is measured to reach the same limb (ITEM_LOCKED, lockSource: 'package', the named base echoed back). A Regime C pin sits beside it, and the pin's comment cites ADR-0126 §2 as the reason the two sentences differ.
  • Docs. content/docs/concepts/metadata-lifecycle.mdx had two sentences that contradicted the new text, and both are corrected:
    • "a create/update aimed at a read-only package … is asked to pick or create a writable base";
    • the permission row's "edit the package and re-publish".
    • A short "Packaged flows, actions and permission sets (ADR-0126)" subsection lists each type's sanctioned path.

No code, status, route or refused write moves.

The Clause-②: no check (the seat answer's stop (a)). assertAllowed / assertDeleteAllowed now take the ref and the operation, and both are private. readOnlyBaseOverrideError's signature is unchanged. Measured on the built declarations, with comments stripped, before against after this round (dist/index.d.ts and dist/index.d.cts): the only lines that differ are the removal of three private static member names on ObjectStackProtocolImplementation (PACKAGED_BASE_REGIME, PACKAGED_BASE_REFUSAL_BY_REGIME, packagedBaseRegimeSentence), which the table move took out of the class. No exported signature moved, and the new module appears nowhere in the declarations. The seat answer's stop (b): no type without a regime row changes its sentence at any door. The controls are page and object here, and page on the booted app below.

Lengths against the REST door's 500-character bound (truncateClientMessage keeps a message only while it is shorter than 500):

Refusal Row Characters before the variable part Longest variable part that arrives whole Pin
Package door / type door (save / removal) flow 411 / 404 before the name 88-character name yes
Package door / type door (save / removal) action 365 / 358 before the name 88-character name yes
Package door / type door (save) permission 317 before the name 88-character name yes
ITEM_LOCKED named base flow 422 before the package id 77-character package id 64-character id
ITEM_LOCKED named base action 376 before the package id 123-character package id 64-character id
ITEM_LOCKED named base permission 328 before the package id 171-character package id 64-character id

The longest package id this repository ships is com.objectstack.platform-objects.activation-ledger, at 50 characters.

Measured

Booted showcase (pnpm dev -- --fresh, local mode, discovery scoping.enabled: false). Before is the previous head of this PR, d9b1a8e8b, byte-identical to origin/main 7fa67dada. After is 9c169378a.

request before after
PUT /api/v1/meta/flow/showcase_task_completed?package=com.example.showcase 403 ITEM_LOCKED "…locked against runtime edits. Edit the source artifact and redeploy, or set OS_METADATA_WRITABLE=flow to grant a runtime escape hatch on this TYPE (…pair it with a package-less write). See docs/adr/0010-…" 403 ITEM_LOCKED "Cannot overlay 'flow' in package 'com.example.showcase': that package is read-only, and its packaged base is locked against in-place edits. Clone it under a new name to customize it (POST /api/v1/automation/:name/clone, body {name, label}), or switch it off (POST /api/v1/automation/:name/toggle, body {enabled: false}; operator-only where one install serves several organizations). See docs/adr/0126-packaged-metadata-customization-model.md."
PUT /api/v1/meta/action/showcase_mark_done (type door) 403 NOT_OVERRIDABLE "'action' is not allowOrgOverride in the registry. Overlay-allowed: view, dashboard, report, translation, email_template. Set OS_METADATA_WRITABLE to enable additional types at runtime." 403 NOT_OVERRIDABLE "Metadata item 'action/showcase_mark_done' is provided by a code package, and its packaged base is locked against in-place edits. Switch it off (POST /api/v1/actions/_activation/:object/:action, body {enabled: false}, :object = global for an object-less action; operator-only where one install serves several organizations). See docs/adr/0126-…"
PUT /api/v1/meta/action/showcase_mark_done?package=com.example.showcase 403 ITEM_LOCKED "…Edit the source artifact and redeploy, or set OS_METADATA_WRITABLE=action …" 403 ITEM_LOCKED "Cannot overlay 'action' in package 'com.example.showcase': that package is read-only, and its packaged base is locked against in-place edits. Switch it off (…). See docs/adr/0126-…"
PUT /api/v1/meta/flow/showcase_task_completed (package-less) Regime C flow sentence identical
PUT /api/v1/meta/permission/showcase_contributor, with and without ?package= plugin-security's authoring gate, already naming the clone identical (that gate answers first on this boot)
DELETE of the packaged flow, action and permission set 200, "No customization overlay found … already at artifact default" identical
control: PUT /api/v1/meta/page/showcase_start_here, with and without ?package= — still the list-and-hatch sentence and the hatch-closed ITEM_LOCKED remedy "…or set OS_METADATA_WRITABLE=page …", which is unchanged

Of the nine readings, exactly the three rows above with a changed "after" column moved.

In process, on an environment kernel (env_1), the real /meta route handler over a real ObjectStackProtocolImplementation and SchemaRegistry, with the package booted as code. These are the earlier rounds' readings, still pinned:

  • PUT action and DELETE action name the switch.
  • PUT permission names the clone.
  • DELETE permission is the supportsOverlay carve-out, not refused.
  • ?package= on both kernel shapes answers ITEM_LOCKED with the row's path.

Docs drift (content/docs/concepts/metadata-lifecycle.mdx, matched by ObjectStackProtocolImplementation). Two sentences contradicted the new text and are corrected, as listed under "What this changes". The page names no OS_METADATA_WRITABLE prescription for a flow, action or permission edit. The docs-drift-check comment (5923480340) also lists four release-owned pages. They were read and none of them states this refusal; they are release-owned and were not touched.

Tests

All at 9c169378a.

  • pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2: 195 files passed and 3 skipped; 2920 tests passed and 19 skipped.
  • pnpm --filter @objectstack/metadata-protocol typecheck: exit 0. The tsconfig includes src/**/*, tests included.
  • pnpm --filter @objectstack/rest typecheck: exit 0, with check:test-typecheck OK.
  • REST pins (rest-meta-packaged-action-permission-refusal.test.ts and rest-meta-packaged-flow-refusal.test.ts): 2 files, 15 tests passed.
  • New and moved pins:
    • sys-metadata-repository.package-writability.test.ts:
    • protocol.packaged-base-refusal.test.ts: the named-base limb through packagedBaseRefusal on both kernel shapes. The earlier per-row, literal-flow and 88-character pins stand.
    • protocol.delete-rewrap-envelope.test.ts: the control-plane removal prose is partitioned by regime row. flow and action now read their removal sentence, and every other type reads the type-door sentence as before.
    • rest-meta-packaged-action-permission-refusal.test.ts: a host-config kernel's PUT action and PUT permission hit the type door and name the path. ?package= for both types on both kernel shapes answers ITEM_LOCKED with the path.
  • Ablation, run after committing 9c169378a, through scripts/ablation-replace.mjs. It deleted every row from the table (anchor 1 → 0, blob 7f06cc1d9f9a → b876c9500402), rebuilt @objectstack/metadata-protocol, and ablation-dist-preflight --absent reported the action-route marker absent from all 24 built files.

Gates

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 9c169378a (no paths, the change set from merge base 2f2fa11d7, 9 paths) derives 92 commands. That is the earlier 62 plus the documentation families the content/docs/concepts/ touch adds. All 92 were run there and every one exited 0.
  • Reconciled with --ran against a command :: exit N record: "92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero.
  • origin/main had not moved past the merge base when the gates were derived.
  • The artifact-roster, wide-population, path-scheduled-job and type-check-lane families are outside the derived list and are left to CI.
  • pnpm lint is CI's run and was not run here.

Acceptance notes

  • The permission row's wording is a copy of plugin-security's (packages/plugins/plugin-security/src/packaged-permission-set-lock.ts), because metadata-protocol cannot import that package. Nothing pins the two spellings equal across packages. Noted, not filed.

Body refreshed for patch round 1 at 2026-10-01T03:16Z.


Generated by Claude Code

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 24 documentable anchor(s).

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

  • content/docs/concepts/metadata-lifecycle.mdx _(via ObjectStackProtocolImplementation (symbol, a top-level class), SysMetadataRepository (symbol, a top-level class), NOT_OVERRIDABLE (literal, a string literal in assertAllowed), /actions/_activation/:object/:action (route, a path literal in a comment on a changed line), /api/v1/actions/activation/:object/:action (route, a path literal in action), /api/v1/automation/:name/clone (route, a path literal in flow), /api/v1/automation/:name/toggle (route, a path literal in flow), /api/v1/data/sys_permission_set (route, a path literal in permission), /automation/:name/clone (route, a path literal in a comment on a changed line), /automation/:name/toggle (route, a path literal in a comment on a changed line))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-1.mdx (via /api/v1/data/sys_permission_set (route, a path literal in permission))
  • content/docs/releases/v17/17-3.mdx (via automation.toggle (sdk, the route ledger binds it to POST /automation/:name/toggle), /api/v1/automation/:name/toggle (route, a path literal in flow), /automation/:name/toggle (route, a path literal in a comment on a changed line))

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.

What this run could not see
  • 6 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 — 11 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 2f2fa11d756f665a4c06160480c1dce15b9d67a4 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 919f1deef732b25a0eb70f6e8f2e44510f2dfd93 — the merge of head 9c169378a40971eb723e266d26be0482fef5fb27 into base 2f2fa11d756f665a4c06160480c1dce15b9d67a4, 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 919f1deef732b25a0eb70f6e8f2e44510f2dfd93 && git checkout 919f1deef732b25a0eb70f6e8f2e44510f2dfd93
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2f2fa11d756f665a4c06160480c1dce15b9d67a4 9c169378a40971eb723e266d26be0482fef5fb27 && git checkout -B drift-repro 2f2fa11d756f665a4c06160480c1dce15b9d67a4 && git merge --no-ff 9c169378a40971eb723e266d26be0482fef5fb27

node scripts/docs-audit/affected-docs.mjs --json 2f2fa11d756f665a4c06160480c1dce15b9d67a4

⚠️ 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 2f2fa11d756f665a4c06160480c1dce15b9d67a4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

claude added 2 commits October 1, 2026 02:31
…e repository's type door and its named-base limb

Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l and removed size/m labels Oct 1, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9c169378a40971eb723e266d26be0482fef5fb27
Local-runs: none

Inputs read: card #20910 (body and all 7 comments: the pointer 5916662651, triage 5916763169, the unlock 5922075346, the claim 5922498707, the round-0 report 5923498345, the seat answer 5923536089 and the round-1 report 5924033416); PR #21026 (body, its 9-file list, and the net diff against the merge base with origin/main, 2f2fa11: 839 insertions, 114 deletions); the 42 check-runs on the head. Read-only shape held: the diff was taken with git fetch and git diff on the shared checkout, sources were read with git show and git grep, and nothing was built, run or re-run.

① Derived judgments

The accept-set — which writes are refused, with which code and status — is unchanged at all three doors; only the prose of a refusal moves. Each judgment named:

  1. Package door (protocol.ts refusePackagedBaseOverride / refusePackagedBaseRemoval, environment-scoped kernel): the only change is the class-private packagedBaseRegimeSentence becoming the module function of the same name, with the same ?? legacy sentence fallback for a type without a row. Predicate, NOT_OVERRIDABLE, 403 untouched. RIGHT.
  2. Type door (SysMetadataRepository.assertAllowed): the new branch sits after the registry limbs, after the named-base limb and after if (hatchOpen) return;, guarded by intent !== 'runtime-only', and throws the same NOT_OVERRIDABLE / 403 that the legacy throw below it threw for the same predicate; runtime-only keeps NOT_CREATABLE and the legacy prose. assertDeleteAllowed still returns early for OVERLAY_CAPABLE_TYPES (the permission carve-out) before reaching it. The sentence's claim "provided by a code package" is sound: every protocol caller derives the intent as artifactBacked ? 'override-artifact' : 'runtime-only' (six sites in protocol.ts), so the branch is reached only for an artifact-backed name. RIGHT.
  3. Named-base ITEM_LOCKED limb (readOnlyBaseOverrideError): the row prescription is consulted only when hatchOpen is false and the type has a row; otherwise the message is the pre-existing text byte for byte. code, status, lockSource, packageId and docs lie outside the hunk and are unchanged; the signature (type, packageId, hatchOpen = false) is unchanged. RIGHT.
  4. Table shape (packaged-base-regime.ts): rows { regime, routes }, the union type refuses a row naming neither route, the builder composes from the row alone (no type branch in any door's prose), flow byte-identical and pinned literally, action the switch only, permission the clone only. One regime, per-type routes, as triage directed. RIGHT.
  5. Routes named exist and are spelled as served: POST /actions/_activation/:object/:action with object-less actions under 'global' (packages/runtime/src/domains/actions.ts); the /automation/:name/clone and /toggle routes unchanged from The packaged-base refusal for a flow (403 NOT_OVERRIDABLE) prescribes "edit the source artifact and redeploy, or set OS_METADATA_WRITABLE", not ADR-0126 Regime C's sanctioned path (clone, or the enable/disable switch) #20819; the permission row is character-for-character the update-branch parenthetical of PackagedPermissionSetLockedError in plugin-security, itself NOT_OVERRIDABLE / 403. No route is added or changed anywhere in the diff. RIGHT.
  6. Public surface: the new module is not re-exported from src/index.ts; package.json exports is "." only and files is dist only, so its exported symbols are internal. assertAllowed and assertDeleteAllowed were private at the merge base and remain private; their new parameters are not a public-surface change. The three removed private static members on ObjectStackProtocolImplementation are not consumer-callable. RIGHT.
  7. Controls: object and page at the package door (the new object removal control included), object at the type door, page at the named-base limb (the moved [Decision] OS_METADATA_WRITABLE unlocks a write on a read-only package while Studio renders a "Read-only" badge — which one is telling the truth? #8146 pin) keep their sentences byte for byte; the deleteMetaItem's catch re-wrap drops the error code, so a repository refusal reaches the caller as a 403 with no catalogued code #7426 delete-rewrap pin now partitions by packagedBaseRegimeRow, every other type's assertion unchanged. RIGHT.
  8. Docs (content/docs/concepts/metadata-lifecycle.mdx): the new prose states what the head does — the three rows, the operator-only clause, :object = global, "403 NOT_OVERRIDABLE for a write that names no base, 403 ITEM_LOCKED for one that names the read-only package", and "which writes are refused, and with which code, is unchanged". The narrowed first paragraph (a create aimed at a read-only package is asked to pick or create a writable base) matches readOnlyBaseCreateError's "not a writable base" text. The anchor slug matches the new heading (Check Documentation Links green). Two precision notes, neither a false statement: (a) "locked against removal" for a flow or an action is a 403 at the environment-scoped door and at the repository door; on a host-config kernel a DELETE with no overlay row is the pre-existing 200 "already at artifact default" no-op (the dev measured it on the booted showcase), so the base is unremovable either way but not always refused; (b) where plugin-security is loaded its authoring gate answers a permission-set write first, with NOT_OVERRIDABLE for both the package-less and ?package= shapes, naming the same clone — the page describes the metadata-protocol doors, and that precedence predates the PR. RIGHT, with those notes.
  9. Checks on the head: 42 check-runs, all completed; 36 success, 6 skipped (Auto Label and Check PR Size on the second trigger, Console Pin Gate, Packed-tarball smoke opt-in), 0 failed, 0 in_progress. Test Core (6 shards), Build Core, every Type Check lane, Lint and Repo Gates, Dogfood (3 shards), Temporal Conformance, Build Docs, Spec property liveness, Governed Surface Queue Guard, and the four governance checks on both triggers are success. Nothing waited on.

② Semver level

.changeset/20910-regime-c-action-permission-paths.md: '@objectstack/metadata-protocol': patch. RIGHT — the only released package whose source changes is @objectstack/metadata-protocol; packages/rest gains a test file only, so nothing publishes from it; content/docs is not a package. A refusal-text fix in a released package takes patch, never skip-changeset. The changeset body describes the three doors and the unchanged codes, and needs no migration.

Clause-②: no — RIGHT. The PR body's second line is Clause-②: no, the changeset carries the same line, and the diff moves no exported signature and adds no public symbol (judgment 6): no accept-set change, so no yes, and neither arm. The seat answer's stop (a) did not fire.

③ Boundary flags

Round-0 open_questions (report 5923498345), both ruled by seat answer 5923536089 and both implemented within its stated boundaries:

  • Q1 → A. The named-base limb speaks for the row with the hatch CLOSED only. Checked on the diff: the hatch-OPEN remedy is the pre-existing text verbatim (pinned: named base, hatch OPEN); code, status, lockSource, packageId, docs untouched (pinned with docs: 'docs/adr/0010-metadata-protection-model.md'); NARROW untouched — a package-less hatch write of a Regime C type still lands package_id: null (pinned); the [Decision] OS_METADATA_WRITABLE unlocks a write on a read-only package while Studio renders a "Read-only" badge — which one is telling the truth? #8146 hatch-closed pin moved to page, a type with no row, measured to the same limb, with a Regime C pin beside it whose comment cites ADR-0126 §2. The answer said "such as object or job"; page meets the stated condition (regime-less, same limb). ANSWERED.
  • Q2 → A. The type door answers the row sentence for override-artifact with the hatch closed; every type without a row keeps the list-and-hatch sentence (pinned on object). Stop (a), an exported signature moving, did not fire (judgment 6). Stop (b), a no-row type changing its sentence at any door, did not fire (judgment 7). ANSWERED.

Round-1 report (5924033416): open_questions empty. Its deviations:

  • Two test files beyond the amended surface, both test-only in packages/metadata-protocol: one required by the ruling (the moved pin and its neighbour), one by the partition. ACCEPTED.
  • A docs subsection added beyond the two corrected sentences: judged in ① item 8. ACCEPTED, with the two precision notes.
  • A tool-stamped refresh line in the PR body; the worktree rebuilt with no diff. Cosmetic, no diff. ACCEPTED.

Acceptance note and out-of-scope finding (both rounds): the permission row copies plugin-security's parenthetical and nothing pins the two spellings equal across packages. Verified equal on this head (judgment 5). A drift pin needs a home that imports both packages, which metadata-protocol cannot be. ESCALATED as a non-blocking follow-up for the seat to file or decline; it does not gate this head.

Docs-drift-check (PR comment 5923480340): one hand-written page named and corrected here; four release-owned pages named, none in the file list. HELD.

Dispatch fences (claim 5922498707): the hatch's fate and the other regimes' sentences (#15206) untouched — no sentence outside the three Regime C rows changes at any door; no route added or changed. HELD.

Implemented-by: claude/issue-20910-regime-c-action-permission-paths
Reviewed-by: session_01Ujdtvqs7ree7WyQmEDwEnG

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 03:31
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 03:31
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 0c5a71b Oct 1, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20910-regime-c-action-permission-paths branch October 1, 2026 04:46
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…a sha, with the no-anchor opt-in (objectui#11174) (objectstack-ai#11332)

Fixes objectstack-ai#11174
Clause-②: no. CI wiring only; no published contract or accept set moves.

objectui's half of objectstack-ai/objectstack#18471 (ruling 只做②). The
half-state patrol stops checking out objectstack `main` unpinned and
running the sweeper from it. It now calls objectstack's composite action
at a pinned 40-character sha, using the no-anchor opt-in that
objectstack-ai/objectstack#20793 (decision A) declared for this board.
Dispatched by `domain:devx` seat 2, session
`https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh`, claim comment
5924897388.

## What changed

- **`.github/workflows/half-state-patrol.yml`.** These steps are gone:
the objectstack checkout (no `ref:`, so `main`), the `node
objectstack-tooling/scripts/pm/check-half-states.mjs` step, the inline
resolve-anchor, update-anchor, summary and fail steps, and the
`ANCHOR_ISSUE` env.
- One step replaces them: `uses:
objectstack-ai/objectstack@0c5a71b`.
- It passes `github-token: secrets.GITHUB_TOKEN`, `anchor-issue: ''`,
`anchor-optional: true` and `closed-floor: '2026-08-28'`.
- `actions/checkout` and `actions/setup-node` (node `'22'`) stay in the
caller. The action needs both: the checkout is the swept subject, and
the action has no Node pin of its own.
- **Unchanged:** the trigger, schedule, permissions, concurrency, job
key and job name. The job name is "Live half-state sweep", the check-run
name that `scripts/dependabot-merge-gate.mjs` classifies. To confirm,
both versions were parsed with `yaml` and `on`, `permissions`,
`concurrency`, `jobs.patrol.name`, `runs-on` and `timeout-minutes` all
compare SAME.
- The prose adopted from upstream is deleted. The header now holds only
what belongs to this board: the pin and how to bump it, the no-anchor
decision (objectui#8740), and the closed-card floor (objectui#5985). The
file shrinks to about a third of its old length.
- **`content/docs/guide/ci-cd-pipeline.md`, Half-State Patrol section.**
It now describes the action call, the pin and how to bump it, the four
inputs, the no-anchor configuration and its accepted cost, the PR-run
blind spot, the floor, and a hand-run recipe. The recipe gains
`PM_SWEEP_CLOSED_FLOOR=2026-08-28` and a note to check out the pinned
sha to reproduce a run.
- The Trigger line and the inventory row are unchanged; both are still
true.
- **`scripts/__tests__/ci-cd-pipeline-doc.test.ts`.**
- The objectui#8043 block compared `PM_SWEEP_*` env keys, and the
workflow no longer sets any. It now compares the section against the
patrol step's *parsed* `with:` inputs (via `yaml`), in both directions.
It also holds the floor value, the anchor configuration (empty
`anchor-issue` + `anchor-optional: true`), and the 40-hex shape of the
ref.
- One `SWEEP_DECLARED_NON_RUN_COMMANDS` entry is added, for
`half-state-patrol.yml: scripts/pm/check-half-states.mjs`. The section
names objectstack's sweeper, and the job now has no `run:` step at all.
This is the same dual-eligible shape as the existing `check-links.yml`
entry.
- **`scripts/check-action-ref-convention.mjs` and its test (see the
surface note below).**
- The sha spelling is off-convention for this repository's Action Ref
Convention gate, so it gets a `DECLARED_EXCEPTIONS` entry with its
reason and issue.
- That gate's two non-vacuity cases used to *replace* the real table
with a synthetic one. They now *append* the synthetic entry to it, so
they hold whatever the real table carries.
- The table's docblock no longer says "deliberately empty". It now says
that an entry is matched by workflow and action, not by spelling, and
names where this entry's "sha, never a branch" half is held.

## The pin

- **Sha:** `0c5a71b0942d54e56ad365f0b2a173fd84c1c5e1`. This is
objectstack `origin/main`'s tip, read at **2026-10-01T04:46:15Z** with
`git -C /home/user/objectstack fetch origin main` and then `git
rev-parse origin/main`. Its commit date is 2026-10-01T03:32:30Z
(objectstack-ai/objectstack#21026).
- **Ancestry:** `git merge-base --is-ancestor
79114850f0f559dcb4fb432f5bcfc12947a03de6
0c5a71b0942d54e56ad365f0b2a173fd84c1c5e1` gives **exit 0**. An "is an
ancestor" answer proves itself even on the shallow objectstack clone, so
no control leg is owed. `git rev-list --count` between them gives 127
commits.
- **Not the action's own last commit.** The pin is `main`'s tip, as the
Q2 ruling requires. At the pin, both `action.yml` (blob `2df36034`) and
`scripts/pm/check-half-states.mjs` (blob `fa35f635`) are byte-identical
to what 79114850f landed. So today the two choices run the same code.
The tip rule is what keeps that true on the next bump.
- **Input names read off `action.yml` at the pin:**
- declared: `anchor-issue`, `anchor-optional`, `closed-floor`,
`github-token`;
  - passed: the same four;
  - passed but undeclared: none; required but not passed: none.
- **Ruling sources:** the Q2 ruling A is comment 5905360637, the
handover is 5908038257, and `anchor-optional` comes from
objectstack-ai/objectstack#20812.

## Acceptance evidence: what this PR's own run can and cannot show

- This PR's `pull_request` run of **Half-State Patrol** exercises the
pin, the inputs and the transport on a real runner. The action's `Locate
the patrol sources` step judges `github-token` and the `anchor-optional`
spelling on every event, `pull_request` included, and the sweep runs.
- **But it skips both anchor steps, so it cannot show the opt-in.** The
live reading is the **first scheduled run (cron `37 1,7,13,19 * * *`) or
`workflow_dispatch` run after this lands**. The expected result:
- `Resolve the anchor issue` writes `configured=false` and emits the
`::notice::` naming `anchor-optional: true`;
  - `Update the pinned anchor issue` is skipped;
  - the run is green.

## Tests

Every check below was run on head `ec0c6e49`, except where a line names
`a13f4e5f`. That earlier head differs only by the header-comment edit in
the second commit.

- **actionlint 1.7.7** (release binary; actionlint is not wired in this
repository): `actionlint -shellcheck= -pyflakes=
.github/workflows/half-state-patrol.yml` gives exit 0.
- Positive control: the same file with `permissions:` misspelled gives
exit 1, `unexpected key "permissionz"`.
- ⚠️ actionlint cannot check a remote composite action's inputs. That
check is the `action.yml` read under The pin.
- **`vitest run scripts/__tests__/`** (through `os-verify-lock`,
`--maxWorkers=2`) on **`ec0c6e49`**: 177 files passed and 2 skipped
(179); 5374 tests passed and 2 skipped (5376); `VERDICT command-exit 0`.
The same result on `a13f4e5f`.
- **Targeted:** `pnpm exec vitest run
scripts/__tests__/ci-cd-pipeline-doc.test.ts
scripts/__tests__/check-action-ref-convention.test.ts` gives 2 files and
104 tests passed. On the base it gave 101.
- **`tsc -p tsconfig.scripts.json --listFiles`** on `a13f4e5f` gives
exit 0, and both edited test files are in the program. `ec0c6e49`
changes only the workflow YAML, which is outside that program and
outside the lint population below.
- **Gates, all exit 0:**
- `pnpm check:doc-fences`, `pnpm docs:check-links`, `pnpm
check:control-bytes`, `pnpm check:action-ref-convention`, `pnpm
check:new-line-citations` (0 new citations), `pnpm
check:test-path-roots` and `node
scripts/check-shell-escape-residue.mjs`;
- `node scripts/check-changeset-presence.mjs`: "no changeset is owed".
Nothing under a released package's source or contract moves, so no
changeset is added and no label is applied;
- `node scripts/check-governed-queue-guard.mjs --test` over the five
paths: NOT GOVERNED.
- **ESLint, narrowed to this diff:** `eslint --format json` over the
three lintable files reports 3 files, 0 errors and 0 warnings. The YAML
and the Markdown are not in the lint population.
- The population is `lint:root`, which is `eslint .` minus `packages/`,
`examples/`, `apps/` and `docs/`. The three files sit inside it.
  - The narrowing excludes nothing that could move:
- `eslint.config.js` has no `parserOptions` or `projectService`, so the
linting is not type-aware;
- `eslint-rules/*.js` read no files
(`readFileSync`/`existsSync`/`readdirSync` give zero hits);
    - so no untouched file's verdict can change.

### Ablations (one-shot; nothing kept)

Each ablation ran through objectstack's `scripts/ablation-replace.mjs`
in WRAP mode on committed head `a13f4e5f`. In each, the anchor hit once,
the blob changed, and the file was restored with blob == HEAD and `git
diff HEAD` empty.

1. `@0c5a71b0…` changed to `@main`: exactly 1 red, "pins the action to a
40-character sha … never a branch".
`check-action-ref-convention.test.ts` stayed **green**, which is the
hole that pin covers: the exception matches by workflow and action, not
by spelling.
2. `anchor-issue: ''` changed to `'9857'`: exactly 1 red, "describes the
anchor configuration the step actually passes".
3. `closed-floor` deleted: 2 red, "names only inputs the workflow
actually passes" and "quotes the closure floor".
4. The doc's `github-token` bullet stripped of its name: 1 red, "names
every input the workflow passes".
5. The gate without the new entry, run as `scan(root, { exceptions: []
})` with no file mutated: 1 offender, `half-state-patrol.yml … [sha]`.

## Surface note for the seat

The claim's surface lists the workflow, the doc section,
`ci-cd-pipeline-doc.test.ts` and "any other `git grep half-state-patrol`
hit whose assertion moves". This PR also edits
**`scripts/check-action-ref-convention.mjs`** and
**`scripts/__tests__/check-action-ref-convention.test.ts`**.

- Neither file was a grep hit before this change. Both read this
workflow's text, and their verdict moves with it, so they fall under the
original claim's definition of the surface (5905159945: "the tests or
scripts that read this workflow's text").
- The ruling's sha pin is off-convention for the blocking Action Ref
Convention gate, and that gate admits exactly one form of exception: a
`DECLARED_EXCEPTIONS` entry.
- No other workflow and nothing in objectstack is touched. The seat may
want to amend the claim's surface to match.

## Acceptance notes (noted, not filed)

- **Prose drift in workflows this claim excludes:**
- `merge-queue-head-patrol.yml`'s header still says
`half-state-patrol.yml` "fails when its anchor variable is unset". That
has been false since objectui#8740. Its env comment cites "the reason
`half-state-patrol.yml` states"; the new header keeps that sentence, so
the citation still resolves.
- `changeset-guard.yml`, `performance-budget.yml`, `lockfile-dedupe.yml`
and the docblock of `render-budget-comment.test.ts` say this workflow
lists `scripts/invoked-as.mjs` in its paths. That has been false since
objectui#10208, not because of this PR.
  - Carrier: none.
- **Dependabot.** Its `github-actions` ecosystem runs monthly here.
Whether it proposes bumps for a sha-pinned ref to a non-tagged action
path inside a monorepo is NOT MEASURED. If it does, the bump arrives as
a PR touching this file, which runs the patrol, so it is a reviewed
moment in any case.

---

_Generated by [Claude
Code](https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: 黄怡 <huangyi@hotoa.com>
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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants