Repository navigation
feat(spec)!: publish refuses an anonymous flow endpoint whose target flow runs as system, in both directions (ADR-0138 D2b) - #23411
Conversation
… system flow at every publish door (WIP) Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude <noreply@anthropic.com>
…e flow publish doors Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude <noreply@anthropic.com>
…s for ADR-0138 D2b Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude <noreply@anthropic.com>
…b-publish-refusal
…b-publish-refusal
📓 Docs Drift CheckThis PR changes 3 package(s): 28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 145 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 a5b05ced1ca5671c89bbea303d112b327af20468 && git checkout a5b05ced1ca5671c89bbea303d112b327af20468
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5656c5638abfcee5081571e6c7c5f5c8998a1812 1bd1ba175673e6f4d194a556fb71b0ae23b1c051 && git checkout -B drift-repro 5656c5638abfcee5081571e6c7c5f5c8998a1812 && git merge --no-ff 1bd1ba175673e6f4d194a556fb71b0ae23b1c051
node scripts/docs-audit/affected-docs.mjs --json 5656c5638abfcee5081571e6c7c5f5c8998a1812
|
Contract reviewServed-tier: Inputs read: card #23225 (body and all 13 comments, the D2b claim ① Derived judgmentsThe eight new The stack-schema refinement — right, and every embedder inherits it. The composition with
The metadata protocol's flow-publish refusal — right, and the one-direction shape is right. Both directions at every door the diff names, each refusal with the prescription — yes. Stack parse: both, as one input. Package publish: both, at the package's own item. Protocol: the flow direction, the only direction that door can hold. The record's prescription, "an authenticated endpoint for a partner that can hold a credential; the signed inbound-hook channel (ADR-0041) for a partner webhook; a public form for anonymous intake", is carried verbatim in substance by both sentences ( The The ADR anchor ( ② Semver levelThe changeset ③ Boundary flagsEvery dev flag (the
Nothing the record's E2 row requires of D2b is missing: the refusal in both directions at every publish door the diff names, each refusal carrying the prescription, each pin with a negative control (the controls are in all three test files), the Check-runs on the head, as found at the read (47 runs): the five required contexts: Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #23225
Clause-②: yes (narrowing)
ADR-0138 card E2, D2b: the card's last leg (slices 1 and 2, D2, landed as
01d503432bandf24574ae0). Publish refuses anauthRequired: falseendpoint oftype: 'flow'whose target flow declaresrunAs: 'system', in both directions, each refusal naming both items and carrying the record's prescription, with oneregisteredADR-0087 semantic entry.The
Clause-②line above is the claim's, copied verbatim as the order requires. The measured reading isyes (narrowing), and the changeset says so. The judge has to be callable from two packages outside@objectstack/spec(the metadata layer's package publish and the metadata protocol's flow publish), so it is exported from@objectstack/spec/api: five functions and three interfaces join the published surface (api-surface/api.json+8). Every landed narrowing that also added exports declaresyes (narrowing)(for example the23015,23072and23165changesets). No new error code is minted. All three packages are gradedminor, so either value passescheck-changeset-no-major. The seat may amend the line.Doors, measured on
origin/main6eb6f7d65(the order's H1 and H2)api, code-only:allowRuntimeCreate: false)ObjectStackDefinitionSchema(defineStack,os validate,os build, the artifact load)apisandflowstogetherapplyApiEndpointGatesMetadataManager.publishPackage(POST /api/v1/packages/:id/publish),@objectstack/metadataapiandflowitems, and the manager's indexallowRuntimeCreate: true)saveMetaItemin publish mode,promoteDraftForPublish(the per-item publish andpublishPackageDrafts)metadataserviceendpoint-publish-gate.ts's entry isvalidateApiEndpointDeclarations(:155) withpolicyGateat:499, as stated. But other packages do import the gate:@objectstack/metadatacallsvalidateApiEndpointDeclarationsingateApiItemsForPublish(the package publish) andidentityFreeEndpointGateFailurein the endpoint matcher's load-time backstop. The package publish is the second endpoint publish door, so D2b's endpoint direction lands there too. The order's clause for a measured producer elsewhere covers it;packages/metadatawas not on the claim's file surface.@objectstack/service-automation. It judges no publish. It registers credential and mutation hooks on the protocol and arms flows after they are published (registerFlow, the mutation sync). A stored flow's publish is judged inmetadata-protocol(saveMetaItem,promoteDraftForPublish). So this PR changes nothing in service-automation, and its suite is run as a consumer reading.customissue atapis[i].authRequired(defineStackthrowsSTACK_SCHEMA_INVALID/ 422); at the package publish, the existingvalidationErrorsresult (success: false); at the protocol,INVALID_METADATA/ 422 withissues[0].path: 'runAs', the envelope of that door's other publish-time contract refusals.runAsdescription misled. It prescribedsystemfor user-less triggers,apiincluded, which is the path the record names. It now names the exception (an anonymous flow endpoint) and what serves that caller. This is a text change;content/docs/references/**is regenerated.What the refusal reads
One pure judge,
anonymousElevatedFlowPairsinendpoint-publish-gate.ts's new section (f), besidepolicyGate. It is not part of the per-endpointfirstFailurechain, because no single declaration carries both halves. It pairs an endpoint withtype: 'flow',authRequired: false(exactly; an absent key is the schema default,true) and a non-emptytargetwith a flow of that name declaringrunAs: 'system'. Two refusal sentences word it per direction:anonymousEndpointElevatedFlowMessagewhen the endpoint is being published,elevatedFlowAnonymousEndpointsMessagewhen the flow is. Both carry the record's prescription: an authenticated endpoint for a partner that can hold a credential, the flow's signed inbound hook (ADR-0041) for a partner webhook, or a public form for anonymous intake.runAs: 'automation'is not prescribed until ADR-0073 M2 adds the value.Not judged, on purpose: a draft save (a draft does not execute; its publish is judged); a
migrate-storedre-save of an existing row (refusing it would leave a credential the move takes out of the row in cleartext); and, by the record's own words, the indirect path where a guest-written row fires a record-change flow.Where it composes with #23375 (
eeb95f7ff, merged into this branch): the D2b leg runs insideapplyApiEndpointGates, before the per-endpoint gates. The new.superRefine(applyPermissionSetOwnerBindingGate)is chained after it, unchanged and unreordered. The protocol edits are in the save and promotion blocks beside the default-record-page refusal.permissionSetOwnerBindingRefusal,assertLockAllowsDeleteandrestoreArtifactRegistryVieware untouched. #23375 covers none of D2b.Tests (at
1bd1ba175, the merged tree)src/api/anonymous-elevated-flow-gate.test.ts, 14 cases:code: 'custom'at['apis', 0, 'authRequired'];defineStackthrowsSTACK_SCHEMA_INVALID/ 422;systemflow, and an anonymous endpoint on auserflow or a default-runAsflow;src/publish-anonymous-elevated-flow-gate.test.ts, 8 cases:validate: falsedoes not disable it;src/protocol.item-name-every-door.test.ts, on that file's pinned double (no new engine double, so no ledger change), 10 cases:publishPackageDrafts, each with its control;metadataservice as well as the registry;metadataservice read fails the publish;migrate-storedsaves are not judged.@objectstack/specpnpm test: 674 files / 20475 tests;@objectstack/metadata: 62 / 888;@objectstack/metadata-protocol: 235 files passed (3 skipped) / 28373 tests;@objectstack/service-automation: 186 / 2486;@objectstack/lint: 138 / 6699;@objectstack/cli--project unit: 287 / 4307. Theintegrationlayer is declared to CI.spec,metadataandmetadata-protocolexit 0. Spec's test layer holds its pinned debt, so the new test file adds no error.scripts/ablation-replace.mjswith its restore trap, run at the pre-merge headf66d8c10c, where the merge touched none of the four sites. The expected direction (red) was declared before each run. No build leg: each suite reads the mutated file fromsrc.HEADandgit diff HEADis empty.Gates (at
1bd1ba175)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths): 125 commands at1bd1ba175, the same list as at the pre-merge head.check:skill-examplesanswered PREREQUISITE NOT MET (exit 3:@objectstack/client-reacthas no built declarations). It is NOT MEASURED until it re-runs after that build.os-dev-reportcomment, not here (this body is written once): the remaining commands, that re-run, and the--ranreconciliation.node scripts/check-changeset-no-major.mjs --base origin/main --eventover this PR's title and body exits 0 ("LEVEL AXIS: … declares clause-②no (narrowing)"). The same run with the line amended toyes (narrowing)also exits 0.check-governed-merges --testover the 16 paths: 0 hit the register, NOT governed.Lint, as a proven narrowing:
eslint --no-inline-config --format jsonover the 10 changed.tsfiles reports 10 files, 0 errors, 0 warnings and 0 ignored.eslint.config.mjsenables no type-aware linting (noparserOptions.project), so a change here cannot move a verdict on an untouched file. The repo-widepnpm lintis left to CI.Consumer sweep
examples/**: zeroauthRequired: falsedeclarations (one comment mentions the key).runAs: 'user'flow (anonymous-door-sweep,declarative-endpoint-anonymous-guest, the runtime endpoint fallback tests).guest-owner-stamp.dogfood.test.tspairs anonymousobject_operationendpoints with asystemrecord-change flow. That is the record's indirect path, not D2b's pair, and it is untouched.showcase_inbound_task_webhooktakes the signed inbound hook withrunAs: 'system'and is untouched.Acceptance notes
rollbackMetaItemandrevertCommitre-activate a stored flow version through the repository's synchronousderiveRestoredBody, which has no async read to ask the endpoints. The record says "the publish path", and no cross-item refusal sits on the restore doors today (the default-record-page refusal does not either). Carrier: none; noted, not filed.buildEndpointIndex) holds no flows. A flow registered straight into the engine (registerFlow) passes no publish. A pair whose endpoint and flow live in two packages of one composed artifact is judged at the package publish, but not at the stack parse, which reads only top-levelapisandflows, the same scope as the per-endpoint gates.refusesElevatedSelfTriggeredStartcovers self-triggered types only. The record places D2b at publish, and the module header now says so.metadataservice'sapiitems as bodies, asgetMetaItemsdoes. A wrapped publish envelope registered by a directmetadata.register()is read only by the package publish, which peels.Generated by Claude Code