Skip to content

Commit 31ed067

Browse files
fix(lint,metadata-protocol)!: /meta refuses a secretless api flow, and the runtime authoring gate reads a withheld-and-stored secret as present (#20611) (#20692)
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
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
'@objectstack/lint': minor
3+
'@objectstack/metadata-protocol': minor
4+
---
5+
6+
The runtime metadata publish gate refuses an `api` flow with no per-flow secret, and reads a secret the flow read path withheld as present (#20611).
7+
8+
Clause-②: yes (narrowing)
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable changes spelling or type: `packages/spec` is untouched, and the start node `config` stays the open record it was. What changes is that the runtime metadata write door now refuses one authored shape at publish: an `api`-bound flow whose start node carries no usable `config.secret`. `objectstack migrate meta` could not rewrite that shape even in principle, because the missing value is a shared secret only the author and the sending system can supply. Rows at rest are not judged or rewritten; the automation engine has refused to register such a flow since 17.5.0, and that load path's disposition is recorded in its own published changelog entry. -->
11+
12+
**BREAKING** — an accept-set narrowing on the runtime metadata write door,
13+
shipped as `minor` under the launch-window convention (`check-changeset-no-major`
14+
refuses `major` until GA; breaking-ness is carried by this banner and the ADR-0087
15+
disposition above, not by the level). An `active` save through `/meta` of an
16+
`api`-bound flow whose start node carries no usable `config.secret` (a
17+
`PUT /api/v1/meta/flow/:name`, or the publish of such a draft) used to be stored;
18+
the automation engine then refused to register it (`400` on the `/automation`
19+
doors, a skip with a warning at boot). It is now refused at the save with
20+
`422 INVALID_METADATA`, the issue naming `flow-api-trigger-secret-missing` at the
21+
start node's `config.secret`, and nothing is stored. A draft save is still
22+
accepted; its publish is refused the same way.
23+
**One-line fix:** set a non-blank `config.secret` on the flow's start node — or,
24+
for a flow that is only ever started explicitly, declare `type: 'autolaunched'`
25+
with no `triggerType: 'api'`.
26+
27+
**What does not change: a signed flow's round trip.** Every served flow definition withholds the start node's `config.secret`, so a body saved back after a read arrives without it, and the save restores the stored secret only after every gate has run, so that no gate handles a restored credential. The gate is now told WHERE the save will restore a credential from the stored row: those positions only, never the values. `flow-api-trigger-secret-missing` reads a secret that was withheld and is stored as present, and one that is absent and not stored as missing. So a GET → edit → PUT of a signed flow, and the first save of a code-authored flow whose secret is in the app's source, keep passing and keep their secret. An explicit empty `config.secret` is the author's own value and is refused as blank.
28+
29+
`@objectstack/lint`:
30+
31+
- `validateFlowApiTriggerSecret` now runs on the runtime publish gate too (`surfaces` `['cli', 'runtime-publish']`, `runtimeTypes: ['flow']`). Its `surfaceReason` is gone.
32+
- `AuthoringRuleContext` gains an optional `restoredCredentialPaths`: a `ReadonlySet<string>` of stack-relative positions in the rules' own finding-path spelling (`flows[0].nodes[1].config.secret`). Only the runtime publish gate sets it; `runAuthoringRules` never forwards it, so `os validate`, `os build` and `os lint` judge the author's own values as before.
33+
- `runRuntimeAuthoringRules` accepts an optional `restoredCredentialPaths`: item-relative dotted positions in the `@objectstack/spec/kernel` redactor registry's `redactedKeys` spelling (`nodes.1.config.secret`). The gate re-spells them against the written item's place in its snapshot.
34+
- `validateFlowApiTriggerSecret(stack, options?)` accepts an optional `{ restoredCredentialPaths }`, and treats a listed start-node secret position as present.
35+
36+
`@objectstack/metadata-protocol`: `saveMetaItem` hands the runtime authoring gate the positions its own credential carry-forward will fill, computed from the same stored body. This costs one indexed `sys_metadata` read on an `active` save of a type with a registered redactor (`datasource`, and `flow` where the automation plugin registers one), and nothing for any other type or for a draft save. The carry-forward itself is unchanged and still runs after every gate. The draft→active promotion judges the stored draft row, which already holds what that draft's save carried forward, so it needs no such positions.

‎packages/lint/src/authoring-rules.ts‎

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,28 @@ export interface AuthoringRuleContext {
337337
* this input or does not judge.
338338
*/
339339
judgeFilter?: IObjectQLEngine['judgeFilter'];
340+
/**
341+
* [#20611] The credential positions of the WRITTEN item that the write path
342+
* restores from the stored row before it persists the item — stack-relative,
343+
* in the rules' own finding-path spelling (`flows[0].nodes[1].config.secret`).
344+
*
345+
* Set only by the runtime publish gate (`runtime-gate.ts`), from what its host
346+
* states; ABSENT on the three CLI commands, whose stacks carry the author's
347+
* own values. It exists because the read path withholds a credential from
348+
* every served definition (a flow's start-node `config.secret`), so a body
349+
* saved back after a read arrives WITHOUT it, and the host's carry-forward
350+
* puts the stored value back only after every gate has run — on purpose, so
351+
* that no gate handles a restored credential. A rule therefore cannot tell a
352+
* withheld credential from a missing one by reading the body, and this set is
353+
* how it is told: a path listed here is WITHHELD AND STORED, and a rule
354+
* judging whether a credential is present reads it as present. A path not
355+
* listed is judged on the body as sent, so a credential that is absent and
356+
* not stored is missing.
357+
*
358+
* ⛔ Positions only, never values: the gate never sees a restored credential.
359+
* Read by `validateFlowApiTriggerSecret` only.
360+
*/
361+
restoredCredentialPaths?: ReadonlySet<string>;
340362
}
341363

342364
export interface AuthoringRule {
@@ -1113,7 +1135,8 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
11131135
// routes nowhere, silently demoting it to a manual flow. #20553 made it five: an
11141136
// `api`-bound flow with no usable `config.secret`, which the engine's own
11151137
// `registerFlow` refuses (ADR-0041) — on its OWN entry below
1116-
// (`validateFlowApiTriggerSecret`), because it is CLI-only for now. None of those verdicts
1138+
// (`validateFlowApiTriggerSecret`), because it reads a context input this entry
1139+
// has no use for (#20611). None of those verdicts
11171140
// can be changed by installing a package, so there is no reading under which
11181141
// the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
11191142
// (the object may come from another installed package — a hedge this rule
@@ -1144,33 +1167,32 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [
11441167
run: (stack) => validateFlowTriggerReadiness(stack),
11451168
},
11461169
// #20553 — `flow-api-trigger-secret-missing`, split out of the entry above as
1147-
// its own exported rule (the `validateSecurityRoleWord` precedent: one rule id
1148-
// sits on ONE side of the runtime wall). Same family, same `error`, all three
1149-
// commands — but NOT the runtime publish gate yet, and #20611 is the card that
1150-
// moves it across. The flow read path withholds `config.secret` from every
1151-
// served definition (#20552) and `saveMetaItem` restores the stored secret only
1152-
// just before the put, AFTER this table has judged the body the caller sent —
1153-
// so on the gate, a signed flow's ordinary GET → edit → PUT reads as
1154-
// secretless. Measured on `825c33ff9f`: with this id on the gate, the two
1155-
// round-trip pins in `protocol.metadata-redaction.test.ts` fail; off it, 26/26.
1156-
// The `/meta` door therefore keeps its pre-rule behaviour (it stores a
1157-
// secretless flow, and the engine refuses it at registration) until the gate
1158-
// judges the carried-forward body — then this entry becomes `CLI_AND_RUNTIME`
1159-
// with `runtimeTypes: ['flow']`, like the one above.
1170+
// its own exported rule. Same family, same `error`, all three commands.
1171+
//
1172+
// #20611 — and the runtime publish gate too. The flow read path withholds
1173+
// `config.secret` from every served definition (#20552), and `saveMetaItem`
1174+
// restores the stored secret only just before the put, AFTER this table has
1175+
// judged the body the caller sent — deliberately, so no gate handles a
1176+
// restored credential. So the gate is handed the POSITIONS that restore will
1177+
// fill (`AuthoringRuleContext.restoredCredentialPaths`), and the rule reads a
1178+
// withheld-and-stored secret as present: a signed flow's ordinary
1179+
// GET → edit → PUT passes, and a secretless `api` flow is refused at `/meta`
1180+
// with this id instead of being stored for the engine to refuse at
1181+
// registration.
11601182
{
11611183
name: 'validateFlowApiTriggerSecret',
11621184
tier: 'gating',
11631185
input: 'normalized',
11641186
commands: ALL,
11651187
source: 'packages/lint/src/validate-flow-trigger-readiness.ts',
1166-
surfaces: CLI_ONLY,
1167-
surfaceReason:
1168-
'Not yet runtime-safe: the publish gate judges a /meta save BEFORE saveMetaItem restores the ' +
1169-
'inbound-hook secret the flow read path withholds, so a signed api flow\'s ordinary GET, edit, PUT ' +
1170-
'round trip reaches this rule secretless and would be refused. It crosses when the gate judges the ' +
1171-
'carried-forward body (the seam follow-up named in the comment above); until then the engine\'s ' +
1172-
'registerFlow refusal is what a secretless flow saved through /meta meets.',
1173-
run: (stack) => validateFlowApiTriggerSecret(stack),
1188+
// Runtime publish gate (#20611): judged on the per-write snapshot like the
1189+
// entry above, with the host's restored-credential positions read as
1190+
// present. The CLI never sets that input, so there the author's own
1191+
// `config.secret` is the whole answer.
1192+
surfaces: CLI_AND_RUNTIME,
1193+
runtimeTypes: ['flow'],
1194+
run: (stack, ctx) =>
1195+
validateFlowApiTriggerSecret(stack, { restoredCredentialPaths: ctx.restoredCredentialPaths }),
11741196
},
11751197
// ADR-0090 D3 fallout — an approval `{ type: 'role' }` resolves against the
11761198
// better-auth org-membership tier, not positions, so a position name authored

‎packages/lint/src/runtime-gate.ts‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,49 @@ export function nameKeyFindingPath(path: string, candidate: AnyRec): string {
832832
return `${stackKey}.${name}${rest}`;
833833
}
834834

835+
/**
836+
* [#20611] The host's restored-credential positions, re-spelled from the
837+
* redactor registry's item-relative dotted form (`nodes.1.config.secret`) into
838+
* the finding-path form the rules emit and compare against, anchored at the
839+
* written item's place in `candidate` (`flows[0].nodes[1].config.secret`).
840+
*
841+
* The written item is the LAST member of its collection in the candidate —
842+
* {@link buildRuntimeWriteSnapshots} appends it — so that index is the anchor.
843+
* Each segment is spelled `[n]` exactly where the item holds an array at that
844+
* point of the walk and `.key` everywhere else: read off the item, never
845+
* guessed from the segment's digits, so a record key that happens to be
846+
* numeric keeps its key spelling. A path that walks off the item keeps the key
847+
* spelling from there on; it names no position any rule reports, so it
848+
* excuses nothing.
849+
*/
850+
function restoredCredentialStackPaths(
851+
candidate: AnyRec,
852+
type: string,
853+
dottedPaths: readonly string[],
854+
): ReadonlySet<string> {
855+
const stackKey = stackKeyForType(type);
856+
const collection = stackKey ? candidate[stackKey] : undefined;
857+
if (!stackKey || !Array.isArray(collection) || collection.length === 0) return new Set();
858+
const index = collection.length - 1;
859+
const item: unknown = collection[index];
860+
const out = new Set<string>();
861+
for (const dotted of dottedPaths) {
862+
let path = `${stackKey}[${index}]`;
863+
let node: unknown = item;
864+
for (const segment of dotted.split('.')) {
865+
if (Array.isArray(node) && /^(0|[1-9][0-9]*)$/.test(segment)) {
866+
path += `[${segment}]`;
867+
node = node[Number(segment)];
868+
} else {
869+
path += `.${segment}`;
870+
node = node !== null && typeof node === 'object' ? (node as AnyRec)[segment] : undefined;
871+
}
872+
}
873+
out.add(path);
874+
}
875+
return out;
876+
}
877+
835878
function runRules(
836879
rules: readonly AuthoringRule[],
837880
stack: AnyRec,
@@ -894,6 +937,20 @@ export function runRuntimeAuthoringRules(args: {
894937
* it skip the engine's judgement and answer as they did without it.
895938
*/
896939
judgeFilter?: IObjectQLEngine['judgeFilter'];
940+
/**
941+
* [#20611] The positions in `item` at which the host's write path restores a
942+
* credential from the stored row before it persists the item — dotted and
943+
* item-relative, the `@objectstack/spec/kernel` redactor registry's
944+
* `redactedKeys` spelling (`nodes.1.config.secret`). The read path withholds
945+
* those credentials, so a body saved back after a read arrives without them,
946+
* and the host restores them only after this gate has run.
947+
*
948+
* Handed to the rules as `AuthoringRuleContext.restoredCredentialPaths`,
949+
* translated into the candidate snapshot's finding-path spelling; see
950+
* {@link restoredCredentialStackPaths}. Omitted, every position is judged on
951+
* the body as sent. ⛔ Positions only: no credential value reaches this gate.
952+
*/
953+
restoredCredentialPaths?: readonly string[];
897954
}): RuntimeGateResult {
898955
const rules = runtimeAuthoringRulesFor(args.type);
899956
const empty: RuntimeGateResult = { errors: [], advisories: [], rulesRun: [] };
@@ -921,6 +978,19 @@ export function runRuntimeAuthoringRules(args: {
921978
sduiManifest: args.sduiManifest,
922979
runtimeWriteType: args.type,
923980
judgeFilter: args.judgeFilter,
981+
// [#20611] Spelled against the CANDIDATE, the one snapshot that holds the
982+
// written item. The baseline pass shares the set and cannot match it: the
983+
// item is not in the baseline, and every other entry sits at an index the
984+
// item does not.
985+
...(args.restoredCredentialPaths !== undefined && args.restoredCredentialPaths.length > 0
986+
? {
987+
restoredCredentialPaths: restoredCredentialStackPaths(
988+
snapshots.candidate,
989+
args.type,
990+
args.restoredCredentialPaths,
991+
),
992+
}
993+
: {}),
924994
};
925995
const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
926996
const added = runRules(rules, snapshots.candidate, ctx)

0 commit comments

Comments
 (0)