Skip to content

Event graph maturation: Producers, external triggers, webhook auth#130

Merged
luokerenx4 merged 11 commits intomasterfrom
dev
Apr 19, 2026
Merged

Event graph maturation: Producers, external triggers, webhook auth#130
luokerenx4 merged 11 commits intomasterfrom
dev

Conversation

@luokerenx4
Copy link
Copy Markdown
Contributor

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

  • c202536 Unified subscribes / emits grammar (single / tuple / '*') — EVM-style declaration for listeners.
  • a155ca5 Event system maturation — AgentEvents metadata (schema + external + description), ctx.fire() convenience producer, wildcard auras, event-metrics listener.
  • 77499ce Removes the legacy trigger event + router now that webhook ingest and Pumpers subsume its role.
  • 6093c85 Introduces Producer (Pumper) as a first-class counterpart to Listener — pure event sources that declare emits and share the registry's name space (mutually exclusive with listeners). CronEngine, Telegram, and WebPlugin refactored to declare producers instead of calling eventLog.append directly.

External triggers

  • 3295dae First external event type: task.requested (external: true) + matching task-router listener running one-shot agent tasks with a dedicated task/default session. webhook-ingest producer narrowed to ['task.requested'] so the Flow view renders a concrete injection edge.
  • d98596e New Webhook sub-tab on /automation: endpoint contract, dynamic external-type docs, curl / fetch snippets with copy buttons, and a Try-it form.
  • f3ffaed Bearer-token auth on /api/events/ingest. data/config/webhook.json holds an array of tokens (rotation without downtime), constant-time comparison, Authorization: Bearer + X-OpenAlice-Token accepted. Default-deny when unconfigured. /api/events/auth-status surfaces 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:
  • b8605bd Hides 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

  • 58da81c Adds TODO.md as a running backlog at the repo root.
  • 4d88024 Seeds three open bugs with starting files for each (Chat scroll lock, Snapshot FX blow-up, Push precision artifacts).

News

  • 6625266 Replaces the four hardcoded RSS defaults with a ~28-entry curated menu across macro / US / EU / Asia markets / crypto. Per-feed enabled and description fields; UI gains a toggle per feed with opacity cue and an "N of M active" summary.

Test plan

  • npx tsc --noEmit clean on root and ui/ workspaces
  • pnpm test — 1069 pass (55 test files)
  • End-to-end live test against running dev server:
    • 503 when no tokens configured
    • 401 missing auth header
    • 403 wrong token / non-external type
    • 201 via Authorization: Bearer
    • 201 via X-OpenAlice-Token
    • Downstream chain fires: task.requestedtask-routertask.done with causedBy linked
    • Hot token rotation (edit webhook.json → effective immediately, no restart)

🤖 Generated with Claude Code

Ame and others added 11 commits April 17, 2026 22:03
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>
@luokerenx4 luokerenx4 merged commit 06d6239 into master Apr 19, 2026
2 checks passed
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