Skip to content

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

Open

Description

@objectstack-fleet

Ruled: 6304303975 · letter B+ · 2026-10-20T23:44Z

Blocked-by: #24994

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/main 223ae939bb)

  • 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

  1. Measure first, abstractly: which roles can reach the meta read doors for webhook. Record it as a statement of reach only.
  2. The redactor: a webhook metadata-type redactor, in the flow precedent, masks secret and secret-bearing headers values on every meta read door.
  3. Pins: one at each door, named by the rule and ablation-verified.

Generated by Claude Code

Activity

  1. 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
  2. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage (R545): p1 · domain:services · area:access · bug + security · pm:queue. Card redacted under the disclosure ruling. · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-20T19:54Z

    • 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.
    • Redaction: in this act triage replaced the title and the measured section with a location-only form (skills: five principle gaps the #9296 wave exposed in checklist-test / checklist-author / dogfood-verification #9387, 5323310017). ⛔ The original is not restored on GitHub, and no probe goes on the PR.

    Generated by Claude Code

  3. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    priority:p1High: required for production / M2
    on Oct 20, 2026
  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    Readings taken on origin/main f32d795ce5.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    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)."
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    决策请求 · domain:services seat 1 · os-justin · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-20T20:53Z · ⛔ 披露纪律(#9387 5323310017)适用:本评论只写位置、规则与抽象触达,不含任何探测、请求形态或取回值。

    依据:os-dev 报告 6301197652(本卡),读数基于 origin/main f32d795ce5。dev 按派发令的 fork 规则停在写代码之前,所以没有 PR。

    一句话问题: webhook 元数据里的签名密钥和可能带凭据的请求头,在元数据读取接口上原样返回;要在哪一层把它们拦下,决定了没装 webhook 插件的部署是否仍然泄露。

    背景(抽象读数)

    • 触达: 任何已登录、没有额外权限的调用方都能在元数据的列表、单项、分层视图与已发布读取上读到这两类字段;匿名调用方被拒;存储行数据门只对管理员开放。
    • 与插件无关: 是否挂载 plugin-webhooks,上述触达完全相同。声明的 webhook 经清单分解进入,保存的 webhook 经元数据保存门进入,而 webhooks 不在常开能力里。
    • 注册表一处即全覆盖: 用一个替身脱敏器实测,所有读取门都经过同一张脱敏注册表,一处注册即可覆盖全部。投递签名读取的来源不受影响。

    Governing text

    • packages/spec/src/kernel/metadata-type-redaction.ts:36-46「Why datasource is 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 in metadata-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 命令)

    1. 注册表目前只有 datasource。re-check:git grep -n "BUILTIN_METADATA_TYPE_REDACTORS" packages/spec/src/kernel/metadata-type-redaction.ts
    2. 没有任何地方注册 webhook 脱敏器。re-check:git grep -n "registerMetadataTypeRedactor('webhook'" -- packages,应为 0 处命中。

    选项 × 真实代价 × 工作量

    选项 做什么 客户可感知的后果 工作量
    A 插件注册 在 plugin-webhooks 的 init() 注册(仿 flow) 装了插件的部署被保护;没装插件、但声明或保存了 webhook 的部署继续泄露,而且看起来一切正常 domain:services 1 个 PR(S);先例 flow-credential-projection.ts
    B spec 内置条目 在 BUILTIN_METADATA_TYPE_REDACTORS 加 webhook,函数放在 spec,与 WebhookSchema 相邻(仿 datasource) 每一种组合从模块加载起即受保护 domain:spec 1 个 PR(S–M);先例 #8300 的 datasource 内置
    C 先 A 后 B A 立即上,B 随后取代 中间这段时间,没装插件的组合仍敞口,而卡看起来已修 2 个 PR;第一个的代码很快作废

    两个随路线一起定的子项(dev 的读数,席位同意):

    • 脱敏形态: 删除 secret 与整张 headers,不做掩码,返回清空形态。依据是 flow 先例「Dropped, not masked」(掩码被存回会成为真实签名密钥),以及 webhook-headers.ts 的整表规则(平台分不清哪个请求头是凭据)。
    • 写路径反向: 注册任何脱敏器都会启用元数据协议的通用写回:保存时缺省的这两个字段保留原存储值,不会因读后编辑保存而被静默删除。这是 datasource 与 flow 已有的设计配套。下一份派发令把它列为允许项。

    业务含义直译:

    • A:只给装了门禁的大楼上锁,没装门禁的楼照样敞开。
    • B:锁装在钥匙本身上。
    • C:先锁一半,再换锁。

    四轴(业务立场)

    • 实际业务需求: 仓内唯一声明 webhook 的示例应用同时装了插件,A 能覆盖它;但保存一个 webhook 根本不需要插件,B 覆盖 A 覆盖不到的那部分。
    • 项目长远合理性: B 正是注册表自己写明的归宿(A per-type metadata redaction seam belongs in @objectstack/spec/kernel — no service package can reach a registry in metadata-protocol #8300 的同一测量结论)。flow 先例选插件注册的两个理由在这里都不成立:webhook 的密钥是接收方的共享密钥,与本组合是否投递无关;spec 已把 secret 声明为 HMAC 签名密钥。
    • 防 AI 犯错: 出错时,B 让一个写了 webhooks: 却忘了 requires: ['webhooks'] 的作者或 agent 照样受保护;A 在这种情况下会静默放行。
    • 创业阶段不扩散: A 和 B 规模相当,都不新增能力面或门禁;C 要付两次。

    推荐:B(脱敏形态与写路径反向同上)

    • 终态句: 两年后,每一种带凭据的元数据类型,其读取脱敏都随类型的声明一起放在 spec,所有门共用一张注册表,与运行组合无关。参照:Kubernetes 的 Secret 在 API 层统一处理,不依赖某个控制器是否安装。
    • 置信缺口: 真实签名投递的对照没有测(还没有实现可测),diff/history 与包导出两个门只读过代码。
    • 其余选项不取的理由: A 在没装插件的组合上失效放行;C 付两次,中间还有一段敞口期。
    • 自检: 只看①选 B;②③④ 是否翻转:否。

    裁后执行(选 B 时)

    1. 席位为 domain:spec 立执行卡:spec 内置 webhook 条目(仿 data/datasource-credential-redaction.ts)、spec 单测、两种组合下的门级 pin(放在 dogfood 套件)、@objectstack/spec patch changeset,以及 Clause-②: no (narrowing) 的达档复核。卡面与 PR 同样受披露纪律约束。
    2. 本卡转 pm:blocked,Blocked-by: 指向那张 spec 卡,spec 卡落地时关闭本卡。

    相关单与 PR

    #8300(datasource 内置先例)、#24900(来源报告)、#9387(披露裁决)。

    os-decision-facets

    • ① 项目长远合理性:B 收缩特例——webhook 与 datasource 同为「凭据随声明」的内置脱敏,一张注册表、不依赖组合;A 会制造一个在部分组合上失效的插件注册。
    • ② 实际业务拉动:任何已登录调用方今天都能在元数据读取门读到已声明或已保存 webhook 的签名密钥(抽象触达,本卡已实测)。
    • ③ 防 AI 犯错:内置条目让「忘了 requires」的 AI 作者也被保护;插件注册在该情形下静默放行。
    • ④ 创业阶段不扩散:不新增能力面与门禁;一个内置条目的维护义务,换来所有组合的闭合。

    Prior rulings read: metadata-type redactor built-in vs plugin · datasource credential redaction · flow credential projection → 2 precedents (#8300 in metadata-type-redaction.ts:36-46; flow-credential-projection.ts); ADR none named; thread: none.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    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/main e4d604efd39f 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

    1. 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.
    2. 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).
    3. 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.
    4. 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 security pm: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.

  8. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    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

    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.

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespm:blockedpriority:p1High: required for production / M2security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions