Skip to content

UC5: accept inbound PP-removal notice and converge#127

Merged
AquiGorka merged 1 commit into
mainfrom
uc5-council-removed-notify
Jun 23, 2026
Merged

UC5: accept inbound PP-removal notice and converge#127
AquiGorka merged 1 commit into
mainfrom
uc5-council-removed-notify

Conversation

@AquiGorka

Copy link
Copy Markdown
Contributor

What

Part of the UC5 PP removal & continuity slice. This is the provider-platform piece: the inbound counterpart to the council-platform removal notify.

The council router was an empty stub. This wires it into a real inbound endpoint — POST /api/v1/council/removed — the live signal a council sends when it removes this provider.

How

  • The notice is low-trust and is not believed on its own. For each PP holding an ACTIVE membership in the named council, we re-query the council's authoritative /public/provider/membership-status endpoint and demote to REJECTED only when the council confirms NOT_FOUND (404). ACTIVE (200) / PENDING (202) / unreachable → left untouched. A forged/stale notice or a transient council error can never knock a still-valid membership offline.
  • The PP's own on-chain event-watcher remains the can't-miss path (it already sets REJECTED on the provider_removed event and replays a removal seen during downtime from its ledger cursor on boot). This endpoint just reacts immediately instead of waiting for the next poll.
  • Convergence logic lives in env-free core/service/council-notify/handle-removal.ts so it is unit-testable with an injectable council query.

Tests

  • Unit (handle-removal.test.ts): NOT_FOUND → demote; still-ACTIVE → ignore forged/stale notice; UNKNOWN/unreachable → untouched; different council → skip; already-non-ACTIVE → skip (idempotent).
  • Integration (tests/integration/http/council-removed.test.ts): drives the real HTTP route through PGlite — 404 → membership REJECTED (202, deactivated:1); 200 → untouched (deactivated:0); missing councilId → 400.

Full suite run locally on pinned deno 2.8.2: fmt --check, lint, CI pay-test job (116), test:unit (133), test:integration (103) — all green.

Version: 0.9.3 → 0.9.4.

Coordination

Pairs with the council-platform outbound notify PR and the provider-stack (Rust standin) removal-honouring PR. Contract unchanged — remove_provider already emits ProviderRemoved.

Wire the empty council router stub into a real inbound endpoint:
POST /api/v1/council/removed, the live signal a council sends when it
removes this provider.

The notice is low-trust: it is not believed on its own. For each PP with
an ACTIVE membership in the named council we re-query the council's
authoritative /public/provider/membership-status endpoint and demote to
REJECTED only when the council confirms NOT_FOUND. A forged or stale
notice, or a transient council error, can never knock a still-valid
membership offline. The PP's own on-chain event-watcher stays the
can't-miss path; this just reacts immediately instead of on next poll.

- New env-free service core/service/council-notify/handle-removal.ts
  (unit-testable convergence logic with injectable council query).
- Integration test drives the HTTP route through PGlite: 404 -> REJECTED,
  200 -> untouched, missing councilId -> 400.

Bumps 0.9.3 -> 0.9.4.
@AquiGorka AquiGorka merged commit 7db45a4 into main Jun 23, 2026
7 checks passed
@AquiGorka AquiGorka deleted the uc5-council-removed-notify branch June 23, 2026 15:45
AquiGorka added a commit that referenced this pull request Jun 24, 2026
## What
Removes the inbound `POST /api/v1/council/removed` notice endpoint added
in #127 (it inverted the pull model and added an unauthenticated inbound
surface), and moves removal-honouring fully onto the pull path.

## How
**Removed:**
- `src/core/service/council-notify/` (handle-removal.ts + its test)
- `tests/integration/http/council-removed.test.ts`
- the `POST /council/removed` route; `buildCouncilRouter` is an empty
router again.

**Pull-path verification (with the endpoint gone):**
- **Watcher demotes on `provider_removed`** → REJECTED via
`deactivateMembership` — already true, unchanged.
- **Bundle submission refuses a removed membership** — already true:
`resolveChannelContext` throws "no active membership" via
`getActiveForPp` (`channel-resolver.ts:108`), unchanged.
- **Boot converge-by-query for MEMBERSHIP was MISSING** — only
channel-status converged on boot; a removal during downtime relied on
retention-bound event replay. **Added**
`convergeMembershipStatusesOnBoot`: on boot, query each council's
authoritative `/public/provider/membership-status` and demote on 404 via
the same `deactivateMembership` path. Env-free + unit-tested. This is
the sibling of the existing channel-status boot convergence — pull
direction (provider→council), no inbound surface.

## Tests
Full suite locally on pinned **deno 2.8.2**: `fmt --check`, `lint`,
`check src/main.ts`, CI pay-test job (116), `test:unit` (132, incl. 4
new boot-converge tests), `test:integration` (99) — all green.

**local-dev lifecycle e2e** (deploy → pay → on-chain `remove_provider`)
against this branch: **passed (exit 0)**. On provider (re)boot the
membership is demoted ACTIVE→REJECTED **via the boot converge-by-query**
(proven by the live DB transition; the on-chain watcher start even
failed on the restart, so the converge backstop alone demoted it —
exactly its purpose), with **no notify endpoint in the path**;
post-demotion there are 0 active memberships so the bundle gate refuses
new submissions.

Version: 0.9.4 → 0.9.5 (standalone bump commit).

## Coordination
Lands with council-platform (removes the outbound push) and
provider-stack (removes the matching inbound endpoint).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant