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
Blocked-by: #9930 (retired 2026-08-20 — #9930 CLOSED; see Hold state below)
Step 3 of the maintainer-ruled retirement recorded on #9930 (ruling comment 5353921673, 2026-08-20, 「其他接受你的建议。」): flip the legacy cleartext read path from warn-and-accept to a loud, dated rejection (enforce-or-remove). Deliberately not implemented in the #9930 dispatch — filed here per that dispatch's instruction.
The surface (anchor by symbol, not line)
packages/plugins/plugin-webhooks/src/auto-enqueuer.ts — AutoEnqueuer.attachHeaders: after resolveWebhookHeaders finds nothing stored, it calls readLegacyHeaders(row?.definition_json) and, when a legacy map is present, warns (CLEARTEXT in definition_json … #7986) and honors it (sub.headers = legacy).
Real-world precondition (why this waits, measured 2026-08-20)
The ruling's own precondition is "once the sweep has run" — and the sweep runs at deployment boot, not at merge. Shipping the rejection before a deployment has booted the sweep turns a working delivery into a hard failure on exactly the rows the migration rescues.
Measured state of the sweep (from the #9930 step-1 census):
⚠️ The sweep is fail-closed per row: with no CryptoProvider the row is left as-is (still cleartext, still delivering). A dated rejection must decide what happens to that deployment class — a hard refusal there converts "exposed but working" into "broken", which is the blast radius the date in the rejection is for.
So the precondition in practice: every deployment class this project supports must have booted a release carrying PR #8114's sweep (with a CryptoProvider wired) before the rejection ships. Record the date chosen and why in the rejection message.
Remaining production channel to close in the same stroke
In-repo producers of definition_json.headers are zero (census on #9930: the seeder splits headers into headers_secret; docs fixed by PR #10067). The one channel left open: a raw data-API write (PATCH /api/v1/data/sys_webhook) can still store a definition_json string whose content embeds a headers (or secret) key — nothing at the write door parses the blob's content (the #8566 gate judges only the headers_secret column's shape). Today such a row is honored-with-warn until the next boot sweeps it. When the read path flips to rejection, that write becomes a delayed hard failure — so consider rejecting the legacy shape at the write door in the same change, where the author is still standing (the #8566 pattern).
Acceptance shape
Rejection tests assert the ADR-0112 envelope (code + status), not bare toThrow.
The rejection message is dated, names the sweep (migrateLegacyWebhookSecrets) and the remedy, per the loud-absence rule.
Changeset: user-visible behavioral change (legacy rows stop delivering) — breaking-changeset discipline applies, including the ADR-0087 disposition marker.
Appended 2026-08-20 by the triage seat (session session_01JTMsq2Gv3qDWMcGWKi6Sb8). Everything above is the filing seat's text, unmodified except the first line, whose card-dependency is retired: #9930 closed 2026-08-20T12:29Z, so this card no longer waits on a card — what remains is the ruling's own deployment-boot precondition plus the breaking-train window, which is a condition, hence pm:blocked → pm:on-hold. The H9 predicate reads issue.body only, so the exit lives here.
Restart-when: a v18 release-candidate tag exists on this repo (git ls-remote --tags origin shows a v18*-rc* tag) — the ADR-0087 breaking window this rejection rides. At that point re-verify the deployment-boot precondition above (has every supported deployment class booted a release at or after PR #8114, with a CryptoProvider wired?) and return the card to the queue.
Blocked-by: #9930(retired 2026-08-20 — #9930 CLOSED; see Hold state below)Step 3 of the maintainer-ruled retirement recorded on #9930 (ruling comment
5353921673, 2026-08-20, 「其他接受你的建议。」): flip the legacy cleartext read path from warn-and-accept to a loud, dated rejection (enforce-or-remove). Deliberately not implemented in the #9930 dispatch — filed here per that dispatch's instruction.The surface (anchor by symbol, not line)
packages/plugins/plugin-webhooks/src/auto-enqueuer.ts—AutoEnqueuer.attachHeaders: afterresolveWebhookHeadersfinds nothing stored, it callsreadLegacyHeaders(row?.definition_json)and, when a legacy map is present, warns (CLEARTEXT in definition_json … #7986) and honors it (sub.headers = legacy).packages/plugins/plugin-webhooks/src/webhook-headers.ts—readLegacyHeadersitself.readLegacySecretinwebhook-secret.tshas the identical warn-and-accept shape one block above (auto-enqueuer.ts,attachSecretpath, [security] The webhook signing secret is stored in cleartext insys_webhook.definition_json#7799) — rejecting one passenger while accepting the other leaves half the column alive.Real-world precondition (why this waits, measured 2026-08-20)
The ruling's own precondition is "once the sweep has run" — and the sweep runs at deployment boot, not at merge. Shipping the rejection before a deployment has booted the sweep turns a working delivery into a hard failure on exactly the rows the migration rescues.
Measured state of the sweep (from the #9930 step-1 census):
migrateLegacyWebhookSecrets(migrate-webhook-secrets.ts) treatsdefinition_json.headersas a second passenger alongsidedefinition_json.secret("a row counts as found when it carries EITHER passenger"), moves both in ONE update, and strips them from the blob in the same write. Landed in PR fix(plugin-webhooks): move webhook custom headers onto the encrypted channel (#7986) #8114 (merged 2026-08-12, implementing [security] webhook customheadersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986).webhook-outbox-plugin.ts(bootDeclaredWebhooks, after the seeder) and idempotency is demonstrated inwebhook-secret-at-rest.test.ts("legacy cleartext headers migration ([security] webhook customheadersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986)": re-run returns{ found: 0, migrated: 0, failed: 0 }and mints no secondsys_secretrow).So the precondition in practice: every deployment class this project supports must have booted a release carrying PR #8114's sweep (with a CryptoProvider wired) before the rejection ships. Record the date chosen and why in the rejection message.
Remaining production channel to close in the same stroke
In-repo producers of
definition_json.headersare zero (census on #9930: the seeder splits headers intoheaders_secret; docs fixed by PR #10067). The one channel left open: a raw data-API write (PATCH /api/v1/data/sys_webhook) can still store adefinition_jsonstring whose content embeds aheaders(orsecret) key — nothing at the write door parses the blob's content (the #8566 gate judges only theheaders_secretcolumn's shape). Today such a row is honored-with-warn until the next boot sweeps it. When the read path flips to rejection, that write becomes a delayed hard failure — so consider rejecting the legacy shape at the write door in the same change, where the author is still standing (the #8566 pattern).Acceptance shape
code+status), not baretoThrow.migrateLegacyWebhookSecrets) and the remedy, per the loud-absence rule.Refs: #9930 (ruling + census) · #7986 · PR #8114 (the sweep's headers half) · #8566 (write-door gate pattern) · #7799.
Generated by Claude Code
Hold state
Appended 2026-08-20 by the triage seat (session
session_01JTMsq2Gv3qDWMcGWKi6Sb8). Everything above is the filing seat's text, unmodified except the first line, whose card-dependency is retired: #9930 closed 2026-08-20T12:29Z, so this card no longer waits on a card — what remains is the ruling's own deployment-boot precondition plus the breaking-train window, which is a condition, hencepm:blocked→pm:on-hold. The H9 predicate readsissue.bodyonly, so the exit lives here.Restart-when: a v18 release-candidate tag exists on this repo (
git ls-remote --tags originshows av18*-rc*tag) — the ADR-0087 breaking window this rejection rides. At that point re-verify the deployment-boot precondition above (has every supported deployment class booted a release at or after PR #8114, with a CryptoProvider wired?) and return the card to the queue.