Commit 96a9719
Fixes #20790
Clause-②: yes (widening)
This PR carries out ruling record 5942356310 (letter A, R2 and C1, the
maintainer's 「同意264」) under claim 5935167060 and its revision
5942559287. It names classes and positions only: no request, header,
route, field path or value.
## Cross-lane files (named before the change list)
- `domain:engine`
- `packages/metadata-protocol/src/protocol.ts`: the per-type
credential-channel registration, the save door's channel step (after the
carry-forward, before the put), the publish gate's read of held
positions, and the rollback and revert callers that pass the strip.
- `packages/metadata-protocol/src/sys-metadata-repository.ts`: the
restore verb gains a body-derivation option shaped like the promote
verb's (R2).
- `domain:spec`:
`packages/spec/src/system/constants/platform-object-names.ts`, one
registry line.
- `domain:cli`
- `packages/runtime/src/flow-clone.ts`: the C1 refusal.
- `packages/runtime/src/domains/automation.ts`: the clone handler
consults the refusal. This file is in claim 5935167060 but not in
revision 5942559287's list (see Acceptance notes).
- Two runtime pins.
- Shared harness: `packages/qa/dogfood/` (one pin, one dev dependency,
one source alias) and `pnpm-lock.yaml`.
- Generated ledgers: the platform-object tenancy census, the
tenant-audit census page and counts file, and the engine-double-contract
ledger. Each was regenerated by its own `--write`.
## What changed
**1. A write-only channel on the existing secret seam
(`service-automation`).**
- The new platform object `sys_flow_credential` holds one row per
credential position of a flow, per lifecycle state (draft or active).
- Its one value field is secret-typed: the engine encrypts it through
the host crypto provider, masks it on every read, and dereferences it
only through the privileged resolver. No second secret mechanism and no
per-door redaction were added.
- The object is private, closed to the generic data door, untracked and
unsearchable. Its unique key is a fixed-width digest of the position.
- `FlowCredentialChannel` handles five operations:
- store: explicit values go in; absent keeps; the cleared form deletes;
a vanished position is dropped.
- strip: takes credentials out of a body.
- held positions: what the runtime gate reads as present.
- promote: draft to active, on publish.
- prune: on delete.
- A draft save never rotates the live credential. Publishing the draft
promotes it.
**2. The save door stores the body the channel returns
(`metadata-protocol`).**
- `registerCredentialChannel(type, channel)` registers a channel.
`saveMetaItem` runs it after the carry-forward and before the put, so
the stored row, every new history row and the content hash carry no
credential.
- The runtime authoring gate reads the channel's held positions as
present, both on an active save and when a draft is published.
- `restoreVersion` takes `deriveRestoredBody`. Rollback and revert pass
the strip. A restore past the move therefore never puts a credential
back at rest, and the channel keeps its current one. No new history copy
is written.
**3. Credentials are read at use time (`service-automation`,
`trigger-api`).**
- An inbound binding carries a resolver that reads the hook secret on
each verification, so a rotation applies to the next post without
re-arming.
- If a held secret cannot be read, the post is answered 503
`SERVICE_UNAVAILABLE`. It is never verified against nothing, and nothing
is enqueued.
- The outbound http node resolves a held signing secret at execution. If
it cannot read the secret, it refuses the node, so nothing is sent
unsigned. The cleared form still sends unsigned on purpose and never
asks the channel.
- For a packaged flow, a channel row wins at verification and the
literal is the fallback (Q3 A).
**4. C1: the clone door refuses a credential-holding source
(`runtime`).**
- The door refuses when the source holds a credential at any position,
whether as a literal (a packaged flow) or held in the channel, the
outbound signing secret included.
- The answer is 409 `RESOURCE_CONFLICT`, names the classes, and gives Q2
A's prescription: author the copy as a new flow with its own secret.
- Accepted cost, stated in the changeset: a packaged inbound flow can no
longer be cloned in one step.
**5. A one-time move with a receipt (`service-automation`).**
- At kernel ready, stored flow rows that still carry a credential are
saved again through the save door itself.
- Each moved flow gets one rotation notice in the log, naming the flow
and its classes and never a value (Q1 B: rotate, don't scrub).
- With no provider, the run defers and writes nothing. A row that fails
to move logs at error and says the row still carries the credential in
cleartext.
- The run is recorded in `sys_migration` as `flow-credential-channel`,
with counts and names only.
- History and audit rows are not rewritten. Packaged flows are not moved
(Q3 A).
**6. No provider means no write.** With no crypto provider, a save that
would land a credential is refused (503) before any row is written.
**7. Spec and docs.**
- Spec: one registry line.
- Docs: the flows page and the lifecycle page's clone row each had one
sentence that this change made false; both are corrected.
- Changeset: `minor` for five packages. It carries the rotation
instruction and the accepted cost, and the ADR-0087 gate reads it as
non-breaking.
## Evidence (head `417ba1fa6`)
Pins (the ruling's list plus the card's four and Q4's outbound set):
- A channel write and its masked reads.
- Draft-to-active promotion.
- R2: a rollback past the move.
- C1: refusal for a literal-held source, a channel-held source and an
outbound-held source.
- The administrator engine read (the reader the MCP stdio transport
serves from) after the move.
- No provider means no write.
- No read surface serves the value.
- The inbound door verifies after the move and after an
edit-and-republish.
- An explicit rotation replaces the credential.
- A packaged flow is untouched.
- Outbound signing reads the held secret.
- Delete drops the credential.
The end-to-end pin is
`packages/qa/dogfood/test/flow-credential-channel.dogfood.test.ts`
(8/8).
Every negative pin was ablated:
- A1 to A18 ran at `a38db79ec` through `scripts/ablation-replace.mjs`.
Each anchor hit, each pin turned red, and after each restore the tree
read clean against `HEAD`. Red counts ranged from 1 to 6 per ablation,
across the channel, trigger, http-node, migration, clone and protocol
pins.
- D1 (the dogfood pin) ran at `457f43476`:
- Mutated build: the marker was present in `dist/` by preflight, and 6
of 8 tests went red. Tests 6 and 7 stayed green, as expected, because
they do not read the ablated step.
- Restore: preflight `--absent` passed, 8/8 green, and the diff against
`HEAD` was empty.
Suites at `80b4647b2`, each in the foreground under the shared verify
lock:
- `service-automation`: 166 files, 2051 passed.
- `metadata-protocol`: 2 shards, 202 files plus 3 skipped; 2985 passed,
19 skipped.
- `trigger-api`: 2 files, 30 passed.
- `runtime` `local` project: 3 shards, 304 files; 4335 passed, 11
skipped.
- `spec` `local` project: 2 shards, 598 files; 17512 passed, 1 todo.
- Dogfood pin: 8/8.
- Typecheck (`service-automation`, `metadata-protocol`, `trigger-api`,
`runtime`, `dogfood`) and `spec` tsc: all exit 0.
Gates: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 129 commands at
`80b4647b2`, and all 129 exited 0. The `--ran` reconciliation answered:
`129 derived, 129 run, 0 NOT-MEASURED, 0 UNRUN`.
Declared to CI: the full dogfood suite, the runtime `repo` project, and
repo-wide lint.
## Contract review round 1
The contract review of record found one wrong judgment: Q3 A at the
inbound door. With a literal start-node secret, the hook reader asked
the credential channel on every post, and the channel throws when it has
no reachable store. A packaged inbound flow on a composition with no
data engine therefore armed on its literal and was answered 503 on every
post.
Commit `84d3d297a` fixes it in the http node's shape, so both doors read
one rule:
- With a literal, the reader asks the channel only when the channel's
index holds that position. Otherwise it answers the literal without
touching the channel.
- A held secret that does not come back still rejects, so the post is
answered 503 and is never verified against the literal. The channel's
own read keeps its throw.
- The index is per process. A row written after its last refresh (boot,
kernel ready, metadata reload, or a channel write in this process) loses
to the literal until the next refresh, exactly as at the http node.
Pins:
- (a) The real channel with no reachable store, and a packaged literal
inbound flow: the reader answers the literal, and a correctly signed
post through the real trigger answers 202.
- (b) The control: the channel holds the position, then its store
becomes unreachable. The reader rejects, and the post answers 503
whether it is signed with the literal or with the held value.
- (c) The existing Q3 A pin (a held row wins over the literal) stays
green.
Ablations at `84d3d297a`. Each anchor hit; each run rebuilt and the dist
preflight found the marker; each restore was proven by the file
equalling its HEAD blob, the `--absent` preflight passed, and both pins
went green again:
- E1, the holds gate removed: pin (a) went red in service-automation (1
of 17) and in dogfood (test 9: 503 where 202 was expected).
- E2, a held but unreadable secret falling back to the literal: pin (b)
went red in service-automation (1 of 17: the reader answered the literal
instead of rejecting) and in dogfood (test 10: 202 where 503 was
expected).
At `417ba1fa6`, after merging `origin/main`:
- service-automation: 166 files, 2053 passed.
- trigger-api: 2 files, 30 passed.
- The dogfood pin: 10/10.
- service-automation and dogfood typecheck: exit 0.
- The full gate union: 130 derived, 130 run, 0 NOT-MEASURED, 0 UNRUN.
The size is now 3646 changed lines (+3557 / −89), of which 1,678 are
added test lines.
## Acceptance notes
- **Size.** 3532 changed lines (+3443 / −89, 36 files) against the ruled
band of 2300 ± 500. That is over the band but under 5000, and 1,578 of
the lines are added test lines. There is no split.
- **Premise.** The premise was re-measured on `main` and still holds:
the stored row and the history row carried both credentials in
cleartext, and an administrator's engine read returned them. The MCP
stdio door had already stopped serving them by the time of this build
(the #21228 change). #21207 remains open. For flows only, this PR also
removes the credential from what that card's checksum exit covers.
- **File surface.** `packages/runtime/src/domains/automation.ts` is
outside revision 5942559287's list and inside claim 5935167060. The
clone handler there is where the C1 refusal is consulted.
- **Package duplication.** Duplicating a package that holds an inbound
flow whose secret the channel holds is now refused by the runtime
authoring gate, because the copy holds no secret. This is consistent
with C1: a copy never shares a secret.
- **Inert migration mode.** In inert mode, the stored re-save tool
refuses a flow row that still carries a literal when no provider is
registered. This is the no-provider rule, applied at that door.
- **Legacy drafts.** A legacy draft that still carries a literal,
published while no provider is registered, is refused (503) for the same
reason.
- **Channel keying.** The channel keys by flow name and state, env-wide
like the engine's flow map. Stored rows of the same name in two packages
therefore share one slot.
- **Durability list.** The receipt write is not on the
durability-critical callee list.
- **Write order.** The channel write precedes the stored put. If the put
fails, the channel is ahead of the row until the next save. No
credential is exposed in that window.
- **Presence index.** The engine's check for whether a flow holds a
credential reads an in-process index. The index is refreshed at boot, at
kernel ready, on metadata reload, and on every channel write in that
process. The value itself is always read live.
- **Stale derivation.** `origin/main` moved at least 10 commits after
the gate derivation at `80b4647b2`. One derivation input changed (a
release script, outside this diff), and a test merge against current
`main` is clean.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent db3fee3 commit 96a9719
36 files changed
Lines changed: 3557 additions & 89 deletions
File tree
- .changeset
- content/docs
- automation
- concepts
- permissions
- docs/audits
- packages
- metadata-protocol/src
- qa/dogfood
- test
- runtime/src
- domains
- services/service-automation/src
- builtin
- spec/src/system/constants
- triggers/trigger-api/src
- scripts
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
271 | 271 | | |
272 | 272 | | |
273 | 273 | | |
274 | | - | |
| 274 | + | |
275 | 275 | | |
276 | 276 | | |
277 | 277 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
134 | 134 | | |
135 | 135 | | |
136 | 136 | | |
137 | | - | |
| 137 | + | |
138 | 138 | | |
139 | 139 | | |
140 | 140 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
86 | | - | |
| 86 | + | |
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
| |||
122 | 122 | | |
123 | 123 | | |
124 | 124 | | |
125 | | - | |
| 125 | + | |
126 | 126 | | |
127 | 127 | | |
128 | 128 | | |
| |||
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
190 | | - | |
| 190 | + | |
191 | 191 | | |
192 | | - | |
193 | | - | |
| 192 | + | |
| 193 | + | |
194 | 194 | | |
195 | 195 | | |
196 | 196 | | |
| |||
200 | 200 | | |
201 | 201 | | |
202 | 202 | | |
203 | | - | |
204 | | - | |
| 203 | + | |
| 204 | + | |
205 | 205 | | |
206 | 206 | | |
207 | 207 | | |
208 | 208 | | |
209 | 209 | | |
210 | | - | |
| 210 | + | |
211 | 211 | | |
212 | 212 | | |
213 | 213 | | |
214 | | - | |
| 214 | + | |
215 | 215 | | |
216 | 216 | | |
217 | 217 | | |
| |||
223 | 223 | | |
224 | 224 | | |
225 | 225 | | |
226 | | - | |
227 | | - | |
228 | | - | |
229 | | - | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
230 | 230 | | |
231 | | - | |
| 231 | + | |
232 | 232 | | |
233 | 233 | | |
234 | 234 | | |
235 | 235 | | |
236 | | - | |
| 236 | + | |
237 | 237 | | |
238 | 238 | | |
239 | 239 | | |
240 | 240 | | |
241 | 241 | | |
242 | | - | |
243 | | - | |
| 242 | + | |
| 243 | + | |
244 | 244 | | |
245 | 245 | | |
246 | 246 | | |
247 | 247 | | |
248 | | - | |
| 248 | + | |
249 | 249 | | |
250 | | - | |
| 250 | + | |
251 | 251 | | |
252 | 252 | | |
253 | 253 | | |
| |||
297 | 297 | | |
298 | 298 | | |
299 | 299 | | |
300 | | - | |
| 300 | + | |
301 | 301 | | |
302 | 302 | | |
303 | 303 | | |
304 | | - | |
305 | | - | |
306 | | - | |
307 | | - | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
308 | 308 | | |
309 | 309 | | |
Lines changed: 16 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
37 | | - | |
38 | | - | |
39 | | - | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | | - | |
| 46 | + | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | | - | |
| 93 | + | |
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
97 | | - | |
98 | | - | |
99 | | - | |
100 | | - | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
| |||
196 | 196 | | |
197 | 197 | | |
198 | 198 | | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
199 | 204 | | |
200 | 205 | | |
201 | 206 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
148 | 148 | | |
149 | 149 | | |
150 | 150 | | |
151 | | - | |
| 151 | + | |
152 | 152 | | |
153 | 153 | | |
154 | 154 | | |
| |||
0 commit comments