Repository navigation
fix(metadata-protocol): a packaged action or permission set's refusal names its own sanctioned path (ADR-0126 Regime C rows) - #21026
Conversation
…their sanctioned path (WIP) Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
… changeset (WIP) Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…gime-c-action-permission-paths
…gime-c-action-permission-paths
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
…gime-c-action-permission-paths
…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>
Contract reviewServed-tier: 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 ① Derived judgmentsThe 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:
② Semver level
Clause-②: no — RIGHT. The PR body's second line is ③ Boundary flagsRound-0
Round-1 report (5924033416):
Acceptance note and out-of-scope finding (both rounds): the permission row copies 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: VERDICT: PASS |
…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>
Fixes #20910
Clause-②: no
What this changes
ADR-0126 §3 puts
flow,actionandpermissionin Regime C, and §2 requires the Regime C refusal to name the sanctioned path. Before this PR, onlyflow'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 theOS_METADATA_WRITABLEhatch 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):
packages/metadata-protocol/src/packaged-base-regime.tsholdsPACKAGED_BASE_REGIME. Each row is{ regime, routes }, androutesdeclares 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.protocol.tsimportssys-metadata-repository.ts, so the table could not live in either file without an import cycle.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/:actionwith{enabled: false}, where:objectisglobalfor 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 asplugin-security's own lock refusal spells it.refusePackagedBaseOverride/refusePackagedBaseRemoval), on an environment-scoped kernel, answers403 NOT_OVERRIDABLE.flow's sentence is byte-identical, held by a literal pin.SysMetadataRepository.assertAllowed) is where a write lands when the/metaprotocol is not environment-scoped, which includes the defaultpnpm devboot. 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.ITEM_LOCKEDlimb (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.code,status,lockSource,packageIdanddocs.OS_METADATA_WRITABLEunlocks 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 onpage, 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.content/docs/concepts/metadata-lifecycle.mdxhad two sentences that contradicted the new text, and both are corrected:permissionrow's "edit the package and re-publish".No code, status, route or refused write moves.
The
Clause-②: nocheck (the seat answer's stop (a)).assertAllowed/assertDeleteAllowednow take the ref and the operation, and both areprivate.readOnlyBaseOverrideError's signature is unchanged. Measured on the built declarations, with comments stripped, before against after this round (dist/index.d.tsanddist/index.d.cts): the only lines that differ are the removal of threeprivate staticmember names onObjectStackProtocolImplementation(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 arepageandobjecthere, andpageon the booted app below.Lengths against the REST door's 500-character bound (
truncateClientMessagekeeps a message only while it is shorter than 500):flowactionpermissionITEM_LOCKEDnamed baseflowITEM_LOCKEDnamed baseactionITEM_LOCKEDnamed basepermissionThe longest package id this repository ships is
com.objectstack.platform-objects.activation-ledger, at 50 characters.Measured
Booted showcase (
pnpm dev -- --fresh, local mode, discoveryscoping.enabled: false). Before is the previous head of this PR,d9b1a8e8b, byte-identical toorigin/main7fa67dada. After is9c169378a.PUT /api/v1/meta/flow/showcase_task_completed?package=com.example.showcase403 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.showcase403 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)PUT /api/v1/meta/permission/showcase_contributor, with and without?package=plugin-security's authoring gate, already naming the cloneDELETEof the packaged flow, action and permission set200, "No customization overlay found … already at artifact default"PUT /api/v1/meta/page/showcase_start_here, with and without?package=ITEM_LOCKEDremedy "…or set OS_METADATA_WRITABLE=page …", which is unchangedOf the nine readings, exactly the three rows above with a changed "after" column moved.
In process, on an environment kernel (
env_1), the real/metaroute handler over a realObjectStackProtocolImplementationandSchemaRegistry, with the package booted as code. These are the earlier rounds' readings, still pinned:PUT actionandDELETE actionname the switch.PUT permissionnames the clone.DELETE permissionis thesupportsOverlaycarve-out, not refused.?package=on both kernel shapes answersITEM_LOCKEDwith the row's path.Docs drift (
content/docs/concepts/metadata-lifecycle.mdx, matched byObjectStackProtocolImplementation). Two sentences contradicted the new text and are corrected, as listed under "What this changes". The page names noOS_METADATA_WRITABLEprescription 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 includessrc/**/*, tests included.pnpm --filter @objectstack/rest typecheck: exit 0, withcheck:test-typecheckOK.rest-meta-packaged-action-permission-refusal.test.tsandrest-meta-packaged-flow-refusal.test.ts): 2 files, 15 tests passed.sys-metadata-repository.package-writability.test.ts:flowandaction;object, byte for byte;flow, byte for byte as before;OS_METADATA_WRITABLEunlocks a write on a read-only package while Studio renders a "Read-only" badge — which one is telling the truth? #8146 pin onpage, and its Regime C neighbour.protocol.packaged-base-refusal.test.ts: the named-base limb throughpackagedBaseRefusalon both kernel shapes. The earlier per-row, literal-flowand 88-character pins stand.protocol.delete-rewrap-envelope.test.ts: the control-plane removal prose is partitioned by regime row.flowandactionnow 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'sPUT actionandPUT permissionhit the type door and name the path.?package=for both types on both kernel shapes answersITEM_LOCKEDwith the path.9c169378a, throughscripts/ablation-replace.mjs. It deleted every row from the table (anchor 1 → 0, blob7f06cc1d9f9a→b876c9500402), rebuilt@objectstack/metadata-protocol, andablation-dist-preflight --absentreported the action-route marker absent from all 24 built files.OS_METADATA_WRITABLEunlocks a write on a read-only package while Studio renders a "Read-only" badge — which one is telling the truth? #8146 pin onpage, theobjecttype-door control, the hatch-open remedy, NARROW, thepagecontrols and the delete-rewrap file (it partitions by the table, by design).DELETEcarve-out and thepagecontrol stayed green.git diff HEADis empty; the rebuild exited 0; the marker is present in 2 built files; the working tree is clean.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat9c169378a(no paths, the change set from merge base2f2fa11d7, 9 paths) derives 92 commands. That is the earlier 62 plus the documentation families thecontent/docs/concepts/touch adds. All 92 were run there and every one exited 0.--ranagainst acommand :: exit Nrecord: "92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero.origin/mainhad not moved past the merge base when the gates were derived.pnpm lintis CI's run and was not run here.Acceptance notes
plugin-security's (packages/plugins/plugin-security/src/packaged-permission-set-lock.ts), becausemetadata-protocolcannot 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