fix(runtime,metadata-protocol): the /automation write doors keep the packaged-base lock the /meta door keeps (#20679) - #20817
Conversation
… the packaged-base lock PUT /automation/:name and DELETE /automation/:name now ask the metadata protocol's own locked-base verdict (packagedBaseRefusal, lifted out of saveMetaItem / deleteMetaItem unchanged) before the engine is called, and relay its refusal verbatim. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…tocol verdict Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…tomation-door-package-lock
Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…rdict Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…tomation-door-package-lock
…owcase Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…tomation-door-package-lock
…ver on body stamps Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…em / deleteMetaItem verbatim The two helpers now carry the original inline lines unchanged (they throw, as that code did); packagedBaseRefusal is the one place a throw becomes a value, and it re-raises anything that is not the refusal. The changeset states the widening. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…tomation-door-package-lock
…-base lock too POST /automation onto a name the engine already holds is an overwrite; it now asks the same locked-base verdict as PUT /:name before anything is read or registered. Bounded in-place fix, adopted onto the claim's surface. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 6 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 — 31 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 4d79331e1f956678f0ee4c3c9ca13c5d499e3e56 && git checkout 4d79331e1f956678f0ee4c3c9ca13c5d499e3e56
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 79114850f0f559dcb4fb432f5bcfc12947a03de6 e4006d68594d433159b1a0e8addd9585e5929f59 && git checkout -B drift-repro 79114850f0f559dcb4fb432f5bcfc12947a03de6 && git merge --no-ff e4006d68594d433159b1a0e8addd9585e5929f59
node scripts/docs-audit/affected-docs.mjs --json 79114850f0f559dcb4fb432f5bcfc12947a03de6
|
The refusal's code and status match the metadata door everywhere; its sentence matches wherever the metadata protocol's own package door answers. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: This is the record of record for PR #20817 at Inputs:
Disclosure is kept at the public item's level: doors, roles, codes and statuses. Seat verification on adoption:
① Derived judgmentsDelta: the changeset sentence now reads "the same code and status the metadata door gives (
The first record's finding is closed. Carried forward, with the code unchanged:
Surface inventory:
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #20679
Clause-②: yes (widening)
The
/automationdefinition doors now refuse a packaged flow, with the same locked-base verdict the metadata door gives. This is the public checklist itemaccess-security.packaged-flow-write-door-parity: clauses 2 and 3 flip to PASS, and clauses 1 and 4 still pass, measured on a booted showcase. The reproduction stays withheld as filed. This body stays at the door level the public item already describes.What changed
packages/runtime/src/domains/automation.tsPUT /:nameandDELETE /:namecallrefusePackagedFlowBaseChangebefore the engine is called, and relay its refusal.manage_metadataauthoring gate, then the body envelope check (PUT and POST only), then the lock, then the engine.POST /onto a name the engine already holds overwrites that flow. It now takes the same lock, right after the name check.3690c44b, before the fix, from a throwaway probe (real protocol over a realSchemaRegistry, deleted afterwards): a create onto a packaged flow's name answered200,registerFlowwas entered once, and the packaged flow's label was overwritten.403 NOT_OVERRIDABLE,registerFlownever entered, and a create under a new name still succeeds.protocolslot withresolveServiceOrLoud, unscoped, exactly as the/metadomain resolves it. So both doors ask the same protocol instance./metadoor to be at parity with.packages/metadata-protocol/src/protocol.ts: the widening, a cross-lane surface the seat adopted.ObjectStackProtocolImplementation.packagedBaseRefusal({ type, name, operation }). It returns the refusal the/metadoor gives for writing ('save') or removing ('delete') an existing item that a code package ships, ornullwhen that door would not refuse on this ground./metaverdict (isArtifactBacked+isOverlayAllowed, and its emitters) was private to this class, so a second door could not ask it any other way.refusePackagedBaseOverrideandrefusePackagedBaseRemoval, carrysaveMetaItem's anddeleteMetaItem's inline package-door code verbatim (proof below).packagedBaseRefusalis the one place a throw becomes a value. It re-raises anything that is not a403NOT_OVERRIDABLE/ITEM_LOCKED.NOT_OVERRIDABLEandITEM_LOCKEDare ledgered under@objectstack/metadata-protocol(ADR-0112).@objectstack/runtime's owner key lists neither.packages/spec.errorFromThrown, the code, status and sentence are the producer's.check:error-code-provenance: 330 stamp sites, 313 listed, 17 waived, OK.What the lock keys on. The flow's NAME, looked up in the registry's artifact-only lookup (
SchemaRegistry.getArtifactItem,packages/objectql/src/registry.ts:3919). That lookup scans the PACKAGE_ID:NAME entries the artifact loader registers.{ type, name, operation }and nothing else.Two refusals on DELETE. The lock (
403 NOT_OVERRIDABLE) answers before the engine's ADR-0126 §7.3 refusal (DELETE_RESTRICTED/409, a packaged subflow that packaged callers still reach).OS_METADATA_WRITABLE=flow.registerFlow/unregisterFlow: the boot pull registers packaged flows through them.What stays open.
POST /:name/clone, the ADR-0126 §7.1 customization path.POST /:name/toggle, the activation switch. Its packaged-only rule from automation toggle door: switching a customer-authored flow off or on answers 400 VALIDATION_FAILED 'Package is required' — the activation ledger requires package_id and toggleFlow writes an empty one #20726 is untouched.OS_METADATA_WRITABLE=flowoperator hatch, which the refusal sentence names. It opens this door exactly as it opens/meta: sameisOverlayAllowed.Lift proof: each lifted helper body, before and after, whitespace-insensitive
diff -wchanged linesrefusePackagedBaseOverriderefusePackagedBaseRemovalThe only added lines compute the locals the inline code read from its enclosing method:
overlayAllowedin the override helper;overlayAllowedandartifactBackedin the removal helper.Each is spelled exactly as in the calling method.
deleteMetaItemkeeps its own copies for itsNOT_CREATABLEcheck.Pins
packages/runtime/src/domains/automation-packaged-base-lock.test.ts(15 cases). RealObjectStackProtocolImplementationover a realSchemaRegistry, with the packaged flow registered the way the loader registers it; the automation service is a spy.saveMetaItem's /deleteMetaItem's thrown refusal: code, status and sentence.packages/metadata-protocol/src/protocol.packaged-base-refusal.test.ts(9 cases):nullfor a name no package ships and for a Regime O type;packages/metadata-protocol/src/protocol.read-verb-canonical-fold.test.ts: the derived fold population gainspackagedBaseRefusal("all fourteen").packages/qa/dogfood/test/packaged-flow-write-door-parity.dogfood.test.ts(5 cases), the real showcase composition over HTTP:PUT /meta/flow/:nameanswers403, codeNOT_OVERRIDABLE, in the REST door's envelope.PUT/DELETE /automation/:nameanswer403 NOT_OVERRIDABLE, and the definition reads back byte-identical.Tests (measured)
At
3690c44b(merge over #20726):@objectstack/runtimelocalproject: 292 files, 4215 passed, 1 skipped.@objectstack/runtimerepoproject: 727 passed.@objectstack/metadata-protocol: 191 files passed (3 skipped); 2801 passed, 19 skipped.@objectstack/metadata-protocoltypecheck: exit 0.@objectstack/objectql, the 20 files pinningNOT_OVERRIDABLE/NOT_CREATABLE/ITEM_LOCKEDonsaveMetaItem/deleteMetaItem: 362 passed.@objectstack/rest, 3 files: 41 passed.At
f5ea0060(head):automation-*.test.tsin@objectstack/runtime: 25 files, 478 passed.@objectstack/runtimetypecheck: exit 0,check:test-typecheckOK.Reverse verification
Each leg was committed first, then mutated through
scripts/ablation-replace.mjs(anchor hit 1, blob changed). Dist legs were proven byablation-dist-preflight(marker in 2 built files). Every restore was proven (blob == HEAD,git diff HEADempty, tree clean, marker absent from dist). Predicted and measured agree on every leg.3690c44b3690c44bpackagedBaseRefusalreturnsnull(rebuilt)3690c44b3690c44b3690c44bf5ea0060After every restore, all pins were green again.
Gates
dispatch-gates --commandsatf5ea0060: 67 derived.--ranreconciliation: 67 run, 0 NOT-MEASURED, 0 unrun, every exit 0.check:error-code-casingand@objectstack/speccheck:error-code-provenance, both exit 0.pnpm lint, narrowed and proven:eslint.config.mjs(the**/*.{ts,…}andpackages/**/*.{ts,…}blocks). All 6 changed.tsfiles are in it.--format jsoncounted 6 files linted, 0 errors, 0 warnings, atf5ea0060.eslint.config.mjsstates it: noparserOptions.project, no typed rules), and its four plugins are local AST rules. So this diff cannot move any untouched file's verdict.Acceptance notes (observed, not filed)
NOT_OVERRIDABLEsentence names "edit the source artifact and redeploy" and theOS_METADATA_WRITABLEhatch. It does not name clone (§7.1). This holds on both doors, because the sentence is one emitter. It is reported to the seat, not changed here./metaanswers through the REST server's refusal envelope,{ error, code }with a flat stringerror./automationanswers through the dispatcher's,{ success, error: { code, message } }. Pre-existing. The dogfood pin reads each where it lives.@objectstack/restinlines the protocol. It declares@objectstack/metadata-protocolas a devDependency, so its builtdist/carries its own copy of the protocol class. Pre-existing. It is rebuilt with the same source.Generated by Claude Code