feat(cloud): send workflowPath so the launcher can skip the $HOME upload#766
feat(cloud): send workflowPath so the launcher can skip the $HOME upload#766khaliqgant merged 3 commits intomainfrom
Conversation
Pair with AgentWorkforce/cloud#276. When `--sync-code` is used, send
the workflow's path relative to the tarball root in the run request
body. The cloud launcher uses it to set `WORKFLOW_FILE` directly to
the in-tree copy at `/project/<path>` and skip uploading the file to
`$HOME` — which means sibling-relative imports like
`../shared/models.ts` resolve against the repo layout instead of `$HOME`.
Relativization rules (validated by the new `relativizeWorkflowPath`):
* Accepts `workflows/foo.ts`, `./workflows/foo.ts`, and absolute paths
that live inside `process.cwd()` (the tarball root).
* Rejects absolute paths outside cwd and any path that would escape
via `..`. When rejected, the field is simply omitted — the server
falls back to the pre-existing `$HOME` upload path (unchanged
behaviour).
* Normalizes to forward slashes so the Linux-side validator gets a
consistent shape regardless of the CLI's OS.
Tests: 6/6 pass. The cloud companion PR's validator blocks any path
that doesn't satisfy the same rules, so even if this function emits
something odd the server will drop it and fall back to the legacy path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 34426dd1b1
ℹ️ 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".
| const workflowPath = relativizeWorkflowPath(workflowArg); | ||
| if (workflowPath) { | ||
| requestBody.workflowPath = workflowPath; |
There was a problem hiding this comment.
Skip workflowPath when workflow arg is inline content
resolveWorkflowInput intentionally supports non-file inputs by treating workflowArg as raw workflow text when it doesn’t resolve to a file, but runWorkflow now unconditionally computes and sends workflowPath from that same string. For inline YAML/TS arguments this produces a synthetic path that does not exist in the synced tarball, so with syncCode enabled the launcher can select a non-existent workflow file path instead of the legacy source-upload flow, causing cloud runs to fail for inline invocations. Gate requestBody.workflowPath on inputs that were actually loaded from a filesystem path.
Useful? React with 👍 / 👎.
CI failed because the new test file was written with `node:test` /
`node:assert`, but the repo's test runner is vitest — vitest tried to
import the file and emitted "No test suite found" because none of the
describes registered through its own globals.
- Imports from 'vitest' (matches packages/cloud/src/auth.test.ts).
- Switched assert.equal(...) to expect(...).toBe(...) /
.toBeNull() so the assertions run through vitest's matcher chain.
- Dropped the `node:assert/strict` import.
- The "absolute path outside cwd" case now realpath()s os.tmpdir()
before comparing — symlink-stable on macOS like the tmpRoot.
Verified locally: `npx vitest run packages/cloud/src/workflows.test.ts`
→ 6/6 pass.
| const workflowPath = relativizeWorkflowPath(workflowArg); | ||
| if (workflowPath) { | ||
| requestBody.workflowPath = workflowPath; | ||
| } |
There was a problem hiding this comment.
🟡 relativizeWorkflowPath sends inline YAML content as a file path to the server
When workflowArg is inline workflow content (not a file path), relativizeWorkflowPath(workflowArg) is still called and can return a non-null garbage value. The CLI supports inline YAML: resolveWorkflowInput at packages/cloud/src/workflows.ts:186-189 falls through to treat workflowArg as raw content when it can't be read as a file and doesn't look like a file path. Since shouldSyncCodeByDefault always returns true (packages/cloud/src/workflows.ts:152-154), the syncCode block executes, and relativizeWorkflowPath is called on the YAML string. For inline content like "version: 1.0\nswarm: ...", path.resolve(cwd, content) produces ${cwd}/${content}, path.relative returns the content itself, which passes all the guards (non-empty, no ../ prefix, not absolute) and gets sent as requestBody.workflowPath. The server would then receive YAML content as a supposed file path in the tarball, potentially causing the server-side launcher to fail to locate the workflow file instead of falling back to the legacy $HOME upload path (which only triggers when workflowPath is absent, not when it's present but invalid).
Prompt for agents
The bug is that relativizeWorkflowPath(workflowArg) is called unconditionally at line 274, but workflowArg might be inline workflow content rather than a file path. This happens when resolveWorkflowInput (line 156-190) falls through to the inline-content return at line 186-189.
The fix should only compute and send workflowPath when the workflow was actually resolved from a file. Options:
1. Add a boolean field (e.g. resolvedFromFile: boolean) to the ResolvedWorkflowInput type and set it to true when readFile succeeds (line 171) and false in the inline fallback (line 186-189). Then guard the relativizeWorkflowPath call with if (input.resolvedFromFile).
2. Alternatively, compare input.workflow !== workflowArg to detect the file case (when a file is read, input.workflow is the file content, not the workflowArg path). This is less explicit but avoids changing the type.
Option 1 is preferred as it's more explicit and robust.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Companion to
AgentWorkforce/cloud#276. When a user runsagent-relay cloud run <file> --sync-code, send the workflow's pathrelative to the tarball root in the run request body.
The cloud launcher uses that path to set
WORKFLOW_FILEdirectly at/project/<path>and skip uploading the file to$HOME— which meanssibling-relative imports like
../shared/models.tsresolve against therepo layout instead of
$HOME(where they currently break).Why
Today, the launcher uploads the workflow source to
$HOME/workflow.ts, discarding the repo-relative path. When bun runs the file there,../shared/models.tsresolves against$HOMEinstead of the repo. The in-sandbox rediscovery matcher that tries to reverse-engineer the path via sha256 fails silently on the first byte-level mismatch (line endings, tarball normalization). Seecloud#276for the full write-up.Preserving the path end-to-end removes the rediscovery guesswork entirely.
Change
packages/cloud/src/workflows.ts:relativizeWorkflowPath(workflowArg)— normalizes to a forward-slash relative path againstprocess.cwd()(the tarball root), returnsnullfor absolute-outside-cwd or..-escaping paths.runWorkflowsync-code branch, compute the relative path and setrequestBody.workflowPathwhen it's safe. Server falls back to the legacy$HOMEupload path when absent.packages/cloud/src/workflows.test.ts— 6 cases covering sibling paths, leading./, absolute-inside-cwd, absolute-outside-cwd,..-escape, and the edge case of cwd itself.Rollout
agent-relay@4.0.37.workflowPathgets sent → cloud uses it (requirescloud#276in prod).$HOMEupload behaviour (still works, just without sibling-relative import support — same as today).Test plan
npx tsx --test packages/cloud/src/workflows.test.ts— 6/6 pass.cloud#276ship, re-run NightCTO's failing workflow:agent-relay cloud run workflows/security-runtime/01-broker-runtime-execution.ts --sync-code. Should succeed end-to-end with no rediscovery-matcher warn log.🤖 Generated with Claude Code