Architecture
Tasuku's Worker, Durable Objects, Workflows, agent runtime container, and publication boundaries.
Tasuku runs as one Cloudflare Worker (tasuku): a Hono API plus the static dashboard, backed by D1, R2, KV, Queues, Durable Objects, and Workflows. Agent code never runs inside that Worker — it runs in a separate agent runtime container, reachable only through a narrow HTTP protocol.
Components
| Component | Responsibility |
|---|---|
| Worker (Hono API + dashboard) | Serves /api/v1 and the static dashboard bundle, verifies inbound GitHub/Slack/Linear webhooks, handles session auth and OAuth, and exposes configuration and query endpoints. |
| D1 | Source of truth: configuration, sealed credentials, work items, workflow runs/attempts/events, reviews, schedules, and audit evidence. |
| R2 | Blob storage: run artifacts, workspace-changes diffs, output files, and sealed run requests (runs/{run}/{attempt}/request.enc). |
KV (CACHE) | Short-lived cache, primarily sealed GitHub installation tokens. |
| Queues | tasuku-inbox, tasuku-effects, tasuku-review-batches, each with a dead-letter queue — wake-up delivery for outbox rows that live in D1. |
| Workflows | RunAttemptWorkflow (one instance per attempt) and SecretRotationWorkflow; both are durable, resumable Cloudflare Workflows. |
| Durable Objects | Scheduler, RunEvents, CodexDeviceAuth, Mutex, UsageCache, Migrator, AgentContainerStandard, AgentContainerLarge — see below. |
| Agent runtime container | Runs Codex, Claude, or OpenCode against a bounded workspace behind an HTTP protocol; the same image runs on Cloudflare Containers, Daytona, or a self-hosted external host. |
| GitHub App | Webhook identity, OAuth, installation access, repository tokens, and trusted publication identity. |
| Slack App | Mentioned requests, thread context, plan actions, result delivery, and optional operational alerts. |
| Linear App | Ticket events and comments; GitHub repositories remain the code targets. |
Durable Objects
| Durable Object | Instancing | Purpose |
|---|---|---|
Scheduler | singleton (idFromName("global")) | Owns every job-status transition: lane accounting (agent work vs. reviews), session FIFO, the review-batch gate, claim, lease recovery, and an alarm-driven tick(). Replaces the Postgres advisory-lock/SKIP LOCKED claim loop. |
RunEvents | one per run | Tails D1 workflow_events and fans them out to the dashboard over SSE; keeps a 500-event ring and replays older events from D1 on reconnect. |
CodexDeviceAuth | one per ${scope}:${org}:${repo} | Drives the Codex ChatGPT device-auth flow and token refresh through alarms, without a control-plane subprocess; status mirrored to D1 device_authorizations. |
Mutex | one per lock key (e.g. linear-token:{org}) | Generic exclusive lock for the handful of sites that needed pg_advisory_xact_lock under Postgres. |
UsageCache | one per organization | 60 s TTL, in-flight singleflight cache for provider usage lookups; also serializes Codex token refresh so concurrent isolates cannot race it. |
Migrator | singleton (idFromName("global")) | Applies pending server/drizzle/*.sql files as one D1Database#batch() per file. Called from /api/* and /readyz middleware (both share the same migrations-gate), and from the first step of every Workflow — not from scheduled(), which reaches it indirectly through that same /api/*//readyz gate whenever it makes a request. |
AgentContainerStandard / AgentContainerLarge | one per attempt (${runId}:${attempt}) | @cloudflare/containers Container subclasses backing the standard-2 (default) and standard-4 instance classes. No pooling — every attempt gets a fresh container and workspace. |
Agent runtime container and protocol
The agent runtime (tasuku-runtime) is a Bun + Hono HTTP server, PID 1 inside the container, running as uid 1000. It is baked into docker.io/amalshaji/tasuku-agent-runtime, and the same image runs unmodified on Cloudflare Containers, Daytona, or a user-hosted external runtime — the Worker talks to all three through one RuntimeClient speaking the same protocol.
| Endpoint | Purpose |
|---|---|
GET /healthz | Protocol version, agent-toolchain profile, and active/max run counts. |
POST /v1/runs | Idempotent start keyed by (run_id, attempt_id). |
GET /v1/runs/:id/events?after=N | NDJSON event tail with disk replay and a heartbeat frame every 5 s. |
GET /v1/runs/:id/result | Final state, exit code, usage, and blob metadata once the run ends. |
GET /v1/runs/:id/files/... | Workspace-changes diff, artifact HTML, and output files, streamed by the Worker into R2. |
DELETE /v1/runs/:id | Cancel (SIGTERM, then SIGKILL after a grace period). |
POST /v1/tools/codex-auth/... | Fallback Codex OAuth executed from container egress when Worker egress to chatgpt.com is blocked. |
Every request carries a bearer token scoped to the instance (Cloudflare, Daytona) or the organization (external) and an X-Tasuku-Protocol version header; a major-version mismatch fails with 426 rather than running against an incompatible runtime. See Sandbox providers for how each provider is configured.
Run lifecycle
Webhook / API / Cron
→ D1 (durable run + job row)
→ Scheduler DO: kick() → claim → RUN_ATTEMPT_WORKFLOW.create()
→ RunAttemptWorkflow: prepare → acquire-sandbox → execute → publish → finish-attempt → release-sandbox → notify-scheduler
│ NDJSON event tail
▼
AgentContainer DO / Daytona / external host — tasuku-runtime (HTTP :8787)
│
D1 workflow_events ◀── flush (1 s / 100 events) ──▶ RunEvents DO ── SSE ──▶ dashboard
└──▶ provider_effects (D1) → tasuku-effects queue → GitHub / Slack / LinearRunAttemptWorkflow runs one instance per attempt (id runId:attempt); a long execution is split into chunks so no single Workflow step needs a 30-minute streaming connection. Inbound webhook and API handlers only write to D1 and call the Scheduler — they never wait on an agent.
Trust boundaries
- Secrets are sealed in D1 with
TASUKU_MASTER_KEY(AES-256-GCM); the sealed run request is written to R2, and only its keys/ids travel through Workflow step results. - Cloudflare provider — credential injection. The sealed
RunRequestcarries placeholders (tasuku-inject-{attempt}-{repositoryId}for GitHub tokens,tasuku-inject-{attempt}-claudeforCLAUDE_CODE_OAUTH_TOKEN, and per-header MCP placeholders). The container class's Outbound Worker rewrites the real header/value only on the way out; the agent process inside the container never holds a GitHub or Claude credential. This iscredential_mode: INJECTED. - Daytona and external-runtime providers — inline credentials. Neither provider can attach to Cloudflare's Outbound Worker hooks, so those two run
credential_mode: INLINE: the sealed credential is placed directly in the sandbox'sauth.json/environment for the run, the same trust model the Go worker used for every provider. - Codex is unsupported on the Cloudflare provider.
chatgpt.com(Codex's inference endpoint) rejects Cloudflare'sCf-Workerheader (confirmed inexperiments/cf-spikes), and@cloudflare/containershas no per-host bypass for its outbound-Worker interception once engaged — so Codex cannot run onCLOUDFLARE_CONTAINERat all. Configuring that combination fails fast: a non-retryable prepare error and a BLOCKING sandbox readiness finding, both with the same message. Codex runs only onDAYTONA/EXTERNAL_RUNTIME, wherechatgpt.comis reached with ordinary direct egress (no Worker interception to begin with). - Deny-by-default egress tiers (
STRICT/BUILD/DEEP_SCAN) are enforced per provider: Outbound WorkerallowedHosts(Cloudflare),network_allow_list(Daytona), a documented firewall (external). See Security for the tier host lists and failure modes.
Data boundary
Tasuku remains a single-instance, self-hosted control plane — now backed by D1 and R2 instead of Postgres and S3. Durable operations depend on D1/R2 availability and backup; see Backup and recovery. The dashboard and API are different views of the same organization-scoped state.