Skip to content

Commit 96a9719

Browse files
feat(automation): a flow's credentials live in a write-only channel on the secret seam, not in its stored definition (#20790) (#21377)
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
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
'@objectstack/metadata-protocol': minor
5+
'@objectstack/trigger-api': minor
6+
'@objectstack/runtime': minor
7+
---
8+
9+
feat(automation): a flow's credentials live in a write-only channel, not in its stored definition (#20790)
10+
11+
Clause-②: yes (widening)
12+
13+
A flow's two credentials, an inbound hook's `secret` on its start node and an `http` node's `signingSecret`, are no longer stored in the flow definition. The metadata save door moves each explicit value into a new platform object, `sys_flow_credential`, owned by `@objectstack/service-automation`. Its one field is `type: 'secret'`, so the engine encrypts it through the host crypto provider, masks it on every read, and dereferences it only through `resolveSecretField`. This is the same seam the webhook signing secret uses. The stored row, every new version-history row and the row's content hash carry no credential. The engine reads the value only when it verifies an inbound post or signs an outbound request. Authoring does not change: you still write the literal, a save that leaves the key out (the form every read serves) keeps the stored secret, `''` clears it, and only an explicit new value rotates it.
14+
15+
**⚠️ Rotate every inbound and outbound flow secret that existed before this release.** On the first boot with a crypto provider, or when a provider registers after a boot without one, each stored flow that still carries a credential is moved into the channel once, and the log prints one notice per flow: `[Automation] flow '<name>' (<state>): … was stored in cleartext … ROTATE: …`. The move guarantees no new copy, but the version-history rows and audit snapshots written before it stay as they were (both are append-only), so an administrator could have read those values. To rotate, save the flow with a new `config.secret` / `config.signingSecret`, then give the new value to whoever signs posts to the hook or verifies its deliveries. The run is recorded in `sys_migration` as `flow-credential-channel` (flow names only, never values). Packaged flows are not moved: a packaged flow's literal stays its source of truth, and where the channel holds a row for it, the row wins at verification.
16+
17+
What else changes:
18+
19+
- **`@objectstack/spec`**: `PLATFORM_OBJECTS_BY_PACKAGE['service-automation']` lists `sys_flow_credential`.
20+
- **`@objectstack/metadata-protocol`**: `registerCredentialChannel(type, channel)` registers a type's write-only credential channel (exported type `MetadataCredentialChannel`). `saveMetaItem` stores the body the channel returns, after the carry-forward and before the put. The runtime authoring gate reads the channel's held positions as present, on an active save and when a draft is published. `SysMetadataRepository.restoreVersion` takes `deriveRestoredBody`, shaped like `promoteDraft`'s `deriveActiveBody`. Rollback and revert pass the channel's strip, so restoring a version written before the move never puts its credential back at rest, and the channel keeps its current credential.
21+
- **`@objectstack/service-automation`**: exports `SysFlowCredential`, `FlowCredentialChannel` and `migrateFlowCredentialsIntoChannel`. `AutomationEngine` gains `setFlowCredentialSource`, `holdsFlowCredential`, `resolveFlowCredential` and `flowCredentialHoldings`. An `api` binding carries `resolveSecret()`, which reads the secret at verification time, so a rotation applies to the next post. A draft save never rotates the live secret; publishing the draft promotes it. Deleting a flow's stored row drops its credentials.
22+
- **`@objectstack/trigger-api`**: `FlowTriggerBinding.resolveSecret` arms a hook without a literal. A post whose secret cannot be read is answered `503 SERVICE_UNAVAILABLE` and is never verified against nothing.
23+
- **Refused now, loudly**:
24+
- With no crypto provider, a save that carries a flow credential is refused with `503 SERVICE_UNAVAILABLE` before anything is written. Register a provider (`setCryptoProvider`) and save again.
25+
- The clone door (`POST /api/v1/automation/:name/clone`) refuses a source that holds a credential, as a literal or in the channel, with `409 RESOURCE_CONFLICT`, because a copy would share it. ⚠️ Accepted cost: a packaged inbound flow can no longer be cloned in one step. Author the copy as a new flow under a new name, with its own secret.
26+
27+
<!-- adr-0087: not-required (no-migration-prescription) the one-time move rewrites stored flow rows through the metadata save door itself, at boot; no authorable key, spelling, export or stored shape is retired, so an author or an upgrading agent has nothing to rewrite. The operator's action is the rotation stated above, which is not a FROM to TO mapping. The gate reads this changeset as non-breaking; the disposition is stated for the migration the ruling named. -->

