Repository navigation
Commit 31ed067
Fixes #20611
Clause-②: yes (narrowing)
> The declaration above is copied verbatim from the dispatch claim. H2
measured a published-surface widening in `@objectstack/lint` (below),
and the claim says its value is then amended to yes. The changeset
already declares yes with the narrowing arm. Whether this line's value
becomes yes is the seat's call.
## What changes
`flow-api-trigger-secret-missing` now runs on the runtime metadata
publish gate as well as on `os validate` / `os build` / `os lint`. The
gate is handed the redaction context, so a signed flow's ordinary round
trip still passes.
- **Why the gate needed context.** The flow read path withholds the
start node's `config.secret` from every served definition (#20552). So a
body saved back after a read arrives without it. `saveMetaItem` restores
the stored secret in `carryForwardRedactedCredentials`, deliberately
AFTER every gate, so that no gate handles a restored credential. At the
gate, a withheld secret and a missing one looked the same.
- **The fix, per triage 5886067949.** The carry-forward is not moved.
`saveMetaItem` computes the POSITIONS the carry-forward will fill from
the stored row. It uses the same stored body and the same plan, and it
grafts nothing. Those positions go to the gate. The rule reads a
withheld-and-stored secret as present, and one that is absent and not
stored as missing.
- **The refusal.** A secretless `api` flow saved `active` at `/meta` (or
a draft of one, at publish) now gets `422 INVALID_METADATA`, with the
issue `flow-api-trigger-secret-missing` at the start node's
`config.secret`. Nothing is stored. Before this change it was stored,
and the engine refused it at registration.
### Where it lands
- `packages/metadata-protocol/src/metadata-redaction.ts`:
`carryForwardRedactedValues` is refactored onto one internal plan
(`planCarryForward`), unchanged in behaviour. The new internal
`redactedPathsCarriedForward(type, incoming, stored)` answers where that
plan lands a value: the positions only, relative to the incoming body.
It is not on the package entry.
- `packages/metadata-protocol/src/protocol.ts`:
- `saveMetaItem` passes the gate a lazily resolved
`restoredCredentialPaths`. It is resolved only after the gate's early
returns, so a draft save, the package-author channel and
`migrate-stored` pay no read.
- `restoredCredentialPathsFor` reads the stored body through the
carry-forward's own reader. That reader is factored out as
`storedBodyForCarryForward`: the active row, else the code layer.
- The read is not wrapped in the collections' best-effort guard. A
failed stored-row read fails the save, as the carry-forward's own read
already does.
- `packages/metadata-protocol/src/runtime-authoring-gate.ts`:
`evaluateRuntimeAuthoringGate` (internal) forwards the positions.
- `packages/lint/src/runtime-gate.ts`: `runRuntimeAuthoringRules`
accepts the item-relative dotted positions (`nodes.1.config.secret`). It
re-spells them against the written item's place in the candidate
snapshot (`flows[0].nodes[1].config.secret`), reading `[n]` versus
`.key` off the item itself. It hands them to the rules as
`AuthoringRuleContext.restoredCredentialPaths`.
- `packages/lint/src/authoring-rules.ts`: the
`validateFlowApiTriggerSecret` entry becomes `CLI_AND_RUNTIME` with
`runtimeTypes: ['flow']`, and the `surfaceReason` naming this card is
gone. `AuthoringRuleContext` gains the optional
`restoredCredentialPaths`. `runAuthoringRules` (the CLI) never forwards
it.
- `packages/lint/src/validate-flow-trigger-readiness.ts`:
`validateFlowApiTriggerSecret(stack, options?)` treats a start-node
secret position listed in `options.restoredCredentialPaths` as present.
### Dispatch hypotheses, measured
- **H1 held, with the second site measured.** The second
`assertRuntimeAuthoringRules` call site is `promoteDraftForPublish`, the
publish and publish-package-drafts door. It judges the STORED draft row,
which the draft's own save already carried forward (the draft
carry-forward falls back to the active row, then the code layer). So it
needs no context.
- Pinned both ways: "a DRAFT save then a publish carries the secret into
the active row" stays green with the rule live. A secretless api draft
saves, and its publish is refused with no active row.
- **H2 held: a published surface widens.** `AuthoringRuleContext` and
`runRuntimeAuthoringRules` are on `@objectstack/lint`'s root entry
(`runRuntimeAuthoringRules` also on `./runtime`). Both gain an optional
field. The exported `validateFlowApiTriggerSecret` gains an optional
second parameter. `@objectstack/metadata-protocol`'s entry is unchanged:
`redactedPathsCarriedForward` and `evaluateRuntimeAuthoringGate` are not
exported from it.
- **H3 held.** The positions are computed from the redactor's paths and
the stored body, by the carry-forward's own plan. The graft is not
re-run early.
- **H4 held.** The gate answers `422` / `INVALID_METADATA` (ADR-0112),
the same envelope as the other gating runtime rules
(`protocol.runtime-authoring-gate.test.ts`). The new pins assert `code`
+ `status` + the rule id and path.
### Pin sweep
- **Flipped:** `protocol.metadata-redaction.test.ts`, "the save door:
the next served read of a relocated flow carries no credential"
(#20590). It saved a relocated `api` flow whose NEW start node has no
secret. The carry-forward drops the old secret there, so this is now
refused.
- The #20590 subject is kept: the relocation now types a secret on the
new start node. It still asserts that the stored secret is carried
nowhere, and adds the new start node's own value at rest.
- A twin pin asserts the secretless relocation is refused (`422` /
`INVALID_METADATA`, the rule at `flows[0].nodes[2].config.secret`) with
the row byte-equal to the seed.
- **Flipped:** `validate-flow-trigger-readiness.test.ts`, the describe
block "one rule id on ONE side of the runtime wall — CLI-only until
#20611". It becomes "on both sides of the runtime wall". The registry
entry is pinned to `['cli','runtime-publish']` / `['flow']` with no
`surfaceReason`. The gate refuses the secretless flow and admits it when
the host restores the position. The CLI table never forwards a
caller-stated set.
- **Repo-wide grep:** every test file that saves through `/meta`
(`saveMetaItem` / `publishMetaItem` / `meta/flow`) and mentions an `api`
trigger. Hits outside these two files are connector or action `type:
'api'`, not flows. No other pin asserts that a secretless `api` flow
saves at `/meta`.
- **Producers:** the one shipped `api` flow (`examples/app-showcase`
`showcase_inbound_task_webhook`) carries a secret. No runtime source in
this repo authors an `api` flow.
`skills/objectstack-automation/SKILL.md` is a finding; see Acceptance
notes.
## Tests
All figures are at `27173c26cc` unless noted.
- `@objectstack/metadata-protocol`, full suite: 189 files passed, 3
skipped; 2789 tests passed, 19 skipped.
- `protocol.metadata-redaction.test.ts`: 47/47. The #20552 pins "GET →
edit → PUT keeps the stored secret; an explicit rotation replaces it"
and "the served body saved straight back persists the overlay row WITH
the stored secret" are green with the rule live.
- New pins:
- a secretless NEW api flow is refused at `/meta` and nothing is stored,
paired in the same harness with the signed round trip admitted;
- the draft door is not a bypass;
- a stored row that never had a secret is refused on its round trip;
- an explicit blank secret is refused;
- `redactedPathsCarriedForward` equals the positions
`carryForwardRedactedValues` changes (served, reordered, rotated,
relocated).
- `@objectstack/lint`, full suite: 115 files, 5382 tests passed.
`validate-flow-trigger-readiness.test.ts`: 90/90.
- **Typecheck:** `@objectstack/lint` (`tsc --noEmit` plus
`check:test-typecheck`, test layer OK) and
`@objectstack/metadata-protocol` (`tsc --noEmit`, 192 test files in its
program) are both green.
- **Downstream:** `@objectstack/objectql`'s 39 test files that call
`saveMetaItem` / `publishMetaItem`: 506/506, against the rebuilt
`metadata-protocol` dist.
- **ESLint** (`--no-inline-config --format json`) on the 8 touched `.ts`
files: 8 files, 0 errors, 0 warnings.
- The config's `files` glob `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`
covers all of them.
- `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`, no typed rules). So this diff cannot move a
verdict on an untouched file. The repo-wide `pnpm lint` is CI's.
### Ablations (committed first; `scripts/ablation-replace.mjs`, restored
with blob equal to HEAD and `git diff HEAD` empty)
1. `protocol.ts`, the gate no longer receives the context
(`restoredCredentialPaths:` renamed to `restoredCredentialPathsAblated:`
at the `saveMetaItem` call). The anchor went 1 to 0 and the blob
`e3825069c5df` became `57762308e583`.
- Red, 3 of 47, each with `[flow-api-trigger-secret-missing]`: both
#20552 round-trip pins, and the new same-harness pin.
2. `validate-flow-trigger-readiness.ts`, the rule ignores the set (`&&
false &&`). The anchor went 1 to 0 and the blob `fd5bd29b1e00` became
`40aa85516a7b`.
- Red, 3 of 90: the three new lint pins that read restored positions.
- Source-resolved: lint's tests import
`./validate-flow-trigger-readiness.js` directly, so no `dist/` was
involved.
3. Reverse type check. `runtime-authoring-gate.ts` passed
`restoredCredentialPaths: 42` into `runRuntimeAuthoringRules`, and `tsc
--noEmit` in `metadata-protocol` failed with TS2345 against the REBUILT
`@objectstack/lint` `.d.ts`. Restored.
### Gates
- `node scripts/pm/dispatch-gates.mjs --commands` at `27173c26cc`
derived 63 families. `--ran` reconciled them: 61 run with exit 0, 2 NOT
MEASURED, 0 unrun.
- NOT MEASURED: `check:dual-build-cjs-loads`, reason: PREREQUISITE NOT
MET (no `dist/` for 59+ packages; the gate needs a whole-repo `pnpm
build`).
- NOT MEASURED: `check:type-check-debt`, reason: PREREQUISITE NOT MET
(18 ledgered packages' dependency closures are not built).
- `check:lean-entry-closure` first exited 3 (no objectql `dist/`). After
`pnpm --filter @objectstack/objectql build` it passed.
- `check-adr-0087-registration` judged the changeset:
`[BREAKING+clause-②-narrowing] not-required
(no-migration-prescription)`.
## Acceptance notes
- **Boundary, per the ruling.** "Withheld and stored" reads as present
whatever the stored value is. The real flow projection withholds any
start-node `secret` except the empty string. So a legacy row whose
stored secret is whitespace-only or a non-string passes the gate on a
round trip, and the engine still refuses it at registration. Judging the
stored VALUE would hand the gate a restored credential, which triage
ruled out.
- **Cost.** One more indexed `sys_metadata` read on an `active` save of
a type with a registered redactor (`datasource` built in; `flow` once
the automation plugin registers its projection). Nothing for other types
or for drafts.
- The gate's read and the carry-forward's read are separate on purpose:
the carry-forward's stays immediately before the put. A write racing
between them could leave the gate's positions one version behind the
carry-forward's.
- **Finding for the seat (class c, not filed here).**
`skills/objectstack-automation/SKILL.md` gives two stale instructions:
- The `config` table's `secret` row says "Strongly recommended — without
it unsigned posts are accepted and a warning is logged". Since 17.5.0
the engine refuses such a flow at registration, since #20553 `os
validate` refuses it, and with this PR `/meta` refuses it.
- The flow-types table describes `api` as "Invoked explicitly via the
API / `engine.execute()`, or bound as an inbound webhook", but every
`type: 'api'` flow binds the inbound trigger and needs a secret.
- An AI author following the skill writes exactly the flow this PR
refuses. `skills/**` is a Tier H surface, so this is not fixed here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0e9ad74 commit 31ed067
9 files changed
Lines changed: 629 additions & 104 deletions
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
337 | 337 | | |
338 | 338 | | |
339 | 339 | | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
340 | 362 | | |
341 | 363 | | |
342 | 364 | | |
| |||
1113 | 1135 | | |
1114 | 1136 | | |
1115 | 1137 | | |
1116 | | - | |
| 1138 | + | |
| 1139 | + | |
1117 | 1140 | | |
1118 | 1141 | | |
1119 | 1142 | | |
| |||
1144 | 1167 | | |
1145 | 1168 | | |
1146 | 1169 | | |
1147 | | - | |
1148 | | - | |
1149 | | - | |
1150 | | - | |
1151 | | - | |
1152 | | - | |
1153 | | - | |
1154 | | - | |
1155 | | - | |
1156 | | - | |
1157 | | - | |
1158 | | - | |
1159 | | - | |
| 1170 | + | |
| 1171 | + | |
| 1172 | + | |
| 1173 | + | |
| 1174 | + | |
| 1175 | + | |
| 1176 | + | |
| 1177 | + | |
| 1178 | + | |
| 1179 | + | |
| 1180 | + | |
| 1181 | + | |
1160 | 1182 | | |
1161 | 1183 | | |
1162 | 1184 | | |
1163 | 1185 | | |
1164 | 1186 | | |
1165 | 1187 | | |
1166 | | - | |
1167 | | - | |
1168 | | - | |
1169 | | - | |
1170 | | - | |
1171 | | - | |
1172 | | - | |
1173 | | - | |
| 1188 | + | |
| 1189 | + | |
| 1190 | + | |
| 1191 | + | |
| 1192 | + | |
| 1193 | + | |
| 1194 | + | |
| 1195 | + | |
1174 | 1196 | | |
1175 | 1197 | | |
1176 | 1198 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
832 | 832 | | |
833 | 833 | | |
834 | 834 | | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
835 | 878 | | |
836 | 879 | | |
837 | 880 | | |
| |||
894 | 937 | | |
895 | 938 | | |
896 | 939 | | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
897 | 954 | | |
898 | 955 | | |
899 | 956 | | |
| |||
921 | 978 | | |
922 | 979 | | |
923 | 980 | | |
| 981 | + | |
| 982 | + | |
| 983 | + | |
| 984 | + | |
| 985 | + | |
| 986 | + | |
| 987 | + | |
| 988 | + | |
| 989 | + | |
| 990 | + | |
| 991 | + | |
| 992 | + | |
| 993 | + | |
924 | 994 | | |
925 | 995 | | |
926 | 996 | | |
| |||
0 commit comments