Skip to content

Add cloud package and CLI integration#642

Merged
willwashburn merged 9 commits intomainfrom
cloud
Mar 26, 2026
Merged

Add cloud package and CLI integration#642
willwashburn merged 9 commits intomainfrom
cloud

Conversation

@willwashburn
Copy link
Member

@willwashburn willwashburn commented Mar 25, 2026

Add cloud CLI commands to support running workflows in the cloud.


Open with Devin

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

Outbound network request depends on
file data
.
Comment on lines +271 to +275
headers: {
"content-type": "application/json",
authorization: `Bearer ${accessToken}`,
...(init.headers ?? {}),
},

Check warning

Code scanning / CodeQL

File data in outbound network request Medium

Outbound network request depends on
file data
.
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

Outbound network request depends on
file data
.
devin-ai-integration[bot]

This comment was marked as resolved.

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.
devin-ai-integration[bot]

This comment was marked as resolved.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 connect in favor of agent-relay cloud connect.
  • Replace legacy cloud “link/sync/agents/send/brokers” CLI logic with browser-based auth (login/logout/whoami) plus workflow run/status/logs/sync.
  • Add new packages/cloud SDK 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.

.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));
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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));
}

Copilot uses AI. Check for mistakes.
Comment on lines +410 to +417
let offset = options.offset ?? 0;
const sandboxId = options.agent ?? options.sandboxId;

while (true) {
const result = await getRunLogs(runId, {
apiUrl: options.apiUrl,
offset,
sandboxId,
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

--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.

Copilot uses AI. Check for mistakes.
Comment on lines +481 to +485
const ig = await buildIgnoreMatcher(absoluteRoot);
const tarStream = tar.create(
{
gzip: true,
cwd: absoluteRoot,
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
Comment on lines 210 to +214
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')
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +99
_workflowArg: string,
_explicitFileType?: WorkflowFileType,
): boolean {
return true;
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
_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;

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +138
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}`);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Noodle and others added 5 commits March 25, 2026 19:05
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.
@github-actions
Copy link
Contributor

Preview deployed!

Environment URL
Web https://pr-642.agentrelay.net

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

Write to file system depends on
Untrusted data
.
@willwashburn willwashburn merged commit 1c0c9ad into main Mar 26, 2026
41 checks passed
@willwashburn willwashburn deleted the cloud branch March 26, 2026 04:52
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.

2 participants