feat(flowsafe): Track E — signal providers (M-007)#32
Merged
Conversation
…, webhook ingress)
The final track (M-007) of the long-running-agents program. New subpath
`@proofoftech/flowsafe/signal-providers` — additive, opt-in, subpath-only.
Pre-flight finding (DL-017): core's SignalProvider delivers in-process
(poll()/handleWebhook() -> notify() -> agent.sendNotificationSignal() on a
connected agent, matching webhooks against an in-memory registry), none of which
fits a cross-process Cloudflare host. So flowsafe drives providers through a
SignalProviderAdapter seam and routes every delivery through Track C's
createThreadTopology into /signal/notification; the DO alarm replaces
startPolling, D1 replaces the registry. Providers still extend core's
SignalProvider (isSignalProvider holds; new Agent({signals}) still merges them).
- SignalProviderHost — per-tenant provider host DO (idFromName(tenantId), the
hub-DO pattern): alarm rehydrates subscriptions from D1, polls the tenant's
providers with per-provider + per-delivery failure isolation.
- D1SubscriptionStoreFactory — the flowsafe-owned, tenant-columned
flowsafe_signal_subscriptions store, INV-2 like the approval store
(.forTenant tenant-bound + .system().listByResource cross-tenant webhook
authority). Reaped by purgeTenant (PurgeTenantResult.subscriptions, the
flowsafe_approvals leg); retention none.
- createWebhookRouter — verify-before-parse over raw bytes, payload->tenant via
the subscription ROW only, per provider+tenant rate cap, bounded forgery audit,
top-level fail-closed. createSubscriptionRouter — human-only HTTP
subscribe/unsubscribe (RA-009: never model tools; P8: no capability).
- githubSignalProvider — binding-gated reference (constant-time
X-Hub-Signature-256 via WebCrypto).
Proven on the workerd spike (spike:verify P-S: E-S2 forged rejected+audited,
E-S1 delivery->inbox, E-S3 D1-rehydration after kill+restart, cross-tenant
fail-closed) + an E-S1 integration test. Adds a subscriptions counter to
PurgeTenantResult and its showcase consumers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
anchorage-showcase | 0f4f70a | Jul 18 2026, 01:44 AM |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wave 7/7 — the FINAL track of the long-running-agents program (plan M-007, DL-017/DL-006/DL-003; RA-009 subscribe/unsubscribe human-only). Additive, opt-in.
What ships
@proofoftech/flowsafe/signal-providers(new subpath): providers driven through a flowsafeSignalProviderAdapterseam — core'sSignalProviderdelivers IN-PROCESS (poll()/handleWebhook()→this.notify()→agent.sendNotificationSignal()on a connected agent, matching an in-memory registry), which fits no Cloudflare host (the agent loop is on a different per-thread DO; the registry is lost on eviction and tenant-blind). So the DO alarm replacesstartPolling, D1 replaces the registry, and every delivery routes through Track C'screateThreadTopology.sendinto/signal/notification. A provider still IS a coreSignalProvider, sonew Agent({signals:[p]})still merges it.SignalProviderHostDO: per-tenantidFromName(tenantId)(the hub-DO pattern;id.nameis the INV-3-validated bare tenantId, fail-closed on a nameless tenant), alarm rehydrates subscriptions from D1 and polls the tenant's providers with per-provider AND per-delivery failure isolation, self-terminating#arm;/pollis the deterministic direct-alarm probe.D1SubscriptionStoreFactory— the flowsafe-OWNEDflowsafe_signal_subscriptionstable (NOTmastra_*), mirroring the approval store's INV-2 posture:.forTenant()tenant-bound + brand (aSystem/unbound store is not type-assignable in request scope),tenant_idcolumn + unconditional predicates;.system().listByResource()is the webhook's cross-tenant authority — a webhook names no tenant, the ROW does. COALESCE metadata-preserve on re-subscribe (both backends).createWebhookRouter— verify the provider signature over the RAW body BYTES BEFORE any parse (constant-time WebCrypto HMAC; empty/absent secret ⇒ route absent, never verify-true); a forged signature ⇒ 401 with no lookup/delivery/state change; payload→tenant/thread via the subscription ROW ONLY (never payload fields); per provider+tenant rate cap; a BOUNDED forgery audit (the reject is unbounded, the log is capped per provider per window).safeDecodeSegmentguards malformed%pre-parse.createSubscriptionRouter— human-only HTTP subscribe/unsubscribe (RA-009: never model tools; P8: a subscription row is addressing/config, never a grant), the house gate order (role-gated mutation, thread-prefix ownership 404,assertNoClientMemoryIds).githubSignalProvider— the binding-gated showcase reference (X-Hub-Signature-256, WebCrypto constant-time).purgeTenantgainsPurgeTenantResult.subscriptions(theflowsafe_approvalsWHERE tenant_id = ?leg, missing-table tolerant, two-tenant survival pinned); retentionnone(standing config, reaped at offboarding). The schema-guard filtersname LIKE 'mastra_%', so aflowsafe_-prefixed table never trips it andMASTRA_TABLESstays 10;subscriptionsis a standalone counter, so Track D's DL-003 compile pins hold.Verification
spike:verify(ladder A–S) — green, run independently twice.mastra_notifications, visible on the read path; R host DO rehydrates subscriptions from D1 after a kill+restart and fires poll delivery; S a valid-signed webhook for a TAMPERED row (foreign thread) delivers to NO thread (topology 404).Deploy template + residuals
deploy/stays unwired (it has no thread DO — the Track C/D/F precedent; delivery requires one, so an undeliverable stub would be incoherent). Proven on the spike (has a thread DO) + an integration test.externalResourceIdstring — a cross-tenant read iff two tenants aim real assets at ONE shared endpoint+secret; mitigation is per-tenant endpoints/secrets, a deployment choice; the verify-before-parse + per-row ownership-404 guarantees hold regardless); host DO is internal-trust (a host MUST address it by the authenticated tenant, the hub-DO precedent — bounded to availability/cost, never a data leak); min-interval-polls-all + no per-provider poll timeout (inert until a second polling provider lands).🤖 Generated with Claude Code