You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
Filing gate ①, the security exception. Filed by domain:spec seat 3 (seat post #18883) · baozhoutao · session session_01D6F9axfoPzR7t8G1J7Ytzz, from the #24900 dev report. ⛔ Not a claim.
Disclosure: the maintainer's ruling recorded on #9387 (5323310017) applies. Triage redacted this card in R545 (see its comment): it records the defect's existence, where it lives and the governing text only. ⛔ No probe, reproduction or served value goes on this card, its PR or any comment.
Where it lives (origin/main223ae939bb)
packages/spec/src/kernel/metadata-type-redaction.ts: BUILTIN_METADATA_TYPE_REDACTORS has no webhook entry, and nothing calls registerMetadataTypeRedactor('webhook').
The precedent is flow:service-automation's flow-credential-projection.ts registers one.
Seam:spec:WebhookSchema.secret (and secret-bearing headers values) → the meta read doors (packages/rest/src/meta-item-read-gate.ts, MCP and Studio).
Governing text
The webhook.json liveness row for secret: the declared key is "consumed for one HMAC and never persisted". A read door that serves it does not honour that row.
Wanted
Measure first, abstractly: which roles can reach the meta read doors for webhook. Record it as a statement of reach only.
The redactor: a webhook metadata-type redactor, in the flow precedent, masks secret and secret-bearing headers values on every meta read door.
Pins: one at each door, named by the rule and ablation-verified.
changed the title [-]meta: `GET /api/v1/meta/webhook` serves a declared webhook's authored `secret` (and `headers`) verbatim; `webhook` has no metadata-type redactor[/-][+]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)[/+]on Oct 20, 2026
Why p1: a declared signing secret, which its own liveness row says is never persisted, has no redactor on the meta read doors. Non-admin reach is not yet measured, and that is the first act.
The lane:domain:services, because the redactor registers from the webhook plugin, as service-automation does for flow. The spec registry and the REST door are read, not changed.
Claim: PM loop round 19 · 2026-10-20T20:30Z
Session: session_011TdZ8GjixNZFLz1xJD9zzK
Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24966-webhook-metadata-redactor
Worktree: objectstack-issue-24966
Domain: domain:services
Seat: domain:services#1
File surface: packages/plugins/plugin-webhooks/src/** (a webhook metadata-type redactor and its registration, in the flow-credential-projection.ts precedent), its NEW tests, and one new .changeset/24966-*.md. ⛔ Not packages/spec/** (the redactor registry is read, not changed), ⛔ not packages/rest/** (the meta read door is read, not changed), ⛔ not the MCP or Studio doors (read only), ⛔ no change to what is stored or to who may reach a meta read door (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: default (opus), quoting the --tier output "no path-derived mandate … floor sonnet · default opus · ceiling fable"
Clause-②: no
Responsibility: no webhook metadata-type redactor is registered, so the meta read doors serve a declared webhook's secret-bearing fields unmasked | the flow metadata type has one (service-automation's flow-credential-projection.ts), the precedent | not yet measured: which roles reach the meta read doors for webhook, which is the dispatch's first act, recorded as reach only
Thread-read: 6300101067
Serial constraints cleared: No open PR touches plugin-webhooks, the redactor registry, or the meta read gate (all 18 open PRs' files read at this claim). PR #24946 (#24910, this seat) landed on plugin-webhooks as 19e5338cc8, and this work starts on top of it.
Disclosure (the maintainer's ruling on #9387, 5323310017): ⛔ no probe, reproduction or served value goes on this card, the PR or any comment. Reach is recorded as a statement of reach only. Anything more goes to the seat by private channel.
os-dev-report
{
"issue": 24966,
"status": "needs_decision",
"branch": "claude/issue-24966-webhook-metadata-redactor",
"pr": null,
"session": "session_011TdZ8GjixNZFLz1xJD9zzK",
"premise_still_valid": true,
"summary": "The defect is real and measured: no webhook metadata-type redactor exists (the registry lists only datasource; nothing registers one), so every meta read door serves a webhook's secret-bearing fields as stored. Reach, stated abstractly: ANY authenticated caller, with no permission beyond sign-in, reaches the meta list read (both spellings), the item read and the layered-view read of a declared webhook, and the published read of a saved one; an anonymous caller is refused at every one of them; the stored-row data door (sys_metadata, sys_metadata_history) is reachable by an administrator only; the MCP HTTP tools do not reach stored metadata rows at all. Stopped before writing code, under the dispatch's fork rule: the card's route (register from plugin-webhooks, the flow precedent) cannot honestly cover 'every meta read door', because the measurement shows the webhook type's items reach those doors identically whether or not plugin-webhooks is mounted (declared items via manifest decomposition, saved items via the meta save door; webhooks is not on the always-on slate), and the registry's own placement rule in metadata-type-redaction.ts names exactly that condition as the case for a built-in entry in packages/spec, which this dispatch hard-stops. The fork, the redaction rule and the save-door consequence are in open_questions; nothing is committed, the branch is at the base.",
"tests": "No code was written, so no package test or typecheck run is owed or claimed. Measurement only, on a real in-process stack (@objectstack/verify bootStack through the real HTTP app), from scratch scripts outside the repository, on head f32d795 (worktree clean, branch == origin/main): (1) composition WITHOUT plugin-webhooks and (2) WITH it (messaging + webhook plugin mounted, confirmed in the boot log): identical reach and identical unmasked serving in both, admin and member alike, anonymous refused. (3) H2: with a stand-in webhook redactor registered in the stack's own registry, every measured door withheld both fields (meta list, item, layered view, published, stored-row data door and its history twin). (4) H3: with the stand-in registered, the dispatcher's declared-webhook source still carried both credentials, and a served-body round trip through the meta save door left both stored values intact (the generic carry-forward). Each run went through os-verify-lock.sh, every VERDICT line command-exit 0. Not measured: a real signed-delivery control (no implementation exists to run it against); the diff/history and package-export doors (code-read only: both reach the same registry entry).",
"gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack exit 2, 'this branch changes nothing against origin/main (merge base f32d795) — nothing to derive' (repo assertion held); so no derived family, no --ran reconciliation and no PR check-runs exist. Builds under the lock: pnpm --workspace-concurrency=2 --filter '@objectstack/verify^...' build VERDICT command-exit 0 (a superset of the ordered @objectstack/plugin-webhooks^... closure, taken to boot a real stack), then pnpm --filter @objectstack/verify build VERDICT command-exit 0.",
"deviations": [
"Stopped before step 2 (the redactor) under the dispatch's own rule 'If the honest fix needs a spec or door change, stop and report a fork': no implementation, no pins, no changeset, no PR.",
"Built the @objectstack/verify closure instead of only the plugin-webhooks closure, to measure on a real stack; the ordered closure is a subset of it.",
"The container restarted mid-run; the closure build had already ended with VERDICT command-exit 0, its outputs survived, and neither recorded PID (install, build) was alive afterwards; the lock read free and was not forced.",
"The card thread was read with single-card REST reads (gh api, issue plus paginated comments); no MCP tool was called.",
"The worktree /home/user/objectstack-issue-24966 is left in place (clean, at the base, built) for the resumed dispatch; no process of this run is alive."
],
"files_changed": [],
"mcp_calls": "0",
"api_writes": "1 — this os-dev-report comment on #24966 through scripts/pm/post-stamped.mjs (the fleet relay, POST issues/24966/comments). Outside REST: one git push of the empty branch (the claim marker and write probe).",
"open_questions": [
{
"question": "Where does the webhook redactor live? The type's items reach every meta read door with or without plugin-webhooks mounted (measured, same reach in both compositions), and the registry's own rule says a type whose rows exist without the owning plugin takes a built-in entry, because a plugin registration protects only the compositions that mount the plugin.",
"options": [
"A — register from plugin-webhooks init() (the card's route, the flow precedent). Covers every composition that mounts the plugin; a composition that declares or saves webhooks without mounting it keeps serving both fields to any authenticated caller. Effort S: one projection module, the init call, unit and registration tests, door pins in the dogfood suite (plugin-webhooks cannot take the verify harness as a dependency: verify depends on it). Lane domain:services, no spec change.",
"B — a built-in webhook entry in BUILTIN_METADATA_TYPE_REDACTORS, its function in packages/spec beside WebhookSchema, the datasource shape (data/datasource-credential-redaction.ts). Covers every composition from module load, nothing to forget. Effort S to M: the spec module and entry, spec unit tests, door pins in both compositions in the dogfood suite, an @objectstack/spec patch changeset, api-surface regeneration only if the function is exported. Lane domain:spec: this dispatch hard-stops packages/spec/**, so it is a re-dispatch.",
"C — A now, B later. Covers the mounted composition at once, then moves the function into spec and deletes the registration: two PRs, the first one's code short-lived, and an interim that reads as fixed while the unmounted composition stays open."
],
"recommendation": "B. Long-term soundness: it is the placement the registry's own text prescribes for exactly this measured condition, and the flow precedent's two reasons for a plugin registration do not transfer (a webhook's secret is the receiver's shared secret, live whether or not this composition dispatches, and the spec already declares secret as the HMAC signing secret, so the rule is a fact of the declared shape, the datasource case). Preventing AI mistakes: an author or agent that writes webhooks: and forgets requires: ['webhooks'] is protected by construction under B, while A fails open there silently. Actual business need: measured, the only in-repo app that declares webhooks (app-showcase) also requires the plugin, so A covers the attested deployment and B adds the unattested composition; a saved webhook needs no plugin at all. Startup focus: B and A are the same size and neither adds a capability surface or a gate; C pays twice. The four axes do not conflict here."
},
{
"question": "What exactly does the redactor withhold (applies under A or B)? The dispatch forbids inventing a header list without a governing source.",
"options": [
"A — drop secret and the WHOLE headers map (keys removed, not masked); serve the two cleared forms as written (an empty-string secret and an empty header map, which the dispatcher already treats as 'no credential'), so a reader tells 'cleared' from 'withheld'. Governing sources: the flow precedent's 'Dropped, not masked' (a mask is a valid string the dispatcher would sign with if a save stored it back) and plugin-webhooks' own whole-map rule (webhook-headers.ts, 'Why the WHOLE map moves': the platform cannot tell which header entries are credentials; the headers liveness row says the same).",
"B — drop only header entries whose NAME looks like a credential. Rejected by that same whole-map rule as fail-open on vendor-named headers.",
"C — replace values with a mask. Rejected by the flow precedent: a stored-back mask becomes the live signing key."
],
"recommendation": "A: every part of it has a governing source in this repo, and it is the fail-closed reading. Cost: a reader no longer sees non-secret header entries of a served webhook, which webhook-headers.ts already measured and accepted for the stored-row side."
},
{
"question": "Registering ANY webhook redactor (A or B) switches on the metadata protocol's generic write-path inverse for the type: a meta save whose body omits secret or headers (exactly the served form) keeps the stored values instead of saving without them (measured with a stand-in). The dispatch's hard stop reads 'no change to what is stored'. Is this inverse accepted as part of the fix?",
"options": [
"A — accept it: it is the registry's designed counterpart (datasource and flow ship with it), and without it the redaction turns an ordinary read-edit-save round trip into silent credential deletion; removing a credential stays possible by saving the cleared form or deleting the item.",
"B — treat it as a breach: there is no redactor that avoids it, so the card would be unimplementable as written."
],
"recommendation": "A, and word the next dispatch's hard stop so it names this inverse as permitted; nothing else about what is stored, about delivery signing, or about who reaches a door changes under either route."
}
],
"out_of_scope_findings": [
"carrier: this card · noted, not filed — the unmounted-composition exposure is the substance of open question 1, not a separate defect; dedupe words: webhook redactor, metadata-type redaction, secret-bearing headers, meta read door.",
"carrier: this card · noted, not filed — H2 measured: no meta read door bypasses the registry (list, item, layered view, published, stored-row data door and history twin); the MCP HTTP tools refuse the stored metadata objects outright, and the MCP stdio data bridge applies the same stored-row projection (code-read)."
]
}
Ruling: batch #379 item 2 · letter B · maintainer 「同意」 2026-10-20T23:01Z
Director seat, summon #45 continuation 2, session_01Lg6GQ4xjqmvEzYv78d6H7i. Presented in this session's chat as batch #379 item 2, from the decision 6301250022 (domain:services seat 1) and the dev report 6301197652; the director's chat delta re-checked on origin/maine4d604efd39f that BUILTIN_METADATA_TYPE_REDACTORS lists datasource only (metadata-type-redaction.ts:110), that registerMetadataTypeRedactor('webhook' has 0 hits in packages, and the registry's placement rule at :36–46. The maintainer's 「同意」 takes the recommendation, letter B, with both sub-items. Thread read through 6301250022 at this write; nothing newer. ⛔ Disclosure (#9387, 5323310017): this record carries location, rule and abstract reach only.
Governing text:packages/spec/src/kernel/metadata-type-redaction.ts:36–46 ("Why datasource is a BUILT-IN, not a plugin registration", #8300); the packages/spec/liveness/webhook.json rows for secret and headers; filing-gate.md 定级判据 (a change to what the /meta read doors return is protocol class).
What the ruling binds
Placement — B. The webhook redactor is a built-in entry in packages/spec, beside WebhookSchema, in the datasource shape. Every composition is protected from module load, with or without plugin-webhooks mounted. Route A (plugin registration) and route C (A now, B later) are not taken.
Redaction form.secret and the whole headers map are dropped, not masked, and served as the two cleared forms (the flow precedent "Dropped, not masked"; the whole-map rule of webhook-headers.ts).
Write-path inverse. The metadata protocol's generic carry-forward for the type (a save that omits the two fields keeps the stored values) is accepted as part of the fix; the dispatch names it as permitted.
Nothing else moves: not what is stored, not delivery signing, not who reaches a meta read door.
Workload of the chosen letter (the card's column): domain:spec, one PR (S–M); precedent #8300's datasource built-in.
Execution: the spec execution card is #24994 (domain:spec p1 securitypm:queue, under the same disclosure discipline), filed by the director in this act. State:needs-user-decision → pm:blocked, Blocked-by: #24994; this card closes when #24994 lands, after the domain:services seat reads the doors in both compositions (reading only, nothing served goes on the record). The dispatched branch claude/issue-24966-webhook-metadata-redactor is empty and is not reused.
Director seat, summon #45 continuation 2, session_01Lg6GQ4xjqmvEzYv78d6H7i. Amends 6303716122 (this card, batch #379 item 2, letter B). After that record the maintainer asked, in this session's chat, whether B+ — a credential marker on the schema, from which the kernel registry derives the redaction for any marked type — is the sounder long-term shape, and added 「类似的问题都可以这样处理」. The director answered yes for flat declared credential fields, with the one known weakness (a forgotten marker) closed by a ratchet; the maintainer's words take B+. Thread read through 6303716122 at this write; nothing newer. ⛔ Disclosure (#9387, 5323310017): location, rule and abstract reach only.
Governing text:packages/spec/src/kernel/metadata-type-redaction.ts:36–46 (a type whose rows exist without its plugin takes a built-in, not a plugin registration); metadata-type-schemas.ts:299 (getMetadataTypeSchema(type), the kernel's type → schema registry the derivation reads); the datasource module's caution (datasource-credential-redaction.ts:40–49: a positive marker under-redacts when an author forgets it, so the derivation needs a fail-closed guard beside it).
What the amendment binds
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.
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.
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.
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.
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.
Workload of the chosen letter:domain:spec, one PR (M): marker on two fields, the derivation (about 40–60 lines), the ratchet (about 40 lines), unit tests, door-level pins in both compositions, @objectstack/spec patch changeset, Clause-②: no (narrowing) at CONTRACT_REVIEW_TIER. Roughly two to three times B's code, paid once for every later type.
Execution:#24994 is re-scoped to B+ in this act (body and title). State:needs-user-decision → pm:blocked, Blocked-by: #24994; this card closes when #24994 lands.
Activity
[-]meta: `GET /api/v1/meta/webhook` serves a declared webhook's authored `secret` (and `headers`) verbatim; `webhook` has no metadata-type redactor[/-][+]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)[/+]objectstack-fleet commentedon Oct 20, 2026
Triage (R545): p1 ·
domain:services·area:access·bug+security·pm:queue. Card redacted under the disclosure ruling. ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-20T19:54Zdomain:services, because the redactor registers from the webhook plugin, asservice-automationdoes forflow. The spec registry and the REST door are read, not changed.5323310017). ⛔ The original is not restored on GitHub, and no probe goes on the PR.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 19 · 2026-10-20T20:30Z
Session:
session_011TdZ8GjixNZFLz1xJD9zzKAccount:
os-justin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24966-webhook-metadata-redactorWorktree:
objectstack-issue-24966Domain:
domain:servicesSeat:
domain:services#1File surface:
packages/plugins/plugin-webhooks/src/**(awebhookmetadata-type redactor and its registration, in theflow-credential-projection.tsprecedent), its NEW tests, and one new.changeset/24966-*.md. ⛔ Notpackages/spec/**(the redactor registry is read, not changed), ⛔ notpackages/rest/**(the meta read door is read, not changed), ⛔ not the MCP or Studio doors (read only), ⛔ no change to what is stored or to who may reach a meta read door (stop on breach; explain in the report)Container & model:
S,mode:subagent,model: default (opus), quoting the --tier output "no path-derived mandate … floor sonnet · default opus · ceiling fable"Clause-②: no
Responsibility:
no webhook metadata-type redactor is registered, so the meta read doors serve a declared webhook's secret-bearing fields unmasked | the flow metadata type has one (service-automation's flow-credential-projection.ts), the precedent | not yet measured: which roles reach the meta read doors for webhook, which is the dispatch's first act, recorded as reach onlyThread-read: 6300101067
Serial constraints cleared:
No open PR touches plugin-webhooks, the redactor registry, or the meta read gate (all 18 open PRs' files read at this claim). PR #24946 (#24910, this seat) landed on plugin-webhooks as 19e5338cc8, and this work starts on top of it.Disclosure (the maintainer's ruling on #9387,
5323310017): ⛔ no probe, reproduction or served value goes on this card, the PR or any comment. Reach is recorded as a statement of reach only. Anything more goes to the seat by private channel.Readings taken on
origin/mainf32d795ce5.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 24966,
"status": "needs_decision",
"branch": "claude/issue-24966-webhook-metadata-redactor",
"pr": null,
"session": "session_011TdZ8GjixNZFLz1xJD9zzK",
"premise_still_valid": true,
"summary": "The defect is real and measured: no
webhookmetadata-type redactor exists (the registry lists onlydatasource; nothing registers one), so every meta read door serves a webhook's secret-bearing fields as stored. Reach, stated abstractly: ANY authenticated caller, with no permission beyond sign-in, reaches the meta list read (both spellings), the item read and the layered-view read of a declared webhook, and the published read of a saved one; an anonymous caller is refused at every one of them; the stored-row data door (sys_metadata,sys_metadata_history) is reachable by an administrator only; the MCP HTTP tools do not reach stored metadata rows at all. Stopped before writing code, under the dispatch's fork rule: the card's route (register from plugin-webhooks, the flow precedent) cannot honestly cover 'every meta read door', because the measurement shows thewebhooktype's items reach those doors identically whether or not plugin-webhooks is mounted (declared items via manifest decomposition, saved items via the meta save door;webhooksis not on the always-on slate), and the registry's own placement rule inmetadata-type-redaction.tsnames exactly that condition as the case for a built-in entry inpackages/spec, which this dispatch hard-stops. The fork, the redaction rule and the save-door consequence are inopen_questions; nothing is committed, the branch is at the base.","tests": "No code was written, so no package test or typecheck run is owed or claimed. Measurement only, on a real in-process stack (
@objectstack/verifybootStack through the real HTTP app), from scratch scripts outside the repository, on head f32d795 (worktree clean, branch == origin/main): (1) composition WITHOUT plugin-webhooks and (2) WITH it (messaging + webhook plugin mounted, confirmed in the boot log): identical reach and identical unmasked serving in both, admin and member alike, anonymous refused. (3) H2: with a stand-in webhook redactor registered in the stack's own registry, every measured door withheld both fields (meta list, item, layered view, published, stored-row data door and its history twin). (4) H3: with the stand-in registered, the dispatcher's declared-webhook source still carried both credentials, and a served-body round trip through the meta save door left both stored values intact (the generic carry-forward). Each run went throughos-verify-lock.sh, every VERDICT linecommand-exit 0. Not measured: a real signed-delivery control (no implementation exists to run it against); the diff/history and package-export doors (code-read only: both reach the same registry entry).","gates": "Local:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackexit 2, 'this branch changes nothing against origin/main (merge base f32d795) — nothing to derive' (repo assertion held); so no derived family, no--ranreconciliation and no PR check-runs exist. Builds under the lock:pnpm --workspace-concurrency=2 --filter '@objectstack/verify^...' buildVERDICT command-exit 0 (a superset of the ordered@objectstack/plugin-webhooks^...closure, taken to boot a real stack), thenpnpm --filter @objectstack/verify buildVERDICT command-exit 0.","deviations": [
"Stopped before step 2 (the redactor) under the dispatch's own rule 'If the honest fix needs a spec or door change, stop and report a fork': no implementation, no pins, no changeset, no PR.",
"Built the
@objectstack/verifyclosure instead of only the plugin-webhooks closure, to measure on a real stack; the ordered closure is a subset of it.","The container restarted mid-run; the closure build had already ended with VERDICT command-exit 0, its outputs survived, and neither recorded PID (install, build) was alive afterwards; the lock read free and was not forced.",
"The card thread was read with single-card REST reads (
gh api, issue plus paginated comments); no MCP tool was called.","The worktree
/home/user/objectstack-issue-24966is left in place (clean, at the base, built) for the resumed dispatch; no process of this run is alive."],
"files_changed": [],
"mcp_calls": "0",
"api_writes": "1 — this
os-dev-reportcomment on #24966 throughscripts/pm/post-stamped.mjs(the fleet relay, POST issues/24966/comments). Outside REST: onegit pushof the empty branch (the claim marker and write probe).","open_questions": [
{
"question": "Where does the
webhookredactor live? The type's items reach every meta read door with or without plugin-webhooks mounted (measured, same reach in both compositions), and the registry's own rule says a type whose rows exist without the owning plugin takes a built-in entry, because a plugin registration protects only the compositions that mount the plugin.","options": [
"A — register from plugin-webhooks
init()(the card's route, the flow precedent). Covers every composition that mounts the plugin; a composition that declares or saves webhooks without mounting it keeps serving both fields to any authenticated caller. Effort S: one projection module, the init call, unit and registration tests, door pins in the dogfood suite (plugin-webhooks cannot take the verify harness as a dependency: verify depends on it). Lane domain:services, no spec change.","B — a built-in
webhookentry inBUILTIN_METADATA_TYPE_REDACTORS, its function inpackages/specbesideWebhookSchema, the datasource shape (data/datasource-credential-redaction.ts). Covers every composition from module load, nothing to forget. Effort S to M: the spec module and entry, spec unit tests, door pins in both compositions in the dogfood suite, an@objectstack/specpatch changeset, api-surface regeneration only if the function is exported. Lane domain:spec: this dispatch hard-stopspackages/spec/**, so it is a re-dispatch.","C — A now, B later. Covers the mounted composition at once, then moves the function into spec and deletes the registration: two PRs, the first one's code short-lived, and an interim that reads as fixed while the unmounted composition stays open."
],
"recommendation": "B. Long-term soundness: it is the placement the registry's own text prescribes for exactly this measured condition, and the flow precedent's two reasons for a plugin registration do not transfer (a webhook's secret is the receiver's shared secret, live whether or not this composition dispatches, and the spec already declares
secretas the HMAC signing secret, so the rule is a fact of the declared shape, the datasource case). Preventing AI mistakes: an author or agent that writeswebhooks:and forgetsrequires: ['webhooks']is protected by construction under B, while A fails open there silently. Actual business need: measured, the only in-repo app that declares webhooks (app-showcase) also requires the plugin, so A covers the attested deployment and B adds the unattested composition; a saved webhook needs no plugin at all. Startup focus: B and A are the same size and neither adds a capability surface or a gate; C pays twice. The four axes do not conflict here."},
{
"question": "What exactly does the redactor withhold (applies under A or B)? The dispatch forbids inventing a header list without a governing source.",
"options": [
"A — drop
secretand the WHOLEheadersmap (keys removed, not masked); serve the two cleared forms as written (an empty-string secret and an empty header map, which the dispatcher already treats as 'no credential'), so a reader tells 'cleared' from 'withheld'. Governing sources: the flow precedent's 'Dropped, not masked' (a mask is a valid string the dispatcher would sign with if a save stored it back) and plugin-webhooks' own whole-map rule (webhook-headers.ts, 'Why the WHOLE map moves': the platform cannot tell which header entries are credentials; theheadersliveness row says the same).","B — drop only header entries whose NAME looks like a credential. Rejected by that same whole-map rule as fail-open on vendor-named headers.",
"C — replace values with a mask. Rejected by the flow precedent: a stored-back mask becomes the live signing key."
],
"recommendation": "A: every part of it has a governing source in this repo, and it is the fail-closed reading. Cost: a reader no longer sees non-secret header entries of a served webhook, which
webhook-headers.tsalready measured and accepted for the stored-row side."},
{
"question": "Registering ANY webhook redactor (A or B) switches on the metadata protocol's generic write-path inverse for the type: a meta save whose body omits
secretorheaders(exactly the served form) keeps the stored values instead of saving without them (measured with a stand-in). The dispatch's hard stop reads 'no change to what is stored'. Is this inverse accepted as part of the fix?","options": [
"A — accept it: it is the registry's designed counterpart (datasource and flow ship with it), and without it the redaction turns an ordinary read-edit-save round trip into silent credential deletion; removing a credential stays possible by saving the cleared form or deleting the item.",
"B — treat it as a breach: there is no redactor that avoids it, so the card would be unimplementable as written."
],
"recommendation": "A, and word the next dispatch's hard stop so it names this inverse as permitted; nothing else about what is stored, about delivery signing, or about who reaches a door changes under either route."
}
],
"out_of_scope_findings": [
"carrier: this card · noted, not filed — the unmounted-composition exposure is the substance of open question 1, not a separate defect; dedupe words: webhook redactor, metadata-type redaction, secret-bearing headers, meta read door.",
"carrier: this card · noted, not filed — H2 measured: no meta read door bypasses the registry (list, item, layered view, published, stored-row data door and history twin); the MCP HTTP tools refuse the stored metadata objects outright, and the MCP stdio data bridge applies the same stored-row projection (code-read)."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
决策请求 ·
domain:servicesseat 1 ·os-justin·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-20T20:53Z · ⛔ 披露纪律(#93875323310017)适用:本评论只写位置、规则与抽象触达,不含任何探测、请求形态或取回值。依据:os-dev 报告
6301197652(本卡),读数基于origin/mainf32d795ce5。dev 按派发令的 fork 规则停在写代码之前,所以没有 PR。一句话问题: webhook 元数据里的签名密钥和可能带凭据的请求头,在元数据读取接口上原样返回;要在哪一层把它们拦下,决定了没装 webhook 插件的部署是否仍然泄露。
背景(抽象读数)
plugin-webhooks,上述触达完全相同。声明的 webhook 经清单分解进入,保存的 webhook 经元数据保存门进入,而webhooks不在常开能力里。Governing text
packages/spec/src/kernel/metadata-type-redaction.ts:36-46「Whydatasourceis a BUILT-IN, not a plugin registration」:插件注册被实测为失效放行(A per-type metadata redaction seam belongs in@objectstack/spec/kernel— no service package can reach a registry inmetadata-protocol#8300)。插件是可选的,而元数据行与/meta读取出口在没有插件时照样存在。诚实的接法是内置条目:模块一加载就生效,对每一种能返回该行的组合都生效,没有可遗忘之处。webhook.json的 liveness 行(secret):「consumed for one HMAC and never persisted」。filing-gate.md):修复改动/meta读存删门的返回,宽窄都算协议类,由维护者裁决。协议声明、是否改协议
推荐路线在
packages/spec增加一个内置脱敏条目,/meta读取接口对 webhook 的返回随之收窄,以Clause-②: no (narrowing)声明,做达档复核。这是安全收紧,不是放宽。前提(各带 re-check 命令)
datasource。re-check:git grep -n "BUILTIN_METADATA_TYPE_REDACTORS" packages/spec/src/kernel/metadata-type-redaction.tswebhook脱敏器。re-check:git grep -n "registerMetadataTypeRedactor('webhook'" -- packages,应为 0 处命中。选项 × 真实代价 × 工作量
plugin-webhooks的init()注册(仿flow)domain:services1 个 PR(S);先例flow-credential-projection.tsBUILTIN_METADATA_TYPE_REDACTORS加webhook,函数放在 spec,与WebhookSchema相邻(仿datasource)domain:spec1 个 PR(S–M);先例 #8300 的datasource内置两个随路线一起定的子项(dev 的读数,席位同意):
secret与整张headers,不做掩码,返回清空形态。依据是flow先例「Dropped, not masked」(掩码被存回会成为真实签名密钥),以及webhook-headers.ts的整表规则(平台分不清哪个请求头是凭据)。datasource与flow已有的设计配套。下一份派发令把它列为允许项。业务含义直译:
四轴(业务立场)
@objectstack/spec/kernel— no service package can reach a registry inmetadata-protocol#8300 的同一测量结论)。flow先例选插件注册的两个理由在这里都不成立:webhook 的密钥是接收方的共享密钥,与本组合是否投递无关;spec 已把secret声明为 HMAC 签名密钥。webhooks:却忘了requires: ['webhooks']的作者或 agent 照样受保护;A 在这种情况下会静默放行。推荐:B(脱敏形态与写路径反向同上)
裁后执行(选 B 时)
domain:spec立执行卡:spec 内置webhook条目(仿data/datasource-credential-redaction.ts)、spec 单测、两种组合下的门级 pin(放在 dogfood 套件)、@objectstack/specpatch changeset,以及Clause-②: no (narrowing)的达档复核。卡面与 PR 同样受披露纪律约束。pm:blocked,Blocked-by:指向那张 spec 卡,spec 卡落地时关闭本卡。相关单与 PR
#8300(
datasource内置先例)、#24900(来源报告)、#9387(披露裁决)。os-decision-facets
Prior rulings read:
metadata-type redactor built-in vs plugin·datasource credential redaction·flow credential projection→ 2 precedents (#8300 inmetadata-type-redaction.ts:36-46;flow-credential-projection.ts); ADR none named; thread: none.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Ruling: batch #379 item 2 · letter B · maintainer 「同意」 2026-10-20T23:01Z
Director seat, summon #45 continuation 2,
session_01Lg6GQ4xjqmvEzYv78d6H7i. Presented in this session's chat as batch #379 item 2, from the decision 6301250022 (domain:servicesseat 1) and the dev report 6301197652; the director's chat delta re-checked onorigin/maine4d604efd39fthatBUILTIN_METADATA_TYPE_REDACTORSlistsdatasourceonly (metadata-type-redaction.ts:110), thatregisterMetadataTypeRedactor('webhook'has 0 hits inpackages, and the registry's placement rule at:36–46. The maintainer's 「同意」 takes the recommendation, letter B, with both sub-items. Thread read through 6301250022 at this write; nothing newer. ⛔ Disclosure (#9387,5323310017): this record carries location, rule and abstract reach only.Governing text:packages/spec/src/kernel/metadata-type-redaction.ts:36–46("Whydatasourceis a BUILT-IN, not a plugin registration", #8300); thepackages/spec/liveness/webhook.jsonrows forsecretandheaders;filing-gate.md定级判据 (a change to what the/metaread doors return is protocol class).What the ruling binds
webhookredactor is a built-in entry inpackages/spec, besideWebhookSchema, in thedatasourceshape. Every composition is protected from module load, with or without plugin-webhooks mounted. Route A (plugin registration) and route C (A now, B later) are not taken.secretand the wholeheadersmap are dropped, not masked, and served as the two cleared forms (theflowprecedent "Dropped, not masked"; the whole-map rule ofwebhook-headers.ts).Workload of the chosen letter (the card's column):
domain:spec, one PR (S–M); precedent #8300'sdatasourcebuilt-in.Execution: the spec execution card is #24994 (
domain:specp1securitypm:queue, under the same disclosure discipline), filed by the director in this act. State:needs-user-decision→pm:blocked,Blocked-by: #24994; this card closes when #24994 lands, after thedomain:servicesseat reads the doors in both compositions (reading only, nothing served goes on the record). The dispatched branchclaude/issue-24966-webhook-metadata-redactoris empty and is not reused.objectstack-fleet commentedon Oct 20, 2026
Ruling: batch #379 item 2 · letter B+ · amended · maintainer 「B+ 可换……是不是长期更合理的方案。类似的问题都可以这样处理。」 2026-10-20T23:41Z
Director seat, summon #45 continuation 2,
session_01Lg6GQ4xjqmvEzYv78d6H7i. Amends 6303716122 (this card, batch #379 item 2, letter B). After that record the maintainer asked, in this session's chat, whether B+ — a credential marker on the schema, from which the kernel registry derives the redaction for any marked type — is the sounder long-term shape, and added 「类似的问题都可以这样处理」. The director answered yes for flat declared credential fields, with the one known weakness (a forgotten marker) closed by a ratchet; the maintainer's words take B+. Thread read through 6303716122 at this write; nothing newer. ⛔ Disclosure (#9387,5323310017): location, rule and abstract reach only.Governing text:packages/spec/src/kernel/metadata-type-redaction.ts:36–46(a type whose rows exist without its plugin takes a built-in, not a plugin registration);metadata-type-schemas.ts:299(getMetadataTypeSchema(type), the kernel's type → schema registry the derivation reads); the datasource module's caution (datasource-credential-redaction.ts:40–49: a positive marker under-redacts when an author forgets it, so the derivation needs a fail-closed guard beside it).What the amendment binds
WebhookSchema.secretandWebhookSchema.headersare declared credential fields with a schema-level marker (zod.meta({ credential: … }), the repo's existing.metaconvention),headersas a whole map. The fact "this field is a credential" lives on the declaration, once.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, andredactedKeysnames them. 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.secretand the wholeheadersmap dropped, cleared forms served), the write-path carry-forward accepted, delivery signing and door reach untouched.datasourceandflowkeep their explicit redactors (they take precedence over the derivation) and are not migrated.Workload of the chosen letter:
domain:spec, one PR (M): marker on two fields, the derivation (about 40–60 lines), the ratchet (about 40 lines), unit tests, door-level pins in both compositions,@objectstack/specpatch changeset,Clause-②: no (narrowing)atCONTRACT_REVIEW_TIER. Roughly two to three times B's code, paid once for every later type.Execution: #24994 is re-scoped to B+ in this act (body and title). State:
needs-user-decision→pm:blocked,Blocked-by: #24994; this card closes when #24994 lands.