Repository navigation
Commit c857a93
feat(spec)!: publish refuses an anonymous flow endpoint whose target flow runs as system, in both directions (ADR-0138 D2b) (#23411)
Fixes #23225
Clause-②: yes (narrowing)
ADR-0138 card E2, D2b: the card's last leg (slices 1 and 2, D2, landed
as `01d503432b` and `f24574ae0`). Publish refuses an `authRequired:
false` endpoint of `type: 'flow'` whose target flow declares `runAs:
'system'`, in both directions, each refusal naming both items and
carrying the record's prescription, with one `registered` ADR-0087
semantic entry.
**The `Clause-②` line above is the claim's, copied verbatim as the order
requires. The measured reading is `yes (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 declares `yes (narrowing)` (for example the
`23015`, `23072` and `23165` changesets). No new error code is minted.
All three packages are graded `minor`, so either value passes
`check-changeset-no-major`. The seat may amend the line.
## Doors, measured on `origin/main` `6eb6f7d65` (the order's H1 and H2)
| item | publish door | what it holds | D2b leg |
|:--|:--|:--|:--|
| endpoint (`api`, code-only: `allowRuntimeCreate: false`) | the stack
parse, `ObjectStackDefinitionSchema` (`defineStack`, `os validate`, `os
build`, the artifact load) | the stack's `apis` and `flows` together |
both directions as one input, in `applyApiEndpointGates` |
| endpoint and flow | `MetadataManager.publishPackage` (`POST
/api/v1/packages/:id/publish`), `@objectstack/metadata` | the package's
`api` and `flow` items, and the manager's index | both directions |
| flow (`allowRuntimeCreate: true`) | the metadata protocol:
`saveMetaItem` in publish mode, `promoteDraftForPublish` (the per-item
publish and `publishPackageDrafts`) | the flow being published | the
flow direction: the declared endpoints are read from the engine registry
and the `metadata` service |
- **H1 partly falsified.** `endpoint-publish-gate.ts`'s entry is
`validateApiEndpointDeclarations` (`:155`) with `policyGate` at `:499`,
as stated. But other packages do import the gate:
`@objectstack/metadata` calls `validateApiEndpointDeclarations` in
`gateApiItemsForPublish` (the package publish) and
`identityFreeEndpointGateFailure` in 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/metadata` was not on
the claim's file surface.
- **H2 falsified for `@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 in `metadata-protocol`
(`saveMetaItem`, `promoteDraftForPublish`). So this PR changes nothing
in service-automation, and its suite is run as a consumer reading.
- **H3 holds.** Existing envelopes only: at the parse, a zod `custom`
issue at `apis[i].authRequired` (`defineStack` throws
`STACK_SCHEMA_INVALID` / 422); at the package publish, the existing
`validationErrors` result (`success: false`); at the protocol,
`INVALID_METADATA` / 422 with `issues[0].path: 'runAs'`, the envelope of
that door's other publish-time contract refusals.
- **H4: the `runAs` description misled.** It prescribed `system` for
user-less triggers, `api` included, 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, `anonymousElevatedFlowPairs` in
`endpoint-publish-gate.ts`'s new section (f), beside `policyGate`. It is
not part of the per-endpoint `firstFailure` chain, because no single
declaration carries both halves. It pairs an endpoint with `type:
'flow'`, `authRequired: false` (exactly; an absent key is the schema
default, `true`) and a non-empty `target` with a flow of that name
declaring `runAs: 'system'`. Two refusal sentences word it per
direction: `anonymousEndpointElevatedFlowMessage` when the endpoint is
being published, `elevatedFlowAnonymousEndpointsMessage` when 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-stored` re-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 inside `applyApiEndpointGates`, 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`, `assertLockAllowsDelete` and
`restoreArtifactRegistryView` are untouched. #23375 covers none of D2b.
## Tests (at `1bd1ba175`, the merged tree)
- **Spec**, `src/api/anonymous-elevated-flow-gate.test.ts`, 14 cases:
- each direction is the edit that completes the pair from an accepted
stack, asserted as `code: 'custom'` at `['apis', 0, 'authRequired']`;
- `defineStack` throws `STACK_SCHEMA_INVALID` / 422;
- the controls: an authenticated endpoint on a `system` flow, and an
anonymous endpoint on a `user` flow or a default-`runAs` flow;
- the judge's edges and the prescription in both sentences.
- **Metadata**, `src/publish-anonymous-elevated-flow-gate.test.ts`, 8
cases:
- the endpoint direction with the flow in the package and in another
package, and the flow direction, each refused at its own item with
nothing published;
- `validate: false` does not disable it;
- a publish-envelope body is peeled first;
- two controls.
- **Metadata protocol**, section 5 of
`src/protocol.item-name-every-door.test.ts`, on that file's pinned
double (no new engine double, so no ledger change), 10 cases:
- the publish-mode save, the per-item publish and
`publishPackageDrafts`, each with its control;
- the endpoint read from the `metadata` service as well as the registry;
- a failing `metadata` service read fails the publish;
- draft and `migrate-stored` saves are not judged.
- **Consumer readings** at the merged head (each package's own exit 0,
run through the shared lock):
- `@objectstack/spec` `pnpm 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. The `integration`
layer is declared to CI.
- **Typecheck**: `spec`, `metadata` and `metadata-protocol` exit 0.
Spec's test layer holds its pinned debt, so the new test file adds no
error.
- **Ablations**: four, through `scripts/ablation-replace.mjs` with its
restore trap, run at the pre-merge head `f66d8c10c`, 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 from `src`.
- A1, the stack leg fed no flows: 5 failed / 9 passed, exactly the five
stack-parse refusals.
- A2, the save-door call removed: 3 failed / 36 passed, the three
save-door refusals.
- A3, the promotion call removed: 2 failed / 37 passed, the per-item and
batch publish refusals.
- A4, the package-publish call removed: 5 failed / 3 passed, the five
package refusals.
- Every anchor went 1 to 0, and every restore was proven: blob equals
`HEAD` and `git diff HEAD` is empty.
## Gates (at `1bd1ba175`)
- **Derived** with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths): 125 commands at `1bd1ba175`, the
same list as at the pre-merge head.
- **At the moment this PR opened**, 71 had run:
- 70 exit 0;
- `check:skill-examples` answered PREREQUISITE NOT MET (exit 3:
`@objectstack/client-react` has no built declarations). It is NOT
MEASURED until it re-runs after that build.
- **The rest is reported on #23225**, in the `os-dev-report` comment,
not here (this body is written once): the remaining commands, that
re-run, and the `--ran` reconciliation.
- **Changeset level axis**: `node scripts/check-changeset-no-major.mjs
--base origin/main --event` over this PR's title and body exits 0
("LEVEL AXIS: … declares clause-② `no (narrowing)`"). The same run with
the line amended to `yes (narrowing)` also exits 0.
- **Governed paths**: `check-governed-merges --test` over the 16 paths:
0 hit the register, NOT governed.
Lint, as a proven narrowing: `eslint --no-inline-config --format json`
over the 10 changed `.ts` files reports 10 files, 0 errors, 0 warnings
and 0 ignored. `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`), so a change here cannot move a verdict on an
untouched file. The repo-wide `pnpm lint` is left to CI.
## Consumer sweep
- `examples/**`: zero `authRequired: false` declarations (one comment
mentions the key).
- In-tree fixtures: every anonymous flow endpoint in a test names a
`runAs: 'user'` flow (`anonymous-door-sweep`,
`declarative-endpoint-anonymous-guest`, the runtime endpoint fallback
tests).
- `guest-owner-stamp.dogfood.test.ts` pairs anonymous `object_operation`
endpoints with a `system` record-change flow. That is the record's
indirect path, not D2b's pair, and it is untouched.
- `showcase_inbound_task_webhook` takes the signed inbound hook with
`runAs: 'system'` and is untouched.
- No hit needed a fate.
## Acceptance notes
- **Restore doors are not judged.** `rollbackMetaItem` and
`revertCommit` re-activate a stored flow version through the
repository's synchronous `deriveRestoredBody`, 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.
- **Not publish doors, so not judged here.** The endpoint matcher's
load-time index (`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-level `apis` and `flows`, the same scope as the per-endpoint
gates.
- **No full runtime counterpart.** The executor's
`refusesElevatedSelfTriggeredStart` covers self-triggered types only.
The record places D2b at publish, and the module header now says so.
- **The protocol reads endpoints as its own read API does.** It reads
the registry's and the `metadata` service's `api` items as bodies, as
`getMetaItems` does. A wrapped publish envelope registered by a direct
`metadata.register()` is read only by the package publish, which peels.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent b5b1bf7 commit c857a93
16 files changed
Lines changed: 1218 additions & 14 deletions
File tree
- .changeset
- content/docs/references
- api
- automation
- packages
- metadata-protocol/src
- metadata/src
- spec
- api-surface
- export-origins
- src
- api
- automation
- migrations/entries/semantic
- scripts/adr-anchors
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
0 commit comments