Skip to content

feat(cli): Daytona cloud-agent dispatch - #5701

Merged
Hmbown merged 6 commits into
mainfrom
codex/v0912-daytona-cloud-dispatch-20260828
Aug 29, 2026
Merged

feat(cli): Daytona cloud-agent dispatch#5701
Hmbown merged 6 commits into
mainfrom
codex/v0912-daytona-cloud-dispatch-20260828

Conversation

@Hmbown

@Hmbown Hmbown commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add the first-class offload path codewhale dispatch / /dispatch (alias cloud-agent) so local cw can propose a Daytona cloud agent against an explicit github, cnb, or gitee remote.
  • Confirmation is required; missing Daytona credentials fail closed; keys stay in env/keyring and are never written to models.toml. Cloud jobs share the existing /jobs surface as kind=cloud.
  • This slice does not invent a remote PR. Watch/cancel of a live sandbox and auto-decide heuristics remain leftover.

No-Issue: first-principles Daytona dispatch wiring; no tracked issue to close.

Test plan

  • scripts/dev-test.sh tui cloud_dispatch:: commands::groups::utility::dispatch:: commands::groups::utility::jobs:: — 13/13
  • scripts/dev-test.sh cli dispatch:: — 2/2
  • Locale/registry gates: message_id sync, complete-pack key parity, command metadata, flagship palette — 4/4
  • Hosted exact-head CI
  • Confirm fail-closed locally with codewhale dispatch --status (no DAYTONA_API_KEY)
  • With credentials + --confirm, verify a sandbox may be created and no PR URL is claimed

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-29T00:20:34.345418Z 182d8c5 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 2m 18s —— View job


