| 1 | # Daytona cloud-agent dispatch |
| 2 | |
| 3 | Local `cw` / Codewhale can offload a coding agent to Daytona the way Cursor |
| 4 | sends a cloud agent: the remote job raises a branch and opens a PR against an |
| 5 | explicit forge. Local stays responsive; spend and push never happen silently. |
| 6 | |
| 7 | The sandboxes are **Codewhale-operated infrastructure**, not a user-facing |
| 8 | product or provider: nothing in the CLI, TUI, job cards, or PR bodies carries |
| 9 | a provider brand, and there is no provider signup or key setup a user needs |
| 10 | to perform. Access ships with Codewhale membership (`codewhale login`) and |
| 11 | fails closed without it. |
| 12 | |
| 13 | ## One obvious offload |
| 14 | |
| 15 | ```sh |
| 16 | codewhale dispatch "open a PR that fixes the flake" --remote github |
| 17 | codewhale dispatch --confirm cloud_<id> |
| 18 | ``` |
| 19 | |
| 20 | Same action in the TUI: |
| 21 | |
| 22 | ``` |
| 23 | /dispatch open a PR that fixes the flake --remote github |
| 24 | /dispatch confirm cloud_<id> |
| 25 | ``` |
| 26 | |
| 27 | `codewhale cloud-agent` and `/cloud-agent` are aliases. `--confirm` / |
| 28 | `/dispatch confirm` is required. A proposal is written first; nothing creates |
| 29 | a sandbox or pushes a branch until that confirmation. |
| 30 | |
| 31 | Cloud jobs are first-class on the existing jobs surface (`kind=cloud`): |
| 32 | |
| 33 | ``` |
| 34 | /jobs list |
| 35 | /dispatch list |
| 36 | /dispatch show <id> |
| 37 | /dispatch cancel <id> |
| 38 | codewhale dispatch --list |
| 39 | ``` |
| 40 | |
| 41 | `/jobs list` shows shell jobs and, when cloud jobs exist, appends the cloud |
| 42 | section; `codewhale dispatch --list` (and `/dispatch list`) shows the cloud |
| 43 | jobs alone. `cloud_*` ids route to cloud show/cancel from both surfaces. |
| 44 | |
| 45 | ## What a confirmed job actually does |
| 46 | |
| 47 | The runner (`crates/tui/src/dispatch_runner.rs`) drives one lifecycle: |
| 48 | |
| 49 | ``` |
| 50 | proposed → launching → running → openingpr → done |
| 51 | │ │ |
| 52 | └── failed ───┘ (+ canceled from any active state) |
| 53 | ``` |
| 54 | |
| 55 | 1. **launching → running** — create the sandbox (labeled with the job id and |
| 56 | forge) and wait until it accepts work. The job record keeps the sandbox |
| 57 | id. |
| 58 | 2. **running** — clone the target forge repository inside the sandbox and run |
| 59 | **one** cloud agent turn through the same one-shot harness entry every |
| 60 | local non-interactive caller uses (`codewhale exec --auto "<prompt>"`). |
| 61 | There is no second engine: the sandbox runs the one `Engine::run_turn` |
| 62 | path, remotely. |
| 63 | 3. **openingpr** — collect the agent's work product (`format-patch` against |
| 64 | the clone's default branch), apply it locally on a fresh shallow clone, |
| 65 | and push the branch with a **plain** push (`--force` is never passed, so a |
| 66 | moved branch fails closed instead of rewriting history). |
| 67 | 4. **done** — open the PR on the target forge and record the URL: |
| 68 | - `github` — the `gh` CLI (`gh pr create`), reusing the repo's existing |
| 69 | gh seam and auth; |
| 70 | - `gitee` — Gitee API v5 `POST /repos/{owner}/{repo}/pulls` with a token |
| 71 | from the Codewhale service slot `gitee`; |
| 72 | - `cnb` — CNB OpenAPI `POST /{repo}/-/pulls` with a token from the |
| 73 | service slot `cnb`. |
| 74 | The PR body is truthful: what the agent did, the receipts Codewhale has |
| 75 | (job id, sandbox id, branch, base, head sha), and an explicit |
| 76 | `No-Issue: cloud dispatch cloud_<id>` line. |
| 77 | 5. **teardown** — the sandbox is deleted on completion, failure, *and* |
| 78 | cancellation; the job note says whether teardown succeeded. |
| 79 | |
| 80 | Every phase persists its transition, so `codewhale dispatch --show <id>` / |
| 81 | `/dispatch show <id>` stream real progress while the run is in flight. |
| 82 | |
| 83 | ### Where the run happens |
| 84 | |
| 85 | - The launcher selects `codewhale-cloud-agent` by default, or the validated |
| 86 | `CODEWHALE_DISPATCH_SNAPSHOT` override. Its single image definition and build |
| 87 | instructions live in |
| 88 | [`computer/snapshots/cloud-agent/`](../computer/snapshots/cloud-agent/). |
| 89 | That image pins its own Engine version; a newer source checkout does not |
| 90 | update it automatically. |
| 91 | - Current source sends the account machine token as `CODEWHALE_API_KEY` in |
| 92 | create-time environment. This is server-visible account identity, not an |
| 93 | inference-provider key. Source wiring for snapshot creation and toolbox |
| 94 | execution does not establish a working account-to-provider credential bridge |
| 95 | or an end-to-end Cloud Agent acceptance result; see the image's documented |
| 96 | limitations before operating it. |
| 97 | - The CLI stays attached: after `--confirm` it prints the launching card and |
| 98 | waits for the runner so a sandbox is never orphaned by an early exit |
| 99 | (Ctrl-C exits the wait; the job record survives, and `--cancel` tears the |
| 100 | sandbox down). |
| 101 | - The TUI detaches the runner so the session stays responsive; the job |
| 102 | record is the source of truth and `/dispatch cancel` works at any time. |
| 103 | |
| 104 | ## Remotes |
| 105 | |
| 106 | Forges are explicit: `github`, `cnb`, `gitee`. |
| 107 | |
| 108 | CWC already treats a remote *named* `github` as authoritative GitHub and |
| 109 | `origin` as the CNB mirror when that URL is `cnb.cool`. Codewhale uses the |
| 110 | same rule: |
| 111 | |
| 112 | | Remote name | URL host | Forge | |
| 113 | | --- | --- | --- | |
| 114 | | `github` | any | `github` | |
| 115 | | `cnb` | any | `cnb` | |
| 116 | | `gitee` | any | `gitee` | |
| 117 | | `origin` or other | `github.com` | `github` | |
| 118 | | `origin` or other | `cnb.cool` | `cnb` | |
| 119 | | `origin` or other | `gitee.com` | `gitee` | |
| 120 | |
| 121 | If more than one forge is present, pass `--remote` / `--remote` on `/dispatch`. |
| 122 | Do not assume `origin` is GitHub. |
| 123 | |
| 124 | ## Access (fail-closed, membership-first) |
| 125 | |
| 126 | Cloud agents ship with the Codewhale account. The gate is sign-in: |
| 127 | `codewhale login`. Until then dispatch proposes but refuses to confirm, and |
| 128 | `codewhale dispatch --status` says exactly that. **There are no provider |
| 129 | setup steps for users** — no provider signup, no dashboard, no user-held |
| 130 | provider key. |
| 131 | |
| 132 | Internally (Codewhale operators only), the sandbox credential is discovered |
| 133 | from the service-side slot exactly as the first landing defined it |
| 134 | (`DAYTONA_API_KEY` / CWC alias / the `daytona` secret slot, plus the |
| 135 | `DAYTONA_API_URL` origin override). It is never printed, never logged, never |
| 136 | written into a job record, and is not a user surface: there is no |
| 137 | `auth set-slot` command for it and no locale string mentions it. |
| 138 | |
| 139 | Forge credentials follow the same rule: GitHub auth comes from the existing |
| 140 | `gh` CLI login; Gitee and CNB tokens live in the Codewhale service slots |
| 141 | `gitee` and `cnb` and are read only at PR-open time. |
| 142 | |
| 143 | The dispatching host also needs the account machine token |
| 144 | (`CODEWHALE_API_KEY`, a `cwc_key_…` key from Account → API keys): it is |
| 145 | injected into the sandbox so the in-sandbox `codewhale` runs as the |
| 146 | account. Without it confirm refuses before any spend — a sandbox whose |
| 147 | agent has no identity is money for nothing. |
| 148 | |
| 149 | ## Confirmation and fail-closed rules |
| 150 | |
| 151 | - No `--confirm` / `/dispatch confirm`: write a `proposed` job, exit success, |
| 152 | do not create a sandbox, do not push. |
| 153 | - Confirm without membership/credentials: write a `refused` job, exit |
| 154 | failure, no sandbox. |
| 155 | - Confirm + credentials: the lifecycle above. Any phase that cannot honestly |
| 156 | complete records a `failed` job with a sanitized, truthful note — a PR URL |
| 157 | is never invented, and a missing forge token fails closed *after* the |
| 158 | branch push with an explicit "no pull request was opened" message. |
| 159 | - `codewhale dispatch` may propose; it never confirms itself. |
| 160 | |
| 161 | ## Cancellation and cost transparency |
| 162 | |
| 163 | - `--cancel <id>` / `/dispatch cancel <id>` / `/jobs cancel cloud_<id>` flip |
| 164 | the record to `canceled` and tear a live sandbox down immediately; a runner |
| 165 | in flight stops at its next checkpoint and tears down too. |
| 166 | - The status card and `--show` surface real receipts: sandbox id, PR URL when |
| 167 | opened, head sha, and a runtime figure in whole minutes. Runtime is |
| 168 | Codewhale's own bookkeeping (created → finished); it is not a provider |
| 169 | bill, and the card says so. |
| 170 | |
| 171 | ## Network safety |
| 172 | |
| 173 | Every credential-bearing outbound call (sandbox control plane, sandbox |
| 174 | toolbox, Gitee, CNB) goes through one origin guard: https only, no userinfo, |
| 175 | and no loopback / private / link-local / reserved / multicast / `.local` / |
| 176 | `.internal` targets. Explicit loopback origins are allowed only in debug |
| 177 | builds for local smoke testing. DNS-rebinding (a public name resolving to a |
| 178 | private address) is out of scope. Branch pushes are plain `git push` through |
| 179 | the machine's existing forge credentials; `--force` is never used. |
| 180 | |
| 181 | ## Live status vs recording tests |
| 182 | |
| 183 | The full lifecycle (create → wait ready → clone → harness turn → collect → |
| 184 | push → PR → teardown), cancellation teardown, PR shapes, host validation, |
| 185 | and the no-force push rule are pinned by offline tests against recording |
| 186 | launchers and local git fixtures. The live network paths (Daytona sandbox |
| 187 | create/execute/delete against a real account, `gh pr create`, and the |
| 188 | Gitee/CNB REST calls) follow the providers' published OpenAPI shapes and |
| 189 | still need one real-sandbox smoke test per forge before the receipts they |
| 190 | produce can be called verified — the PR body and job notes never claim more |
| 191 | than the receipts shown. |
| 192 | |
| 193 | ## Leftover |
| 194 | |
| 195 | - Live watch / log tail of a running sandbox. |
| 196 | - Auto-decide heuristics (Codewhale may propose; it must not confirm itself). |
| 197 | - Private-repo clones in the sandbox (needs a credential pass-through design |
| 198 | that does not widen secrets into the agent process). |
| 199 |