Execute a shell command on a remote machine

Runs command through the machine's own shell. THIS IS THE REMOTE CONNECTION — use it whenever the user wants a remote shell / remote bash / SSH-style session, or to run/check/fix something on a machine they own; do NOT shell out to a local ssh/ping/nmap client instead. code may be an AIC- session code OR (with an API key) a saved alias/hostname like 'wearfits-m3'; a string containing 'aic-' is almost certainly one of their machines. The command has full control of the machine at the agent's effective identity — commonly root on a headless Linux install — so treat destructive commands with caution and confirm with the user first.

WHICH SHELL DEPENDS ON THE MACHINE'S OS: read platform from GET /api/v1/status (fleet) or GET /api/v1/status/{code} first. POSIX machines ('darwin'/'linux') run the command via /bin/sh -c. Windows machines ('win32') run it via cmd.exe, where POSIX habits fail in ways that LOOK like success: ; is not a command separator, so echo a ; echo b prints the rest of the line as literal text and still exits 0; POSIX tools are absent (ls -la → "'ls' is not recognized as an internal or external command"); heredocs do not exist (cat > f <<'EOF' → "<< was unexpected at this time."). On Windows either chain steps with && on ONE line — a multi-line command is REJECTED there (it used to run only the first line and return 0) — or set shell: "powershell" and write PowerShell, which accepts ; and multi-line scripts — but under PowerShell judge the result by stderr, not by exitCode alone, because a non-terminating error leaves the exit code at 0 — the code tracks the last statement, not whether errors occurred (see the shell field). shell (sh/bash on POSIX, cmd/powershell on Windows) is the supported way to change interpreter; a value the machine cannot run is rejected with 403 rather than ignored, and it cannot be combined with elevated.

OUTPUT IS BUFFERED, never streamed: the response is a single JSON object once the command finishes. Accept: text/event-stream sends : heartbeat comments while it runs and then one event: result frame carrying that same object — it does not stream the output itself. When the call ends without the command finishing (timeout, relay error, agent disconnect) the error body carries partial — the output buffered so far — plus a warning: treat it as an UNKNOWN outcome, not a failure with empty output.

LIMITS — two caps that behave DIFFERENTLY. TIME (1 hour maximum, 5 minutes default, set with timeout_ms) is a hard kill: at the deadline the command's whole process tree is terminated. OUTPUT (1 MiB total, stdout+stderr combined) is NOT a kill: everything past 1 MiB is dropped from the reply, which is marked truncated. The relay does send a best-effort stop request to the machine, but it travels several network hops and races the command, so a command that dumps a lot of output and then finishes quickly wins that race — it runs to completion and returns its REAL exit code. Never rely on the byte cap to stop anything, and never read a truncated reply as 'the work stopped': its side effects happened. For anything expected to run longer than a few minutes, or to print more than a trickle (ML training, fine-tuning, dataset processing, long builds, large downloads) use POST /api/v1/jobs instead: a detached job has neither cap, writes its full output to a file on the machine, keeps running after the call returns, and survives the client going away. A job outlives the AGENT PROCESS on every platform; what differs is what can still take it down. On macOS the job reparents to PID 1, which puts it out of reach of ANYTHING aimed at the app — a crash, a hard kill, even an explicit kill of the whole process tree. On Windows it survives the agent process dying by itself (a crash, or a taskkill /F /IM of that one process without /T) but is LOST to a TREE kill — Task Manager's 'End task', taskkill /T, or an installer that stops the app and everything it started; treat an auto-update as a tree kill unless that machine's installer is known to do otherwise — because Windows never reparents. On Linux a job started by an agent that already has this feature is launched into its own transient systemd scope (aic-job-<jobId>.scope), outside the agent service's control group, so stopping or restarting the service — including an agent upgrade — leaves it running; that requires a systemd host with the agent running as root, and without both jobs stay in the service's control group and a restart still ends them. A job that was already running when the agent was upgraded TO that version is in no scope either, and the restart the upgrade performs is what ends it — an upgrade is safe only for jobs started after it.

post/api/v1/exec

Request

  • Base URL: https://aicommander.dev
  • URL: https://aicommander.dev/api/v1/exec
  • Auth: one of:
    • none
    • HTTP bearer

Request body

codestring required

How the user named the machine, passed exactly as given: an AIC- session code, or — with an API key — a saved alias/hostname (e.g. 'wearfits-m3', 'aic-wearfits'). A string containing 'aic-' is almost certainly one of the user's machines.

commandstring required

Shell command to execute (maximum 32,768 characters). The dialect depends on the machine's platform and on the optional shell field: by default /bin/sh -c on 'darwin'/'linux' and cmd.exe on 'win32' — see the operation description, and note that a multi-line command is rejected on Windows unless shell is powershell.

cwdstring

