Repository navigation
fix(metadata-protocol): a packaged flow's refusal names the clone and the on/off switch, not a redeploy or OS_METADATA_WRITABLE (#20819) - #20909
Conversation
…and the switch The package doors' NOT_OVERRIDABLE sentence is now chosen per ADR-0126 regime from one table keyed on the type's declared regime. A Regime C type (flow) names its sanctioned paths, the clone under a new name and the enable/disable switch, and cites ADR-0126; it no longer prescribes a redeploy or the OS_METADATA_WRITABLE hatch. Every other type keeps its sentence byte for byte. Code, status and the refused set are unchanged. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
… doors Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
…gime-c-refusal-path
📓 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:
⛔ 3 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 74de859475b488f1cf9caeb263056082d9ac6c53 && git checkout 74de859475b488f1cf9caeb263056082d9ac6c53
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 165c1d49e3544494660eff2de6e9be3473be1832 88e3fcf451c16334089f8a676a28b642fdcda3e7 && git checkout -B drift-repro 165c1d49e3544494660eff2de6e9be3473be1832 && git merge --no-ff 88e3fcf451c16334089f8a676a28b642fdcda3e7
node scripts/docs-audit/affected-docs.mjs --json 165c1d49e3544494660eff2de6e9be3473be1832
|
Contract reviewServed-tier: Inputs read: card #20819 (body; comments 5908835963, 5909059434, 5915426751, 5916420712); ADR-0126 §2, §3, §5, §7.1, §7.2 (and §8, §9, §10 where the diff cites them); ADR-0131 D6/D7/D13 with #15206; PR #20909 (body, file list, three-dot diff against The PR head was Check-runs on the head, as read at the moment of this record (33 runs): 25 ① Derived judgmentsAccept set and public surface, from the diff (5 paths:
② Semver level
③ Boundary flagsThe regime declaration — the three questions.
Dev flags.
Implemented-by: VERDICT: PASS Generated by Claude Code |
…e and removal A write naming the shipping package with ?package= takes the named-base limb and is refused ITEM_LOCKED, unchanged by this change; say so. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Delta review after the at-tier record 5916639859 on The PR head was Check-runs on the head, as read for this record (38 runs): 16 ① Derived judgmentsThe seat asked for one correction to the changeset's opening sentence (5916639859 ②): a write that names the shipping package is refused
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #20819
Clause-②: no
What changes
When a write or a removal targets a flow that a code package ships, without naming that package with
?package=, the package doors inpackages/metadata-protocolrefuse it with403 NOT_OVERRIDABLE. (A write that names the shipping package takes the named-base limb and is refused403 ITEM_LOCKED, unchanged by this PR; corrected by the seat after review 5916639859.) The refusal's sentence is now chosen per ADR-0126 regime, from one table keyed on the type's declared regime:PACKAGED_BASE_REGIME(protocol.ts:14704) is the regime declaration. It holdsflow: 'C'and nothing else.PACKAGED_BASE_REFUSAL_BY_REGIME(protocol.ts:14732) is the per-regime sentence table. It has one row, Regime C.packagedBaseRegimeSentence(protocol.ts:14749) looks up the table. The two emitters,refusePackagedBaseOverridefor save andrefusePackagedBaseRemovalfor delete, ask it for their sentence and fall back to their old literal, which is kept byte for byte. There is notype === 'flow'branch in the emitters.The Regime C sentence, as served by
PUT /api/v1/meta/flow/pkg_alert_flow:On removal, "against in-place edits" reads "against removal". The sentence names the clone (§7.1) and the switch (§7.2), cites ADR-0126, and does not mention
OS_METADATA_WRITABLEor a redeploy.What does not move (Clause-②
no): the code, the status, and which writes are refused. The predicate is unchanged, andOS_METADATA_WRITABLE=flowstill opens the lock at both doors (the existing hatch pin stays green; only its comment changed). The named-baseITEM_LOCKEDlimb is unchanged. Every type with no declared regime keeps its sentence byte for byte.Zone 2 readings (PM hypotheses)
packagedBaseRefusalis atprotocol.ts:14513(about:14506at base33b6e8bec). Every door reaches the same two emitters, and every caller relays the sentence unchanged:saveMetaItem/deleteMetaItemcall the emitters directly, behindenvironmentId !== undefined.tenantAuthoredWriteRefusalreturnspackagedBaseRefusalfor a flow in the loader's set. It is asked bysaveMetaItem(the/metaPUT, on both topologies) and by runtimerefuseUnauthoredFlowWrite(POST /automation,PUT /automation/:name, and the clone target). The runtime relays it throughdeps.errorFromThrown(refusal).refusePackagedFlowBaseChangecoversDELETE /automation/:name, and PUT/POST when the protocol has no authoring rule. It relays throughdeps.errorFromThrown(refusal)./metaroutes relay it throughhandleRouteError, verbatim below 500 characters.git grep -i regimeoverpackages/{spec,metadata-protocol,metadata-core,runtime,core,types}/srcfinds prose comments only.DEFAULT_METADATA_TYPE_REGISTRYentries carry no regime field. So the smallest declaration went in beside the door: a map from type to regime, holding Regime C =flow.flow;permission;action(the disable half; the clone half is not chartered, §8 item 2); pre-chartedtool,skillandposition.actionreaches it on save and delete.permissionreaches it on save only: its delete is exempted by thesupportsOverlaycarve-out./automationroutes, which exist for a flow alone.action's switch isPOST /api/v1/actions/_activation/:object/:actionand it has no clone;permissionhas its own clone machinery.tool,skillandpositionreach the door only if a package ships one. The measurement recorded inisNestedArtifactField's comment found none on the showcase.actioncontrol pin holds the undeclared sentence byte-identical. Measured at the REST route, see the Acceptance notes.main:POST /automation/:name/toggleatautomation.ts:2427andPOST /automation/:name/cloneatautomation.ts:2513, under the/automationdomain prefix.manage_metadata, then byrefuseUngrantedActivationWrite, which requires the platform operator in thegroup/isolatedpostures (ADR-0126 §5).{ name, label }needs both keys. The toggle body is{ enabled?: boolean }.200on a packaged flow.CLIENT_MESSAGE_MAX = 500(packages/rest/src/error-response.ts:72) truncates the tail at 500 characters.pkg_alert_flow. Any name up to 88 characters arrives whole.Tests (all at head
d58d5578f6unless stated)packages/metadata-protocol/src/protocol.packaged-base-refusal.test.ts, with 8 new cases:NOT_OVERRIDABLE, clone and toggle named, ADR-0126 cited, no hatch, no redeploy, under 500 characters;/metadoors throw the regime sentence;pagekeeps its sentence byte for byte;actionkeeps its save and delete sentences byte for byte.Result:
Tests 17 passed (17).packages/runtime/src/domains/automation-packaged-base-lock.test.ts:PUTandDELETE /automation/:name(both topologies) andPOST /automationonto a packaged name now also assert that the relayed message names clone and toggle and does not name the hatch. The hatch test's title and comment are corrected: the refusal no longer names the hatch, and the hatch still opens the door.Tests 15 passed (15).packages/rest/src/rest-meta-packaged-flow-refusal.test.ts(new) drives the realPUT/DELETE /api/v1/meta/:type/:nameroutes over a real protocol andSchemaRegistry: flow PUT on both topologies, flow DELETE on an environment kernel, and apagecontrol compared byte for byte.Tests 4 passed (4).@objectstack/metadata-protocolsuite, run ataf8a9ee7a9(the merge since then brought no change to this package):Test Files 193 passed | 3 skipped (196),Tests 2870 passed | 19 skipped (2889).af8a9ee7a9:pnpm --filtertypecheckpasses for@objectstack/metadata-protocol,@objectstack/restand@objectstack/runtime.tsc --listFilescounts each of the three test files once in its package's program.check:test-typecheckis OK for rest (0 held) and runtime (the identity-pinned ledger is unchanged).packages/clitiers were not touched. The runtime and rest full suites are left to CI; this PR changes only the one test file in each.Ablation on the regime choice (committed fix first;
node scripts/ablation-replace.mjswith its restore trap,pnpm --filter @objectstack/metadata-protocol build, thennode scripts/ablation-dist-preflight.mjs; rest and runtime read thedist/)Predicted before running: leg A turns the flow pins red and leaves the controls green; leg B turns only the
pagecontrols red. Both came out as predicted.flow: 'C',f1e6267e→787e1532f1e6267e,git diff HEADemptypage: 'C'f1e6267e→fbb29b3bpagecontrol) / 16 passedpagecontrol) / 3 passedgit status --porcelainemptyThe first leg-B attempt was a no-op:
ablation-replacerefused it because the replacement contained the anchor ("anchor count moved 1 → 1") and restored the file. It was re-run with a non-overlapping anchor, and the row above is that run. Both runs were ataf8a9ee7a9.Gates
node scripts/pm/dispatch-gates.mjs --commands(no paths), atd58d5578f6, derived 63 commands. The change set is 5 paths, 366 changed lines against the 5000-line human-merge threshold: under. All 63 were run atd58d5578f6, and--ranreports:✓ dispatch-gates --ran: 63 derived famil(ies) accounted for — 61 run, 2 NOT-MEASURED (0 DERIVED from a recorded exit 3, 2 claimed).check:doc-authoring(the new sentence carries no tracker number),check:nul-bytes,check:engine-double-contract,check:cross-package-test-inputs,check:test-source-alias,check:issue-citationsandcheck:durability-log-level.check:dual-build-cjs-loadsandcheck:type-check-debt, reason: PREREQUISITE NOT MET (exit 3). Both read the whole workspace's built output. A full packages build is 50 uncached turbo tasks,@objectstack/consoleincluded (dry run), which is beyond the shared box's foreground cap. CI's lint.yml builds that closure first.origin/main(d58d5578f6) because the first derivation warned of a stale tree (scripts/pm/check-governed-queue-guard.mjshad moved). The incoming commits touchdriver-memoryand that script only. The re-derivation printed the same 63 commands with no stale warning. The build closure was refreshed and the three pinned files re-run atd58d5578f6(17 / 15 / 4 passed).Lint was narrowed to the changed files, not the repo-wide
pnpm lint(that run is CI's). Three pieces of evidence make that narrowing complete:eslint --no-inline-config --print-configresolves a config for each of the 4 changed.tsfiles, so none is ignored.eslint --no-inline-config --format jsonon those files countsfiles 4 errors 0 warnings 0, atd58d5578f6.eslint.config.mjsnever enables type-aware linting ("noparserOptions.project, no typed@typescript-eslintrules", at its line 328), so this diff cannot move the verdict on any file it did not touch.Acceptance notes
actionandpermissionin Regime C. Driving the real REST/metaroute in-process (real protocol, registry double, environment kernel) gives these answers:PUT /api/v1/meta/action/pkg_approveanswers403 NOT_OVERRIDABLEwith "Edit the source artifact and redeploy, or set OS_METADATA_WRITABLE to grant a runtime escape hatch. See docs/adr/0005-…";PUT /api/v1/meta/permission/pkg_permgives the same answer;DELETEon the action answers the removal sentence, which names no path at all.The ruling scoped this card to
flow. Their own sanctioned paths differ (see H2), so they need their own table rows. That is left to the seat, together with feat(metadata-core,metadata-protocol,objectql,plugin-security): thesys_metadatafamily goes tenant-less; the per-organization overlay axis retires; managed content is sealed (ADR-0131 D6/D7/D13) #15206, which owns whether the hatch survives for sealed managed content.ITEM_LOCKEDlimb (SysMetadataRepository.readOnlyBaseOverrideError,sys-metadata-repository.ts:1745, shared with the repository door) still prescribes "Edit the source artifact and redeploy, or set OS_METADATA_WRITABLE=TYPE" for any type, flow included, and cites ADR-0010. It is reached byPUT /api/v1/meta/flow/:name?package=PKGwhen PKG is read-only. Reach NOT MEASURED: the harness's engine double reports every package as writable, so the probe got this PR'sNOT_OVERRIDABLEsentence instead. It is outside this card's file surface. Carrier: none.orgScopedWriteRefusaland the code-only refusals also name the hatch. They answer different conditions (an org-scoped write of any flow; code-only types), and whether the hatch survives is feat(metadata-core,metadata-protocol,objectql,plugin-security): thesys_metadatafamily goes tenant-less; the per-organization overlay axis retires; managed content is sealed (ADR-0131 D6/D7/D13) #15206's question.Deviations
d58d5578f6) fromorigin/main; see Gates.Generated by Claude Code