Event graph maturation: Producers, external triggers, webhook auth#130
Merged
luokerenx4 merged 11 commits intomasterfrom Apr 19, 2026
Merged
Event graph maturation: Producers, external triggers, webhook auth#130luokerenx4 merged 11 commits intomasterfrom
luokerenx4 merged 11 commits intomasterfrom
Conversation
Both ends of a Listener now share one grammar — a single type, an enumerated tuple, or the wildcard '*'. Rename `eventType` → `subscribes` to match the EVM vocabulary and make the input/output shapes symmetric. ListenerContext exposes normalized `subscribes` and `emits` arrays at handle time (the "pointer self-awareness" — what can trigger me, what I can emit). The entry is a discriminated union on `type` so handlers of multi-type listeners get proper payload narrowing via switch(). Webhook endpoint generalized from hardcoded `trigger` to accepting any event type in a new `EXTERNAL_EVENT_TYPES` allowlist. External actors can now fire specific events (`trigger` for now, future types as business needs dictate) and internal events (`cron.done`, `heartbeat.*`) remain forgery-proof. Frontend topology view consumes the new response shape — listener info uses the `subscribes[]` array, event type nodes show an external flag (dashed outline for allowlisted types). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…etrics Several coordinated extensions to the event-driven surface: 1. Metadata-driven registry — AgentEvents is now the single source of truth per event type, holding schema + external flag + description. AgentEventSchemas and isExternalEventType become derived views. Adding a new event type drops from touching 3 separate structures to a single entry with all metadata colocated. 2. In-process fire() helper — EngineContext.fire is an ergonomic typed facade over eventLog.append for plugins / hacks / extension code that doesn't want to stand up an HTTP client or plumb the raw event log. fire.trigger() shortcuts the common "poke Alice" case. Same pipeline as the webhook path, same listener fan-out. 3. Wildcard listener aura — listeners declaring subscribes: '*' or emits: '*' now get a directional glowing halo in the Flow tab instead of N individual edges (which would explode the graph). Breathing animation, color matches edge style (blue left for subscribe, green right for emit). Backend topology response preserves subscribesWildcard / emitsWildcard flags so frontend can render the declared semantics rather than the expanded set. 4. event-metrics observer listener — a real wildcard subscriber that keeps per-type in-memory counts and last-seen timestamps. Provides cheap observability of the event bus and exercises the subscribe-wildcard aura visually. Foundation for future fire-count badges on Flow nodes. 5. Event type descriptions — each event type entry can carry a human-readable description, surfaced as a hover tooltip on Flow nodes so users can understand what heartbeat.skip or trigger.done mean without reading code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With the webhook generalized to accept any external event type, the 'trigger' catch-all no longer has a unique purpose. External systems send specific event types directly; in-process code uses ctx.fire() with specific types. Removed: - trigger / trigger.done / trigger.error event types and schemas - trigger-router listener (src/task/trigger/) - fire.trigger() shortcut on EventBus - 'trigger' from NotifyOpts source enum With this, EXTERNAL_EVENT_TYPES is effectively empty — webhook ingest returns 403 until someone registers a new event type with external: true. The infrastructure is ready for specific external event types (price.alert, order.filled, etc.) when real use cases arrive; no more generic fallback middleman. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce Producer (Pumper) as a first-class counterpart to Listener: a pure event source with no subscriptions. Listener and Producer share one name space in the registry and are mutually exclusive — a name cannot be both. - New `src/core/producer.ts`: ProducerDecl / ProducerHandle / ProducerInfo - ListenerRegistry gains `declareProducer()` + `listProducers()`, returning a constrained, runtime-validated emit function. causedBy is not auto-filled (producers have no parent entry). - CronEngine, TelegramPlugin, and WebPlugin now declare producers (`cron-engine`, `telegram-connector`, `web-chat`) instead of calling `eventLog.append` directly. Lifecycle owned by each module — `谁用谁负责`. - `/api/topology` returns producers alongside listeners. Automation Flow page adds a leftmost producer column with purple inject edges; former orphan events (cron.fire, message.received/sent) now have visible sources. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce `task.requested` as the first externally-ingestable event type and
a matching `task-router` listener that drives Alice's one-shot reply loop
(parallel to cron-router).
- agent-event.ts: `task.requested` (external=true), `task.done`, `task.error`
- src/task/task-router/: factory-pattern listener with its own `task/default`
session; runs agentCenter.askWithSession → connectorCenter.notify → emit
done/error. Serial processing guard, same shape as cron-router.
- WebPlugin declares a `webhook-ingest` producer narrowed to `['task.requested']`
so the Flow graph renders a concrete injection edge rather than an aura.
`/api/events/ingest` now emits via this producer while keeping the existing
isExternalEventType gate.
- NotifyOpts.source gains `'task'` for attribution.
End-to-end: `curl -X POST /api/events/ingest -d '{"type":"task.requested","payload":{"prompt":"..."}}'`
lights up webhook-ingest → task.requested → task-router → task.done in the Flow view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new "Webhook" tab that explains how external callers can trigger Alice via POST /api/events/ingest. - Endpoint contract card (method, body shape, status codes, auth note) - Dynamic list of accepted event types, sourced from /api/topology and filtered by `external: true`. Each type renders a static docstring with payload fields, curl snippet, and fetch snippet — both with Copy buttons. - Per-type docs live in `EXTERNAL_DOCS` in AutomationWebhookSection.tsx; new external types are added with one entry there. - Try-it form that POSTs `task.requested` from the browser, showing the returned seq on success; combined with the Flow tab this demonstrates the full injection → listener → reply loop end-to-end. - api.events.ingest() helper on the UI side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lightweight place to note deferred work so it doesn't live only in conversation history. Seeded with the three concrete items from recent discussions: silent opt-out on task.requested, auth gating on /ingest, and per-caller sessions for task-router. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Chat page scroll-up is stuck (suspected state lock) - Snapshot FX conversion produces wildly wrong numbers (root cause unknown) - Trading Push approval UI shows precision artifacts in rendered values Each entry includes a starting file to cut down ramp-up time when someone else picks them up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously every event type rendered on both left and right columns even when one side had no edges — 11 dead nodes in the current topology (7 emit-only events dead on the left, 4 input-only events dead on the right). New layout: - An event's left node renders only if a concrete producer injects it or a concrete listener subscribes to it - Its right node renders only if a concrete listener emits it - Wildcards are excluded from liveness (they already render as auras) - Two-phase row ordering: both-sided events share a y index across the two event columns (the alignment property we kept); single-sided events pack their own column beneath the shared block - Column heights now drive the listener/producer centering against the tallest column rather than against the raw event count Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Default-deny auth gate on the webhook ingest endpoint. Without this, any
process that can reach the web port can fire `task.requested` and make
Alice run arbitrary prompts — unacceptable the moment the server isn't
localhost-only.
Backend:
- data/config/webhook.json + webhookSchema with `tokens: [{ id, token,
createdAt }]`; array shape so callers can rotate without downtime.
- Per-request config read (no restart needed on rotation).
- Accepts `Authorization: Bearer <token>` or `X-OpenAlice-Token` header.
- Constant-time comparison via `timingSafeEqual`.
- Responses: 503 (no tokens configured / default-deny), 401 (missing
header), 403 (invalid token), then the existing isExternalEventType /
schema gates.
- New `GET /api/events/auth-status` exposes `{ configured, tokenCount,
tokenIds }` — non-secret labels for the admin UI, no tokens leaked.
Frontend:
- Webhook tab gains an auth-status card. When unconfigured, shows a red
panel with a `openssl rand -hex 32` recipe and a webhook.json template.
- Curl and fetch snippets now include the Authorization header.
- Try-it form has a password input for the token, cached in localStorage.
Meta:
- CLAUDE.md: new "Working with TODO.md" section telling future sessions
to scan the backlog before starting non-trivial work.
- TODO.md: removes the completed `/ingest auth` entry; adds a Security
section with the broader API-surface audit, admin UI for tokens, and
per-token event scoping as follow-ups.
Tests: 13 new cases covering header extraction and token comparison
(missing / wrong length / wrong case / multi-token list / etc.).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the four hardcoded default feeds with a ~28-entry curated menu across macro, US/EU/Asia markets, and crypto — a sensible subset on by default, the rest shipped as a discovery list so users can toggle feeds on without having to hunt for URLs. - RSSFeedConfig gains optional `enabled` (default true) and `description` fields; NewsCollector.fetchAll filters disabled feeds before fetching and reports active-count in its log line. - NewsCollectorPage renders a per-feed Toggle with an opacity cue for disabled entries, shows the description + categories under each feed, and surfaces an "N of M active" summary. Add-feed form gains an optional description field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
Eleven commits that land the event-driven runtime, open it to external callers safely, expose it visually, and expand news ingestion.
Event graph — structure
c202536Unifiedsubscribes/emitsgrammar (single / tuple /'*') — EVM-style declaration for listeners.a155ca5Event system maturation — AgentEvents metadata (schema + external + description),ctx.fire()convenience producer, wildcard auras, event-metrics listener.77499ceRemoves the legacytriggerevent + router now that webhook ingest and Pumpers subsume its role.6093c85Introduces Producer (Pumper) as a first-class counterpart to Listener — pure event sources that declareemitsand share the registry's name space (mutually exclusive with listeners). CronEngine, Telegram, and WebPlugin refactored to declare producers instead of callingeventLog.appenddirectly.External triggers
3295daeFirst external event type:task.requested(external: true) + matchingtask-routerlistener running one-shot agent tasks with a dedicatedtask/defaultsession.webhook-ingestproducer narrowed to['task.requested']so the Flow view renders a concrete injection edge.d98596eNew Webhook sub-tab on/automation: endpoint contract, dynamic external-type docs, curl / fetch snippets with copy buttons, and a Try-it form.f3ffaedBearer-token auth on/api/events/ingest.data/config/webhook.jsonholds an array of tokens (rotation without downtime), constant-time comparison,Authorization: Bearer+X-OpenAlice-Tokenaccepted. Default-deny when unconfigured./api/events/auth-statussurfaces config state to the UI without leaking secrets. Try-it form caches a token in localStorage.Flow visualization
dd7eb27(already on master) opened the Producer column story; this PR adds:b8605bdHides dead-side event nodes. An event's left node renders only if a concrete producer injects it or a concrete listener subscribes to it; its right node only if a concrete listener emits it. Two-phase row layout keeps both-sided events aligned at the top while single-sided events pack their own columns beneath.Process / backlog
58da81cAddsTODO.mdas a running backlog at the repo root.4d88024Seeds three open bugs with starting files for each (Chat scroll lock, Snapshot FX blow-up, Push precision artifacts).News
6625266Replaces the four hardcoded RSS defaults with a ~28-entry curated menu across macro / US / EU / Asia markets / crypto. Per-feedenabledanddescriptionfields; UI gains a toggle per feed with opacity cue and an "N of M active" summary.Test plan
npx tsc --noEmitclean on root andui/workspacespnpm test— 1069 pass (55 test files)Authorization: BearerX-OpenAlice-Tokentask.requested→task-router→task.donewithcausedBylinkedwebhook.json→ effective immediately, no restart)🤖 Generated with Claude Code