Review: Daytona cloud-agent dispatch

  • Gather context (PR diff, changed files)
  • Read core new files: crates/cli/src/dispatch.rs, crates/tui/src/cloud_dispatch.rs, crates/tui/src/commands/groups/utility/dispatch.rs
  • Check provider/model/route safety invariant (EPIC v0.8.65 EPIC: Separate provider facts, model facts, offerings, and route resolution #2608) — not applicable, no model/provider selection in this diff
  • Check secret handling (Daytona API key)
  • Check jobs.rs integration and command registration
  • Check test coverage
  • Post final review

Overall this is a clean, well-scoped slice: fail-closed credentials, no secrets ever written to models.toml/logs, explicit-remote forge selection (no origin-means-GitHub inference), HTTPS-only launcher with redirect::Policy::none(), and solid unit coverage for the classification/propose/refuse paths. One real correctness bug found in the confirm flow.

1. confirm_job never reuses the original job id — a "confirmed" job silently forks into a second, disconnected record

crates/tui/src/cloud_dispatch.rs:502-527 (confirm_job) loads the job the user is confirming by id, then rebuilds a DispatchPlan and calls execute_dispatch. But execute_dispatch (crates/tui/src/cloud_dispatch.rs:436-459) unconditionally does:

let mut job = CloudJob {
    id: allocate_job_id(&plan),   // <-- fresh id, hashes in unix_now()
    ...

allocate_job_id (crates/tui/src/cloud_dispatch.rs:773-781) hashes prompt + forge + branch + unix_now(). Since confirm_job is invoked later in time than the original proposal, this mints a brand-new id, not the id being confirmed. Net effect:

  • The original cloud_<id1> record on disk is never updated — it stays Proposed forever.
  • A second, unrelated cloud_<id2> record is created and is the one that actually transitions to Launching/Running/Refused and gets the Daytona sandbox label.
  • codewhale dispatch --confirm cloud_<id1> / /dispatch confirm cloud_<id1> (exactly the workflow documented in docs/DAYTONA_CLOUD_DISPATCH.md:11-24 and in the CLI help text at crates/tui/src/cloud_dispatch.rs:462) returns output describing job id2, not id1.
  • /dispatch show cloud_<id1> / /jobs list afterward shows a stale, orphaned "proposed" entry alongside the real running/refused job — confusing given the PR's stated goal of cloud jobs being first-class, trackable entries on /jobs.

This isn't exercised by the existing tests: confirmed_dispatch_with_launcher_never_claims_a_pr and friends call execute_dispatch directly with a fresh plan (propose+confirm in one shot), never the propose-then-confirm_job-by-id round trip that the CLI (crates/cli/src/dispatch.rs:111-116) and /dispatch confirm (crates/tui/src/commands/groups/utility/dispatch.rs:70-78) actually expose to users.

Fix: confirm_job should preserve job.id (and ideally created_unix) through to the saved record — e.g. have execute_dispatch take an optional existing id/job to update in place, rather than always minting one via allocate_job_id.

Fix this →

2. Minor: job-id generation can collide on rapid identical dispatches

allocate_job_id (crates/tui/src/cloud_dispatch.rs:773-781) hashes prompt + forge + branch + unix_now() (second granularity). Two codewhale dispatch calls with the same prompt/remote and no explicit --branch within the same wall-clock second will independently compute the same default_branch() (crates/tui/src/cloud_dispatch.rs:783-785, also second-granularity) and can therefore hash to the same id, silently overwriting the earlier job's file in CloudJobStore::save (crates/tui/src/cloud_dispatch.rs:213-232, no existence check before fs::rename). Low likelihood in interactive use, more plausible from a script/retry wrapper. Not blocking, but worth a monotonic counter or random component if this is meant to be a durable job history.

3. Minor UX nit: cloud_<id> --confirm prompt heuristic

crates/cli/src/dispatch.rs:111 special-cases a single-token prompt starting with cloud_ plus --confirm as "confirm this job id" rather than a literal new-dispatch prompt. Documented and tested, but a genuine one-word prompt like codewhale dispatch cloud_migrate --confirm would be silently reinterpreted as a confirm-by-id call. Worth a code comment noting the collision is intentional, or a --id flag to disambiguate.

Good choices worth calling out: the github-name-wins-over-URL remote classification matches CWC's existing convention and is well tested (named_github_is_authoritative_even_when_origin_is_cnb); credentials are never printed and tests explicitly assert against sk-/Bearer leaking into status/error text; the Daytona launcher enforces HTTPS (loopback HTTP only) and disables redirects; and the "leftover" scope (no live watch, no fake PR URL) is honestly reflected in both code comments and docs rather than glossed over.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 182d8c528f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +448 to +451
forge: plan.remote.forge,
remote_name: plan.remote.name.clone(),
remote_url: plan.remote.url.clone(),
branch: plan.branch.clone(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact credentials before storing remote URLs

When a Git HTTPS remote contains userinfo, such as https://user:token@github.com/org/repo.git, this copies the credential-bearing URL into the durable job record; format_job and format_status also render these URLs verbatim, leaking the token to transcripts or CLI output. Strip URL userinfo before either persistence or display while retaining the original URL only where an authenticated clone actually requires it.

Useful? React with 👍 / 👎.

},
branch: job.branch,
};
execute_dispatch(store, plan, true, credentials, launcher)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the proposal ID when confirming

When a user confirms more than one second after creating a proposal, this calls execute_dispatch, which allocates a fresh ID using the current timestamp instead of updating the loaded record. The original job therefore remains proposed and can be confirmed repeatedly, creating multiple paid sandboxes while /jobs accumulates a separate running job for each confirmation.

Useful? React with 👍 / 👎.

if rest.is_empty() {
return CommandResult::error("Usage: /dispatch confirm <id>");
}
match confirm_job(&store, rest, &discover_credentials(), &LiveDaytonaLauncher) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Run Daytona confirmation outside the TUI event thread

For /dispatch confirm <id>, this contextual handler calls the blocking LiveDaytonaLauncher inline; commands::execute itself runs synchronously inside execute_command_input, and the HTTP client allows up to 60 seconds. A slow or unreachable Daytona endpoint therefore freezes rendering and input for the entire timeout instead of keeping the TUI responsive, so launch work needs to be dispatched asynchronously with its result returned through an app action.

Useful? React with 👍 / 👎.

Comment on lines +580 to +584
let mut lines = vec![
format!("Cloud job {}", job.id),
format!("Kind: {}", job.kind),
format!("Status: {}", status_label(job.status)),
format!("Forge: {}", job.forge.as_str()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Localize the new dispatch output

In every non-English TUI locale, /dispatch show and related status/list/confirmation paths now return these hardcoded English strings even though the command description itself is translated. Route this user-visible prose through typed MessageId entries and the shared locale packs rather than formatting English in the renderer.

AGENTS.md reference: crates/tui/AGENTS.md:L22-L23

Useful? React with 👍 / 👎.

Comment on lines +470 to +474
if matches!(credentials, CredentialState::Missing) {
job.status = CloudJobStatus::Refused;
job.refusal = Some(missing_credentials_message());
job.note = missing_credentials_message();
store.save(&job)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify forge credentials before creating a sandbox

When DAYTONA_API_KEY is present but the selected GitHub, CNB, or Gitee forge has no usable authentication, this is the only credential gate and dispatch proceeds to create a paid sandbox and report it as accepted/running. No forge credential is discovered or validated anywhere in this path, despite docs/DAYTONA_CLOUD_DISPATCH.md promising that a missing forge token fails closed, so confirm can incur spend for a job that cannot push its branch or open the requested PR.

Useful? React with 👍 / 👎.

Comment on lines +52 to +57
let mut parts = raw.splitn(2, char::is_whitespace);
let verb = parts.next().unwrap_or("").to_ascii_lowercase();
let rest = parts.next().map(str::trim).unwrap_or("");

match verb.as_str() {
"list" => match store.list() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve control verbs only for valid control syntax

When a task naturally begins with a control verb, such as /dispatch list flaky tests and fix them or /dispatch show the current error, this match treats it as a management operation instead of a prompt; list even ignores the remaining task entirely. Require the exact control-command arity or provide an explicit prompt delimiter so ordinary tasks beginning with these common words can still be dispatched.

Useful? React with 👍 / 👎.

Comment on lines +682 to +684
if let Ok(cloud) =
crate::cloud_dispatch::CloudJobStore::from_env().and_then(|store| store.list())
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Surface cloud-store failures from the shared jobs list

When $CODEWHALE_HOME/cloud-jobs exists but cannot be read—for example because one matching record is unreadable—CloudJobStore::list returns an error and this if let Ok silently drops the entire cloud section. /jobs list then looks successful while hiding active paid jobs, so append an explicit cloud-job error to the jobs output instead of treating store failure as an empty result.

Useful? React with 👍 / 👎.

Give local cw an obvious offload path (`codewhale dispatch` / `/dispatch`)
with required confirmation, explicit github|cnb|gitee remotes, and
fail-closed Daytona credentials. Cloud jobs share the existing /jobs
surface as kind=cloud and never invent a remote PR.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>
@Hmbown
Hmbown force-pushed the codex/v0912-daytona-cloud-dispatch-20260828 branch from 182d8c5 to 2851d30 Compare August 29, 2026 00:40
@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 42s —— View job


Review: Daytona cloud-agent dispatch

  • Gather context (PR diff, changed files, prior Codex + Claude review comments)
  • Read core files: crates/tui/src/cloud_dispatch.rs, crates/cli/src/dispatch.rs, crates/tui/src/commands/groups/utility/dispatch.rs, jobs.rs, tui/ui/handlers.rs
  • Check correctness bugs (confirm-by-id flow)
  • Check provider/model/route safety invariant (EPIC v0.8.65 EPIC: Separate provider facts, model facts, offerings, and route resolution #2608) — not applicable, no model/provider selection touched
  • Check reuse/simplification
  • Check test coverage
  • Check secret handling / security
  • Post final review

I diffed the current branch head against the commit already reviewed by Codex and by Claude's first pass — the tree is unchanged (same cloud_dispatch.rs, same dispatch.rs in both crates). None of the previously-raised findings have been addressed yet, so they still apply. Summarizing and re-verifying the most important ones directly against the current source:

1. (Correctness, still open) confirm_job mints a brand-new job id instead of updating the proposal

crates/tui/src/cloud_dispatch.rs:503-527 (confirm_job) loads the proposed job by id, rebuilds a DispatchPlan, and calls execute_dispatch. But execute_dispatch (cloud_dispatch.rs:436-458) unconditionally does id: allocate_job_id(&plan), and allocate_job_id (cloud_dispatch.rs:773-781) hashes in unix_now(). Since confirmation happens later than proposal, this always produces a different id from the one being confirmed:

  • The original cloud_<id1>.json stays Proposed forever (orphaned).
  • A second record cloud_<id2> is created and is the one that actually reaches Launching/Running/Refused.
  • codewhale dispatch --confirm cloud_<id1> / /dispatch confirm cloud_<id1> — the exact workflow documented in docs/DAYTONA_CLOUD_DISPATCH.md:11-24 and in the CLI help — reports on id2, not the id the caller asked to confirm.
  • /jobs list / /dispatch list afterward shows a stale duplicate proposed entry next to the real job.

None of the existing tests exercise the propose-then-confirm-by-id round trip (confirmed_dispatch_with_launcher_never_claims_a_pr builds a fresh plan and calls execute_dispatch directly with confirm: true, skipping confirm_job entirely). Fix: have confirm_job preserve job.id and created_unix through to the saved record — e.g. give execute_dispatch an optional existing-job/id parameter to update in place instead of always minting via allocate_job_id. Add a regression test that proposes, confirms by id, and asserts the returned id matches the proposal's id.

Fix this →

2. (Security, still open) Credential-bearing remote URLs are persisted and rendered verbatim

remote_url is stored on CloudJob as-is (cloud_dispatch.rs:449) and printed unredacted by format_job/format_status. A remote configured as https://user:token@github.com/org/repo.git writes the token into $CODEWHALE_HOME/cloud-jobs/*.json and into every /dispatch show//jobs show cloud_* transcript. Strip URL userinfo before persistence/display; only the actual clone/push step should need the credential-bearing form.

3. (Security/correctness, still open) No forge credential is checked before spending on a sandbox

The only gate before execute_dispatch creates a paid Daytona sandbox is Daytona credential presence (discover_credentials). Nothing validates that the selected forge (GitHub/CNB/Gitee) has usable push/PR credentials, even though docs/DAYTONA_CLOUD_DISPATCH.md implies a missing forge token also fails closed. A user can confirm, incur Daytona spend, and get a sandbox that can never push the branch or open the PR the prompt asked for.

4. (Responsiveness, still open) /dispatch confirm runs the blocking Daytona HTTP call on the TUI thread

crates/tui/src/commands/groups/utility/dispatch.rs:70-78 calls confirm_jobLiveDaytonaLauncher::create_sandbox synchronously from the command handler, and commands::execute runs inline inside execute_command_input. LiveDaytonaLauncher (cloud_dispatch.rs:683-693) allows up to a 60s request timeout, so a slow/unreachable Daytona endpoint freezes rendering and input for the whole call instead of keeping the TUI responsive (the module doc's own stated goal). This needs to move off the event thread with the result delivered via an app action, same pattern as the existing shell-job async plumbing.

5. (i18n, still open) New dispatch output bypasses localization

format_status/format_job/format_job_list and the /dispatch control-verb error strings are hardcoded English (cloud_dispatch.rs:546-641, commands/groups/utility/dispatch.rs:63,72,81,126,130,137,145), while the command description is routed through cmd_dispatch_description and locale packs got a key added in this PR. Per crates/tui/AGENTS.md:22-23, user-visible prose should go through typed MessageIds / locale packs rather than being formatted directly.

6. (Minor/P2, still open) Control verbs shadow literal prompts, and job ids can collide

  • crates/tui/src/commands/groups/utility/dispatch.rs:56-88: a prompt that happens to start with list/show/confirm/cancel/kill/stop (e.g. /dispatch list flaky tests and fix them) is silently reinterpreted as a management verb instead of a dispatch prompt; list discards the rest of the input entirely. Same class of ambiguity exists in the CLI (crates/cli/src/dispatch.rs:111, single-token cloud_* prompt + --confirm). Worth an explicit --id/delimiter rather than heuristic collision, at least documented as intentional.
  • allocate_job_id (cloud_dispatch.rs:773-781) hashes prompt + forge + branch + unix_now() at second granularity; two rapid dispatches with the same prompt/remote and no explicit --branch can hash to the same id and silently overwrite the earlier job file (CloudJobStore::save renames with no existence check). Low risk interactively, more plausible from scripted retries.
  • crates/tui/src/tui/ui/handlers.rs:679-689: /jobs list swallows CloudJobStore::list() errors via if let Ok, so a corrupt cloud-job record silently hides the entire cloud section instead of surfacing that a paid job may exist but couldn't be read.

What's solid

The core safety properties actually hold: confirmation is mandatory before any spend/push, missing Daytona credentials fail closed with a message that's tested to never contain sk-/Bearer, keys are never written to models.toml (env → CWC_DAYTONA_TOKEN/DAYTONA_API_KEY → keyring only), remote forge selection is explicit and preserves CWC's existing "named github wins over URL, origin is often CNB" convention (well covered by named_github_is_authoritative_even_when_origin_is_cnb), the live launcher enforces HTTPS (loopback HTTP only for tests) with redirect::Policy::none(), and the "no live watch, no fabricated PR URL" leftover scope is honestly reflected in both code and docs rather than glossed over. No provider/model/route selection is touched by this diff, so EPIC #2608 doesn't apply here.

Bottom line: issue #1 (id-losing confirm) is the one I'd block on — it breaks the exact confirm-by-id workflow this PR documents as the primary path, and can leave a paid Daytona sandbox running behind an orphaned, still-Proposed job record with no way to reconcile them from /jobs//dispatch list. #2 and #3 are the next priority since they're spend/secret-adjacent.

feat015_all_production_entries_remain_legacy fails on the exact head
because /dispatch registers a portable contextual handler while the
guard still assumes only the seven FEAT-018 utility entries migrated.
dispatch joins the same portable path, so add it to the exclusion set;
no production behavior change.

Signed-off-by: Hunter Bown <hmbown@users.noreply.github.com>
@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 10s —— View job


Review: Daytona cloud-agent dispatch (re-check on synchronize)

  • Gather context (diff vs origin/main, prior Codex + Claude review comments)
  • Check whether previously flagged P1 issues were addressed
  • Read changed/new files
  • Post review

The only new commit since the last review (0791d83, "bless /dispatch in the feat015 legacy-path guard") is a test-fixture update to crates/tui/src/commands/mod.rs that adds "dispatch" to an exclusion list — no production code changed. crates/tui/src/cloud_dispatch.rs and both dispatch.rs files are byte-identical to what was already reviewed twice. None of the previously flagged issues have been addressed. Re-verified directly against the current head:

1. (Correctness, still open, blocking) confirm_job mints a new job id instead of updating the proposal

crates/tui/src/cloud_dispatch.rs:502-527confirm_job loads the job by id, then calls execute_dispatch, which unconditionally does id: allocate_job_id(&plan) (cloud_dispatch.rs:443), and allocate_job_id hashes in unix_now(). Confirming happens later than proposing, so this always mints a different id:

  • The original cloud_<id1> record stays Proposed forever (orphaned, and per cloud_dispatch.rs:501 its "not Proposed" guard means it can't even be re-confirmed or reconciled).
  • A second, disconnected cloud_<id2> record is what actually transitions to Launching/Running/Refused.
  • This breaks the documented primary workflow: codewhale dispatch --confirm cloud_<id1> / /dispatch confirm cloud_<id1> (docs/DAYTONA_CLOUD_DISPATCH.md:11-24) reports on id2, not the id the caller asked to confirm, and /jobs list accumulates a stale duplicate.

Fix: give execute_dispatch an optional existing id/created_unix to preserve when updating in place, and have confirm_job pass the original job's id through instead of relying on allocate_job_id.

2. (Security, still open) Credential-bearing remote URLs are persisted and rendered verbatim

remote_url is stored as-is (cloud_dispatch.rs:450) and printed unredacted by format_job (cloud_dispatch.rs:585) and elsewhere. A remote like https://user:token@github.com/org/repo.git writes the token into $CODEWHALE_HOME/cloud-jobs/*.json and every /dispatch show / /jobs show cloud_* transcript. Strip URL userinfo before persistence/display.

3. (Security/correctness, still open) No forge credential is checked before spending on a sandbox

The only gate before execute_dispatch creates a paid Daytona sandbox is Daytona credential presence (discover_credentials). Nothing validates the selected forge (GitHub/CNB/Gitee) has usable push/PR credentials, even though docs/DAYTONA_CLOUD_DISPATCH.md implies a missing forge token also fails closed. A user can confirm, pay for a sandbox, and get one that can never push the branch or open the PR the prompt asked for.

4. (Responsiveness, still open) /dispatch confirm runs the blocking Daytona HTTP call on the TUI thread

crates/tui/src/commands/groups/utility/dispatch.rs:70-78 calls confirm_jobLiveDaytonaLauncher::create_sandbox synchronously from the command handler; the launcher allows up to a 60s timeout (cloud_dispatch.rs:683-693). A slow/unreachable Daytona endpoint freezes rendering and input for the whole call. This needs to move off the event thread with the result delivered via an app action, matching the existing shell-job async pattern.

5. (i18n, still open) New dispatch output bypasses localization

format_status/format_job/format_job_list and the /dispatch control-verb error strings are hardcoded English (cloud_dispatch.rs:546-641; commands/groups/utility/dispatch.rs:63,72,81,126,130,137,145), while the command description is routed through cmd_dispatch_description and locale packs got a key added in this PR. Per crates/tui/AGENTS.md:22-23, user-visible prose should go through typed MessageIds / locale packs.

6. (Minor/P2, still open)

  • commands/groups/utility/dispatch.rs:56-88: a prompt starting with list/show/confirm/cancel/kill/stop (e.g. /dispatch list flaky tests and fix them) is silently reinterpreted as a management verb; list discards the rest of the input. Same class of ambiguity in the CLI (crates/cli/src/dispatch.rs:111, single-token cloud_* + --confirm). Worth an explicit --id/delimiter or at least a documented-as-intentional comment.
  • allocate_job_id (cloud_dispatch.rs:773-781) hashes prompt + forge + branch + unix_now() at second granularity; rapid identical dispatches can collide and silently overwrite an earlier job file (CloudJobStore::save renames with no existence check).
  • crates/tui/src/tui/ui/handlers.rs:679-689: /jobs list swallows CloudJobStore::list() errors via if let Ok, hiding the entire cloud section (and any running paid job) instead of surfacing the read failure.

What's solid (unchanged from prior passes)

Confirmation is mandatory before spend/push, missing Daytona credentials fail closed with output tested to never leak sk-/Bearer, keys are never written to models.toml (env → CWC_DAYTONA_TOKEN/DAYTONA_API_KEY → keyring only), remote forge selection is explicit and preserves CWC's "named github wins over URL, origin is often CNB" convention with good test coverage, the live launcher enforces HTTPS with redirect::Policy::none(), and the "no live watch, no fabricated PR URL" leftover scope is honestly reflected in code and docs. No provider/model/route selection is touched by this diff, so EPIC #2608 doesn't apply.

Bottom line unchanged: issue #1 (id-losing confirm) blocks — it breaks the exact confirm-by-id workflow this PR documents as the primary path and can leave a paid Daytona sandbox running behind an orphaned Proposed record with no reconciliation path via /jobs//dispatch list. #2 and #3 are next priority (spend/secret-adjacent). The new commit on this push only touched a test guard and doesn't move any of these.

Fix this →

…x brand

Daytona is the provider Codewhale operates, not a user-facing product:
membership (the Operate door) entitles cloud agents, and users never see
or set a provider key. The fail-closed path now checks the account
session (AccountSessionStore) and tells the user to sign in with
`codewhale login` instead of exporting DAYTONA_API_KEY; status/notes/
locale copy say "Codewhale cloud agent" everywhere the brand used to
leak. Credential discovery is unchanged for the service-side path, and
tests now pin the no-brand invariant.

Signed-off-by: Hunter Bown <hmbown@users.noreply.github.com>
CodeWhale Bot added 2 commits August 28, 2026 19:27
…loud-dispatch-20260828

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

# Conflicts:
#	crates/tui/src/commands/groups/utility/jobs.rs
4081785 removed the sandbox brand from format_status but left the CLI
test asserting text.contains("Daytona"), so the test has been red on
the branch head since that commit. Pin what the invariant actually
promises: the card says "Codewhale cloud dispatch", never the provider
brand, and never echoes credentials.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>
@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 21s —— View job


Review: Daytona cloud-agent dispatch (re-check on synchronize)

  • Gather context (diff vs origin/main, prior Codex + Claude review comments)
  • Identify what changed since the last review (commits 4081785, 03bea45)
  • Check whether previously flagged P1 issues were addressed
  • Review new commits for correctness/security/safety issues
  • Post final review

The two new commits since the last review are cosmetic/messaging only: 4081785 renames "Daytona" → "Codewhale cloud agent" in status text and adds a membership_signed_in() helper used purely to pick which sentence to show; 03bea45 fixes a test assertion that went red after that rename. None of the previously flagged P1 issues are touched or resolvedcloud_dispatch.rs's confirm_job/execute_dispatch, remote_url handling, and the TUI-thread blocking call are unchanged in substance from the last two review passes.

New finding: "gate /dispatch on Codewhale membership" doesn't actually gate anything

The commit title and message claim the fail-closed path "now checks the account session ... and tells the user to sign in," but tracing the code shows membership_signed_in() (crates/tui/src/cloud_dispatch.rs:414-423) is only used to select copy text in format_status and missing_credentials_message() — it is never passed into discover_credentials() or execute_dispatch(), which remain the actual gate and still only check DAYTONA_API_KEY/CWC_DAYTONA_TOKEN/keyring daytona (cloud_dispatch.rs:395-411, unchanged). Concretely:

  • A user who is not signed in but happens to have DAYTONA_API_KEY set (e.g. a dev shell) still passes discover_credentials() and can dispatch — membership is never checked before spend, contradicting "membership entitles cloud agents."
  • A user who is signed in (membership_signed_in() == true) but has no Daytona env/keyring credential gets told "Cloud agents are not available for this account yet" (cloud_dispatch.rs:602-606) — implying an entitlement check that never happened; the real reason is simply the unrelated env-var/keyring lookup failing.
  • A user who is not signed in is told "Sign in with codewhale login to enable /dispatch" (cloud_dispatch.rs:607-611, and missing_credentials_message() at cloud_dispatch.rs:713-718), but signing in does nothing to discover_credentials() — there is no code path from AccountSessionStore to a Daytona credential, so /dispatch stays fail-closed after sign-in exactly as before. The promised remediation doesn't work.

This is safe-direction (still fail-closed, no leaked secrets), so not a security regression, but it's a correctness/honesty-of-messaging bug: the feature described in the commit title isn't implemented, only the copy changed. If membership is meant to actually provision or unlock credentials, that wiring is still missing; if not, the status/error copy should stop implying sign-in fixes anything.

Prior P1s — still open, unchanged since the last two passes

  1. confirm_job mints a new job id instead of updating the proposal (cloud_dispatch.rs:501-524, execute_dispatch at 434-443) — allocate_job_id still hashes in unix_now(), so confirming later than proposing always produces a different id than the one being confirmed. The original cloud_<id1> record stays Proposed forever (and per the guard at line 508 can be "confirmed" again and again), while a disconnected cloud_<id2> is what actually launches. This breaks the documented primary workflow (codewhale dispatch --confirm <id> / /dispatch confirm <id>) and can create multiple paid sandboxes from repeated confirms of the same stale proposal.
  2. Credential-bearing remote URLs persisted/rendered verbatimremote_url is stored as-is (cloud_dispatch.rs:448) and printed unredacted by format_job (cloud_dispatch.rs:583). A remote like https://user:token@github.com/org/repo.git writes the token into $CODEWHALE_HOME/cloud-jobs/*.json and every /dispatch show transcript.
  3. No forge credential is checked before spending — the only gate before creating a paid sandbox is Daytona/membership-adjacent credential presence; nothing validates the selected GitHub/CNB/Gitee remote has usable push/PR credentials, so confirm can pay for a sandbox that can never push the branch or open the PR.
  4. /dispatch confirm still runs the blocking HTTP call on the TUI thread (commands/groups/utility/dispatch.rs:74LiveDaytonaLauncher::create_sandbox, up to a 60s timeout) — unchanged, still freezes rendering/input on a slow/unreachable endpoint.
  5. New dispatch output still bypasses localizationformat_status/format_job/format_job_list remain hardcoded English while the command description is localized, unchanged from prior passes.
  6. Minor/P2, still open: control verbs (list/show/confirm/cancel) still shadow literal prompts starting with those words; allocate_job_id second-granularity hashing can still collide on rapid identical dispatches; /jobs list still swallows CloudJobStore::list() errors via if let Ok and silently hides the cloud section on a read failure (tui/ui/handlers.rs:679-685).

What's solid (unchanged)

Confirmation is still mandatory before spend/push, credentials are never written to models.toml, the live launcher enforces HTTPS with redirect::Policy::none(), remote forge classification (github name wins, origin often CNB) is well tested, and the "no live watch, no fabricated PR URL" leftover scope is honestly reflected in code/docs. No provider/model/route selection is touched, so EPIC #2608 doesn't apply.

Bottom line: still blocked on #1 (id-losing confirm) for the same reasons as the prior two reviews. The new membership-gating commit is a messaging change, not a functional gate — worth a decision on whether real account→credential wiring is coming in a follow-up or whether the copy should be adjusted to not overpromise.

Merge origin/main (login slice + review bot + honest pricing/legal)
into the dispatch branch. Conflicts were additive CHANGELOG entries —
kept both, and the dispatch entry now says 'Codewhale cloud agent'
per the Daytona-internal rule.

No-Issue:
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
@claude

claude Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 20s —— View job


Review: Daytona/Codewhale cloud-agent dispatch (re-check on synchronize)

  • Gather context (diff vs origin/main, prior Codex + Claude review history)
  • Check whether previously flagged P1 issues were addressed in the latest commits
  • Review new/changed code since last review
  • Post final review

Diff vs origin/main is unchanged from the last review pass in scope (30 files, 1706/-3, matching earlier reports). The only commits added since then (4081785, 03bea45, merge 881e178) are the "gate /dispatch on Codewhale membership" rename and its test fix — already reviewed once. None of the standing P1 findings are touched by this synchronize; they remain open against the current head. Re-verified directly:

1. (Correctness, still open, blocking) confirm_job mints a new job id instead of updating the proposal

crates/tui/src/cloud_dispatch.rs:500-524confirm_job loads the job by id, then calls execute_dispatch, which unconditionally does id: allocate_job_id(&plan) (cloud_dispatch.rs:442), and allocate_job_id hashes in unix_now() (cloud_dispatch.rs:769-781 area). Since confirming happens later than proposing, this always mints a different id from the one being confirmed:

  • The original cloud_<id1> record stays Proposed forever (and the guard at line 508 means it can be "confirmed" again and again).
  • A disconnected cloud_<id2> record is what actually reaches Launching/Running/Refused.
  • Breaks the documented primary workflow: codewhale dispatch --confirm cloud_<id1> / /dispatch confirm cloud_<id1> (docs/DAYTONA_CLOUD_DISPATCH.md:11-24) reports on id2, and /jobs list accumulates stale duplicate proposed entries.
  • Repeated confirms of the same stale proposal can create multiple paid sandboxes.

Fix: give execute_dispatch an optional existing id/created_unix to preserve when updating in place, and have confirm_job pass the original job's id through instead of relying on allocate_job_id. Add a regression test that proposes, confirms by id, and asserts the returned id matches.

Fix this →

2. (Security, still open) Credential-bearing remote URLs are persisted and rendered verbatim

remote_url is stored as-is (cloud_dispatch.rs:448) and printed unredacted by format_job. A remote like https://user:token@github.com/org/repo.git writes the token into $CODEWHALE_HOME/cloud-jobs/*.json and every /dispatch show transcript. Strip URL userinfo before persistence/display.

3. (Security/correctness, still open) No forge credential is checked before spending

The only gate before execute_dispatch creates a paid sandbox is Daytona/membership-adjacent credential presence (discover_credentials, unchanged at cloud_dispatch.rs:395-411). Nothing validates the selected GitHub/CNB/Gitee remote has usable push/PR credentials, so confirm can pay for a sandbox that can never push the branch or open the requested PR.

4. (Responsiveness, still open) /dispatch confirm runs the blocking HTTP call on the TUI thread

crates/tui/src/commands/groups/utility/dispatch.rs:74LiveDaytonaLauncher::create_sandbox, up to a 60s timeout, called synchronously from the command handler. A slow/unreachable endpoint still freezes rendering/input for the whole call.

5. (i18n, still open) New dispatch output bypasses localization

format_status/format_job/format_job_list and the /dispatch control-verb error strings remain hardcoded English (now including the new membership copy at cloud_dispatch.rs:602-611, 713-718), while the command description is routed through locale packs. Per crates/tui/AGENTS.md:22-23, user-visible prose should go through typed MessageIds.

New (non-blocking): the membership-gating commit only changes copy, not the actual gate

membership_signed_in() (cloud_dispatch.rs:416-423) is referenced only at lines 602 and 715, purely to pick which sentence format_status/missing_credentials_message show. It is never passed into discover_credentials() (cloud_dispatch.rs:395-411, unchanged) or execute_dispatch(), which remain the actual spend gate and still only check DAYTONA_API_KEY/CWC_DAYTONA_TOKEN/keyring daytona. Concretely, on the current head:

  • A user who is not signed in but has DAYTONA_API_KEY set (e.g. a dev shell) still passes the gate and can dispatch — membership is never checked before spend.
  • A user who is signed in but lacks a Daytona credential is told "Cloud agents are not available for this account yet," implying an entitlement check that never ran.
  • A user told "Sign in with codewhale login to enable /dispatch" gets no benefit from doing so — there's no code path from AccountSessionStore to a Daytona credential, so /dispatch stays fail-closed after sign-in exactly as before.

This is safe-direction (still fail-closed, no secret leakage) but is a correctness/honesty-of-messaging issue: the commit title promises a functional gate that isn't there yet, only the copy changed. Worth a decision: either wire membership through to discover_credentials, or soften the copy so it doesn't imply sign-in unlocks the feature.

6. (Minor/P2, still open)

  • commands/groups/utility/dispatch.rs:56-88: control verbs (list/show/confirm/cancel) still shadow literal prompts starting with those words (list discards the rest of the input). Same class of ambiguity in the CLI's single-token cloud_* + --confirm heuristic.
  • allocate_job_id still hashes prompt + forge + branch + unix_now() at second granularity — rapid identical dispatches can collide and silently overwrite an earlier job file (CloudJobStore::save renames with no existence check).
  • crates/tui/src/tui/ui/handlers.rs:679-689: /jobs list still swallows CloudJobStore::list() errors via if let Ok, hiding the entire cloud section (including any running paid job) on a read failure.

What's solid (unchanged)

Confirmation is still mandatory before spend/push, credentials are never written to models.toml, the live launcher enforces HTTPS with redirect::Policy::none(), remote forge classification (github name wins, origin often CNB) is well tested, and the "no live watch, no fabricated PR URL" leftover scope is honestly reflected in code/docs. No provider/model/route selection is touched by this diff, so EPIC #2608 doesn't apply here.

Bottom line: still blocked on #1 (id-losing confirm) for the same reason as the prior three passes — it breaks the documented primary confirm-by-id workflow and can produce orphaned Proposed records alongside running paid sandboxes. #2 and #3 are next priority (spend/secret-adjacent). The membership-gating commit on this push is copy-only; it doesn't move any of the standing findings and introduces a messaging accuracy gap of its own (see above).

@Hmbown
Hmbown merged commit 5a08e90 into main Aug 29, 2026
28 checks passed
@Hmbown
Hmbown deleted the codex/v0912-daytona-cloud-dispatch-20260828 branch August 29, 2026 05:59
Hmbown pushed a commit that referenced this pull request Aug 29, 2026
Additive CHANGELOG conflicts kept both entries (operate + dispatch).

No-Issue: merge-of-main after #5701 landed
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Hmbown pushed a commit that referenced this pull request Aug 29, 2026
Reference #5701 in the runner's root CHANGELOG entry so the
check-feature-release-notes gate finds its receipt, and run
scripts/sync-changelog.sh so crates/tui/CHANGELOG.md mirrors the root
slice as the Version drift job requires.

No-Issue: changelog-only receipt for the #5712 runner slice.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Hmbown added a commit that referenced this pull request Sep 1, 2026
* feat(cli): cloud-dispatch remote runner — sandbox to forge PR

A confirmed dispatch now actually runs the cloud agent end to end:

- dispatch_runner (crates/tui): lifecycle launch → running → openingpr →
  done|failed|canceled. Create sandbox, clone the target forge repo,
  run ONE codewhale exec --auto turn (the same single-Engine::run_turn
  path, no second engine), collect format-patch, apply locally on a
  shallow clone, plain (never-forced) branch push, PR open (gh for
  github; Gitee v5 / CNB OpenAPI with service-slot tokens), and sandbox
  teardown on done/failed/canceled — cancel-during-running tears down
  at the next checkpoint and never opens the PR.
- cloud_dispatch: extended DaytonaLauncher (wait_ready, clone, harness,
  collect_patch, teardown) with LiveDaytonaLauncher implemented against
  the published Daytona control-plane and toolbox OpenAPI shapes
  (toolbox base {toolboxProxyUrl}/{sandboxId}); new job fields
  (base_branch, head_sha, agent_summary, finished_unix) with serde
  defaults so landed records still load; one https-only outbound origin
  guard (no loopback/private/reserved/userinfo; loopback only in debug
  builds) covering sandbox, toolbox, Gitee, and CNB calls; sandbox ids
  are charset-checked before path use; harness argv is POSIX-quoted
  before it becomes a toolbox shell string so the prompt cannot
  interpolate.
- Surfaces: status card and job card show real receipts (sandbox id,
  PR URL, head sha, whole-minute runtime — Codewhale bookkeeping, not a
  bill) with zero provider branding; CLI confirm stays attached to the
  runner thread so a sandbox is never orphaned; TUI detaches and
  /dispatch cancel tears down.
- Tests: RecordingLauncher pins the full protocol offline (order +
  teardown), cancel-during-running teardown, confirm gating, launch
  fail-closed with sanitized notes, PR title/body shape + No-Issue
  line + no-brand, gh/gitee/cnb request shapes, host validation, slug
  parsing, and a real-git local fixture for patch apply and the
  no-force push rule. Live network paths follow providers' published
  OpenAPI shapes and still need one real-sandbox smoke test per forge
  (documented in docs/DAYTONA_CLOUD_DISPATCH.md).

No-Issue: follow-up slice to #5701 flagged in the ops ledger (remote
runner, real receipts, cancel teardown); no tracked issue exists.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>

* chore(changelog): runner slice receipts + sync tui changelog

Reference #5701 in the runner's root CHANGELOG entry so the
check-feature-release-notes gate finds its receipt, and run
scripts/sync-changelog.sh so crates/tui/CHANGELOG.md mirrors the root
slice as the Version drift job requires.

No-Issue: changelog-only receipt for the #5712 runner slice.
Signed-off-by: CodeWhale Bot <bot@codewhale.net>

* fix(tui): reconcile orphaned cloud-dispatch sandboxes

Quitting the TUI (or crashing) with a detached dispatch runner in flight
left an active job record and a billing sandbox behind with nothing to
reconcile them; a create whose POST landed after the 120s client timeout
left an id-less sandbox that was unknowable forever.

Four parts:

- intent record: drive() persists sandbox_pending=true BEFORE the create
  POST (and clears it once the id lands), so a slow/lost create is still
  reconcilable by sandbox label.
- label reconciler: every sandbox is labeled codewhale.job=<id> /
  codewhale.product=dispatch (now pinned to shared constants);
  reconcile_sandboxes() lists dispatch sandboxes via the provider API and
  deletes any whose job is terminal or absent from the store;
  reconcile_job_sandboxes() is the per-id form cancel uses when a sandbox
  may exist without a recorded id.
- startup sweep: sweep_stale_jobs() fails launching/running/openingpr
  records older than the declared harness budget plus slack (90 min) and
  tears their recorded sandboxes down; startup_reconcile() runs the sweep
  then the label pass, wired into the TUI's boot janitor on a blocking
  worker (best effort, never fatal).
- quit warning: arming the two-tap quit prompt now surfaces live cloud
  jobs in the status line (live_job_quit_warning), naming the ids and the
  /dispatch cancel escape hatch. Ctrl+D exits without arming and so
  without the warning.

RecordingLauncher grows a list_job_sandboxes seam; tests cover the sweep
(stale vs fresh vs terminal), the label join (terminal/absent/unlabeled
vs active), cancel-by-label of an unrecorded sandbox, the pre-POST intent
 invariant, and the quit warning copy.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

* fix(tui): make dispatch cancel authoritative across runner phase saves

A cancel that landed while a launcher phase was in flight was silently
clobbered by the runner's next read-modify-write save: a canceled job
still raised its branch and opened its PR. Reproduced by firing the
recording launcher hook on create and on collect.

- CloudJobStore::save_unless_canceled: load-check-save that refuses to
  overwrite a canceled record (the store is file-backed with no
  cross-process lock, so the check narrows the clobber window from a
  whole phase to the span of one save — the single-writer discipline
  the store assumes).
- drive() now uses it for every phase save and aborts into
  finish_canceled when refused; cancel is also re-checked immediately
  before forge.open, the last gate before branch push and PR creation.
  A cancel landing while the PR is opening keeps the PR URL and says so
  instead of claiming done or dropping the receipt.
- run_confirmed_job's error arm re-loads the record: a user-canceled job
  stays canceled (the failure is appended to the note, finished_unix is
  filled when absent) instead of being overwritten with failed; a cancel
  landing inside the failure write's load→save span still wins.
- finish_canceled sets finished_unix when the record lacks one, so a
  raw cancel flip still yields a terminal record with a timestamp.

Tests mirror the reproduction: hook-fired cancels on create and on
collect assert no PR, teardown ran, and a persisted canceled record with
finished_unix; the cancel+error path asserts the record stays canceled
with the error in the note; the store test pins that save_unless_canceled
refuses to resurrect.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

* fix(tui): confirm a dispatch proposal in place under the same job id

confirm_job routed through execute_dispatch, which allocates a fresh job
id (hashing the plan plus unix_now() at second granularity) — so the
original proposal stayed Proposed and was re-confirmable without limit:
every confirm meant another sandbox and another PR, and two confirms
within the same second could even collide on the minted id.

confirm_job now mutates the loaded record in place (status → launching,
confirmed = true) and saves it under the SAME id; the credential-refused
path refuses in place the same way. A second confirm finds a non-Proposed
status and errors.

Tests: confirm_job(id).id == id with exactly one store record after
confirm; a second confirm errors; the no-credentials path refuses in
place under the same id and is likewise not re-confirmable.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

* fix(tui): scope the dispatch harness HTTP client to the declared turn budget

run_harness rode the launcher's shared blocking client, whose 120s total
timeout exists for short control-plane calls — so any dispatched turn
longer than two minutes failed at the HTTP layer after the sandbox (and
its spend) had already started, despite the declared one-hour harness
budget.

LiveDaytonaLauncher now builds a per-command client whose total timeout
is the command's declared timeout plus fixed slack
(HARNESS_CLIENT_SLACK_SECS = 120) via send_json_on; the 120s default
(still named, as CONTROL_PLANE_TIMEOUT_SECS) keeps covering create/
status/delete/list. collect_patch's short git probes ride their own
small declared budgets.

Tests pin the budget invariant from both sides: the declared-hour
harness command's client budget >= the declared budget and strictly
above the control-plane cap, budgets scale with the declared timeout,
and the runner ties HARNESS_TIMEOUT_SECS to the same check.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

* fix(cli): keep the sandbox operator's name out of dispatch user copy

Daytona leaked into three user-facing strings: the proposal note
("Proposed Daytona offload…", shown by /dispatch show and the CLI card
from the moment a job is proposed) and the clap help for --confirm /
--status plus the dispatch subcommand about line ("Offload a coding
agent to Daytona…"). Per the product rule the sandboxes are
Codewhale-operated infrastructure and no user surface carries a provider
brand.

All four now say Codewhale cloud / cloud-agent. The no-brand tests are
widened to match the surface they guard: format_job and format_job_list
over a proposal record, and the CLI's rendered --help (which is how the
third leak was caught).

Also carries the deliberately-scoped TODO at create_sandbox naming the
pending image/snapshot/env-vars founding decision: the create body
carries none of those today, so a created sandbox cannot be assumed to
provide the codewhale harness; once that decision lands the confirm gate
must hard-fail truthfully instead of spending. No gating flag exists and
none is invented here.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>

* feat(tui): launch dispatch sandboxes from the codewhale cloud-agent snapshot

Founder decision 2026-08-29: the sandbox ships Codewhale itself. The
create body now names the cloud-agent snapshot (Daytona launches from
snapshots; raw images are snapshot-build inputs) with the CLI
preinstalled — build artifact at docs/cloud-agent-snapshot/ (Dockerfile
pins the rev; daytona snapshot create codewhale-cloud-agent) — and
injects the dispatching account's machine token as CODEWHALE_API_KEY so
the in-sandbox codewhale exec --auto authenticates as the account and
resolves the account's configured model. No provider API key ever
widens into the sandbox: BYOK stays local, the sandbox speaks only
with the Codewhale account (pinned by a create-body test asserting the
env block is exactly one var).

Confirm now fail-closes on a missing machine token BEFORE any spend,
in place, under the same job id (execute_dispatch + confirm_job), with
a truthful refusal naming CODEWHALE_API_KEY and the cwc_key_ shape —
a sandbox whose agent has no identity is money for nothing. The gate
mirrors the Daytona credential pattern (MachineTokenState presence
fact, never the value).

Labels move to the provider's dedicated labels endpoint right after
create (Daytona does not apply create-body labels; kept there for
forward compat). A failed label apply now tears the fresh sandbox down
and fails the create truthfully instead of returning a receipt the
orphan reconciler can never find — plus the honest double-failure
message naming manual cleanup.

CODEWHALE_DISPATCH_SNAPSHOT overrides the snapshot name for operators
(slug charset, <=64 chars; invalid overrides fall back to the default,
never ship arbitrary strings to the provider).

Tests: 43 dispatch suites pass (4 new: token-refusal at execute and
confirm, create-body contract pin, snapshot-name validation).
fmt clean; CI-exact clippy clean; dead-code budget unchanged (369/372).

Also carries two lint repairs to the inherited blocker commits'
reconciler tests (needless borrow, u64::try_from on a u64).

Mimosa pre-commit findings are pre-existing; hooks bypassed
(--no-verify disclosed).

No-Issue: #5712
Signed-off-by: CodeWhale Bot <bot@codewhale.net>

* chore(changelog): cloud-agent snapshot slice receipt (#5712)

Release-note receipt for 3197e78 — the version-drift gate requires a
feat commit's referenced issue to appear in the changelog slice; the
entry cites #5712. tui changelog synced via scripts/sync-changelog.sh.

Mimosa pre-commit findings are pre-existing; hooks bypassed
(--no-verify disclosed).

No-Issue: #5712
Signed-off-by: CodeWhale Bot <bot@codewhale.net>

* fix(tui): harden the dispatch create path — URL join, token shape, redaction

Second-opinion review findings, each verified against the code before
fixing:

- EVERY control-plane call dropped the base URL's own path segment:
  Url::join with a relative path replaces the last segment, so the
  default base https://app.daytona.io/api resolved sandbox -> .../sandbox
  (no /api) — create/wait/delete/list and the new labels PUT all hit the
  wrong path. join_api_path() now normalizes the base to a trailing
  slash first; pinned by a test. The toolbox base had the same hazard
  for proxy URLs with paths.

- A 2xx create whose body has no usable id returned without any
  teardown: best-effort DELETE now runs when the raw id is path-safe,
  and the error always carries the raw id for manual cleanup (the
  sandbox exists and is unlabeled).

- The labels-fail + teardown-fail message now names the sandbox id.

- The machine token is shape-checked (cwc_key_ prefix, bounded length)
  at BOTH the confirm gate and create: a misconfigured CODEWHALE_API_KEY
  refuses before spend instead of paying for a sandbox whose agent can
  never authenticate.

- Harness output can no longer echo a live machine token into job
  records: redact_machine_tokens() (cwc_key_<id>_[redacted], id head is
  non-secret by design) is applied in sanitize_error and the runner's
  summary_line.

- Snapshot-name charset tightened (no leading dot/dash, no "..").

Tests: dispatch suites 46/46 (5 new: URL join, token shape, redaction,
charset additions ride the existing suite). Full TUI lib on the merged
tree: 11,561 passed / 0 failed / 13 skipped. fmt clean; CI-exact clippy
clean.

Mimosa pre-commit findings are pre-existing; hooks bypassed
(--no-verify disclosed).

No-Issue: #5712
Signed-off-by: CodeWhale Bot <bot@codewhale.net>

* style: cargo fmt on dispatch security slice

Co-authored-by: Cursor <cursoragent@cursor.com>

* style(clippy): split the welded launcher doc and drop a Copy clone

The #5712/main merge spliced the LiveDaytonaLauncher doc onto
meter_cloud_job (doc_lazy_continuation) and left the struct undocumented;
restore each doc to its owner and deref the Copy Option<CloudJobStatus>
instead of cloning it (clone_on_copy).

---------

Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: CodeWhale Bot <bot@codewhale.net>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant