Skip to content

feat(plugin-webhooks): 18.x step 1 (8b-0g′): declared webhooks are read from the registry, the seeder retires, expand-first #24529

Open

Description

@objectstack-fleet

Filing gate ③: a task the maintainer directed. These are steps 1–3 of the 18.x line, under ruling records 6266750393 and 6267391172 on #15194. The second records the maintainer's 「用量很多,不用节约」: steps 1–3 start now, in parallel.

Part of #15212 (the C8 card), from decision #24435 Q2 A (ruling 6259141885) · Step 1: 8b-0g′, declared webhooks read from the registry · ⛔ Not an 18.0 gate.

The line's rule (binding)

Once merged, this PR leaves main releasable:

Expand first. This step only adds: the new write shape, plus reads that accept both the 18.0 shape and the new one. It never moves or deletes an 18.0 row and never drops a column. Each such removal is written down for the step-5 ceremony.

Merge gate. The PR stays a draft until #24522's harness has merged. It lands after it has merged main and is green on R1–R4.

Scope

Acceptance

  • The new shape is pinned on a fresh install. The dual read is pinned on an 18.0-shaped row.
  • The harness stays green.
  • The report lists every removal the step-5 ceremony owes, table by table.
  • Measure first. If expand-first is impossible or unsafe for any part, stop and report that part with the alternative. Do not build a destructive change.

