Security
Trust boundaries, per-run runtime tokens, egress tiers, credential injection, and artifact isolation.
Tasuku assumes the Cloudflare account and the selected sandbox provider are operator-controlled. Agent model output and repository content are untrusted inputs.
Webhooks and GitHub
- Verify every webhook signature before persistence.
- Deduplicate by GitHub delivery ID.
- Use the App installation for repository scope and publication identity.
- Keep the private key, client secret, and webhook secret sealed in D1 with
TASUKU_MASTER_KEY.
Per-run runtime authentication
Every call from the Worker to a tasuku-runtime instance carries a bearer token scoped to that instance (Cloudflare, Daytona) or that organization (external runtime host), plus an X-Tasuku-Protocol version header. A missing or wrong token is rejected before any run state is touched; a major protocol-version mismatch fails the request with 426 rather than running against an incompatible runtime — see Troubleshooting.
Deny-by-default egress
Every workflow now runs under an explicit egress policy — not only deep scans, as in the previous Go version. Three tiers, computed per attempt:
| Tier | Used by | Allowed hosts |
|---|---|---|
STRICT | reviews, plan/implement, questions, research without package installs | github.com, api.github.com, codeload.github.com, raw.githubusercontent.com, objects.githubusercontent.com, the selected agent's model host(s) (below), configured MCP server hosts, and the instance's own public URL. |
BUILD | workflows that install dependencies | Everything in STRICT, plus registry.npmjs.org, pypi.org, files.pythonhosted.org, proxy.golang.org, crates.io, and static.crates.io. |
DEEP_SCAN | deep scans (DEEP_SCAN, DEEP_SCAN_DEPENDENCY_AUDIT) | The same base list as STRICT; the dependency-audit phase specifically (DEEP_SCAN_DEPENDENCY_AUDIT only — not the agent scan phase) additionally allows api.osv.dev. |
Every host above is a literal hostname — there is no wildcard/subdomain matching. The only way to add hosts is the Daytona sandbox profile's network_allow_list (hostnames, unioned with the tier's list); Cloudflare Containers and external runtimes have no such field. Model hosts depend on the configured agent: Codex adds chatgpt.com, api.openai.com, and auth.openai.com; Claude adds api.anthropic.com; OpenCode adds api.anthropic.com and api.openai.com. Codex is unsupported on CLOUDFLARE_CONTAINER (see below), so its hosts are only ever relevant on DAYTONA/EXTERNAL_RUNTIME.
Enforcement is provider-specific: Outbound Worker allowedHosts on Cloudflare, network_allow_list on Daytona, and a documented firewall you configure yourself for an external runtime host (see runtime/deploy/docker-compose.external.yml's hardening flags). Tasuku does not maintain a single central destination allowlist independent of the active tier — the tier is the allowlist for that attempt.
Credential injection vs. inline credentials
- Cloudflare Containers use
credential_mode: INJECTED: the sealed run request carries placeholders (tasuku-inject-{attempt}-{repositoryId}for GitHub tokens,tasuku-inject-{attempt}-claudeforCLAUDE_CODE_OAUTH_TOKEN, and per-header MCP placeholders), and 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 token. - Daytona and an external runtime host use
credential_mode: INLINE: neither can attach to Cloudflare's Outbound Worker hooks, so the sealed credential is placed directly in the sandbox'sauth.json/environment for the run — the same trust model every provider used before this rewrite. - Codex is unsupported on Cloudflare Containers.
chatgpt.comrejects Cloudflare'sCf-Workerheader (confirmed inexperiments/cf-spikes), and@cloudflare/containershas no per-host bypass for its outbound-Worker interception once it's engaged — there is no way to reachchatgpt.comfrom a Cloudflare Container at all. ConfiguringCODEXwithCLOUDFLARE_CONTAINERfails fast: a non-retryable prepare error, and a BLOCKING sandbox-stage readiness finding, both with the same message. UseDAYTONAorEXTERNAL_RUNTIMEfor Codex instead — both already useINLINEcredentials and were never behind a Cloudflare Outbound Worker to begin with, so their egress isn't intercepted the same way.
Security headers
Every response — API JSON, the SPA shell, error envelopes, 404s, redirects, and the workflow-run event stream — carries the same baseline:
| Header | Value |
|---|---|
Content-Security-Policy | default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self'; font-src 'self' data:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none' |
X-Content-Type-Options | nosniff |
Referrer-Policy | no-referrer |
X-Frame-Options | DENY |
Permissions-Policy | camera=(), microphone=(), geolocation=() |
Cross-Origin-Opener-Policy | same-origin |
Strict-Transport-Security | max-age=63072000; includeSubDomains; preload — Worker-produced responses only send this when the request is https; web/public/_headers (below) sends it unconditionally instead. |
Worker-produced responses get these from server/src/http/middleware/security-headers.ts. wrangler.jsonc's assets.run_worker_first routes only /api/*, /artifact/*, /healthz, /readyz, and /__scheduled through the Worker — everything else, including the SPA shell and every static asset (JS, CSS, images) a browser actually loads, is served directly by the ASSETS binding and never reaches that middleware. Those responses get the same header values from web/public/_headers instead, with one deliberate difference: _headers rules can't inspect the incoming request's protocol, so its Strict-Transport-Security is unconditional rather than https-only. That's fine in production (a real Cloudflare deployment is always https), and merely inert — not wrong — on the one http path that exists, local wrangler dev (RFC 6797 §8.1 has browsers ignore HSTS received over a non-secure connection).
The Worker separately stamps every response it handles with X-Request-ID (server/src/http/middleware/request-id.ts — echoes an incoming value or mints one) for request tracing. Like the headers above, this is Worker-only: static assets served directly by ASSETS carry no X-Request-ID at all, for the same run_worker_first reason.
X-Content-Type-Options/Referrer-Policy match the previous Go server's values exactly; the rest (CSP, X-Frame-Options, Permissions-Policy, COOP, HSTS) are new in this rewrite.
Artifact isolation
Generated HTML artifacts (deep-research reports) are served from R2 through the Worker with Content-Security-Policy: sandbox allow-scripts allow-forms allow-popups allow-downloads — an opaque, unique origin per response, isolating the artifact from Tasuku's own dashboard origin even when served from the same hostname. This route intentionally keeps that narrower policy instead of the table above: the dashboard's own frame-ancestors 'none'/X-Frame-Options: DENY would break web/src/pages/artifact-viewer.tsx's same-origin <iframe> embed, so a response that sets its own Content-Security-Policy never gets the SPA's. An optional ARTIFACT_PUBLIC_BASE_URL puts artifacts on a second hostname instead. See Artifact storage.
Agent isolation
- Do not supply GitHub credentials to the agent process.
- Clone at recorded commits and keep on-demand supporting repositories read-only.
- Validate bounded artifact paths before creating Git objects.
- Apply CPU, memory, process, filesystem, and network controls appropriate to the sandbox provider — see Sandbox providers.
Secret handling
User-managed secrets and GitHub App credentials depend on TASUKU_MASTER_KEY. Plaintext is never returned by read APIs. Structured logging excludes secret plaintext and webhook bodies.
Keep the master key outside D1 backups and restore both together. For planned key changes, follow the master-key rotation runbook.
Access control
Authorization is organization-scoped and capability-based through USER, ADMIN, and SUPER_USER. Disabling a user revokes active sessions. A transactional D1 batch prevents removal of the last active super user.
Signup uses exact verified-email domain matching. Do not treat example.com as authorizing sub.example.com; list both explicitly when intended.
External systems
HTTPS MCP servers, Daytona, an external runtime host, agent providers, and GitHub are separate trust domains. Evaluate how each stores prompts, repository content, logs, and credentials.