Repository navigation
spec: WebhookSchema marks secret and headers as credential fields, and the kernel registry derives the meta read-door redaction for any marked type — webhook first, with a ratchet for unmarked credential-named fields (ruling B+ on #24966) #24994
Description
Ruled: 6304303975 · letter B+ · 2026-10-20T23:43Z
Filing gate ③ — the maintainer's ruling B+ on #24966 (batch #379 item 2; first 「同意」 to B, then 「B+ 可换……是不是长期更合理的方案。类似的问题都可以这样处理。」, recorded as 6304303975), filed by the director seat (summon #45 continuation 2, session_01Lg6GQ4xjqmvEzYv78d6H7i) and re-scoped to B+ in the same act. ⛔ Not a claim. Disclosure: the maintainer's ruling on #9387 (5323310017) applies to this card, its PR and every comment: location, rule and abstract reach only; ⛔ no probe, reproduction or served value.
Reader who acts: the domain:spec seat. One PR (M), @objectstack/spec patch changeset, Clause-②: no (narrowing) (the /meta read doors return less for webhook), contract review at CONTRACT_REVIEW_TIER. #24966 is pm:blocked on this card and closes when it lands.
Dedupe: the 19 open domain:spec cards' titles read in this act: 0 name a webhook, a redactor or a credential marker; #24966's own dedupe (webhook redactor, metadata-type redaction, secret-bearing headers, meta read door) found only itself.
What the ruling binds (origin/main e4d604efd39f)
The shape: declare the fact on the schema, derive the behaviour once in the kernel (the maintainer's standing word on 6304303975: 「类似的问题都可以这样处理」).
- The marker, on the declaration.
WebhookSchema(packages/spec/src/automation/webhook.zod.ts:136) markssecretandheadersas credential fields with a schema-level marker in the repo's existing zod.meta({ … })convention (the marker's key and vocabulary are the spec seat's to name; the shape distinguishes a credential VALUE from a credential MAP, soheadersis marked as a whole map). Nothing else about the schema changes. - The derivation, once, in the registry.
getMetadataTypeRedactor(type)inpackages/spec/src/kernel/metadata-type-redaction.ts(:126 today: extra registration, then built-in) gains a third resolution: a redactor DERIVED from the type's registered schema (getMetadataTypeSchema(type),metadata-type-schemas.ts:299). It walks the declared shape, 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 inredactedKeys. Pure, no I/O, as theMetadataTypeRedactorcontract requires. Any metadata type that marks a field is redacted on every meta read door with nothing else to write;webhookis the first type served this way. - The guard. A spec ratchet test reads every registered metadata-type schema (
listMetadataTypeSchemaTypes()) 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 asks for (datasource-credential-redaction.ts:40–49: a positive marker under-redacts when forgotten). - Precedence and the two legacy types. An explicit registration or built-in still wins over the derivation;
datasource(schema-reading plus URL scrubbing) andflow(nested regions, write-path strip) keep their explicit redactors and are NOT migrated. - Unchanged from B: the write-path carry-forward (a save that omits the two fields keeps the stored values) is accepted and keyed, as today, on "a redactor resolves for the type"; the dispatcher's declared-webhook source keeps carrying both credentials (delivery signing unaffected); who may reach a meta read door does not change; plugin-webhooks registers nothing.
What done looks like
WebhookSchema.secretand.headerscarry the marker; a spec unit test shows the derivedwebhookredactor drops both, serves the cleared forms, leaves every other field intact, and that an unmarked type derives no redactor (undefined, "serve as is").- The ratchet: a marked field passes; a credential-named field without a marker reds the suite; the allow-list is explicit and empty or near-empty.
- Door-level pins in the dogfood suite, in BOTH compositions (with and without plugin-webhooks): the meta list (both spellings), item, layered-view and published reads, and the stored-row data door with its history twin, withhold both fields; ablation-verified (remove the marker, the pin reads red).
- A save round trip of the served form leaves the stored values intact (the carry-forward pin).
@objectstack/specpatch changeset,Clause-②: no (narrowing); api-surface regenerated for any exported helper.- ⛔ No probe output, request shape or served value in the PR, its tests or their names; the pins assert absence by rule name.
Related
#24966 (the ruled card: dev report 6301197652, decision 6301250022, ruling 6303716122 and its amendment 6304303975) · #8300 (datasource built-in precedent) · #9387 (disclosure) · #24900 (source report).
Activity
[-]spec: a built-in `webhook` metadata-type redactor — `secret` and the whole `headers` map are withheld on every meta read door, with or without plugin-webhooks mounted (ruling B on #24966)[/-][+]spec: `WebhookSchema` marks `secret` and `headers` as credential fields, and the kernel registry derives the meta read-door redaction for any marked type — `webhook` first, with a ratchet for unmarked credential-named fields (ruling B+ on #24966)[/+]objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 24
Session:
session_01D6F9axfoPzR7t8G1J7YtzzAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-24994-schema-derived-credential-redactorWorktree:
objectstack-issue-24994Domain:
domain:specSeat:
domain:spec#3(seat post #18883)File surface (at
origin/mainfbb42d900for later; stop on a breach and explain it in the report):packages/spec/src/automation/webhook.zod.ts(the credential marker onsecretand on theheadersmap);packages/spec/src/kernel/metadata-type-redaction.ts(the schema-derived third resolution ingetMetadataTypeRedactor);listMetadataTypeSchemaTypes(), with its allow-list;6304303975) names;.changeset/24994-*.md.Container & model:
M,mode:subagent,model: default (no path-derived mandate)Clause-②: no (narrowing: the
/metaread doors return less forwebhook)Responsibility:
ruled B+ on #24966 (6304303975)Thread-read: none
Serial constraints cleared: read at this act; the dev re-checks open PRs touching
metadata-type-redaction.ts/webhook.zod.tsbefore opening its own.Disclosure: the maintainer's ruling on #9387 (
5323310017) applies. The PR and every comment carry location, rule and abstract reach only, ⛔ no probe, reproduction or served value. The PR owes aCONTRACT_REVIEW_TIERcontract review.Read on
origin/mainfbb42d900fat 2026-10-20T23:50Z:BUILTIN_METADATA_TYPE_REDACTORShas nowebhookentry.Generated by Claude Code
feat(spec): schema-derived credential redactor; WebhookSchema marks s…
chore(changeset): ADR-0087 disposition for #24994
test(dogfood): webhook meta read doors withhold declared credentials …
fix(spec): the credential walk reads a schema's public def, so an ins…
objectstack-fleet commentedon Oct 21, 2026
os-dev-report
{
"issue": 24994,
"status": "done",
"branch": "claude/issue-24994-schema-derived-credential-redactor",
"pr": "#25036",
"session": "session_01D6F9axfoPzR7t8G1J7Ytzz",
"premise_still_valid": true,
"summary": "Ruling B+ implemented. WebhookSchema now marks secret with .meta({ credential: 'value' }) and the headers map as a whole with .meta({ credential: 'map' }). A new internal module, packages/spec/src/kernel/metadata-credential-fields.ts, holds the marker vocabulary, a walk over the declared object shapes (nested ones included), the cleared forms, and a memoised pure redactor built from the markers. getMetadataTypeRedactor now resolves an explicit registration first, then a built-in, then this schema-derived redactor; listMetadataTypeRedactorTypes includes the derived types, and datasource and flow are untouched. A spec ratchet (metadata-credential-fields.test.ts) covers every registered metadata-type schema plus the stack-collection kinds; this was needed because webhook lives in UNREGISTERED_KIND_SCHEMAS, so listMetadataTypeSchemaTypes alone never reads it. Dogfood door pins cover both compositions. Reading choice, stated in the PR: a withheld field is REMOVED (the flow precedent, and dev report option A), and a stored cleared form is served as written. This is what makes ruling item 5's 'a save that omits the two fields keeps the stored values' hold, because the carry-forward compares the incoming value with the served one. Replacing the withheld value with '' would have made a served-body save look like a deliberate clear.",
"tests": "Head 0cf6eed. Spec: the two touched files give 2 files and 39 tests passed. spec typecheck: exit 0. spec build, then check:generated: all 14 artifacts up to date, nothing regenerated. Full spec test exit 0 at 441ce01; the later commit only changes zodDefOf to read the public .def and adds one test. rest: test exit 0 (307 files; 6157 passed, 326 skipped) and typecheck exit 0, at 0cf6eed. An earlier head turned rest red in rest-exposure-gate-execution-read.test.ts, and the cause was this change: a _zod read through a second Proxy around a lazySchema Proxy throws a Proxy invariant error. It was fixed by reading .def, and the fix is pinned by a doubly-proxied unit test. metadata-protocol and plugin-webhooks: test and typecheck exit 0 at 441ce01. dogfood: typecheck exit 0; the new pin file plus webhook-materialization and wall-declared-webhook-deployment-wide give 3 files and 24 tests passed at 0cf6eed. Ablation, with the fix committed first: both credential markers were rewritten to a non-marker key through scripts/ablation-replace.mjs (anchor 1 to 0 each; on-disk counts 0 and 0). spec was rebuilt, and ablation-dist-preflight --absent found both markers absent from all 232 built files. The pin file then read 12 failed and 3 passed; the 3 passes are the composition guards and the dispatcher-source pin. Restore: blob equal to HEAD, git diff HEAD empty, spec rebuilt, the preflight found both markers present in 18 built files, and the pin file read 15 of 15 passed. A first ablation attempt was refused by the tool's replacement-count check before any mutation ran (a no-op, counted as nothing). Every locked run returned VERDICT command-exit 0, apart from queue-timeout 99s that were retried under the same slot.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack derived 87 families (stderr names objectstack-ai/objectstack at 0cf6eed), and --ran reconciled 87 derived, 87 run, 0 unrun. All exit 0 except one. The spec, changeset and citation subset (33 commands, check:generated among them) was run again at 0cf6eed, all exit 0. check:type-check-debt ran under the lock at 0cf6eed: re-measure OK, no entry above its recorded number. NOT MEASURED: check:dual-build-cjs-loads, because it needs every workspace package built and only the dogfood closure was built; declared to CI. No ① closure beyond the dogfood closure was built (turbo build --filter='@objectstack/dogfood^...': 66 tasks, 66 successful).",
"deviations": [
"The card names listMetadataTypeSchemaTypes() for the ratchet. The ratchet reads that list PLUS listUnregisteredKindSchemaTypes(), because webhook is not a registered kind; without the second list the guard would never read the one type the card is about.",
"Card item 2's wording 'serves its cleared form' was read as: remove the withheld key, and serve a stored cleared form as written. The PR body explains why. If the PM reads the ruling as 'substitute the cleared form', that is a change to two lines in deriveCredentialRedactor, and the carry-forward on omission would then stop holding.",
"Door set pinned: the card's list (meta list both spellings, item, layered view, published, stored-row data door and history twin), plus the metadata protocol's getMetaItems and getMetaItem. No MCP pin: the card's list does not name one, and #24966's dev report measured that the MCP HTTP tools refuse the stored metadata objects.",
"The four new spec exports (walker helpers) live in a module that is not re-exported from the kernel barrel, so api-surface is unchanged. The marker key also appears in the generated JSON Schema of webhook (zod copies .meta into the JSON Schema node); no committed artifact changed.",
"No label written: the dispatch named none, and skip-changeset does not apply (a spec patch changeset ships).",
"Worktree /home/user/objectstack-issue-24994 removed after the PR opened (node_modules deleted, worktree remove without force succeeded)."
],
"files_changed": [
"packages/spec/src/automation/webhook.zod.ts",
"packages/spec/src/kernel/metadata-credential-fields.ts",
"packages/spec/src/kernel/metadata-credential-fields.test.ts",
"packages/spec/src/kernel/metadata-type-redaction.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"
],
"mcp_calls": "0",
"api_writes": "2 relay dispatches through scripts/pm/with-fleet.sh --via dispatch. (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft), then POST /repos//issues/25036/assignees with baozhoutao. (2) This os-dev-report comment: POST /repos//issues/24994/comments. Outside REST: git push of the branch. Reads were gh api REST only.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed — a schema wrapped in a second Proxy around a lazySchema Proxy throws a Proxy invariant TypeError on any _zod read, because lazySchema's getOwnPropertyDescriptor reports a non-configurable _zod its function target lacks. It is reachable only by a caller that proxies a registered schema (today, one rest test); this PR's walker avoids it by reading .def. Dedupe words: lazySchema proxy invariant, _zod getOwnPropertyDescriptor, double proxy schema."
]
}
Generated by Claude Code
chore(changeset): #24994 ships minor with a BREAKING banner (a narrow…
objectstack-fleet commentedon Oct 21, 2026
os-dev-report
{
"issue": 24994,
"status": "done",
"branch": "claude/issue-24994-schema-derived-credential-redactor",
"pr": "#25036",
"session": "session_01D6F9axfoPzR7t8G1J7Ytzz",
"premise_still_valid": true,
"summary": "Fix round for the contract-review FAIL, one point. The changeset is re-graded from patch to minor, as the launch-window narrowing rule in scripts/check-changeset-no-major.mjs requires (maintainer ruling 2026-09-04, batch #35, on #15294). It now also carries a BREAKING banner and a spec!: headline, the precedent shape of the no (narrowing) changesets on main. origin/main was merged in, with no rebase and no force-push. New head: e86f322. The PR is still a draft, and its body's changeset line plus a fix-round section were updated through the relay.",
"tests": "At e86f322: pnpm --filter @objectstack/spec test passed 703 files and 21388 tests (1 skipped, 1 todo), and typecheck passed, under the lock with VERDICT command-exit 0. These 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.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at e86f322 gives the same 87 families as the previous head. The delta-touched gates all exit 0: check:nul-bytes, check:issue-citations, check:doc-authoring, check:pm-changeset-deadline-census, check-closing-keyword-parity, check:objectui-changeset, check:closed-ceremony-text. The rest are declared to CI.",
"mcp_calls": "0",
"api_writes": "2 relay dispatches. (1) issue_patch: PATCH /repos//issues/25036, the body, 12727 bytes stored identically. (2) This comment: POST /repos//issues/24994/comments. Also one git push.",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
Close-out — landed
e939357b66(single-parent squash onmain).e86f32235f(756aed0967).6307628923on the PR.WebhookSchemanow marks its credential-bearing fields, and the redactor registry derives the metadata read-door redaction from the schema. Those fields are dropped from the read, not masked, in line with ruling B+ on meta: thewebhookmetadata type has no redactor, so its secret-bearing fields are not masked on the meta read doors (detail withheld under the disclosure ruling) #24966.webhookmetadata type has no redactor, so its secret-bearing fields are not masked on the meta read doors (detail withheld under the disclosure ruling) #24966 belongs to another lane (domain:services,pm:blocked). This seat leaves it as it stands.pm:dispatchedremoved.