Activity

  1. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 10 · 2026-10-19T12:40Z
    Session: session_01Y5583bs2RDd53MH6kM1ca3
    Account: os-steve (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-24529-8b0g-webhooks-registry. If the dev splits this step into several independently releasable PRs, each PR's branch is claude/issue-24529-8b0g-webhooks-registry-<part> and carries Part of #24529.
    Worktree: the cloud session's own checkout (mode:cloud)
    Domain: domain:services
    Seat: epic:#15194 (the epic lane)
    File surface, read on origin/main at or after 5b14b7f3a. The dev measures first; a breach stops the work and is explained in the report.

  2. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24529,
      "status": "done",
      "branch": "claude/issue-24529-8b0g-webhooks-registry",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24588",
      "session": "session_01PDSxBZE561TFe1N2iEjKXZ",
      "premise_still_valid": true,
      "summary": "Claim verified: the newest Claim on #24529 is 6267446928 and it names this branch. The boot seeder bootstrapDeclaredWebhooks is deleted, and a fresh boot writes zero sys_webhook rows for a declared webhook. The dispatcher reads declarations from the registry: DeclaredWebhookSource in plugin-webhooks, bound as the declaredWebhooks service. The dispatcher projects each one into its subscription cache (ref_id declared:NAME), and its secret and headers are held in memory and never stored. A declared webhook is switched under regime C, through sys_metadata_activation rows of type webhook, by the runtime catalog activation door POST /security/_activation/webhook/:name, the sharing_rule door with webhook admitted. The ledger overrides isActive in both directions, and a ledger write refreshes the dispatcher in the same request. Dual read on an 18.0 DB: the seeder's rows stay. A pristine mirror (managed_by package, not customized) is ignored in favour of its declaration, so there is no double fire. A customized seeded row, or an admin row under the name, answers as itself and the declaration is withheld, which keeps an admin's active:false. Measured on a fresh main showcase: the 18.0 seeded row carries the Default Organization and delivers under single. So under a posture with no wall, a declared webhook is deployment-wide: it takes every organization's events and stamps the event's organization on each delivery. Under a wall the org-less refusal stands. The redelivery veto judges declared:NAME deliveries, and pristine-mirror replays, by the declaration.",
      "tests": "At head 6b3042a8. plugin-webhooks vitest: 19 files, 212 passed. typecheck exit 0 (check:test-typecheck OK). runtime full suite: 422 files, 6652 passed, 19 skipped; typecheck exit 0; catalog-activation-door.test.ts +7 webhook pins. Dogfood webhook-materialization.dogfood.test.ts: 1/1 (no sys_webhook row; dispatch through the bound declaredWebhooks service, object to object_name). check:liveness exit 0. The 18.0-shaped fixture: seeded18_0Row (fake engine) and seed18_0Rows (real ObjectQL, encrypted columns) reproduce the 18.0 seeder's mapWebhookToRow plus provenance; the dual read is pinned by 7 cases against them. Ablations through scripts/ablation-replace.mjs, each restored with blob == HEAD and git diff HEAD empty (tests import src relatively, so no dist leg): mirror skip 3 red; withheld rule 3; ledger override 6; ledger self-heal 1; deploymentWide 2; plugin posture read 1; guard declared branch 2; guard mirror branch 1; door declaredWebhooks lookup 5. Fresh showcase boot at 6b3042a8 (single): sys_webhook 0 rows; door enabled:true returns 200 and one ledger row (package com.example.showcase); undeclared name 404; a task create gives a sys_http_delivery row with ref_id declared:showcase_task_changed and Default Organization; enabled:false and a second create add no delivery. The same steps on fresh main: seeded row org-stamped, delivery ref_id whk_ in the same org.",
      "gates": "Local, head 6b3042a8: dispatch-gates --commands --repo objectstack-ai/objectstack gave 130 families, plus the 50-family artifact-roster block (STDERR): 178 commands, exit codes written to a file before any pipe. 173 exit 0. NOT MEASURED, 5: check-closing-target-claim, check-single-claim-paths and check-governed-queue-guard (exit 2/2/1: no PR or event context, CI runs them); check-partof-closing-keyword (no context in the sweep; rerun with PR_BODY = the PR body: exit 0, as is check-closing-keyword-parity); check-plugin-teardown-shape --self-test (exit 3: its pinned fixture needs an unshallowed clone; checker health only). --ran: 130 derived, 130 run, 0 NOT-MEASURED. Real findings fixed during the sweep: tenant-audit census 231 to 229 (regenerated with the tool, prose figures corrected); objectql-double-limit and where-matcher on the renamed test double (made drivable and conforming, baseline entry dropped); spec liveness state-counts regenerated with check:generated --fix. CI check-runs on #24588: in_progress at report time.",
      "mcp_calls": "3 MCP reads, 0 writes: issue_read get on #24529, issue_read get_comments on #24529, pull_request_read get on #24588. Ruling records read with gh api GET (reads only).",
      "api_writes": "2 relay strokes as objectstack-fleet[bot] via with-fleet.sh --via dispatch: (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /issues/24588/assignees [os-steve]; (2) this comment, POST /repos/objectstack-ai/objectstack/issues/24529/comments. Plus git pushes to the branch (not REST).",
      "ceremony_owes": [
        "sys_webhook: delete the pristine declared mirrors (managed_by package or platform, customized not true, name still declared).",
        "sys_webhook: settle each customized seeded row the way the sharing-rule ceremony settles its customised copies: a copy under a new name, plus the declared webhook switched off in sys_metadata_activation.",
        "sys_webhook inventory: in packages/cli/src/utils/organization-ownership-inventory.ts, turn the row's declared category into a mirror-deletion departure (ADR-0131 D10 fate 2). Not edited here, because the apply executes departures and so it is ceremony behaviour.",
        "sys_http_delivery: re-source the deleted mirrors' tails, ref_id MIRROR_ROW_ID to declared:NAME, so history and the redelivery veto follow the declaration.",
        "sys_secret: delete the cipher rows the deleted mirrors' signing_secret and headers_secret referenced (orphans after deletion).",
        "sys_webhook columns: customized, and the package/platform values of managed_by, describe nothing once no mirror exists. Retire them in the ceremony or at step 7. Their help text still mentions boot seeding, left unedited so the i18n bundles are not churned.",
        "Code owed at step 7: classifyDeclaredNameHolder, the mirror skip in AutoEnqueuer.doRefresh, the redeliver guard's pristine-mirror branch, bindWebhookProvenanceStamp, and migrateLegacyWebhookSecrets for package rows."
      ],
      "open_questions": [
        {
          "question": "How is a sys_webhook row holding a declared name read on an 18.0 DB?",
          "options": [
            "A recognise a pristine seeded row and ignore it in favour of the registry entry; let any edited or admin row answer and withhold the declaration",
            "B always ignore seeded rows",
            "C always defer the declaration to any same-name row"
          ],
          "recommendation": "A, decided and built: it reproduces exactly what the 18.0 seeder did on each boot (it overwrote pristine rows and never touched edited or admin ones). So there is no double fire, an admin's active:false survives, and a changed declaration still propagates."
        },
        {
          "question": "Organization dimension of a declared webhook (measured: the 18.0 seeded row carries the Default Organization and delivers under single)",
          "options": [
            "A deployment-wide when the posture builds no wall (deliveries stamped with the event's organization); org-less refusal under a wall",
            "B org-less everywhere (silently delivers nothing under single, a regression from 18.0)"
          ],
          "recommendation": "A, decided and built. It keeps R2 behaviour and follows the sharing plugin's declared-rule scope reading of the posture."
        },
        {
          "question": "Refuse a new sys_webhook row under a declared name with 409, as sharing_rule's guard does?",
          "options": [
            "A not in this expand step (the row answers and the declaration is withheld, as under 18.0)",
            "B add it now"
          ],
          "recommendation": "A, decided: expand-first adds no new refusal. Take it up at step 7, or as its own card, when the mirrors are gone."
        },
        {
          "question": "Files edited outside the claim's surface list",
          "options": [
            "A accept them as necessary consequences",
            "B split them out"
          ],
          "recommendation": "A. Deleting the seeder forced these edits: spec liveness rows and comments; the dogfood webhook proof and fixture (not #24522's harness); the platform checklist item; content/docs webhooks guide; tenant-audit census regenerated; the showcase docblock; four scripts/ ledgers re-keyed or shrunk. No ADR, no releases page, no #24522 harness file."
        }
      ],
      "out_of_scope_findings": [
        "class: none filed. label and description on WebhookSchema are now unread: the seeded Setup row was their only reader. The liveness ledger marks them dead, owed enforce-or-remove (ADR-0049): a Setup surface that lists declared webhooks, or retirement. carrier: none; noted in the PR's Acceptance notes. Dedupe words: webhook label dead, declared webhook display, enforce-or-remove webhook",
        "class: none filed. AutoEnqueuer.admitsOrganization's docblock says a single-posture deployment stamps nothing on events; measured on the showcase, single-posture record events carry the Default Organization (since 8b). The comment is stale; its behaviour is unchanged here. carrier: none; noted only."
      ]
    }

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions