# Fileaify agent API — Executor — The workspace executor: SSH destinations Part of the Fileaify agent protocol. Start page and index: `/agent`. Everything in one document: `/agent/full`. ## Executor **Start duration.** The human Start button and accepted `executor.start` requests start a **one-hour** session by default on every eligible plan. Humans may select 10 minutes, 30 minutes or 1 hour (Solo and above); 2 or 5 hours (Pro and above); or no scheduled stop (Premium/Enterprise). The UI calls human-only `POST /api/v1/executor/start` with `duration_seconds` (600, 1800, 3600, 7200, 18000 or null). Availability is enforced on the server. A limited plan's remaining budget may shorten the session; the deadline counts from launch, including startup. A new start call does not extend a running session. `stop_at` reports the effective deadline. The worker, VM lease and provider termination schedule enforce it, with short safety margins; running jobs end when the executor stops. Existing running instances keep their previous deadline policy until the next launch. The executor is a small server dedicated to the workspace that uses the workspace's declared SSH keys for the agent, so the key never reaches the agent's machine or conversation. A human starts it from the workspace header; the time it may stay on depends on the plan (Solo 1 h per 6-hour window, Pro up to 12 h per day, Premium always on, Free none; the outgoing IP changes at every start unless a Pro or Premium owner turns on the "Fixed IP" option). Its only inbound door is the direct-SSH port range (TCP 41000–48999, one port drawn at each start, key authentication only; see below); the header shows its state (red, orange, green), remaining time and outgoing IP. - **Where the executor may connect** is the `## Hosts` table of the project's infrastructure document (`infra.md`, or the first document that has one): `| ref | host | user | port | key | via | notes |` (example row: `| web1 | web1.example.net | deploy | 22 | vault://ovh/my-key | | prod |`). Any document of the project may hold it: `infra.md` wins when several do, otherwise the first file name alphabetically, and the others are listed in `problems` as `multiple_hosts_tables`. `key` is the `vault://…` reference of an SSH key file of the project (optional when the project has exactly one), `via` is the `ref` of another row used as a bastion (same key). Only the owner-accepted text counts. `GET /api/v1/projects/{project_id}/executor/targets` lists the effective destinations, the document they come from and the rows that could not be used (`problems`). Humans may also declare a destination in the workspace (Services → Executor SSH targets) or by `POST` with an optional `jump` object. The `root@HOST` example below connects to Fileaify’s project sandbox, not to the user’s production server. It does not require the user to provide a root credential for an external system. Connections to external servers use separately configured destinations, accounts and credentials. Choose privileges appropriate to the task; using Fileaify for documentation requires none of these connections. - **Direct SSH into your project's sandbox**: `POST /api/v1/projects/{project_id}/executor/ssh-access` with `{"public_key":"ssh-ed25519 …"}` (create one with `ssh-keygen -t ed25519`; without a key Fileaify generates a pair and returns the private key once). It needs `executor.run` and a ready executor (`409 executor_not_ready` / `ssh_not_ready` otherwise). The reply gives `host`, `port` (drawn at random at each executor start, so always read it again), `host_key_line` (use it as `known_hosts`, never accept an unknown key), `expires_at` (the end of your `executor.run` permission) and `session_id`. Connect with `ssh -p PORT -i KEY root@HOST "command"`: you land in this project's folder inside the sandbox, nothing else is reachable. `scp`, `sftp` and `rsync` work and are not filtered; port forwarding and agent forwarding are refused. In a command, `${vault://service/key}` is replaced by Fileaify for that command only (it needs `secrets.use` on the key) and masked in what comes back, with a key file placed in a private folder when the secret is a file; `${vault-agent://service/key}` loads an SSH key into an ssh-agent for that command instead of a file. A reference inside single quotes is refused: use double quotes. The shell without a command is line-based (no full-screen programs). List your sessions with `GET …/executor/ssh-access`, end one with `DELETE …/executor/ssh-access/{session_id}`; MCP tools `executor_ssh_access`, `executor_ssh_sessions`, `executor_ssh_revoke`. The masking is a safety net against accidental leaks: do not try to print or transform a secret. - `POST /api/v1/projects/{project_id}/executor/ssh-run` with `{"host_ref":"web1","command":"…"}` (or `{"target_id","command"}`) queues one non-interactive command. It needs the project permission `executor.run` and a `secrets.use` grant on the key's binding. Reading a job result (`GET …/executor/jobs/{job_id}`) needs `executor.run` too. The server key is pinned on first contact and never silently replaced: a changed key fails the command until a human forgets the pin. - **If the executor is off**, `409 executor_not_ready`: ask for it instead of waiting. `GET /api/v1/projects/{project_id}/executor/sandbox` tells whether the account has one (`executor.available`, `executor.state`); the executor belongs to the workspace owner's account and is shared by all their workspaces, while every project keeps its own `/work` folder. If available, `POST /api/v1/projects/{project_id}/access-requests` with `{"capability":"executor.start","reason":"…"}`: a human accepts in the inbox and the executor starts at once. It grants nothing that outlives the start (`409 executor_unavailable` when the plan has no executor; `200 already_running` when it is on). Do not poll in a loop: `GET /api/v1/access-requests?wait=120` keeps the call open until a pending request is decided or expires (0–120 seconds); without `wait`, read it no more than once a minute. Then retry. - **Sandbox**: `POST /api/v1/projects/{project_id}/executor/run` with `{"command":"…"}` (through `sh -c`) or `{"argv":[…]}` (no shell), optional `env` (`{"TOKEN":"vault://service/key"}`), `network` (default true), `workspace` (default true) and `timeout` (seconds, max 300). Needs the project grant `executor.run`; every secret in `env` consumes one `secrets.use` of its binding. The command runs in a disposable bubblewrap jail on the workspace's executor VM, never on the Fileaify server: read-only system, private `/tmp`, no vault, no Fileaify files, unprivileged user, no cloud metadata. A `{{vault://…}}` placeholder inside the command is refused (400): pass secrets by `env`. `GET …/executor/sandbox` describes the jail and its limits: read it rather than guessing. - **`/work`** is the project's persistent workspace inside the jail (clone, venv, builds survive between calls). It lives on the executor VM's disk: it is erased when that VM is terminated (stop on Solo, end of budget, plan change). Everything else in the jail is erased after each call. Use `{"workspace":false}` for a run that leaves nothing. - **File transfer**: through the direct SSH access above (`scp`, `sftp`, `rsync`, unfiltered). With `executor/run` alone, produce files with the command itself (heredoc, base64) or have the host pull from its repository. - **Waiting for a result**: add `"wait":120` (seconds, 0–120) to `executor/run` or `ssh-run` to receive the output in the same call; the request stays open on the server, which is cheaper than polling. Without it, read the job no more than once a minute. - `GET /api/v1/projects/{project_id}/executor/jobs/{job_id}` returns `status` (`queued`, `running`, `done`, `failed`), `exit_code` and `output`. Output is truncated at 64 KiB and secret values are redacted. Redaction is a safety net, not a boundary. If the executor is stopped (budget used up, plan change, user), pending jobs fail with `executor_stopped`; do not retry automatically. The same calls exist as MCP tools: `executor_sandbox`, `executor_targets`, `executor_ssh_run`, `executor_run`, `executor_job`, `executor_ssh_access`, `executor_ssh_sessions` and `executor_ssh_revoke` (same names as REST paths, same permissions). A connector obtains `executor.run` and `executor.start` through `request_access`; consent never grants them.