Working directory on the remote machine (optional; maximum 4,096 characters).

timeout_msnumber

Timeout in milliseconds: minimum 1000 (1 s), default 300000 (5 min), maximum 3600000 (1 hr). VALIDATED, NOT CLAMPED — a value outside that range is rejected with 400 naming the field. In particular 0 is NOT 'no timeout': it is below the minimum, and it used to be raised silently to 1000 ms, killing the command after one second. Omit the field entirely to get the default.

envobject

Extra environment variables for this one command, applied on top of the machine's own environment: at most 128 entries, keys up to 256 characters, string values up to 8,192 characters. Non-string or oversized values reject the request with 400; they are not dropped or coerced. This is how you fix PATH or set LANG or an API token without wrapping the command in env … sh -c. NOT accepted together with elevated: true — that combination is refused with 400 rather than silently dropping the variables, because the privileged path runs through a signed capability that carries no environment. An elevated command must set its variables inside the command string itself.

shell'sh' | 'bash' | 'cmd' | 'powershell'

Which interpreter runs the command. Omit for the machine's default — /bin/sh -c on 'darwin'/'linux', cmd.exe on 'win32'. Windows machines accept cmd and powershell; macOS/Linux machines accept sh and bash. A value the target cannot run (e.g. powershell on a Mac, or a misspelling) is REJECTED with 403 and a message listing what that machine does accept — it is never silently replaced by the default. powershell is Windows PowerShell 5.1 run as -NoProfile -NonInteractive; the agent hands it the script base64-encoded, so a MULTI-LINE script is allowed there (unlike cmd) but the script is capped at roughly 3,000 characters and a longer one is rejected rather than truncated. On that path only, stderr is post-processed: the same encoding makes PowerShell serialize its error/warning/progress streams as CLIXML, so the agent strips the #< CLIXML framing and <Objs> envelope, drops the per-call module-loading progress records, and reassembles the <S> fragments into the text a console shows; anything it cannot positively identify as PowerShell's own framing (a truncated block, or CLIXML-shaped output the script printed itself) comes back verbatim, and cmd/sh/bash stderr is untouched. WHAT IT DOES NOT BUY YOU IS A TRUSTWORTHY EXIT CODE: a PowerShell non-terminating error — Write-Error, a failed cmdlet, most runtime errors — writes to the error stream and the script carries on, so the exit code tracks the last statement, not whether errors occurred. Measured: Write-Output "stdout-line"; Write-Error "this-is-a-real-error" returns exitCode: 0 with the error text in stderr — the exact shape of a success — and an error in the middle of a script that then does something successful leaves 0 just the same; a script whose final statement is the failing one returns exitCode: 1, which does not mean the error you care about happened either. Uninformative in both directions. That is PowerShell's own semantics, not something AI Commander does to the command, and neither cmd.exe nor POSIX shells behave that way. Under powershell read stderr rather than trusting exitCode: 0 on its own, and/or begin the script with $ErrorActionPreference = 'Stop' to make those errors terminating; nothing injects it for you, because it would change the script's own control flow. Cannot be combined with elevated: true. A machine whose agent is too old to honour this field REFUSES the call (503) instead of running the default shell.

elevatedboolean

Run as root (macOS) / LocalSystem (Windows) through the privileged helper. Account-only, and only honored on mac/Windows machines that have the helper installed. Most commands do NOT need this, and it cannot be combined with env or shell. An elevated command's lifetime is bound to the agent connection that requested it: the helper kills it the moment that connection drops, so a privileged process never outlives its authorization. A command that restarts, replaces or kills AI Commander itself is therefore self-defeating in-band — the app is the agent, so it drops the connection and the helper kills the command mid-run, returning Agent disconnected during command execution, which reads like a network error while the privileged work was cut in half. Launch such work detached instead: a scheduled task on Windows (schtasks /Run /TN "AI Commander Update"), a job (POST /api/v1/jobs) on macOS, a transient systemd unit outside the agent's control group on Linux.

Example request

{
  "code": "AIC-7K3P-WX9M-RTBN",
  "command": "df -h",
  "env": {
    "LANG": "C"
  }
}

Response

Command completed (any exit code).

oktrue required
exitCodeinteger required
durationMsinteger required
stdoutstring required
stderrstring required
truncatedboolean required

True when the combined stdout+stderr hit the 1 MiB cap and the REPLY was cut short. It says nothing about the command: the relay's stop request is best-effort and usually loses its race with the process, so a truncated reply commonly accompanies a command that ran to completion — exitCode here is that command's real exit code. Never read truncated: true as 'the command was stopped'; its side effects happened. Re-run something narrower (head/grep/tail), or use POST /api/v1/jobs, whose full output is kept on the machine and paged.

Changes

No recorded changes to this operation across all 1 revision of this API.