Commit 3f45b6c
Part of #20590
Clause-②: no
## What this changes
This PR closes the stored-credential positions that the first instance's
projection (PR #20585) did not reach. Each position was measured first.
Its pin was committed red on the unfixed code (`99a75023`) and then
closed (`1f17293b`). The defect is stated abstractly here, per the
security-family disclosure rule.
- **Position 1: a second credential kind in a flow node's `config`.**
The registered `flow` projection
(`service-automation/src/flow-credential-projection.ts`) withheld only
the start node's hook secret.
- It now withholds every position in one table keyed by node kind,
`FLOW_NODE_CREDENTIAL_KEYS`: the start node's `secret` and the `http`
node's `signingSecret`.
- It walks every ADR-0031 region (a `loop` body, `parallel` branches,
`try_catch` try and catch) through `FLOW_REGION_SLOTS_BY_TYPE`. A
credential-holding node nested at any depth is therefore covered.
- **Round trip:** the rule is unchanged. The withheld form (the key
absent) keeps the stored value, and an explicit value replaces it.
- **Removal door, with no spec change:** the empty string.
- `HttpConfigSchema` already accepts it, and it holds no secret.
- The messaging outbox signs only with a non-empty secret.
- The projection serves it as written, so it round-trips as "cleared".
Absent is never read as "remove".
- **Enumeration pin:** `flow-credential-positions.test.ts`.
- It reads every declared node config contract: each builtin executor's
descriptor `configSchema`, the schemaless builtins' spec Zod contracts,
and the approval node's contract.
- It requires the table to equal the credential-named keys it finds, and
each to be withheld at the top level and in every region kind.
- A newly declared credential key turns it red until the key is covered,
or reviewed out with a reason. It is a test in the same package, not a
gate.
- **Position 2: a list fallback that served stored bodies on a protocol
fault.** The runtime dispatcher's `/meta` list branch
(`runtime/src/domains/meta.ts`) no longer swallows a throw from the
protocol's list read. The throw is answered as itself
(`errorFromThrown`). That is one option, per triage's call.
- **Position 3: identity versus kind.** `carryForwardRedactedValues`
(`metadata-protocol/src/metadata-redaction.ts`) now does two more
things.
- It re-runs the type's redactor over what it grafted, and drops any
carried value whose new position the read would serve. This is the
remedy the at-tier review named.
- It walks an array element that has no `id` by the identified node
beneath it on the same path. A `parallel` branch has no `id`. Position 1
needs this: once a secret inside a branch is withheld, the old "skip the
path" would have deleted it silently on every round trip, including one
that reorders the branches.
## The dispatch's mechanism assumptions, measured
- **A1 held. Position 1 is REACHED at a member-level read.**
- At `c96beb27` (the unfixed code), on a composed in-process boot, the
registered projection passed `signingSecret` through at every depth.
- The boot was `@objectstack/verify`'s `bootStack` on `examples/app-crm`
with the automation capability loaded, one member signed up, and the
flow authored by the seeded admin.
- The member's item, list and published reads each served both values:
the top-level `http` node's and the one inside a `loop` body.
- The start node's secret was withheld there, which confirms the #20552
projection was live in that boot.
- After this change, none of the three reads carries either value.
- **A2 partly falsified: there is no "unknown type" error to
discriminate on.**
- `ObjectStackProtocolImplementation.getMetaItems` is the one
implementation in this repository. It answers a type it holds nothing
for with an empty list, because it merges the metadata service's
runtime-registered items itself.
- What it throws is a store fault (503), a metadata app's marked
refusal, a redactor failing closed, or a refused spelling (400). So
every throw now propagates, which is what "a fault propagates, an
unknown type still falls through" reduces to.
- The metadata-service fallback stays for a protocol slot with no list
verb.
- **Position 2 is REACHED only under a forced fault, and only on a
dispatcher-routed host.** A member read on the real `HttpDispatcher`
with a forced protocol fault served a flow's hook secret and a
datasource's password, `200`.
- On the composed boot above, the `/meta` list is `RestServer`'s route.
It has no fallback, and a forced fault there answered `503` with nothing
served.
- **A3 held. Position 3 is REACHED at a member-level read after an
authoring round trip.**
- On the same composed boot at `c96beb27`, an author's ordinary save
kept a node's `id` and changed its kind. The member's next item and list
reads then served the old hook secret on that node, and the row at rest
held it there.
- After this change the carried value is dropped. The node's config at
rest is empty, and nothing is served.
- **A4: none, as specified.** Details are under Acceptance notes. No
in-repo composition serves `/meta` without the automation capability
*while sharing a store with one that loads it*. The plugin-absent half
does exist in the repository, measured below.
## Tests
The pins below were all committed red first, at `99a75023`.
- `service-automation`, `flow-credential-positions.test.ts` (new): the
enumeration, every position at each depth, exact paths, the cleared
form, and a lookalike key on another kind.
- `metadata-protocol`, `protocol.metadata-redaction.test.ts`:
- relocation through the pure inverse and through the save door
(followed by the served reads);
- nested carry-forward, including reordered branches and twin branches;
- the removal door: the empty string clears, absent keeps, and a value
replaces;
- the save-door round trip for nested secrets.
- `runtime`:
- `meta-list-protocol-fault.test.ts` (new): 503, 400 and an undeclared
throw are each answered as themselves, with the fallback never called.
It also preserves the empty-list answer and the no-list-verb host.
- `automation-flow-credential-projection.test.ts`: the relocating `PUT`,
then the member's read.
- `meta-list-read-gate-parity.test.ts`: its double now reaches the
fallback exits by answering no list instead of throwing.
Run at `fee1d6b9b`:
- Package suites:
- `service-automation`: 152 files, 1866 passed.
- `metadata-protocol`: 2775 passed, 19 skipped.
- `runtime` (`--project local`): 4197 passed, 1 skipped.
- `typecheck` is green on all three. `check:test-typecheck` is green on
`service-automation` and `runtime`. `metadata-protocol`'s `tsconfig`
includes its tests; `--listFiles` counts the edited test once.
- Gates: `dispatch-gates.mjs --commands` derives 64 commands from this
diff, and all 64 exit 0. `--ran` reconciles 64 derived, 64 run, 0
NOT-MEASURED and 0 UNRUN, every line carrying its exit code.
- `check:dual-build-cjs-loads` first answered `PREREQUISITE NOT MET`,
because 8 unrelated packages had no `dist/`. After building them it
exited 0.
- Lint: `eslint --no-inline-config` over the 8 changed `.ts` files
reports 0 errors and 0 warnings.
- The config lints `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` outside its
never-linted directories.
- It enables no type-aware rule (`eslint.config.mjs`, "never enables
type-aware linting"), so the diff cannot move an untouched file's
verdict.
### Ablations
Each leg ran on the committed fix and went through
`scripts/ablation-replace.mjs`: the anchor hit once, the blob changed,
and the restore was proven as "blob == HEAD and `git diff HEAD` empty".
The outer shell also carried a restore trap.
| # | Put back | Pins that went red |
|---|---|---|
| A1 | the `http` row of the table | 8 of 15: the table no longer equals
the declared set (`expected [ 'start.secret' ] to deeply equal [
Array(2) ]`), and each `signingSecret` placement (`… not to contain
'credential-position-sentinel-20590'`) |
| A2 | the region walk | 9 of 15: every nested placement, for both kinds
|
| A3 | serving the cleared form as written | 1 of 15 |
| A4 | the list branch swallowing the protocol throw | 4 of 6: `expected
'{"success":true,"data":{"type":"flow"…' not to contain
'stored-hook-secret-20590'`, and the datasource password |
| A5 | keeping every graft (no position check) | 3 of 33 in
`metadata-protocol`: `expected [ 'inbound_hook', …(17) ] to not include
'stored-hook-secret-20552'`. In `runtime`, which reads
`metadata-protocol` from `dist/`, the PUT pin went red once the mutation
was rebuilt and `ablation-dist-preflight` found the marker in 2 built
files. On restore, the package was rebuilt, the marker was absent from
all 24 built files, the tree was clean, and 8 of 8 passed. |
| A6 | skipping an id-less element (no anchor) | 3 of 33: `expected
undefined to be 'stored-signing-secret-20590'` |
A first run of the `dist` leg of A5 used a mutation that failed the
package's DTS step (unused locals). Its JS bundles carried the mutation,
but its preflight never ran. It was rerun with a mutation that
type-checks, and the numbers above are from that rerun.
## Deviations
- **Three test files outside the claim's named file surface**, each a
test of a door this PR changes:
- `runtime/src/domains/automation-flow-credential-projection.test.ts`,
the automation-plane half of position 3;
- `runtime/src/domains/meta-list-read-gate-parity.test.ts`, whose double
modelled "unknown type" as a throw;
- the new `runtime/src/domains/meta-list-protocol-fault.test.ts`.
- **No change reaches a published package's `exports`.** The new
constants in `flow-credential-projection.ts` are not re-exported from
`@objectstack/service-automation`'s entry. `carryForwardRedactedValues`
keeps its signature; its behaviour changes as described. `Clause-②: no`
stands as claimed.
## Acceptance notes
- **Position 4 measurement: none, as specified.** Nothing in this
repository composes a `/meta`-serving host without the automation
capability *over a store shared with one that loads it*.
- Searched: the `requires` of every example and dogfood fixture, where
every stack declaring flows requires `automation`.
- Searched: `os serve`'s always-on slate, where `automation` is not on
it and loads only by `requires`, and its presets.
- Searched: the CLI commands that boot their own kernel. `os verify`
boots in memory, and `os meta` goes through HTTP.
- Searched: the one store-sharing seam, `bootStack`'s `databaseFile`,
where all 4 in-repo uses pass `automation: true`.
- **The plugin-absent half does exist.** `@objectstack/verify`'s
`bootStack` loads the automation capability only when `automation: true`
is passed (default `false`), whatever the stack's `requires` says. `os
verify` boots it that way.
- On `bootStack(showcase)` without the flag, the `flow` redactor was
absent from the registry, `/automation/*` answered 501, and a member's
`/meta/flow` read served an authored `api` flow's start-node secret.
- Its store is private (in memory, seeded from the app's own source), so
no secret live elsewhere is served there. Recorded for the seat, which
owns the position-4 route.
- **A second open-map position, same family, not covered here.**
- `HttpConfigSchema.headers` is an open string map, and so is
`connectorConfig.input`. A static credential typed into one is served
with the definition, because it is marked by a header or parameter name,
not by a declared key.
- The enumeration pin covers declared keys only, and no shipped example
authors one.
- Named in the report for the seat. It is not filed here, per the
family's fold rule.
- **Remaining fall-throughs on the dispatcher list.** A protocol slot
with no list verb, or a protocol answering no list, still reaches the
metadata service's list without per-type redaction. The one in-repo
protocol always answers a list, so this is dormant.
- **Observations, not filed (no credential served):**
- The dispatcher's item read swallows a protocol fault, then falls to a
`getItem` that no in-repo metadata service implements, so a fault there
reads as 404.
- The dispatcher's `/published` read swallows a layered-read fault and
serves the code-layer snapshot, which #20585 made redacting.
- **Boundary:** a plugin-registered node kind that declares a credential
key is outside the table. The enumeration reads builtin and
spec-declared contracts.
## Patch round 1 (the seat's append; the dev writes a body only once)
- **R1**, the at-tier record `5886643746`: a node moved across regions
no longer loses its credential.
- When the stored path to the credential's container does not resolve in
the incoming body, `carryForwardRedactedValues` finds the owning element
by its `id` across the whole incoming body. It uses that element only on
exactly one match, then lets the existing position check decide where
the value lands.
- The pins were committed red on `fee1d6b9` as `0661a9a0`, with 3
failed. The fix and the changeset sentence are `5116194e`.
- **The pins:** a node moved out of a `loop` body, and a node moved into
a `parallel` branch, each kept, both directly and through the save door.
A node moved and changed in kind is dropped. An `id` duplicated across
regions grafts nothing.
- **The ablation** removed the by-id fallback. Exactly the 3 keep-pins
went red, and the restore was proven.
- The earlier test titled as a cross-region move is retitled to what it
asserts.
- A datasource path holds no identified element, so it is unaffected.
- **Also in this round:** two stored paths that land on one incoming
position carry neither, instead of one silently overwriting the other.
With a flow's single id space this cannot be reached, and it errs on the
side of not carrying.
- **R2** (measured only): the inline `http` arm, and the durable arm's
no-outbox fallback, send the request unsigned. Filed as #20628. Not
changed here.
- **At `5116194e`:** `metadata-protocol` passed 2780, with 19 skipped.
`runtime`'s pins for these doors passed 14. `typecheck` exit 0.
`dispatch-gates --commands` derived 64 commands, all 64 exit 0, and
`--ran` reconciles 64 / 64 / 0 / 0.
## Patch round 2 (the seat's append)
- **N1**, the at-tier record `5888558573`: the relocation's match set is
scoped to where the owner stood.
- It counts only elements of arrays held under the same key as the
owner's own array in the stored path. That key is read from the stored
hops and never named in code. For a flow it is `nodes`, at the top level
or in any region.
- An edge, or a config value that carries the same `id`, no longer
blocks the relocation. Uniqueness is still required within the scoped
set.
- An owner whose array sits directly inside another array has no key and
is not relocated. No registered redactor produces such a path.
- The pins were committed red on `5116194e` as `2b7e04d3`: the edge-twin
case, directly and through the save door, 2 failed. The fix is
`60a5f765`.
- **Ablation:** putting back the whole-body match turned exactly the 2
edge-twin pins red. The restore was proven.
- **Changeset:** the "Moving a node" sentence now states the condition
the code enforces.
- **At `60a5f765`:** `metadata-protocol` passed 2783, with 19 skipped.
`runtime`'s pins for these doors passed 14. `typecheck` exit 0. After a
full build, `dispatch-gates --commands` derived 64 commands, all 64 exit
0, and `--ran` reconciles 64 / 64 / 0 / 0.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent a918fe7 commit 3f45b6c
9 files changed
Lines changed: 1356 additions & 113 deletions
File tree
- .changeset
- packages
- metadata-protocol/src
- runtime/src/domains
- services/service-automation/src
| 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 | + | |
Large diffs are not rendered by default.
Lines changed: 449 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 26 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
164 | 164 | | |
165 | 165 | | |
166 | 166 | | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
Lines changed: 143 additions & 0 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 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
Lines changed: 11 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
139 | 139 | | |
140 | 140 | | |
141 | 141 | | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
146 | 151 | | |
147 | | - | |
| 152 | + | |
148 | 153 | | |
149 | 154 | | |
150 | 155 | | |
151 | | - | |
| 156 | + | |
152 | 157 | | |
153 | 158 | | |
154 | 159 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1874 | 1874 | | |
1875 | 1875 | | |
1876 | 1876 | | |
1877 | | - | |
1878 | | - | |
| 1877 | + | |
| 1878 | + | |
| 1879 | + | |
| 1880 | + | |
| 1881 | + | |
| 1882 | + | |
| 1883 | + | |
| 1884 | + | |
| 1885 | + | |
| 1886 | + | |
| 1887 | + | |
| 1888 | + | |
| 1889 | + | |
| 1890 | + | |
1879 | 1891 | | |
1880 | 1892 | | |
1881 | 1893 | | |
1882 | 1894 | | |
1883 | 1895 | | |
1884 | 1896 | | |
1885 | 1897 | | |
| 1898 | + | |
| 1899 | + | |
1886 | 1900 | | |
1887 | 1901 | | |
1888 | 1902 | | |
| |||
0 commit comments