Skip to content

spec: WebhookSchema marks secret and headers as credential fields; the redactor registry derives meta read-door redaction from the schema (ruling B+ on #24966) - #25036

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-24994-schema-derived-credential-redactor
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-24994-schema-derived-credential-redactor

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

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 [WIP] Add query enhancements and advanced validation features #35, on finding(changeset): two independent contract reviews read the repo's own history to opposite bumps for "add an exported symbol to a published index" #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 meta: the webhook metadata type has no redactor, so its secret-bearing fields are not masked on the meta read doors (detail withheld under the disclosure ruling) #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

…ecret and headers (#24994)

Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com>
…in both compositions (#24994)

Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com>
…trumented lazy schema derives instead of throwing (#24994)

Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 21, 2026
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 25 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/webhooks.mdx (via WebhookSchema (symbol, a top-level const))
  • content/docs/data-modeling/external-datasources.mdx (via credentialsRef (literal, a string literal in a comment in DeclaredField))
  • content/docs/kernel/contracts/data-engine.mdx (via DERIVED (symbol, a top-level const object))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via credentialsRef (literal, a string literal in a comment in DeclaredField))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3b3830ca223eaea7875babb155e47eebbcd0300f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7e2e81c7037d7ed348bd8bea9f2229eabbb474d7 — the merge of head e86f32235fc03bc032d3b1aaf271e23feaad69b5 into base 3b3830ca223eaea7875babb155e47eebbcd0300f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7e2e81c7037d7ed348bd8bea9f2229eabbb474d7 && git checkout 7e2e81c7037d7ed348bd8bea9f2229eabbb474d7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3b3830ca223eaea7875babb155e47eebbcd0300f e86f32235fc03bc032d3b1aaf271e23feaad69b5 && git checkout -B drift-repro 3b3830ca223eaea7875babb155e47eebbcd0300f && git merge --no-ff e86f32235fc03bc032d3b1aaf271e23feaad69b5

node scripts/docs-audit/affected-docs.mjs --json 3b3830ca223eaea7875babb155e47eebbcd0300f

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 3b3830ca223eaea7875babb155e47eebbcd0300f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e86f32235fc03bc032d3b1aaf271e23feaad69b5
Local-runs: none
Scope: packages/spec/src/automation/webhook.zod.ts, packages/spec/src/kernel/metadata-credential-fields.ts, packages/spec/src/kernel/metadata-type-redaction.ts + packages/spec/src/kernel/metadata-credential-fields.test.ts, packages/spec/src/kernel/metadata-type-redaction.test.ts, packages/qa/dogfood/test/webhook-meta-door-credential-redaction.dogfood.test.ts, .changeset/24994-webhook-credential-marker.md, packages/spec/src/kernel/metadata-type-schemas.ts (getMetadataTypeSchema, listMetadataTypeSchemaTypes, listUnregisteredKindSchemaTypes, read on main), packages/metadata-protocol/src/metadata-redaction.ts (redactorFor, hasMetadataRedactor, planCarryForward, read on main), packages/metadata-protocol/src/protocol.ts (carryForwardRedactedCredentials, read on main), packages/services/service-automation/src/flow-credential-projection.ts (the flow precedent's served-form doctrine, read on main), packages/spec/src/data/datasource-credential-redaction.ts (the fail-closed caution the ruling cites, read on main), scripts/check-changeset-no-major.mjs (the level-axis rule that reds this head, read on main)

Inputs: card #24994 (body, claim 6304417396, dev reports 6307031456 and 6307426041); #24966 and its ruling record 6304303975; PR #25036 body, file list, and the three-dot diff against origin/main at the head; the check-runs on the head. Disclosure (#9387, 5323310017) holds in this record: location, rule and abstract reach only.

Re-review of head e86f32235fc0 after the FAIL rendered on 0cf6eed565 (one point: the changeset level). The new head is 0cf6eed565 + a merge of origin/main (bae44d37be) + one commit. The delta on the PR's seven files, read by explicit sha (git diff 0cf6eed565 e86f32235fc0 -- PR files), touches ONLY .changeset/24994-webhook-credential-marker.md: patch becomes minor, the headline gains the spec!: prefix, and a **BREAKING:** banner is added; the Clause-②: no (narrowing) line and the ADR-0087 marker are unchanged. The net three-dot diff against origin/main lists the same seven files; the six code and test files are byte-identical to the previous head, so every ① and ③ judgment below is re-adopted unchanged. The PR body's semver line and a "Fix round" section were edited; neither carries a probe, reproduction or served value.

① Derived judgments

The marker (ruling item 1). WebhookSchema.secret carries .meta({ credential: 'value' }) and WebhookSchema.headers carries .meta({ credential: 'map' }), on the existing zod .meta convention; headers is marked as a whole map. No key is added, removed or re-typed; the two fields keep .optional() and their descriptions. The marker vocabulary is closed (value | map) and declared once in metadata-credential-fields.ts (CREDENTIAL_FIELD_KINDS). Faithful.

The derivation, once, in the registry (ruling item 2, shape). getMetadataTypeRedactor(type) now resolves EXTRA_METADATA_TYPE_REDACTORS → BUILTIN_METADATA_TYPE_REDACTORS → derivedCredentialRedactorFor(getMetadataTypeSchema(type)). The precedence the ruling binds (explicit, then built-in, then derived) is the literal ?? chain. datasource keeps its built-in (pinned: config.password still the datasource redactor's answer) and flow keeps its plugin registration (pinned generically: an explicit registration wins over a derived one). Neither is migrated. The derivation reads getMetadataTypeSchema(type) as ruled (metadata-type-schemas.ts:299), which resolves extras → built-ins → UNREGISTERED_KIND_SCHEMAS, so webhook is reached with no registration call; the unit test pins "zero registration calls" and that an unmarked type derives undefined ("serve as is"). listMetadataTypeRedactorTypes() includes derived types. Faithful.

Purity. deriveCredentialRedactor never mutates its input: withoutKeyAt copies every container on the path; an item with nothing to withhold is returned by reference with redactedKeys: []; redactedKeys is sorted and dotted, as the MetadataRedactionResult contract requires. No I/O. The walk invokes lazy getters and .meta() reads, both deterministic. Memoisation is a WeakMap on schema identity, so the walk runs once per schema object and a re-registered schema derives afresh. A marker outside the vocabulary fails closed (read as value, or map on a record) and the ratchet reds on the spelling. Faithful.

Key question — removal versus substitution (card item 2 against item 5). The literal text: card item 2 "drops every marked field (not masked) and serves its cleared form (an empty string for a value, an empty map for a map), and names them in redactedKeys"; ruling item 2 "every marked field is dropped (not masked) and served as its cleared form"; ruling item 4 "the redaction form (secret and the whole headers map dropped, cleared forms served)". The dev REMOVES the key from the served item when it holds credential material, names it in redactedKeys, and serves a stored cleared form ('' / {}) as written without naming it. The two sentences of item 2 cannot both be read as "substitute": a served '' is a blank mask, and item 2 says dropped, not masked. Read against item 5 of the card (and item 4 of the ruling), which keeps the write-path carry-forward "accepted and keyed, as today" and requires that "a save that omits the two fields keeps the stored values": planCarryForward (metadata-protocol, on main, unchanged by this PR) grafts the stored value only where the incoming value sameValues what the redactor would have served for the row at rest. Under substitution, an incoming '' would equal the served '' and be grafted over, so a credential could never be cleared through the save door, and a save that OMITS the key (undefined ≠ '') would NOT be grafted and would lose the stored value — the exact opposite of item 5's literal sentence. Under removal, an omitted key equals the served absence and is carried forward (item 5 holds), and '' is the one door that clears, which is the doctrine the ruling's own cited precedent states in so many words (flow-credential-projection.ts: "Dropped, not masked"; "the cleared form is served as written, so a reader can tell cleared from withheld"). Judgment: removal is WITHIN the ruling — it is the only reading of item 2 under which item 5 holds with the carry-forward "as today", it matches both existing redactors the ruling names as precedent, and it was disclosed in the PR body, the dev report's deviations list and the changeset. It is not a deviation that needs the maintainer: the maintainer's words took the shape (marker + derivation, 「类似的问题都可以这样处理」); the served-form sentence is the director's wording, and the ruling's item 4 presents the form as "unchanged from B". The B record (6303716122) was outside this review's inputs; the coordinating seat has since read it and reports its item 2 as "dropped, not masked", which closes the one residual the first round left open. Pinned in the diff: drop + redactedKeys: ['headers', 'secret'], cleared forms served by reference and unreported, a nested value marker inside a wrapper, the stored-row seam in both spellings, and a dogfood round trip of the served form that keeps the stored values in both compositions.

The ratchet (ruling item 3). metadata-credential-fields.test.ts reads listMetadataTypeSchemaTypes() PLUS listUnregisteredKindSchemaTypes(). The card names only the first; the dev's first deviation widens it. This is a faithful reading of item 3, not a departure: the ruling's purpose is the fail-closed half for "any metadata type that marks a field", and webhook — the one type the card is about — is bound in UNREGISTERED_KIND_SCHEMAS and would never be read by the first list alone (metadata-type-schemas.ts:346–353 says so deliberately). The ratchet reads exactly the set getMetadataTypeSchema(type) can resolve, which is the registry the derivation reads (ruling's governing text, :299). It imposes none of the KIND obligations that comment reserves for the registered list; it reads shape only. The credential vocabulary (secret, password, passwd, token, signing, credential, api key; name words or first sentence of the description), the "can hold a credential" limit (string, record, union, unknown; a numeric token budget does not red), and the allow-list (one entry, datasource:external.credentialsRef, with a cannot-rot pin) are as ruled: explicit and near-empty. Two further honesty pins (closed vocabulary, map only on a record; no marker where the walk cannot reach it, with a probe that the probe itself sees such a marker) are additive and within the spirit of the datasource caution. Synthetic red-then-green case present.

Door pins. Both compositions boot (with and without plugin-webhooks, guarded by a composition pin); every door the card names is pinned (meta list in both spellings, item, layered view, published, stored-row data door and its history twin), plus the protocol's getMetaItems/getMetaItem; admin and member tokens for the meta doors, admin for the data door; each read carries a positive control (200 and the webhook's name) so an empty or refused answer cannot pass; the stored row is pinned unredacted; the dispatcher's declared-webhook source is pinned to still carry both fields with the plugin mounted (delivery signing untouched, item 5). The ablation (marker removed, pins red; restore, pins green) is REPORTED by the dev; this review ran nothing and does not vouch for it. Note (not a defect): hasMetadataRedactor('webhook') is now true, so the save door performs the carry-forward's stored-row read for webhook saves — intended by item 5.

② Semver level

Changeset at this head: @objectstack/spec: minor, a spec!: headline and a **BREAKING:** banner naming the narrowing (the /meta read doors no longer serve a webhook's secret or headers), Clause-②: no (narrowing) on both the PR body and the changeset, ADR-0087 marker not-required (no-migration-prescription) in the repo's exact convention (eleven precedents on main), with the reason stated (no authorable key, export or stored shape removed or renamed). No public export added: the new module is not re-exported from the kernel barrel; Lint · CI, release & engine gates (which carries check:api-surface and check:generated) is green on this head.

Level, judged: the card and ruling 6304303975 wrote "@objectstack/spec patch changeset, Clause-②: no (narrowing)", and the previous head followed that literally — which Check Changeset refused on the level axis ("a declared narrowing is a BREAKING change; during the launch window it ships minor", scripts/check-changeset-no-major.mjs, maintainer ruling 2026-09-04, decision batch #35, on #15294). That is the maintainer's standing rule and outranks the director seat's workload line; every Clause-②: no (narrowing) changeset on main (eight read) is graded minor, and this one now is. The narrowing declaration itself is correct (the doors return less for webhook), and minor is the right grade for it. No maintainer escalation is owed: the governing ruling already decides it. Check Changeset is green on this head (two runs, both success).

③ Boundary flags

  • Dev deviations, answered: (1) ratchet over registered + unregistered-kind schemas — accepted, see ①. (2) removal rather than substitution — accepted as within the ruling, see ①; B's record 6303716122 ("dropped, not masked") confirms it, per the coordinating seat. (3) door set widened by the protocol's two reads, no MCP pin — accepted: the card names no MCP door, and the widening is additive. (4) walker helpers exported from a non-barrel module; credential now appears in the generated JSON Schema of webhook as an annotation — accepted; the dev reports check:generated with all 14 artifacts up to date and no committed artifact changed; CI will confirm. (5) no label written — not this review's concern. (6) worktree removed — not this review's concern.
  • Disclosure (skills: five principle gaps the #9296 wave exposed in checklist-test / checklist-author / dogfood-verification #9387): the PR body, the changeset, the dev report and all three test files carry location, rule and abstract reach only. Fixture credentials are self-evidently fake placeholders; every pin asserts absence of a declared key by rule, with the door path as location. Ablation and gate output appear as counts, never as a served value. Held.
  • Check-runs on head e86f32235fc0 at this read (2026-10-21): no red. Green: Auto Label, Build Core, Check Changeset (two runs), Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, Test Core (1/6), The card this PR closes must claim this branch, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter. In progress: Test Core (2/6) through (6/6). Skipped (opt-in or not applicable): 18.x upgrade harness, Build Docs, Console Pin Gate, Dogfood Regression Gate (sharded), Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance, and the duplicate skipped rows of Auto Label and Check PR Size from the second workflow run. The queue guard reads the merge group; the remaining Test Core shards are CI's to finish.
  • Previous head 0cf6eed565: FAIL rendered by this seat on the one point the fix round answers (Check Changeset, level axis). Superseded by this record.
  • check:dual-build-cjs-loads declared NOT MEASURED locally by the dev; it is CI's to decide.
  • Out-of-scope finding noted by the dev (a doubly proxied lazySchema throws on _zod reads) is handled in this PR by reading .def first and is pinned; the finding itself carries no carrier and is not filed. Not this review's concern.

Implemented-by: claude/issue-24994-schema-derived-credential-redactor
Reviewed-by: session_01D6F9axfoPzR7t8G1J7Ytzz

VERDICT: PASS

Seat re-read of the check-runs at landing: all 49 runs on e86f32235f have completed: 40 succeeded, the rest were skipped, and none is red.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 21, 2026 03:54
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 21, 2026 03:54
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit e939357 Oct 21, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24994-schema-derived-credential-redactor branch October 21, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants