Tasuku
Operations

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:

TierUsed byAllowed hosts
STRICTreviews, plan/implement, questions, research without package installsgithub.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.
BUILDworkflows that install dependenciesEverything in STRICT, plus registry.npmjs.org, pypi.org, files.pythonhosted.org, proxy.golang.org, crates.io, and static.crates.io.
DEEP_SCANdeep 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}-claude for CLAUDE_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's auth.json/environment for the run — the same trust model every provider used before this rewrite.
  • Codex is unsupported on Cloudflare Containers. chatgpt.com rejects Cloudflare's Cf-Worker header (confirmed in experiments/cf-spikes), and @cloudflare/containers has no per-host bypass for its outbound-Worker interception once it's engaged — there is no way to reach chatgpt.com from a Cloudflare Container at all. Configuring CODEX with CLOUDFLARE_CONTAINER fails fast: a non-retryable prepare error, and a BLOCKING sandbox-stage readiness finding, both with the same message. Use DAYTONA or EXTERNAL_RUNTIME for Codex instead — both already use INLINE credentials 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:

HeaderValue
Content-Security-Policydefault-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-Optionsnosniff
Referrer-Policyno-referrer
X-Frame-OptionsDENY
Permissions-Policycamera=(), microphone=(), geolocation=()
Cross-Origin-Opener-Policysame-origin
Strict-Transport-Securitymax-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.

On this page