┌────────────────────────────────────────────────────────────────────────────┐
│ CHARON │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Ingress │ │ Processor │ │ Egress │ │
│ │ │ │ │ │ │ │
│ │ - Webhooks │────►│ - Sanitizer │────►│ - Handlers │──────► Output │
│ │ - Scheduler │ │ - Composer │ │ │ │
│ │ │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Observability │ │
│ │ [Event Log] [State Store] [Metrics] [Audit Trail] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────┘
Responsible for receiving and validating incoming triggers.
- Exposes HTTP endpoints:
POST /webhook/{trigger-id} - Validates trigger exists and is enabled
- Captures raw payload and headers
- Emits
trigger.receivedevent
- Manages cron expressions for all configured triggers
- Fires at scheduled times
- Emits
trigger.scheduledevent with trigger context
The core logic pipeline for transforming triggers into task descriptors.
- Loads and executes sanitization scripts from
sanitizers/folder - Direct TypeScript imports (no sandboxing)
- Handles script errors gracefully
- Returns extracted context or
null(skip) - This is the decision point: returning
nullmeans "no action needed"
- Loads task template for trigger
- Interpolates placeholders with sanitized context
- Merges static context with dynamic payload
- Produces composed task description string
Passes Task Descriptors to the configured handler.
- Handlers are loaded dynamically from
egress/folder - Each trigger config specifies which handler to use
- Handler receives the Task Descriptor and acts on it
console- Logs the Task Descriptor (for development/debugging)cli- Executes shell commands (Claude CLI, Aider, etc.) - see EGRESS_CLI.md
1. HTTP POST /webhook/github-issues
Body: { "action": "opened", "issue": { ... } }
2. Ingress validates trigger "github-issues" exists
→ Emits: trigger.received { trigger_id, payload, timestamp }
3. Sanitizer executes sanitizers/github-issue.ts
→ Input: raw payload
→ Output: { issue_number: 123, title: "Bug...", ... }
→ Emits: trigger.sanitized { trigger_id, context }
(If sanitizer returns null)
→ Emits: trigger.skipped { trigger_id, reason: "sanitizer_null" }
→ Flow ends, no task emitted
4. Composer interpolates template
→ Template: "Fix issue #{issue_number}: {title}"
→ Result: "Fix issue #123: Bug in login form"
→ Emits: trigger.composed { trigger_id, description }
5. Egress passes TaskDescriptor to configured handler
→ Handler executes (e.g., console.log, POST, etc.)
→ Emits: trigger.completed
1. Scheduler fires at 09:00 for trigger "daily-review"
2. Ingress creates trigger event
→ Context: { trigger_time: "2025-01-05T09:00:00Z", trigger_id: "daily-review" }
→ Emits: trigger.scheduled { trigger_id, context }
3. (No sanitizer for cron triggers - always proceeds)
4. Composer interpolates template
→ Template: "Review commits since yesterday and summarize changes"
→ Merges with static context from config
→ Emits: trigger.composed { trigger_id, description }
5. Egress passes TaskDescriptor to configured handler
- In-memory cache of trigger configurations
- Hot-reloaded on config file changes
- Schema validated on load
- Persistent log of all trigger executions
- Indexed by: trigger_id, timestamp, status
- Retained for configurable period (default: 30 days)
- Sanitizers loaded from
sanitizers/folder - Egress handlers loaded from
egress/folder - Hot-reloaded on file changes
| Component | Technology | Rationale |
|---|---|---|
| Runtime | Bun | Fast, built-in SQLite, TypeScript native |
| Backend | Hono | Lightweight, fast, Bun-native HTTP framework |
| Frontend | React SPA (Vite) | Fast builds, HMR, simple distribution |
| Database | SQLite (bun:sqlite) | Simple, embedded, sufficient for single-node |
| Scheduler | node-cron | Lightweight, well-tested |
| UI | shadcn/ui + Tailwind | Modern, accessible, composable |
| Sanitizers | Direct TypeScript imports | Simple, testable, no sandboxing overhead |
# triggers.yaml
triggers:
- id: github-issues
name: GitHub Issue Handler
type: webhook
enabled: true
template: |
New issue opened in {repo}:
#{issue_number}: {title}
{body}
Determine if this requires code changes.
sanitizer: github-issue # loads from sanitizers/github-issue.ts
egress: console # loads from egress/console.ts
context:
priority: normal
- id: daily-standup
name: Daily Standup Summary
type: cron
schedule: "0 9 * * 1-5"
enabled: true
template: |
Generate a standup summary for today.
Review recent commits and open PRs.
Highlight any blockers or items needing attention.
egress: console
context:
team: platform
settings:
retention:
days: 30- Script syntax errors → log error, emit
trigger.error, skip trigger - Runtime exceptions → log error, emit
trigger.error, skip trigger
- Missing placeholder in context → log warning, use empty string
- Template syntax error → log error, emit
trigger.error, skip trigger
When in doubt, don't emit a task. False negatives (missing a task) are better than false positives (spurious tasks).
This design assumes single-node deployment. For scaling:
- Horizontal API scaling - Stateless webhook handlers behind load balancer
- Shared state - Move SQLite → PostgreSQL for shared run log
- Distributed scheduling - Use external scheduler (e.g., Temporal, Bull) for cron
- Rate limiting - Add per-trigger rate limits at ingress
These are future concerns, not initial requirements.