Skip to content

feat(flowsafe): Track E — signal providers (M-007)#32

Merged
gcharang merged 1 commit into
devfrom
feat/track-e-signal-providers
Jul 18, 2026
Merged

feat(flowsafe): Track E — signal providers (M-007)#32
gcharang merged 1 commit into
devfrom
feat/track-e-signal-providers

Conversation

@gcharang

Copy link
Copy Markdown
Contributor

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 flowsafe SignalProviderAdapter seam — core's SignalProvider delivers 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 replaces startPolling, D1 replaces the registry, and every delivery routes through Track C's createThreadTopology.send into /signal/notification. A provider still IS a core SignalProvider, so new Agent({signals:[p]}) still merges it.
  • SignalProviderHost DO: per-tenant idFromName(tenantId) (the hub-DO pattern; id.name is 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; /poll is the deterministic direct-alarm probe.
  • D1SubscriptionStoreFactory — the flowsafe-OWNED flowsafe_signal_subscriptions table (NOT mastra_*), mirroring the approval store's INV-2 posture: .forTenant() tenant-bound + brand (a System/unbound store is not type-assignable in request scope), tenant_id column + 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). safeDecodeSegment guards 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).
  • DL-003 for an owned table: purgeTenant gains PurgeTenantResult.subscriptions (the flowsafe_approvals WHERE tenant_id = ? leg, missing-table tolerant, two-tenant survival pinned); retention none (standing config, reaped at offboarding). The schema-guard filters name LIKE 'mastra_%', so a flowsafe_-prefixed table never trips it and MASTRA_TABLES stays 10; subscriptions is a standalone counter, so Track D's DL-003 compile pins hold.

Verification

  • Gate: lint + typecheck + 1778 tests (93 files) + build + spike:verify (ladder A–S) — green, run independently twice.
  • Spikes on real workerd+D1: P forged webhook rejected BEFORE parse + audited, no lookup/delivery/state; Q subscribe → signed webhook → notification in 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).
  • Reviews: implementer-side architect ("ship it") + QA (fixed a CRITICAL pre-auth decode DoS + HIGH no-catch/empty-secret + MEDIUMs, with regression tests); orchestrator-side independent verification: SOUND — webhook verify-before-parse + constant-time compare + row-only tenant mapping + the flowsafe-owned table not tripping the schema-guard all confirmed from source, tests non-vacuous, spikes load-bearing.

Deploy template + residuals

  • The minimal 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.
  • Documented residuals: subscribe-time resource authz (any allowed-role tenant can subscribe to any externalResourceId string — 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

…, 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>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 18, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
anchorage-showcase 0f4f70a Jul 18 2026, 01:44 AM

@gcharang
gcharang merged commit 03b881f into dev Jul 18, 2026
3 checks passed
@gcharang
gcharang deleted the feat/track-e-signal-providers branch July 18, 2026 01:46
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