Skip to content

feat(remote): input-side file boundary — flows, screenshot-diff, profiler reports, workspace tools#319

Draft
filip131311 wants to merge 2 commits into
mainfrom
filip/remote-file-boundary
Draft

feat(remote): input-side file boundary — flows, screenshot-diff, profiler reports, workspace tools#319
filip131311 wants to merge 2 commits into
mainfrom
filip/remote-file-boundary

Conversation

@filip131311

Copy link
Copy Markdown
Collaborator

Why

PR #278 fixed the output half of the remote file boundary: files a tool produces travel to the client as ArtifactHandles. But seven tools still break when the tool-server runs on a different machine than the agent, in one (or both) of two ways:

  • Output-side leftoversnative-profiler-analyze and screenshot-diff still returned raw host paths (and embedded them in report/summary prose), so the agent can't open them remotely.
  • Input-side gap — nothing moved a file the agent owns onto the tool-server. Any tool taking a client-local path as a parameter (screenshot-diff baselines, the three flow tools' project_root, react-profiler-component-source, gather-workspace-data) silently read/wrote the wrong machine's disk. Flows were fully broken in remote mode: the YAML landed on the server's disk under the agent's path.

What

Output side (apply the existing pattern)

  • native-profiler-analyzereportFile is now an ArtifactHandle (mirrors react-profiler-analyze's fileArtifact()); the inline report now says "use the Read tool on the reportFile path in this result" instead of embedding the server path.
  • screenshot-diffdiffPath / contextDiffPath are now ArtifactHandles; the MCP adapter renders the context-diff image from materialized bytes (legacy string-path results from older servers still render co-located). The summary references the result fields instead of embedding host paths.

Input side (new, symmetric mechanism)

A declarative file-input boundary, mirroring the artifact gate in the opposite direction:

  • A tool declares fileInputs: [{ target, path: "${param}…", kind }] on its ToolDefinition; the declaration ships to the client via GET /tools.
  • Before sending a call, the client (@argent/tools-client, shared by CLI and MCP) interpolates each template from the args and replaces the target arg with a wrapper: { __argentFileInput, path, size, mtimeMs, content? }. File bytes are base64-inlined only when routed to an external tool-server (argent link / ARGENT_TOOLS_URL) — unlinked local calls never pay for encoding.
  • The server resolves wrappers before zod validation, so tools always execute against a plain local path:
    • path matches on the server's own filesystem (existence for dirs, size+mtime for files) → used in place, zero copies — the exact dual of the artifact materializer's co-located gate;
    • otherwise kind: "file" content is materialized into a temp file (with size-integrity check, 32 MB cap);
    • kind: "directory" (trees that can't ride in a call) → 422 with actionable remote-mode guidance;
    • kind: "probe" → never fails; the tool learns host-presence via ctx.fileInputs and adapts.
  • The reverse channel: a tool whose output belongs in the agent's project returns a __argentClientFile directive (path + content); the client writes it and rewrites the directive to the path. Writes are constrained to **/.argent/flows/<safe-name>.yaml so a tool-server can't direct writes anywhere else on the client machine.

Tool migrations

Tool Boundary use
flow-start-recording probe on project_root: present → host persistence exactly as before; absent (remote) → recording kept in server memory, YAML returned as a client-write directive
flow-add-step / flow-add-echo / flow-finish-recording follow the session's persist mode; every mutation returns savedTo (path locally, directive remotely) so the agent-side file is always current
flow-execute / flow-read-prerequisite file input derived from ${project_root}/.argent/flows/${name}.yaml into an internal flow_file param — read in place locally, uploaded when remote
screenshot-diff baselinePath/currentPath as file inputs; outputDir now optional (temp default) and probe-ignored when absent on the host
react-profiler-component-source, gather-workspace-data directory gate — unchanged locally; clear error remotely instead of a silent empty AST index / all-nulls snapshot

Version skew

Both halves degrade to today's behavior: an old client sends plain strings (the resolver passes them through, tools take their legacy paths), and an old server doesn't advertise fileInputs (the new client never wraps). Wrappers on undeclared params fail the tool's own schema validation, so nothing can be smuggled through free-form args.

Tests

Unit (all green: registry 68, tool-server 992, tools-client 107, argent-mcp 66, argent-cli 117):

  • tool-server/test/file-inputs.test.ts — in-place gate, stat-mismatch → upload, truncated-upload rejection, directory gate, probe semantics, undeclared-target smuggling.
  • argent-tools-client/test/file-inputs.test.ts — wrapping (stat-only vs inlined content), multi-param template derivation, explicit-override respect, directive writes incl. path-validation refusals (relative, traversal, outside .argent/flows, bad charset/extension).
  • tool-server/test/flows/flow-remote-recording.test.ts — remote recording stays in memory + returns directives, nothing written on the host, replay/read from a boundary-resolved flow_file.
  • Updated existing screenshot-diff / native-profiler tests to the artifact-handle contract.

Local / co-located (unlink scenario) — isolated tool-server + CLI on an iPhone 17 Pro simulator: full flow record→finish→read-prerequisite→execute loop (YAML written via the same path as before, in-place gate hit, no byte copies); screenshot-diff with and without outputDir; wire-level checks of the unlinked client shape (wrapper without content → in-place read) and the legacy plain-string shape; missing-file → 422 with a precise message.

Remote (Lima Linux VM as the agent machine, tool-server on the host Mac, --host bind + bearer auth) — via both the CLI and the real MCP stdio server:

  • flow recording from the VM: YAML lands in the VM's project, savedTo rewritten to the VM path, no agent-path directories created on the host; replay from the VM uploads the YAML and drives the host simulator.
  • screenshot materializes into the VM temp cache; screenshot-diff round-trips both directions (VM baseline uploaded, live capture on host, diff artifacts downloaded back; MCP returns image,text content with the context diff inlined).
  • gather-workspace-data / react-profiler-component-source with VM-only paths → the new actionable 422.

Notes

  • native-profiler-analyze was not exercised live (xctrace export is unusable on this machine's iOS 26.4 simulators — known, unrelated); its change is the same one-line fileArtifact pattern react-profiler-analyze already ships, covered by unit tests.
  • react-profiler-analyze's own project_root (AST resolve stage) is left as-is deliberately: it degrades gracefully (report still renders) and gating it would break currently-working remote analysis. Tracked as follow-up alongside nested invocations (run-sequence/flow-execute steps bypass the HTTP boundary by design).

🤖 Generated with Claude Code

filip131311 and others added 2 commits June 10, 2026 13:10
…ote mode

PR #278 made tool-produced files work across the remote boundary (output
side). This closes the remaining gaps:

Output side (existing artifact pattern):
- native-profiler-analyze: reportFile is now an ArtifactHandle; the inline
  report references the result field instead of embedding the host path
- screenshot-diff: diffPath/contextDiffPath are now ArtifactHandles; the
  summary references the result fields instead of embedding host paths

Input side (new, symmetric mechanism):
- Tools declare fileInputs ({target, path template, kind}) on their
  ToolDefinition, advertised via GET /tools
- The client (tools-client; used by both CLI and MCP) wraps declared args
  as __argentFileInput {path, size, mtimeMs, content?}; file bytes are
  inlined only when routed to an external tool-server
- The server resolves wrappers before zod validation: in place when the
  path matches on its own filesystem (co-located: zero copies), otherwise
  materialized from the uploaded content into a temp file; 'directory'
  kind gates with an actionable remote-mode error; 'probe' kind only
  reports host presence to the tool via ctx.fileInputs

Tool migrations:
- flows: recording keeps server-side session state; when the client is
  remote (project_root probe miss) mutating tools return a
  __argentClientFile directive and the client writes the YAML into the
  agent's project (constrained to .argent/flows/*.yaml); replay/read
  derive the flow file as a 'file' input so the YAML is read in place
  locally and uploaded when remote
- screenshot-diff: baselinePath/currentPath are 'file' inputs; outputDir
  is now optional (temp default) and ignored when absent on the host
- react-profiler-component-source, gather-workspace-data: 'directory'
  gate — unchanged locally, clear error remotely instead of silent
  empty results

Both halves of the version-skew matrix degrade to today's behavior: an
old client sends plain strings (no wrappers), and an old server doesn't
advertise fileInputs (client never wraps).
Allow direct screenshot-diff tests to pass artifact context now that diff outputs are registered as artifacts.

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