Repository navigation
Commit e939357
Closes #24994
Refs #24966
Clause-②: no (narrowing)
## What the ruling binds (B+ on #24966, record 6304303975)
Quoted from the ruling record, items as recorded:
> 1. **The marker, in spec.** `WebhookSchema.secret` and
`WebhookSchema.headers` are declared credential fields with a
schema-level marker (zod `.meta({ credential: … })`, the repo's existing
`.meta` convention), `headers` as a whole map. The fact "this field is a
credential" lives on the declaration, once.
> 2. **The derivation, once, in the kernel registry.**
`getMetadataTypeRedactor(type)` resolves, after the explicit
registrations and built-ins, a redactor DERIVED from the type's
registered schema: every marked field is dropped (not masked) and served
as its cleared form, and `redactedKeys` names them. Any metadata type
that marks a field is redacted on every meta read door with nothing else
to write. `webhook` is the first type served this way.
> 3. **The guard.** A spec ratchet test reads every registered
metadata-type schema and fails when a field whose name or description
reads as a credential (secret, password, token, signing, api key,
credential) carries no marker and sits on no explicit allow-list — the
fail-closed half the datasource module asked for.
> 4. **Unchanged from B:** the redaction form (`secret` and the whole
`headers` map dropped, cleared forms served), the write-path
carry-forward accepted, delivery signing and door reach untouched.
`datasource` and `flow` keep their explicit redactors (they take
precedence over the derivation) and are not migrated.
> 5. **Standing word, recorded here:** 「类似的问题都可以这样处理」 — where a per-type
behaviour is a function of the declared shape, declare the fact on the
schema and derive the behaviour once in the kernel, rather than writing
a per-type function. Seats cite this ruling when the same choice recurs.
Disclosure: the maintainer's ruling on #9387 (`5323310017`) applies.
This PR carries location, rule and abstract reach only.
## What changed
- **The marker** (`packages/spec/src/automation/webhook.zod.ts`):
`secret` carries `.meta({ credential: 'value' })` and `headers` carries
`.meta({ credential: 'map' })`. Nothing else about the schema changes:
no key is added, removed or re-typed. The marker key is `credential` and
its vocabulary is closed: `value` (one credential, cleared form the
empty string) and `map` (a map whose entries cannot be told apart, so
the whole map is the credential, cleared form the empty map).
- **The derivation**
(`packages/spec/src/kernel/metadata-credential-fields.ts`, new, not
re-exported from the `kernel` barrel): walks the type's declared object
shape and each nested object shape below it (through optional, nullable,
default, pipe and lazy wrappers), collects the marked fields, and builds
a pure redactor. It is memoised per schema identity, so the walk runs
once per schema, not per read.
- **The third resolution**
(`packages/spec/src/kernel/metadata-type-redaction.ts`):
`getMetadataTypeRedactor(type)` resolves explicit registration, then
built-in, then the redactor derived from `getMetadataTypeSchema(type)`.
`listMetadataTypeRedactorTypes()` now includes the types whose schema
derives one. The module note records the ruling and the precedence.
- **The guard**
(`packages/spec/src/kernel/metadata-credential-fields.test.ts`): the
ratchet, described below.
- **Door pins**
(`packages/qa/dogfood/test/webhook-meta-door-credential-redaction.dogfood.test.ts`),
in both compositions.
- **Changeset** `.changeset/24994-webhook-credential-marker.md`:
`@objectstack/spec` **minor**, with a **BREAKING** banner and a `spec!:`
headline. The declaration is `Clause-②: no (narrowing)`, and the
ADR-0087 disposition is `not-required (no-migration-prescription)`.
During the launch window a narrowing ships as `minor`, not `patch`. That
rule comes from `scripts/check-changeset-no-major.mjs` (maintainer
ruling 2026-09-04, batch #35, on #15294) and outranks the card's `patch`
workload line. Every `no (narrowing)` changeset on main is graded
`minor`, and this one now is too.
### The served form, read precisely
A marked field that holds credential material is **removed** from the
served item and named in `redactedKeys`. A stored cleared form (an
empty-string `secret`, an empty `headers` map) holds nothing, so it is
served as written and not reported. This is how the governing precedents
put it: the `flow` projection's "Dropped, not masked", and the dev
report's option A ("keys removed, not masked; serve the two cleared
forms as written … so a reader tells 'cleared' from 'withheld'"). It is
also what ruling item 5 of the card needs: the carry-forward keeps the
stored values for a save that **omits** the two fields. If the cleared
form were put in place of the withheld value, a save of the served body
would be indistinguishable from a deliberate clear. A reviewer who reads
card item 2 ("serves its cleared form") as "substitute the cleared form"
should note that this reading was weighed and not taken, for that
reason.
### The ratchet, and what "reads as a credential" means
- The schema set is `listMetadataTypeSchemaTypes()` **plus**
`listUnregisteredKindSchemaTypes()`. `webhook` is a stack-collection
kind bound in `UNREGISTERED_KIND_SCHEMAS`, not in the registered-kind
list, so the first list alone would not read the one type this card is
about. `getMetadataTypeSchema('webhook')` resolves through that map, and
the derivation reads it from there.
- A field the walk reaches reads as a credential when its name, split
into words, contains secret / password / passwd / token / signing /
credential (or a plural), or `api` then `key`; or when the first
sentence of its description contains one of those words. Only fields
that can hold a credential count: a string, a record, a union, an
unknown. A number, boolean, enum or array cannot hold a secret, and
without this limit token budgets would be flagged.
- Allow-list: one entry, `datasource:external.credentialsRef`. It is a
reference into the secrets store, never the credential itself. A pin
fails if an entry stops matching a field the rule flags, so the list
cannot rot.
- Two more pins keep the marker honest: every marker uses the closed
vocabulary and a `map` marker sits on a record; and no marker sits where
the derivation cannot reach it (inside an array, a union or a record
value). A probe test checks that the second pin can see such a marker.
- A synthetic case shows the ratchet fails on an unmarked
credential-named field (top-level and nested) and passes once that field
is marked.
### Unchanged (ruling item 5 of the card)
- **The write-path carry-forward** still keys on "a redactor resolves
for the type". A save of the served form keeps the stored values; the
dogfood file pins it in both compositions.
- **The dispatcher's declared-webhook source** reads the registry's raw
declaration and re-parses it, never a served copy. Pinned with
plugin-webhooks mounted: both credentials are still present there, so
delivery signing is unaffected.
- **Who may reach a meta read door** does not change.
- **plugin-webhooks** registers nothing.
- **`datasource` and `flow`** keep their explicit redactors, which take
precedence. A unit test pins that an explicit registration wins over the
derivation and that the datasource built-in still resolves.
## Door pins (both compositions: without plugin-webhooks, and with it
mounted)
Each pin reads one door and asserts three things by rule: the read
answers 200, it names the webhook (a positive control, so a refusal or
an empty answer cannot pass), and it holds neither declared credential
field. The doors:
- the meta list, both spellings
- the item read
- the layered view
- the published read
- the metadata protocol's list and item reads
- the stored-row data door and its history twin
Meta doors are read as an administrator and as a signed-in member. The
data door is read as an administrator, the only caller who can reach it.
The fixture's credentials are obviously-fake placeholders.
## Verification (head `0cf6eed565`)
- `@objectstack/spec`: `pnpm --filter @objectstack/spec test` green (run
at `441ce0128d`; the later commit touches only the walker's definition
read and adds one test). At `0cf6eed565`: the two touched files
`src/kernel/metadata-credential-fields.test.ts` and
`src/kernel/metadata-type-redaction.test.ts` give 2 files and 39 tests
passed, and `typecheck` passed. After `spec build`, `check:generated`
reports all 14 generated artifacts up to date, so no artifact
regenerated. No public export was added: the new module is not
re-exported from the `kernel` barrel, and `check:api-surface` is green.
- `@objectstack/rest`: `test` green at `0cf6eed565` (307 files; 6157
passed, 326 skipped), and `typecheck` passed. An earlier head failed
`rest-exposure-gate-execution-read.test.ts`, and the cause was in this
PR. That test wraps the registered `object` schema in a second Proxy. A
`_zod` read through a Proxy over a `lazySchema` Proxy throws a Proxy
invariant error, so the walk read that way turned the instrumented
type's meta reads into 500s. The walker now reads the public `.def`, and
a unit test pins derivation through a doubly proxied schema.
- `@objectstack/metadata-protocol` and `@objectstack/plugin-webhooks`:
`test` and `typecheck` green (run at `441ce0128d`).
`@objectstack/dogfood` `typecheck` is green.
- Dogfood at `0cf6eed565`: the new pin file plus
`webhook-materialization` and `wall-declared-webhook-deployment-wide`
give 3 files and 24 tests passed.
- **Ablation** (marker removed, the pin reads red). Both `credential`
markers on `WebhookSchema` were rewritten to a non-marker key through
`scripts/ablation-replace.mjs`: anchor 1 to 0 for each, on-disk counts 0
and 0. `@objectstack/spec` was rebuilt, and
`scripts/ablation-dist-preflight.mjs --absent` confirmed both markers
absent from all 232 built files. The pin file then read 12 failed and 3
passed. The 3 that stayed green are the two composition guards and the
dispatcher-source pin, which do not depend on redaction. Restore: blob
equal to HEAD and `git diff HEAD` empty, spec rebuilt, the preflight
confirmed both markers present in 18 built files, and the pin file read
15 of 15 passed. A first attempt was refused by the replace tool before
anything ran, because its replacement-count check rejected the anchor
shape; nothing was mutated, and that attempt counts for nothing.
- Derived gate families (`scripts/pm/dispatch-gates.mjs --commands`, 87
families): all 87 run, with the spec and changeset subset run again at
`0cf6eed565`, and all exit 0 except one. `check:type-check-debt` was run
under the lock at `0cf6eed565`: the re-measure is OK, and no ledger
entry is above its recorded number. The exception:
`check:dual-build-cjs-loads` is NOT MEASURED, because it needs every
workspace package built and only the dogfood closure was built. That one
is declared to CI.
### Fix round (head `e86f32235f`)
This round merged `origin/main` and re-graded the changeset to `minor`,
adding the BREAKING banner. The changeset gates all exit 0:
`check-changeset-no-major` (`--base origin/main` and `--self-test`),
`check-adr-0087-registration` (both), `check-empty-changeset` (both),
and `check:changeset-gate-self-tests`. After them, `pnpm --filter
@objectstack/spec test` passed 703 files and 21388 tests (1 skipped, 1
todo), and `typecheck` passed. Both ran under the lock, with VERDICT
command-exit 0. The `dispatch-gates --commands` derivation is unchanged
from the previous head (same 87 families). The gates this round's delta
touches also exit 0: `check:nul-bytes`, `check:issue-citations`,
`check:doc-authoring`, `check:pm-changeset-deadline-census`,
closing-keyword parity, `check:objectui-changeset` and
`check:closed-ceremony-text`.
## Acceptance notes
- `.meta({ credential })` is carried into the generated JSON Schema of a
webhook (zod copies metadata into the JSON Schema node), so
`/meta/types` and the IDE schema now show `credential: 'value' | 'map'`
on the two properties. This is an annotation only: no gate artifact
changed (`check:generated` reports all 14 up to date).
- The derivation reads a type's schema the first time that type's
redactor is resolved, which means a lazily built schema is built on its
first meta read. This is memoised per schema identity.
- MCP: the card's door list does not name an MCP door. The dev report on
#24966 measured that the MCP HTTP tools refuse the stored metadata
objects, and that the stdio data bridge applies the same stored-row
projection this registry drives. No MCP pin is added here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent eddffc3 commit e939357
7 files changed
Lines changed: 838 additions & 10 deletions
File tree
- .changeset
- packages
- qa/dogfood/test
- spec/src
- automation
- kernel
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 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 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 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 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
193 | 193 | | |
194 | 194 | | |
195 | 195 | | |
196 | | - | |
197 | | - | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
198 | 204 | | |
199 | 205 | | |
200 | 206 | | |
201 | 207 | | |
202 | | - | |
203 | | - | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
204 | 216 | | |
205 | 217 | | |
206 | 218 | | |
| |||
0 commit comments