‎content/docs/automation/flows.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ nothing is assigned or written in its place.
271271
```
272272

273273
<Callout type="warn" title="Do not put a secret in an http node's url or headers">
274-
A flow definition, including an `http` node's `url` and `headers`, is served to every member who can read flows. A token, API key or signed webhook url written there is readable by all of them. Route an outbound credential by where it sits, and call the connector with a `connector_action` node instead — see [Connectors](/docs/automation/connectors#authentication). A credential in a header goes to a declarative connector with `bearer`, `basic` or `api-key` auth, whose `auth.credentialRef` names the secret. A key in the query string goes to `api-key` auth with `paramName`. No `credentialRef` variant carries a secret in the url path, so an incoming-webhook url (whose path is the secret) cannot be routed that way: call the service through a token-authenticated connector instead, such as the `slack` connector, whose bot token is supplied to the plugin by host code rather than written in the flow (it is registered by that plugin, not declared as a `connectors:` instance). Otherwise such a url is served with the definition. Only `signingSecret` (and a start node's `secret`) is withheld when a definition is served; `url` and `headers` are served as written.
274+
A flow definition, including an `http` node's `url` and `headers`, is served to every member who can read flows. A token, API key or signed webhook url written there is readable by all of them. Route an outbound credential by where it sits, and call the connector with a `connector_action` node instead — see [Connectors](/docs/automation/connectors#authentication). A credential in a header goes to a declarative connector with `bearer`, `basic` or `api-key` auth, whose `auth.credentialRef` names the secret. A key in the query string goes to `api-key` auth with `paramName`. No `credentialRef` variant carries a secret in the url path, so an incoming-webhook url (whose path is the secret) cannot be routed that way: call the service through a token-authenticated connector instead, such as the `slack` connector, whose bot token is supplied to the plugin by host code rather than written in the flow (it is registered by that plugin, not declared as a `connectors:` instance). Otherwise such a url is served with the definition. Only `signingSecret` and a start node's `secret` are kept out of a flow saved through the metadata API: the metadata save door stores each in a write-only, encrypted flow credential store, a read never returns it, and the engine reads it only to verify an inbound post or sign an outbound request. A packaged flow keeps its literal in its package source. The literal is withheld when the definition is served, and a credential store row for that flow, where one exists, wins when the engine verifies or signs. `url` and `headers` are stored and served as written.
275275
</Callout>
276276

277277
**Script:**

‎content/docs/concepts/metadata-lifecycle.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ See [ADR-0005](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/
134134

135135
| Type | Sanctioned path the refusal names |
136136
| :--- | :--- |
137-
| `flow` | Clone it under a new name (`POST /api/v1/automation/:name/clone`, body `{name, label}`), or switch it off (`POST /api/v1/automation/:name/toggle`, body `{enabled: false}`). |
137+
| `flow` | Clone it under a new name (`POST /api/v1/automation/:name/clone`, body `{name, label}`), or switch it off (`POST /api/v1/automation/:name/toggle`, body `{enabled: false}`). A flow that holds a credential (an inbound hook's `secret`, an `http` node's `signingSecret`) is not cloned in one step: the clone door refuses it with `409`, because a copy would share the secret, and the copy is authored as a new flow with its own. |
138138
| `action` | Switch it off (`POST /api/v1/actions/_activation/:object/:action`, body `{enabled: false}`; `:object` is `global` for an object-less action). No clone is offered. |
139139
| `permission` | Clone it under a new name: the "Clone" action on the permission set, or `POST /api/v1/data/sys_permission_set` with a new name. |
140140

‎content/docs/permissions/tenant-audit-census.mdx‎

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ what moved this page's population from 225 to 227; nothing about the two sites
8383
changed, only whether this instrument could see them.
8484

8585
**The expensive failure direction is a keyword.** Sites whose receiver the author
86-
typed `any` have no type to read, and there are 44 of them — just under a fifth
86+
typed `any` have no type to read, and there are 48 of them — just over a fifth
8787
of the population, concentrated in exactly the seed and bootstrap paths this
8888
control exists for. Scoring an unreadable receiver as "not an engine" would have
8989
dropped every one of them silently, with a clean exit and a smaller number that
@@ -122,7 +122,7 @@ are reported as `undecidable` rather than assumed either way.
122122

123123
The same holds twice over for the context. An options argument spelled as a
124124
literal can be read; one spelled `options`, `{ ...opts }`, or handed through a
125-
forwarding shim cannot, and **67 of the 219 sites are spelled that way**. A
125+
forwarding shim cannot, and **67 of the 229 sites are spelled that way**. A
126126
context resolved from an inline literal or a local `const` can be tested for
127127
`isSystem`; one arriving from a helper call cannot.
128128

@@ -187,10 +187,10 @@ reproduce them. Where it disagrees, it disagrees on the page:
187187

188188
| carried figure | where it survives | this census |
189189
| :--- | :--- | ---: |
190-
| 175 write call sites | quoted in the merged changeset | **219** |
190+
| 175 write call sites | quoted in the merged changeset | **229** |
191191
| 24 carrying no tenant context | quoted in the merged changeset | **9** provable and tenancy-enabled; **34** more whose options argument is unreadable |
192-
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **144 of 219** decidable, **75** undecidable |
193-
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 100 decidably elevated, 0 decidably not, 102 undecidable |
192+
| 127 of 175 statically decidable, 48 runtime-parameter-name sites | restated on the `isSystem`-scoping card | **152 of 229** decidable, **77** undecidable |
193+
| 135 (77%) silenced by the `isSystem` guard before the posture gate | the lost issue body — **no surviving corroboration** | **not reproduced**: 110 decidably elevated, 0 decidably not, 102 undecidable |
194194
| 141 and 132, two independent re-derivations | the card that filed this work | — |
195195

196196
**The differences are not reconciled, and deliberately so.** The old census's
@@ -200,18 +200,18 @@ be stated is what this instrument counts, which is written above and re-runnable
200200
at any commit.
201201

202202
Two structural facts do plausibly widen this reading against any hand or regex
203-
one, and both are counted in the generated tables below: the 44 sites reached
204-
through an erased (`any`) receiver, and the 43 that name their object through a
203+
one, and both are counted in the generated tables below: the 48 sites reached
204+
through an erased (`any`) receiver, and the 51 that name their object through a
205205
`const` rather than inline. An instrument that read either the way a person does
206206
would report a smaller number and would not say so.
207207

208208
The fourth row is the one worth flagging to anyone citing it. **The 135 / 77%
209209
figure has no surviving corroboration anywhere in the tree.** This census reads
210-
100 of 219 (46%) as decidably elevated, with 102 more whose elevation is a
210+
110 of 229 (48%) as decidably elevated, with 102 more whose elevation is a
211211
run-time fact — so the claim is neither confirmed nor refuted, and the honest
212212
answer is that a static reading cannot settle it.
213213

214-
⇒ **Cite `9 / 219`, and say what it is**: the sites whose options argument was
214+
⇒ **Cite `9 / 229`, and say what it is**: the sites whose options argument was
215215
READ and holds no tenant context, against a decidably tenancy-enabled object.
216216
That is the control's provable yield surface. ⛔ Do not cite it as "the sites
217217
without tenant context" — **34 further sites** have an options argument this
@@ -223,31 +223,31 @@ cannot read, and they are neither in nor out.
223223

224224
| what | count |
225225
| :--- | ---: |
226-
| write call sites on the application surface | **219** |
227-
| …whose object name is statically decidable | 144 |
228-
| …whose object name is chosen at run time | 75 |
229-
| …against an object with tenancy ENABLED | 143 |
226+
| write call sites on the application surface | **229** |
227+
| …whose object name is statically decidable | 152 |
228+
| …whose object name is chosen at run time | 77 |
229+
| …against an object with tenancy ENABLED | 151 |
230230
| …against an object that declares tenancy off | 1 |
231-
| threading a tenant context | 135 |
231+
| threading a tenant context | 145 |
232232
| PROVABLY carrying none (options read, no context key) | **17** |
233233
| …of those, against a decidably tenancy-enabled object | **9** |
234234
| options argument UNREADABLE — may or may not carry one | 67 |
235235
| …of those, against a decidably tenancy-enabled object | 34 |
236-
| threading a decidably ELEVATED (`isSystem`) context | 100 |
236+
| threading a decidably ELEVATED (`isSystem`) context | 110 |
237237
| threading a context that is decidably NOT elevated | 0 |
238238
| threading a context whose elevation is a run-time fact | 102 |
239239

240240
| how the instrument reached the site | count |
241241
| :--- | ---: |
242-
| receiver carried a readable engine type | 175 |
243-
| receiver erased, placed by the object NAME | 24 |
242+
| receiver carried a readable engine type | 181 |
243+
| receiver erased, placed by the object NAME | 28 |
244244
| receiver erased, placed by an `object: string` PARAMETER | 15 |
245245
| receiver erased, placed by an `UNTYPED_RECEIVERS` row | 5 |
246246

247247
| object name spelled inline | 101 |
248-
| object name spelled through a `const` | 43 |
248+
| object name spelled through a `const` | 51 |
249249
| object name is an `object: string` parameter | 17 |
250-
| object name is some other run-time expression | 58 |
250+
| object name is some other run-time expression | 60 |
251251

252252
### Subtractions the census could NOT defend — enforced
253253

@@ -297,13 +297,13 @@ holds still. They are required to be HERE and to say WHEN they were true;
297297
their values are not compared. The reasoning, and the measurement behind it,
298298
are in `scripts/check-tenant-audit-census.mjs`.
299299

300-
Measured on 2026-10-01 at `752173845`.
300+
Measured on 2026-10-02 at `c41817b12`.
301301

302302
| corpus scale (not enforced) | count |
303303
| :--- | ---: |
304-
| tracked non-test sources scanned | 586 |
305-
| engine-shaped types recognised | 64 |
306-
| declared objects in the registry | 115 |
307-
| same-named calls subtracted as non-engine | 145 |
304+
| tracked non-test sources scanned | 599 |
305+
| engine-shaped types recognised | 66 |
306+
| declared objects in the registry | 116 |
307+
| same-named calls subtracted as non-engine | 150 |
308308

309309
{/* END GENERATED: tenant-audit-census */}

‎docs/audits/2026-08-tenant-audit-write-call-sites.counts.md‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,17 @@ silent, and `node scripts/tenant-audit-census.mjs --write` is the resolution.
3333

3434
| Measure | Value |
3535
|---|---:|
36-
| Write call sites | 219 |
37-
| Object name statically decidable | 144 |
38-
| Object name chosen at run time | 75 |
39-
| Against a tenancy-enabled object | 143 |
36+
| Write call sites | 229 |
37+
| Object name statically decidable | 152 |
38+
| Object name chosen at run time | 77 |
39+
| Against a tenancy-enabled object | 151 |
4040
| Against an object declaring tenancy off | 1 |
41-
| Threading a tenant context | 135 |
41+
| Threading a tenant context | 145 |
4242
| Provably carrying none | 17 |
4343
| …and decidably tenancy-enabled | 9 |
4444
| Options argument unreadable | 67 |
4545
| …and decidably tenancy-enabled | 34 |
46-
| Threading a decidably elevated context | 100 |
46+
| Threading a decidably elevated context | 110 |
4747
| Threading a decidably non-elevated context | 0 |
4848
| Threading a context of undecidable elevation | 102 |
4949

@@ -90,14 +90,14 @@ holds still. They are required to be HERE and to say WHEN they were true;
9090
their values are not compared. The reasoning, and the measurement behind it,
9191
are in `scripts/check-tenant-audit-census.mjs`.
9292

93-
Measured on 2026-10-01 at `752173845`.
93+
Measured on 2026-10-02 at `c41817b12`.
9494

9595
| corpus scale (not enforced) | count |
9696
| :--- | ---: |
97-
| tracked non-test sources scanned | 586 |
98-
| engine-shaped types recognised | 64 |
99-
| declared objects in the registry | 115 |
100-
| same-named calls subtracted as non-engine | 145 |
97+
| tracked non-test sources scanned | 599 |
98+
| engine-shaped types recognised | 66 |
99+
| declared objects in the registry | 116 |
100+
| same-named calls subtracted as non-engine | 150 |
101101

102102
## Every site
103103

@@ -196,6 +196,11 @@ Measured on 2026-10-01 at `752173845`.
196196
| `packages/services/service-automation/src/builtin/crud-nodes.ts` | `delete` | `objectName` | undecidable | context, elevation undecidable | 1 |
197197
| `packages/services/service-automation/src/builtin/crud-nodes.ts` | `insert` | `objectName` | undecidable | context, elevation undecidable | 1 |
198198
| `packages/services/service-automation/src/builtin/crud-nodes.ts` | `update` | `objectName` | undecidable | context, elevation undecidable | 1 |
199+
| `packages/services/service-automation/src/flow-credential-channel.ts` | `delete` | `sys_flow_credential` | enabled | elevated | 5 |
200+
| `packages/services/service-automation/src/flow-credential-channel.ts` | `insert` | `sys_flow_credential` | enabled | elevated | 1 |
201+
| `packages/services/service-automation/src/flow-credential-channel.ts` | `update` | `sys_flow_credential` | enabled | elevated | 2 |
202+
| `packages/services/service-automation/src/flow-credential-migration.ts` | `insert` | `DATA_MIGRATION_FLAG_OBJECT` | undecidable | elevated | 1 |
203+
| `packages/services/service-automation/src/flow-credential-migration.ts` | `update` | `DATA_MIGRATION_FLAG_OBJECT` | undecidable | elevated | 1 |
199204
| `packages/services/service-automation/src/flow-dispatch-store.ts` | `insert` | `sys_flow_dispatch` | enabled | elevated | 1 |
200205
| `packages/services/service-automation/src/flow-dispatch-store.ts` | `update` | `sys_flow_dispatch` | enabled | elevated | 1 |
201206
| `packages/services/service-automation/src/suspended-run-store.ts` | `delete` | `sys_automation_run` | enabled | elevated | 3 |

‎packages/metadata-protocol/src/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export type { ClusterMetadataMutationPayload } from './protocol.js';
148148
// kernel-wide `metadata:reloaded` announce. Exported for the same reason its
149149
// mutation sibling is: the subscriber lives in another package.
150150
export type { MetaItemPublishedEvent } from './protocol.js';
151-
export type { MetadataAuthoringGate, MetadataAuthoringGateContext } from './protocol.js';
151+
export type { MetadataAuthoringGate, MetadataAuthoringGateContext, MetadataCredentialChannel } from './protocol.js';
152152

153153
export { SysMetadataRepository, resetEnvWritableMetadataTypes } from './sys-metadata-repository.js';
154154
export type {

0 commit comments

Comments
 (0)