feat(sdk): hosted CLI — run --cloud --sync-code, sync, deploy/deployments/undeploy

Merged
KH
2mo ago
+1887-33

Summary

Two commits, one theme: the hosted side of the flows CLI, Cloud-API (Cloudflare/R2) only — the SDK gains no AWS client anywhere.

1. flows run --cloud --sync-code and flows sync (718127b5)

v1's agent-relay cloud run --sync-code uploaded the working tree so the hosted run executed inside it, and cloud sync <run-id> pulled the sandbox's diff back. Cloud already honours both for v2 (the v2 executor's cwd is the code mount; patch generation runs after either engine) — only the CLI was missing.

  • prepare → gzip'd ustar of the invoking directory → PUT /api/v1/workflows/runs/<id>/storage/<key> with the run-scoped credential → submit against the prepared run ID. prepare must answer workflowStorage.backend: "cloud-api"; anything else is refused as unsupported_storage_backend before a byte is uploaded. Sync precedes submission, so a refused backend never leaves a launched run pointing at a tree Cloud does not hold.
  • Archive = git ls-files --cached --others --exclude-standard in a checkout (never .git/node_modules); 256 MiB uncompressed cap; in-process tar writer, deterministic.
  • flows sync [--dir <path>] <run-id>: GET …/patch, git apply --check, git apply; conflicts leave the tree untouched; multi-path runs refused.
  • flows run --cloud --input … x.flow.ts: previously the parser refused --input with --cloud, but Cloud requires input for an authored flow, so no .flow.ts could be submitted from the CLI at all.

2. flows deploy <flow.ts>, flows deployments, flows undeploy (951b8777)

The agentrelay.com onboarding deploys an issues-triggered flow through one call, POST /api/v1/flows/deploy, which already admits a cli:auth token. This is its CLI:

flows deploy issue-triage.flow.ts --repo owner/name --on github:labels=agent --approver <handle> [--agents claude,codex] [--draft]
flows deployments
flows undeploy <deployment-id>

deploy means Cloud; the digest form flows deploy <flow>@sha256:… --to file://… is unchanged and the positional decides. Trigger settings are validated client-side against the launcher prefilter's per-provider vocabulary; a GitHub source without repository is scoped to --repo. Refusals from the deploy routes (flow_model_not_connected, flow_name_taken, …) are named via a narrow detail mode on cloudFetch that surfaces only { code, error }.

Credentials: every hosted verb now falls back to the agent-relay cloud login store when neither token nor FLOWS_CLOUD_TOKEN is set, taking its API URL as the default base; expired logins are refused with the re-login remedy.

Test plan

  • tests/cloud-sync.test.ts (19), tests/cloud-deploy.test.ts (33): tar packing verified with system tar; backend refusal before upload; request sequences and bearer selection; CLI argv refusals; deploy body shape incl. mode/agents/scoped sources; structured 409s; login-store fallback, precedence and expiry; undeploy.
  • cloud-run, cli, observer-link suites unchanged and green; typecheck, typecheck:tests, build.
  • Full SDK suite against a freshly built kernel: 1752 passed, 0 failed (authored-node-runtime skipped: local Bun 1.3.14 vs pinned 1.4.0).
  • Live on production: synced run 8701c899-… completed success with a gate on a file that only exists in the uploaded tree; flows sync applied the run's new file locally. Deployed a label-scoped issues listener 8b78c766-… against AgentWorkforce/flows (DEPLOYED … listening), listed it, removed it with undeploy. Details in the comments below.

🤖 Generated with Claude Code


[!NOTE]
Medium Risk
Uploads working trees and applies remote patches locally, with careful gitignore/git-failure guards, but still a broad CLI surface touching auth, deploy listeners, and filesystem mutation via git apply.

Overview
Adds the hosted development loop and listener deployment surface to the flows CLI, all via the Cloud API (no direct AWS uploads).

Code sync: flows run --cloud --sync-code prepares a run, packs the invoking directory (Git ls-files semantics or a guarded walk), uploads gzip tar through PUT …/storage/<key>, then submits against that run ID. Non-cloud-api prepare backends fail before upload. flows sync <run-id> pulls the sandbox patch and applies it with git apply --check. Cloud runs on authored .flow.ts now accept --input like local runs. Interrupts before submission report submission_aborted instead of admission_unknown.

Deploy: flows deploy <flow.ts> (distinct from digest --to file://…), plus deployments and undeploy, call POST /api/v1/flows/deploy with validated --on triggers, --repo, and required --approver. Deploy/list/undeploy need interactive cli:auth; run/sync still work with scoped tokens.

Credentials: Hosted verbs fall back to agent-relay cloud login (cloud-auth.json) when FLOWS_CLOUD_TOKEN is unset, binding token and base URL to the issuing deployment.

Other: cloudFetch gains PUT/DELETE, run-scoped upload bearer, and structured { code, error } refusals for deploy routes. Preflight probes path-like deterministic commands against process.cwd() (Cloud code mount), not the flow file directory. Docs in CLOUD.md and exports in index.ts follow the new behavior; contract tests in cloud-sync.test.ts and cloud-deploy.test.ts.

Reviewed by Cursor Bugbot for commit 966bacd5e5534f5a07d0a78a6657ee18dea0c686. Bugbot is set up for automated code reviews on this repo. Configure here.