Skip to content

fix(plugin-webhooks, cli): a departed package's activation row switches no remaining webhook holder, and the ownership ceremony never picks a holder for a two-holder copy (ADR-0150 D8) - #33253

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/area-E2-32695
Nov 14, 2026

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Fixes #32695

Clause-②: yes (widening; narrowing on a departed row switched on) — widening: a departed package's row switched off no longer keeps a remaining holder off, so its outbound deliveries start on upgrade. Narrowing: a departed package's row switched on no longer turns on a holder shipped inactive.
Metadata-protocol: none

Area-lease pilot #33059 · area E2 · seat domain:server#5 · os-elon-musk · session_016WV4cyrFFk86Jqf7Mp5Rzw

维护者速读

  • 改了什么:(S1) webhook 的启停开关里,一条点名“已不再声明该名字的包”的激活行,不再被当作剩下持有者的开关;(S4) 18.x 归属仪式里,两个包同名声明时,未写包的定制副本不再随便挑“列表最后一个包”,改写成不点名包的行并在报告里点名两个持有者。
  • 为什么改:卸载包留下的旧开关会把另一个包的 webhook 打开或关掉(真实对外推送),属于 ADR-0150 D8 禁止的“替别人做决定”;仪式里按列表顺序挑包也是猜。
  • 风险与回滚:只改 plugin-webhooks 的一个判定函数和 CLI 仪式一步;不点名包的行、持有者自己的行读法不变。回滚即 revert。
  • 先看:packages/plugins/plugin-webhooks/src/declared-webhooks.ts 的 webhookSlotSwitch 与 reportUnownedRows;packages/cli/src/utils/organization-ownership-apply.ts 的 declared-webhooks 步骤。
  • 两分钟验证:pnpm --filter @objectstack/plugin-webhooks exec vitest run src/declared-webhooks.test.ts,预期 59 个全过(含 [#32695] 四条)。
  • 需要你做的:无,区域线程在复审通过后落地。

What changes

S1 — a departed package's activation row (plugin-webhooks). webhookSlotSwitch read, for a slot with no row of its own, every row that is no slot's own: rows naming no package AND rows naming a package that no longer declares the name. Under one holder those were its switch (if (slots.length <= 1) return unowned.every(Boolean)), so a departed package's row switched the remaining holder in both directions; under two, an off row kept both off. Now only a row naming no package (while the environment declares nothing of the name) is read that way — the D8 row a name-keyed reader wrote before. A departed package's row switches no slot, at any holder count, and DeclaredWebhookSource says so once per name at warn (new export departedWebhookRows), naming the departed package, the holders and the per-holder address ?package=PACKAGE (PR #33204).

S4 — the 18.x ownership ceremony (cli). The declared-webhooks step built new Map(list.map(d => [d.name, d.packageId])), so under two holders of a name the holder listed LAST was the fallback for a customized copy whose definition names no package. Now the step keeps every holder per name; such a copy's activation row names the name's one holder, or — under two or more — no package (the name-keyed switch it was: off keeps each holder with no row of its own off, on turns none on), and the step's notes name every holder and the per-holder address. The card offered "refuse or report"; reporting keeps the run from blocking on a copy whose switch has a non-guessing spelling, and nothing is picked.

FROM → TO:

Case FROM TO
one holder, departed row off, holder shipped on holder off holder on (declared default)
one holder shipped off, departed row on holder on holder off (declared default)
two holders, departed row off both off each at its own row / declared default
ceremony: two package holders, customized copy naming no package activation row names the last-listed holder row names no package; note names both
ceremony: the environment and a package hold the name, customized copy naming no package (either order) row names the last-listed holder no switch row written; note reports the copy's switch and names both (review S1)
row naming no package; a holder's own row; one-holder ceremony unchanged unchanged

On Clause-②: re-declared yes (widening; narrowing on a departed row switched on) per review S2, the spelling review#3 accepted on #33252: the departed-off rows start outbound deliveries on upgrade with no operator action (widening), the departed-on row stops one (narrowing). The changeset carries the operator line: to keep such a webhook off, switch the holder off at POST /api/v1/security/_activation/webhook/NAME?package=PACKAGE; the warn names that address. No export in the published index moves (departedWebhookRows is module-internal).

Review round 1 (head 399e550, review#3 FAIL) → 9eefd98: S1: when the environment is one of a name's holders, the ceremony no longer writes a package-less row for a copy naming no package (that row is the environment slot's own switch, so it picked the environment); it writes none and reports the copy's switch naming every holder. A clone of such a copy is not switched either and says so. S2: Clause-② re-declared, operator line added. N1: reportUnownedRows no longer returns after the departed-row warning, so a name with both a departed row and a package-less row under two holders is told both, each once.

Tests

  • plugin-webhooks/src/declared-webhooks.test.ts: the [D8] one holder pin no longer pins the departed row as the switch (that is the defect); new describe [#32695] a departed package's row switches no remaining holder: one holder both directions; two holders in both registration orders; said once naming departed package, holders and the address; CONTROL package-less row and own row; two holders with a departed row and a package-less row — each warning said once (N1).
  • cli/src/utils/organization-ownership-apply.generation-1.test.ts (integration project): two holders with the second listed last and first — the row names no package, one note naming both; the environment and a package, the environment listed last and first — no switch row, one note naming both, and not the name-keyed note; CONTROL a copy naming its package beside a second holder listed first; CONTROL one holder, copy naming no package.

Acceptance notes

  • setActivation (cli) reads and updates an existing activation row by (metadata_type, name) only, whatever its package_id. Pre-existing and unchanged here; on an 18.x database the ceremony runs before any per-package row exists. Noted, not filed (no repro on a real upgrade path).
  • The pristine-copy re-source still writes declared:NAME (no package) for deliveries; a two-holder redelivery of such a row is refused naming both ([Q4] pin, unchanged).

Verification

Ablation (both source files reverted to origin/main, tests kept)

plugin-webhooks declared-webhooks.test.ts: Tests 3 failed | 56 passed (59) — one-holder both directions, two holders both orders, said-once red; CONTROL green. cli generation-1: Tests 2 failed | 19 passed (21) — both two-holder orders red; CONTROLs green. Restored with git checkout HEAD -- …, git diff HEAD empty.

Ablation, review round 1 (the fix reverted to 399e550, new pins kept)

cli generation-1: Tests 2 failed | 21 passed (23) — both environment-and-package orders red. plugin-webhooks: Tests 1 failed | 59 passed (60) — the N1 pin red. Restored from the saved fixed copies, diff re-checked.

Local runs at fab0533
@objectstack/plugin-webhooks full suite :: Test Files 25/25 · Tests 300/300
@objectstack/plugin-webhooks typecheck :: exit 0
@objectstack/cli vitest --project integration organization-ownership-apply.generation-1.test.ts :: 21/21
@objectstack/cli typecheck :: exit 0

The repo gate farm is left to CI on this PR.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw

…es no remaining webhook holder; a two-holder copy's switch names no package

Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

github-actions Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/plugin-webhooks, touching 7 documentable anchor(s).

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

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

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

Which tree this was computed on

This run read content/docs from e2c976000d4c5184968fc6b189a965b59f21911e — the merge of head 9eefd98b899ad3563fe5a10c31063df1cb3ebba4 into base 199e3624426faa50f413e18657abc44ea20ff3cb, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e2c976000d4c5184968fc6b189a965b59f21911e && git checkout e2c976000d4c5184968fc6b189a965b59f21911e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 199e3624426faa50f413e18657abc44ea20ff3cb 9eefd98b899ad3563fe5a10c31063df1cb3ebba4 && git checkout -B drift-repro 199e3624426faa50f413e18657abc44ea20ff3cb && git merge --no-ff 9eefd98b899ad3563fe5a10c31063df1cb3ebba4

node scripts/docs-audit/affected-docs.mjs --json 199e3624426faa50f413e18657abc44ea20ff3cb

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head 399e550 · 2026-11-14T15:14:52Z

review#3 (area-lease pilot #33059, areas A1/A2/E2/E2b).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 399e5501075e21e7f43cf9e794aa6c88a2d5b29b
Local-runs: none

① Derived judgments

  • S1 (webhookSlotSwitch) holds. Only a package-less row while the environment declares nothing of the name is read as a name-keyed switch; a departed package's row answers no slot in either direction, at any holder count. A holder's own row still wins. departedWebhookRows excludes null and sorts. Matches ADR-0150 D8 (a row is its holder's switch, never another's).
  • should-fix S1 — ceremony: environment + package holders of one name. In runDeclaredWebhooks, a customized copy naming no package under holders.length > 1 writes package_id = null. When one of those holders is the environment (null in declaredWebhooks.list()), that row is not the name-keyed switch the note describes: webhookSlotSwitch returns it as the environment slot's OWN row (rows.get(null)), and the package holder ignores it (!slots.includes(null)). So the step silently picks the environment, and the note's sentence "switched off it keeps each holder with no row of its own off, switched on it turns none on" is false for that composition. Red line: ambiguity is reported, never guessed. Fix: for holders including null (and ≥2), either write no row and report naming every holder, or say truthfully that the row binds the environment's slot; add a pin for [null, pkg] in both list orders.
  • should-fix S2 — declaration. The off-direction rows move persisted state's effect on upgrade with no operator action: "one holder, departed row off, holder shipped on: FROM holder off TO holder on" means outbound deliveries start for a webhook an operator had switched off. That is a public behaviour move on outbound HTTP, so Clause-② is yes (mixed: the departed-off cases widen delivery, the departed-on case narrows it), not no. Re-declare in the PR body and changeset, and add one operator line to the changeset: to keep such a webhook off, switch the holder off at POST /api/v1/security/_activation/webhook/NAME?package=PACKAGE (the warn already names the address).
  • nit N1: reportUnownedRows returns after the departed-row warning, so under ≥2 slots a package-less row on the same name is never reported (once-per-name set). Fold both into one message or accept; no behaviour effect.
  • setActivation matching by (metadata_type, name) regardless of package_id: pre-existing, noted in the PR, not in scope.

② Semver level

patch for @objectstack/plugin-webhooks and @objectstack/cli is acceptable for a D8 fix of a read the ADR already rules on; no export in the published index moves (departedWebhookRows is module-internal). Metadata-protocol: none holds. Clause-② → yes per S2.

③ Boundary flags

  • Author login objectstack-fleet[bot] (relay); implemented by area E2 session_016WV4cyrFFk86Jqf7Mp5Rzw.
  • Outbound side effect (webhook delivery) on upgrade, see S2.
  • CI at this head: in progress when read (15:16Z), nothing red yet. No governed path, no packages/spec/src/**.

Implemented-by: claude/area-E2-32695
Reviewed-by: session_016WV4d1PGptBW3QRQNmy5e6

VERDICT: FAIL

S1 (code + pin) and S2 (declaration) → delta review by review#3 on the new head. N1 optional.

…when the environment is a holder; tell both unowned-row kinds

Review round 1 on the departed-row fix. In the 18.x ceremony's
declared-webhooks step, a customized copy naming no package under holders
that include the environment wrote a package-less row, which the switch
reads as the environment slot's own: it picked the environment. It now
writes no switch row and reports the copy's switch naming every holder.
reportUnownedRows tells a name both a departed row and a package-less row
under two holders. The changeset re-declares Clause-② (mixed) and carries
the operator line for a webhook switched off through a departed row.

Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head 9eefd98 · 2026-11-14T15:29:41Z

review#3 delta.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9eefd98b899ad3563fe5a10c31063df1cb3ebba4
Local-runs: none
Scope: packages/plugins/plugin-webhooks/src/declared-webhooks.ts, packages/plugins/plugin-webhooks/src/declared-webhooks.test.ts, packages/cli/src/utils/organization-ownership-apply.ts, packages/cli/src/utils/organization-ownership-apply.generation-1.test.ts + none

Delta review of 399e5501..9eefd98b by the reviewer of record 6879546273. With a narrowing arm now declared, this record is at the narrowing face over the whole non-prose diff (the webhook switch read at 399e550 is unchanged apart from N1).

① Derived judgments

  • S1 fixed: ambiguous = unnamed && holders.includes(null). When the environment is a holder, a copy naming no package writes no activation row on either path (switch-only re-source and clone), and the note reports the copy's on/off and names every holder plus the per-holder address (no package for the environment). Two or more package holders without the environment keep the name-keyed package-less row, which webhookSlotSwitch reads as described. Pins for [null, pkg] in both list orders; ablation reported red without the fix.
  • Accepted consequence, stated in the notes: in the ambiguous case a switched-off copy's state is not carried to any holder, so each holder follows its own row or declared default until the operator switches it. That is reporting, not guessing (red line), and the note gives the copy's state and the address.
  • S2 fixed: Clause-②: yes (widening; narrowing on a departed row switched on) in the body and changeset, with an operator line that names the upgrade effect (deliveries resume) and the ?package= address.
  • N1 fixed: reportUnownedRows no longer returns after the departed warning; both kinds said once per name in one call (pinned).
  • The S1 switch semantics reviewed at 399e550 hold unchanged.

② Semver level

patch for @objectstack/plugin-webhooks and @objectstack/cli; the arm is now correct and complete. Metadata-protocol: none holds. No published export moves.

③ Boundary flags

  • Outbound side effect on upgrade (webhook deliveries resume for departed-off rows), now declared with an operator action.
  • Author login objectstack-fleet[bot] (relay); area E2 session_016WV4cyrFFk86Jqf7Mp5Rzw.
  • CI at this head when read: 8 green, 18 running, none red. ACCEPT re-reads every check.

④ Design judgment

ADR-0150 D8 makes an activation row its holder's switch, never another's; reading a departed package's row as nobody's switch is the direct application, and the ceremony now refuses to pick a holder in the one composition where a package-less row would silently mean the environment. The outbound-delivery consequence is real but is the ADR's intended answer, and the release note plus warn give the operator the one action needed. Holds against the cited code and the card's scenario.

Implemented-by: claude/area-E2-32695
Reviewed-by: session_016WV4d1PGptBW3QRQNmy5e6

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyrFFk86Jqf7Mp5Rzw
Request: review · area E2 · head 9eefd98

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyrFFk86Jqf7Mp5Rzw
ACCEPT · area E2 (area-lease pilot #33059) · Fixes #32695 · head 9eefd98

  • Review: review#3 PASS at 9eefd98 (record 6879759091, after FAIL 6879546273 on 399e550: S1 code + S2 declaration fixed).
  • Governed: check-governed-merges --pr 33253 NOT governed (0 paths hit the register); +262/-27 changed lines (≤ 3000).
  • Checks: required checks still running at ACCEPT; auto-merge lands only once they pass.
  • Main: no conflict; not merged per pilot rule (merge only when dirty or main touched a file here).
  • Landing: maintainer's instruction in the area thread (15:21Z) — PRs in this area that passed review go ready + auto-merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 15:34
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit 71ade1a Nov 14, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/area-E2-32695 branch November 14, 2026 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants