Tool-server API reference
This page shows how a client sends the files of an e2e test run to a tool-server over a link. The argent CLI and the MCP server are clients. The e2e test runs on the tool-server. The project files stay on the client. The client sends the files of the run in the file input of the call. These are the e2e test file, its run: fragments, the e2e tests that its tool: e2e-execute steps run, the snapshot baselines of each run and the file arguments of its tool: steps. The tool-server returns each new baseline in the result. While the agent records an e2e test, the client sends the files of each recorded step in the same way. For the user-facing behavior, see E2E tests over a link.
collect in GET /tools
The GET /tools entry of a tool lists its file inputs in fileInputs. The flow_path and flow_file inputs of e2e-execute have "collect": "flow":
{
"name": "e2e-execute",
"fileInputs": [
{
"target": "flow_path",
"path": "${flow_path}",
"kind": "file",
"optional": true,
"unwrapWhenSet": "name",
"collect": "flow"
},
{
"target": "flow_file",
"path": "${project_root}/.argent/flows/${name}.yaml",
"paths": [
"${project_root}/.argent/e2e/${name}.yaml",
"${project_root}/.argent/flows/${name}.yaml"
],
"kind": "file",
"skipWhenSet": "flow_path",
"collect": "flow"
}
]
}
paths lists the places of the file in lookup order. The client sends the first file that exists, or the first path when none exists. A client that does not know paths uses path.
"collect": "flow" tells the client that the file is an e2e test. Over a link, the client then also sends each e2e test file that the run: steps of the e2e test reach, each e2e test that its tool: e2e-execute steps run, the snapshot baselines of each run, and the file arguments of the tool: steps. A client that does not know the field sends only the e2e test file.
The project_root input of e2e-add-step carries "collect": "step":
{
"name": "e2e-add-step",
"fileInputs": [
{ "target": "project_root", "path": "${project_root}", "kind": "probe", "collect": "step" }
]
}
"collect": "step" tells the client to send the files that the recorded step makes the tool-server read. See Members of a recorded step.
An entry with "readOnly": true is a tool that only reads state. The MCP adapter sends it with the readOnlyHint annotation.
GET /tools also lists each old flow-* name as an entry with "hideFromMcp": true and aliasOf, the new name. A call to the old name runs the new tool.
The e2e test file input
Over a link, the client replaces the file argument with a file-input object. This is also true for a link to a tool-server on the same computer. For an input with collect, the object has three more fields:
{
"__argentFileInput": true,
"path": "/work/proj/.argent/e2e/withrun.yaml",
"size": 64,
"mtimeMs": 1791300000000,
"content": "<base64>",
"canonical": "/work/proj/.argent/e2e/withrun.yaml",
"spelling": { "state": "listed" },
"members": [
{
"role": "flow",
"key": "/work/proj/.argent/e2e\u0000frag-echo.yaml",
"path": "/work/proj/.argent/e2e/frag-echo.yaml",
"canonical": "/work/proj/.argent/e2e/frag-echo.yaml",
"spelling": { "state": "listed" },
"size": 33,
"mtimeMs": 1791300000000,
"content": "<base64>"
}
]
}
| Field | Value |
|---|---|
canonical | The real path of path on the client. The run: targets of the e2e test resolve beside this file |
spelling | How the directory of path on the client lists the basename of path. See Spelling |
members | One entry for each run: target and each nested e2e test that the e2e test reaches, and one for each baseline and each tool file that the client sends. Empty when no run: step, nested e2e test, snapshot: step or file argument exists |
The client sends these fields only when it can read the e2e test file.
Members
The client collects the members before the call:
- The client reads the
run:targets of the e2e test file, in all steps and in allwhen:blocks. - The client resolves each target beside the real file that names it. When no file is there, the file is in
.argent/e2eor.argent/flowsof a project, and the target has no/, the client looks in the other of these two directories. The runner on the tool-server uses the same rule. - The client reads the
run:targets of each fragment that it finds, and continues in the same way.
The client resolves each pair of directory and target one time. The client stops at 20 levels, the maximum depth of a run: chain. A cycle stops when a pair repeats.
The client also follows each tool: e2e-execute step that names its e2e test with name and an absolute project_root without a .. segment, in each e2e test that it found. The e2e test that such a step names is a nested e2e test. The client sends it as a member with the directory <project_root>/.argent/e2e of the step and the target <name>.yaml, or with <project_root>/.argent/flows when only that directory has the file, and collects its members in the same way. A nested e2e test counts as one level of the same depth limit. The client does not follow a step that names its e2e test with flow_path.
A flow member has these fields:
| Field | Value |
|---|---|
role | flow. For a baseline or a tool file, see Baselines and Tool files |
key | The real directory of the file that names the target, a NUL character, and the target as written. For a nested e2e test, <project_root>/.argent/e2e or <project_root>/.argent/flows, a NUL character, and <name>.yaml. The tool-server finds the member by this key |
path | The real directory of the file that names the target, a path separator, and the target as written. For a nested e2e test, <project_root>/.argent/e2e/<name>.yaml or <project_root>/.argent/flows/<name>.yaml |
canonical | The real path of the target on the client. For a missing file, the real path of the nearest directory that exists, with the rest of the path |
spelling | How the directory of path lists the basename of the target |
content | The file as base64, when the file is in the call body |
uploadId | The id that POST /upload returned, when the client sent the file to POST /upload. Set together with contentHash |
contentHash | The SHA-256 digest of the uploaded archive |
size | The size of the file in bytes |
mtimeMs | The modification time of the file in epoch milliseconds |
state | missing when no file is at canonical. refused when the client does not send the file. listed when the client sends only the name of a baseline. Not set when the member has its bytes |
error | The reason, when state is refused |
The client puts the members in the call body until they are 256 KiB in total. The client sends each other member to POST /upload as one gzipped tar, as for an input of kind tar-upload. Thus the body limit of a reverse proxy applies to the e2e test file with up to 256 KiB of members, and to one member at a time. When a proxy refuses an upload with the status 413, the client does not send the call. The error gives the body size that the proxy must accept on POST /upload, for example with client_max_body_size in nginx.
Spelling
spelling has one of three shapes:
| Shape | Meaning |
|---|---|
{ "state": "listed" } | The directory lists the basename as written. The client also sends listed when it cannot read the directory |
{ "state": "case_folded", "actual": "<name>", "addressable": <bool> } | The directory lists the name with a different case. actual is the listed name. addressable is true when actual is a valid e2e test file name |
{ "state": "absent" } | The directory lists no name that matches, in any case |
Client policy
The client sends a member only when the real path of the member is under one of these directories, by their real location:
project_rootand its.argent/e2eand.argent/flowsdirectories- the directory of the e2e test file and the directory of its real file
- the project
<P>of an e2e test file under<P>/.argent/e2e/or<P>/.argent/flows/, by its path or by its real path
The client marks a member refused in these cases:
| Case | error |
|---|---|
| The real path is outside all the directories above | <target> is outside every root this client serves (<directories>) |
A .yaml name links to a file that is not YAML | <target> links to a file that is not a YAML file |
| The path is a directory | EISDIR: illegal operation on a directory, read |
| The file is larger than 32 MiB | <canonical> is larger than the 32 MiB cap on a file sent to the tool-server |
| The client cannot read the file, or resolve a link | The error of the operating system, for example EACCES or ELOOP |
The client does the directory check first. Thus a missing file outside these directories is refused, not missing.
When the environment of the client has ARGENT_E2E_FILES_LOG=1, the client prints one line on stderr for each member before the call: [flow-files] flow <canonical>: inline <bytes>, : upload <bytes>, : missing or : refused (<reason>). A baseline line starts with [flow-files] baseline <key> and can also end with : listed. A tool file line starts with [flow-files] tool <key>. After the call, the client prints [flow-files] baseline <path>: written for each baseline that it wrote. A line never holds the content. With argent e2e run --json, each line is a warning object on stderr.
Baselines
When the e2e test or a fragment that it reaches has a snapshot: step, the client also sends the baselines of the run. The baselines are in the directory <dir>/__baselines__/<key>/. <dir> is the directory of canonical of the e2e test file. <key> is the stem of canonical, or the stem of path when the first is not a valid e2e test name. The runner looks for the baselines at the same paths. A nested e2e test is a run of its own. Its baselines are beside canonical of that e2e test, and the name of its step takes the place of the stem of path.
| Run | Members |
|---|---|
| A run that updates baselines | One member with state: "listed" and no content for each .png file in the directory. The client writes the baselines of the result only into the directories of such runs |
| A run that compares baselines | One member with the bytes for each <name>__*.png file in the directory, where <name> is a snapshot name of the e2e test or of a fragment. When device starts with chromium-cdp-, or the call sets platform, only the files of that platform |
The call updates baselines when it sets updateBaselines: true. A nested run updates baselines when its step sets updateBaselines: true. It also updates them when its step does not set updateBaselines and the run that starts it updates baselines. For a nested run, the client applies the platform filter of the call. It ignores the device and platform of the step. When a run that compares and a run that updates use the same directory, the client sends the bytes of each file that the run that compares reads. It sends each other .png file of the directory by name.
A baseline member has role: "baseline", and its key and path are the absolute client path of the file. Its bytes travel as the bytes of a flow member: inline within the same 256 KiB, else through POST /upload. The client marks a baseline refused in the cases of the client policy table, with PNG in place of YAML. The client marks a link to nothing missing, or refused in a run with updateBaselines: true.
The client sends nothing for a directory outside the directories of the client policy, and writes nothing there.
Tool files
The client also sends the file arguments of the tool: steps in each e2e test that it found, in all when: blocks. A file argument is an argument that the GET /tools entry of the tool declares as a file input whose path is only that argument ("${<target>}"). Its value is an absolute path that ends in .png or .yaml, in any case. A tool: e2e-execute step has no file argument: the client follows it as a nested e2e test. The client skips an input whose unwrapWhenSet parameter is also set. The client never sends other arguments, for example an absolute .png path that a keyboard step types.
A tool file member has role: "tool", and its key and path are the argument as written. The client sends each path once. When a baseline has the same path, the tool file member stands for the baseline too. The bytes of a tool file member travel as the bytes of a flow member. The client marks a tool file member:
missingwhen nothing is at the pathrefusedwhen its real path is outside the directories of the client policyrefusedwhen the name links to a file that does not end in.pngor.yaml:<path> links to a file that is not one of .png, .yamlrefusedfor a directory, a file that the client cannot read, or a file larger than 32 MiB
The call
The client asks for a stream for each call with a members field, also when the list is empty. It sends Accept: application/x-ndjson and Accept-Encoding: identity.
The tool-server sends the stream with Cache-Control: no-cache, no-transform and X-Accel-Buffering: no. A proxy that holds the stream delays the progress lines, but the result does not change. The tool-server sends a progress line after each step. A proxy ends the response when no data arrives from the tool-server within its read timeout. The default is 60 seconds in nginx. Thus a step that runs longer than this timeout fails the call.
The client sends a call with a members field only one time. When the connection closes before the result arrives, the client does not send the call again. The MCP server sets no timeout for this call. The error says that the tool possibly ran, for example The connection to the tool-server closed before e2e-execute finished (fetch failed: other side closed). The tool may have run; check its effect before you run it again.
What the tool-server does with the members
The tool-server reads the members before it validates the arguments:
- The tool-server keeps the bytes of each member that has bytes. It applies the 32 MiB limit and the size check of a file input. It also applies the 32 MiB limit to the total size of the flow members of the call. It reads an upload as for a
tar-uploadinput. - When the bytes of a member fail a check, or the tool-server does not have its upload, the call fails with the status 422. A file input fails in the same way.
- The tool-server deletes each upload that the call names before the e2e test starts, also when the call fails. An
uploadIdis valid for one call only. - A member becomes
refusedwhen it has no bytes and nostate. A flow member also becomesrefusedwhen itscanonicalorspellingis not valid. - The tool-server ignores a member with an unknown
role, and a member with akeythat it already has.
Then the runner uses the members as the files of the client:
- Before the first step, the runner checks the e2e test and each member that it can reach, to the maximum depth of a
run:chain. A rejection has the error codeFLOW_FILE_INVALID. The error lists each step that caused it, for examplestep 1 in /work/proj/.argent/e2e/inner-script.yaml: script: { path: ../../scripts/hello.mjs }. Each of these stops the run:- a
script:step - a
tool:step that records an e2e test - a
tool: e2e-executestep that does not name its e2e test withnameand an absoluteproject_rootwithout a..segment - a
tool:step with a file input that is not a file argument: a directory, an app, an output directory, a path that the tool builds from two or more arguments, a relative path, or a file that does not end in.pngor.yaml - a
refusedflow member
- a
- When the e2e test has a
run:orsnapshot:step, therun:targets and the baselines of the e2e test resolve beside thecanonicalpath of the e2e test file. Acase_foldedspelling of the e2e test file stops the run, as it does without a link. - A
run:step finds its member bykey. The step fails when no member has that key. The runner applies the same checks as without a link: a cycle, a chain deeper than 20 levels, and acase_foldedspelling each fail the step. The runner never opens thecanonicalpath. It uses the path only as a key and in messages. - A
missingmember makes itsrun:step reportcould not load fragment "<target>": ENOENT: no such file or directory, open '<canonical>'. Arun:step in awhen:block that does not run needs no file. - A
tool: e2e-executestep looks up its e2e test bykey. The nested run gets that e2e test and all members of the call, and finds its own fragments, nested e2e tests, baselines and file arguments there. The cycle guard and the depth limit countrun:fragments and nested runs together. Amissinge2e test makes the step reporterror:ENOENT: no such file or directory, open '<canonical>'. Acase_foldedspelling makes the step reporterrorwithInvalid e2e test name, as on one computer. - A
snapshot:step that compares reads its baseline from the members. When no member has the path of the baseline, the step fails as for a missing baseline. Arefusedbaseline makes the step reporterrorwith the reason of the client. - A
snapshot:step withupdateBaselineskeeps the new baseline for the result. The step reportsbaseline captured; the client writes it when the run ends (<path>).<path>is the client path of the baseline. When a member or an earlier step of the call has the same path,writesbecomesupdates. A later step of the call reads the new baseline, also in another nested run. Arefusedbaseline makes the step reporterrorwith the reason of the client. - A
tool:step looks up each file argument by the path as written: a baseline that an earlier step of the call wrote first, then the members. The tool gets the bytes in a temporary file, as for a direct call. Amissingmember, or no member, makes the step reporterror:the client has no file at "<path>" (argument <name> of <tool>). Arefusedmember makes the step reporterrorwith the reason of the client.
Baselines in the result
The result of a run over a link that wrote baselines has a baselineWrites list. The list holds one client-file directive for each baseline path, with the last bytes for that path. The list of the call also holds the baselines that its nested runs wrote. A nested run returns no list of its own:
{
"__argentClientFile": true,
"path": "/work/proj/.argent/e2e/__baselines__/withsnap/home__chromium-1000x800.png",
"content": "<base64>",
"encoding": "base64"
}
The client writes a directive with encoding: "base64" only when path ends in .png, has no .., and is directly in the baseline directory of a call with updateBaselines: true. The client replaces the file in one rename. The client refuses a link to nothing, a link out of the directory, a .png name that links to a file that is not a PNG file, and a file that is not a regular file. The client replaces each directive with the path that it wrote, or with { "path": "<path>", "error": "<reason>" }. The client prints the reason of each baseline that it did not write. argent e2e run then exits with code 1. The MCP server adds the line ✗ baseline not written: <path>: <reason> to the report.
The client writes the baselines only when the result arrives. When the connection closes before the result, the client writes no baseline of that call.
Members of a recorded step
The project_root input of e2e-add-step has the kind probe and carries "collect": "step". Over a link, the client adds members to its file-input object. The object has no content, canonical or spelling, and the tool-server passes project_root through unchanged. The client builds the step from command and the JSON text of args, and sends the files that this one step makes the tool-server read:
| Step | Members |
|---|---|
A tool: step with file arguments | Each file argument, as in Tool files |
command: e2e-execute with name and an absolute project_root in args | The recording file, by its directory and file name. The file with the e2e test name of the step beside the real file of the recording. The e2e test that the step names, with its members and the baselines of its run, as for a nested e2e test |
command: e2e-execute with a flow_path in args, in the directory of the recording or in the other e2e test directory of the project | The members of the row above for the name of that file. Also that file, by its directory and the file name of flow_path |
command: e2e-execute with another flow_path | The recording file |
The recording file is <project_root>/.argent/e2e/<name>.yaml, with the project_root and the name of the call. A recording that the client keeps is in <project_root>/.argent/flows/<name>.yaml when the client does not send the x-argent-e2e-dir header. The client policy applies, with the recording file in place of the e2e test file.
The run of a recorded e2e-execute step updates baselines only when the step sets updateBaselines: true. The result of e2e-add-step then has a baselineWrites list, as the result of a run. A step that reads no file sends an empty members list. The call is still a stream, as The call describes. A call without a valid name, without an absolute project_root, or with args that are not the JSON text of an object also sends an empty list.
The x-argent-e2e-dir header
The client sends the header x-argent-e2e-dir: 1 with each POST /tools/<name> call. The header tells the tool-server that the client writes recording files in .argent/e2e/. A client that is older than the rename writes recording files only in .argent/flows/. For a call without the header, e2e-start-recording puts a recording that the client keeps in <project_root>/.argent/flows/<name>.yaml. A recording that the tool-server writes stays in .argent/e2e/.
The x-argent-linked header
The client sends the header x-argent-linked: 1 with each POST /tools/<name> call over a link. A link to 127.0.0.1 counts. The e2e test recorder of the tool-server reads the header:
- A recording that
e2e-start-recordingstarts with the header keeps its file on the client. The tool-server writes nothing, andsavedTois a directive that the client applies. This is also true when theproject_rootpath also exists on the computer of the tool-server. e2e-add-scriptrefuses the call, because a replay over a link rejects ascript:step. No script runs, and the recorder records no step.e2e-add-steprefuses a step that a replay over a link rejects. An example is atool:step that takes a directory, an app or an output directory. No tool runs, and the recorder records no step.e2e-add-steprefuses ane2e-executestep when the nested e2e test does not pass. The recorder records no step.
Without the header, e2e-start-recording keeps the file on the client only when the project_root path does not exist on the computer of the tool-server. For such a recording, the recorder applies the refusals of the list above also to a call without the header.
An argent client that is older than the tool-server sends no header and no members of a recorded step. The result depends on the place of the recording file:
- The tool-server writes the file over a link to
127.0.0.1, or when theproject_rootpath also exists on its computer. The recorder then applies none of the refusals above. Such a client can record steps that a replay over that link rejects, ascript:step among them. - The client keeps the file when the
project_rootpath does not exist on the computer of the tool-server. The recorder applies the refusals. It also refuses these steps, because such a client sends no file with the call, and the error tells you to update the argent CLI or the MCP adapter:- an
e2e-executestep withname - an
e2e-executestep with theflow_pathof an e2e test beside the recording - a
tool:step with a file argument
- an
Keep the client and the tool-server at the same version.
The x-argent-client-secrets header
The client sends this header with a POST /tools/<name> call over a link when the forward-secrets flag is on and the call contains a {{secret:NAME}} placeholder. See Secrets over a link. The value is base64 text of a JSON object. Each key is a secret name, and each value is the secret value as a string:
x-argent-client-secrets: eyJBUFBfUEFTU1dPUkQiOiJodW50ZXIyIn0=
The object is empty when the client does not define any of the names. The tool-server then knows that the client forwards secrets. When a name resolves nowhere, the error says that the client did not provide the secret.
The tool-server resolves each placeholder of the call with these values first, then with its own sources. A sub-tool call of run-sequence or e2e-execute uses the same values. The tool-server accepts 16 KiB of request headers in total. The tool-server answers 400 and runs no tool when the header is not base64 text of a JSON object, a key is not a valid secret name, or a value is not a string.
Older clients and tool-servers
| Client | Tool-server | Result for a run: step, a snapshot: step, a nested e2e test or a file argument over a link |
|---|---|---|
| Sends members | Reads members | The runner reads the fragments, the nested e2e tests, the baselines and the file arguments from the members |
| Sends members | Does not read members | The listing has no collect. The client sends only the e2e test file. The tool-server rejects the e2e test before the first step |
| Does not send members | Reads members | The tool-server rejects the e2e test before the first step and adds: "This tool-server runs run: steps for a client that sends their fragments with the call. Update the argent CLI or MCP adapter on the client." For a snapshot: step, it names "snapshot: steps for a client that sends their baselines with the call", for a nested e2e test "tool: e2e-execute steps that name an e2e test with name, for a client that sends those e2e tests with the call", for a file argument "tool: steps with file arguments for a client that sends them with the call" |
| Does not send members | Does not read members | The tool-server rejects the e2e test before the first step |