Add cloud package and CLI integration#642
Conversation
Introduce a new packages/cloud workspace (package.json, tsconfig.json) implementing cloud API client, auth, workflows, types and index files. Wire cloud support into the CLI by updating src/cli/commands/cloud.ts and src/cli/commands/connect.ts. Update root package.json and package-lock.json to include the new package and dependency changes.
| headers: { | ||
| "content-type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ refreshToken: auth.refreshToken }), |
Check warning
Code scanning / CodeQL
File data in outbound network request Medium
| headers: { | ||
| "content-type": "application/json", | ||
| authorization: `Bearer ${accessToken}`, | ||
| ...(init.headers ?? {}), | ||
| }, |
Check warning
Code scanning / CodeQL
File data in outbound network request Medium
| await fetch(revokeUrl, { | ||
| method: 'POST', | ||
| headers: { 'content-type': 'application/json' }, | ||
| body: JSON.stringify({ token: auth.refreshToken }), |
Check warning
Code scanning / CodeQL
File data in outbound network request Medium
Replace and update cloud-related expectations across CLI tests. bootstrap.test.ts: update expected cloud subcommands (replace link/unlink/agents/send/brokers with login/logout/whoami/connect/run/logs) and adjust the total leaf command count. src/cli/commands/cloud.test.ts: refactor tests to remove filesystem/API integration scaffolding and many scenario tests; simplify createHarness to return only program and deps, use program.exitOverride, adjust exit mock, and add lighter unit tests that assert command names, descriptions, and key options (e.g. --follow, --poll-interval, --dry-run). Overall this moves tests to focus on command signatures and options rather than end-to-end behavior.
Add @agent-relay/cloud@3.2.15 to package.json and include it in bundleDependencies so the cloud package is bundled with the app. Update cloud CLI error handling to build a clearer error message (start.error || start.message || `${createResponse.status} ${createResponse.statusText}`) and throw that instead of awaiting getErrorDetails(createResponse), simplifying and making failures more informative when session creation fails.
There was a problem hiding this comment.
Pull request overview
This PR introduces a new @agent-relay/cloud package and refactors the CLI’s cloud command group to use it, adding workflow execution/log streaming/sync capabilities and moving provider “connect” into agent-relay cloud connect.
Changes:
- Deprecate
agent-relay connectin favor ofagent-relay cloud connect. - Replace legacy cloud “link/sync/agents/send/brokers” CLI logic with browser-based auth (
login/logout/whoami) plus workflowrun/status/logs/sync. - Add new
packages/cloudSDK implementing auth + workflow run/log/sync helpers and wire it into the CLI build/deps.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/cli/commands/connect.ts | Deprecates the top-level connect command and points users to cloud connect. |
| src/cli/commands/cloud.ts | Major rewrite of cloud CLI commands to use @agent-relay/cloud and add workflow run/logs/sync. |
| src/cli/commands/cloud.test.ts | Updates tests to reflect new command set (now mostly registration checks). |
| src/cli/bootstrap.test.ts | Updates expected leaf commands for new cloud subcommands. |
| packages/cloud/tsconfig.json | Adds TS build config for the new cloud package. |
| packages/cloud/src/workflows.ts | Implements workflow submission, optional code upload, log polling, and patch download helpers. |
| packages/cloud/src/types.ts | Defines shared types/constants (auth file path, supported providers, workflow response shapes). |
| packages/cloud/src/index.ts | Public exports for the cloud SDK. |
| packages/cloud/src/auth.ts | Implements browser-based login + stored token refresh + authorized fetch. |
| packages/cloud/src/api-client.ts | Adds an API client wrapper with refresh behavior and URL building utilities. |
| packages/cloud/package.json | Declares new @agent-relay/cloud package and its dependencies. |
| package.json | Adds @agent-relay/cloud dependency and bundles it in release artifacts. |
| package-lock.json | Locks new dependencies (AWS SDK S3, tar, ignore, etc.) and workspace link for @agent-relay/cloud. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/cli/commands/cloud.ts
Outdated
| .option('--json', 'Print raw JSON response', false) | ||
| .action(async (runId: string, options: { apiUrl?: string; json?: boolean }) => { | ||
| const result = await getRunStatus(runId, options); | ||
| deps.log(JSON.stringify(result, null, 2)); |
There was a problem hiding this comment.
The cloud status command defines a --json option but currently prints JSON unconditionally. Either respect options.json (and provide a human-readable default output), or drop the flag to avoid misleading UX.
| deps.log(JSON.stringify(result, null, 2)); | |
| if (options.json) { | |
| deps.log(JSON.stringify(result, null, 2)); | |
| return; | |
| } | |
| // Default human-readable output | |
| if (result && typeof result === 'object') { | |
| for (const [key, value] of Object.entries(result as Record<string, unknown>)) { | |
| if (value && typeof value === 'object') { | |
| deps.log(`${key}: ${JSON.stringify(value)}`); | |
| } else { | |
| deps.log(`${key}: ${String(value)}`); | |
| } | |
| } | |
| } else { | |
| deps.log(String(result)); | |
| } |
| let offset = options.offset ?? 0; | ||
| const sandboxId = options.agent ?? options.sandboxId; | ||
|
|
||
| while (true) { | ||
| const result = await getRunLogs(runId, { | ||
| apiUrl: options.apiUrl, | ||
| offset, | ||
| sandboxId, |
There was a problem hiding this comment.
--agent is documented as selecting an agent, but the value is currently passed to getRunLogs() as sandboxId (and it also overrides an explicitly provided --sandbox-id). This looks like a parameter mix-up; pass an agent query parameter (if supported) or keep sandboxId strictly sourced from --sandbox-id.
| const ig = await buildIgnoreMatcher(absoluteRoot); | ||
| const tarStream = tar.create( | ||
| { | ||
| gzip: true, | ||
| cwd: absoluteRoot, |
There was a problem hiding this comment.
createTarball() falls back to archiving "." with only a small denylist when git ls-files fails. In non-git directories this can unintentionally upload sensitive/untracked files (e.g., .env, keys, local credentials) to S3. Consider requiring git for code sync, or expanding the default exclude set to cover common secret files and dotfiles (and/or adding an allowlist mode).
| cloudCommand | ||
| .command('unlink') | ||
| .description('Unlink this machine from Agent Relay Cloud') | ||
| .action(async () => { | ||
| const dataDir = deps.getDataDir(); | ||
| const { configPath } = getPaths(dataDir); | ||
| .command('connect') | ||
| .description('Connect a provider via interactive SSH session') | ||
| .argument('<provider>', `Provider to connect (${SUPPORTED_PROVIDERS.join(', ')})`) | ||
| .option('--api-url <url>', 'Cloud API base URL') |
There was a problem hiding this comment.
The new cloud subcommands (connect, run, logs, sync) contain substantial behavior (network requests, polling, SSH interaction, patch application), but the updated tests only assert command registration. Adding focused unit tests (e.g., mocking authorizedApiFetch / getRunLogs / syncWorkflowPatch and verifying option handling and error paths) would help prevent regressions.
| _workflowArg: string, | ||
| _explicitFileType?: WorkflowFileType, | ||
| ): boolean { | ||
| return true; |
There was a problem hiding this comment.
shouldSyncCodeByDefault() currently always returns true, which means runWorkflow() will upload the current working directory unless callers explicitly pass syncCode: false. This has potentially large performance/cost implications and may surprise users running inline workflows; consider a heuristic default (or defaulting to false) and requiring an explicit --sync-code opt-in.
| _workflowArg: string, | |
| _explicitFileType?: WorkflowFileType, | |
| ): boolean { | |
| return true; | |
| workflowArg: string, | |
| _explicitFileType?: WorkflowFileType, | |
| ): boolean { | |
| const looksLikeFile = path.isAbsolute(workflowArg) || | |
| workflowArg.includes(path.sep) || | |
| inferWorkflowFileType(workflowArg) !== null; | |
| return looksLikeFile; |
| if (!options.force) { | ||
| const existing = await readStoredAuth(); | ||
| if (existing && existing.apiUrl === apiUrl) { | ||
| const expiresAt = Date.parse(existing.accessTokenExpiresAt); | ||
| if (!Number.isNaN(expiresAt) && expiresAt - Date.now() > 60_000) { | ||
| deps.log(`Already logged in to ${existing.apiUrl}`); |
There was a problem hiding this comment.
The login command hard-codes the 60_000 refresh window when deciding whether the stored token is still usable. Since @agent-relay/cloud already exports REFRESH_WINDOW_MS, consider reusing it here to keep behavior consistent if the window changes.
Move the decorative radial gradients into the dark .page context and explicitly set :global(html[data-theme='dark']) .heroSection to background: transparent. This ensures the dark-theme gradients apply at the page level while the hero section itself is cleared (preserving child stacking and layout).
Update cloud CLI and workflows to improve security and robustness: - packages/cloud/src/workflows.ts: expand CODE_SYNC_EXCLUDES to omit environment files, keys, AWS/SSH credentials and other sensitive artifacts from code syncs. - src/cli/commands/cloud.ts: import crypto and use crypto.randomUUID() for temporary patch filenames to avoid collisions; derive provider help text dynamically from CLI_AUTH_CONFIG and PROVIDER_ALIASES; import REFRESH_WINDOW_MS and use it to determine auth refresh timing; rewrite getErrorDetails to prefer parsing response bodies (JSON or text) with safer fallbacks. These changes prevent leaking sensitive files during sync, produce clearer error messages, reduce tmp file name collisions, and make auth refresh behavior configurable.
|
Preview deployed!
This preview will be cleaned up when the PR is merged or closed. |
- auth.ts: Validate state param (CSRF) before user-controlled error param to prevent user-controlled bypass of security check - workflows.ts: Remove stat-then-read TOCTOU race by reading file directly and handling EISDIR in the catch - cloud.ts: Use mkdtempSync + restrictive permissions (0o600) for temp patch file instead of predictable path in os.tmpdir() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| const { execSync } = await import('node:child_process'); | ||
| const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cloud-sync-')); | ||
| const tmpPatch = path.join(tmpDir, 'changes.patch'); | ||
| fs.writeFileSync(tmpPatch, result.patch, { mode: 0o600 }); |
Check warning
Code scanning / CodeQL
Network data written to file Medium
Add cloud CLI commands to support running workflows in the cloud.