Skip to content
DocsAll docs
Console

Tool server API

Lists the tools and the routes of the tool server of a workspace.

Last updated

On this page

Reaching it

The address comes from GET /v1/workspaces/{id}/tools, which answers the workspace's owner alone:

json
{ "address": "10.42.1.17:7788", "token": "eyJhbGciOi…" }

The address is reachable from your session and through the ssh tunnel. From a laptop, kloudlite workspaces ide <workspace> opens the tunnel and prints the local address, normally http://localhost:7788.

Every route except /healthz needs Authorization: Bearer <token> with the workspace's own token. The ssh tunnel sends none, so a tunnel alone is not enough. Get the token:

  • From outside, the token field of the response above. It is left out until the platform has written it.
  • Inside the workspace, the file /etc/kloudlite/ssh/workspace-token. The server reads it on every request. The platform renews it about every five minutes. Read it again after a 401 instead of keeping a copy.

A request with no token, a wrong token, or a server that cannot read its own token file is 401 with WWW-Authenticate: Bearer and {"error": "the tool server needs the workspace's token"}. The examples below assume:

CLI
export TOKEN=<the workspace token>export TOOLS=http://localhost:7788

A sandbox has no tool server. It runs one command, and you read its output with logs or type to it with attach.

The HTTP shape

GET /tools

Lists every tool with its JSON schema, and the state of the workspace's fences.

CLI
curl -H "Authorization: Bearer $TOKEN" $TOOLS/tools
json
{  "tools": [{ "name": "read", "description": "Read text files with line numbers. …", "schema": { "type": "object", "properties": {}, "required": [] } }],  "fence": { "sandbox": "active", "network": "open" }}

fence.sandbox is active when exec runs inside the sandbox wrapper, and unavailable when it runs unwrapped. fence.network is fenced when the outgoing-traffic fence is up, otherwise open.

POST /tools/{name}

Runs one tool. The JSON body is the tool's arguments; no body means no arguments.

CLI
curl -X POST -H "Authorization: Bearer $TOKEN" $TOOLS/tools/exec -d '{"cmd":"ls"}'
json
{ "exit_code": 0, "stdout": "Cargo.toml\nsrc\n", "stderr": "", "truncated": false, "timed_out": false, "ms": 6 }

A tool's own answer is the response body with status 200. Anything that stopped it from running is a status with {"error": "…"}:

StatusMeaning
400A missing required argument, an argument of the wrong type, a value outside an enum, an unknown tree, or an absolute path outside the working directory
401No or wrong token
403A path that resolves outside the working directory
404An unknown tool
413A body over 11 MiB, the file limit plus room for the rest of the JSON
500The tool ran and failed

Arguments are checked against the schema once, before any tool runs, and the error names the field. The server accepts a JSON-encoded array or object in a string where the schema declares one. It accepts a decimal string for a number and "true" for a flag. Only top-level keys are checked; nested shapes are checked by the tool.

Working directories and paths

Every tool takes an optional tree: the name of an extra working directory under the .agents/ folder of the workspace. Left out, or main, it means the workspace itself. ?tree=name on the URL does the same, and a tree in the body wins when both are given. Every path in and out of a call is relative to the root of the working directory, which is also the default cwd. An absolute path under the root is accepted as that path relative to the root. Any other absolute path is 400, and a path that resolves outside the root, symlinks followed, is 403 naming the path.

Files are limited to 10 MiB for read and write. At most 50 files go in one read or edit call.

Other routes

RouteAnswer
GET /healthz{"ok": true, "root": "<workspace>", "trees": ["main"]} with no token. trees lists the working directories served so far, main first
POST /hold204. Counts as activity, so the workspace's idle stop waits. For a long local job that holds no connection
GET /stream/process/{id}WebSocket, see Streams
GET /stream/process/{id}/linesThe same frames as NDJSON over plain HTTP
GET /stream/watch/{id}WebSocket, see Streams

/fs/tree, /fs/stat, /fs/file, /fs/git, /fs/changes, /fs/diff and /fs/log also exist. They are what a console renders a workspace from. They are read-only, answer with an ETag and honor If-None-Match, are not tools, and never appear in GET /tools.

Files

read

Read text files with line numbers. A binary file answers its size and mime type and no bytes.

ArgumentTypeRequiredRule
pathstringnoOne file. Give path or paths
pathsarrayno1 to 50 entries, each a path string or {path, offset, limit}. Each file gets its own answer
offsetintegernoFirst line, 1-based. Default 1
limitintegernoLines to return. Default 2000
treestringno

Result for one file: {path, content, total_lines, truncated}, each line prefixed with its number and a tab. truncated is true when lines remain past limit. For a binary file: {path, binary: true, size, mime}. With paths: {files: [...]}, one answer per entry in order, an entry that failed being {path, error}. A file over 10 MiB is a 500 naming its size.

write

Create or overwrite a file atomically, creating parent directories.

ArgumentTypeRequiredRule
pathstringyes
contentstringyesAt most 10 MiB
treestringno

Result: {path, bytes, created}.

edit

Make exact string replacements, all or nothing across the whole call: if one edit does not apply, nothing is written.

ArgumentTypeRequiredRule
pathstringnoOne file. Use with edits
editsarraynoObjects {old, new, replace_all?}. old and new are strings, replace_all a boolean. old must occur exactly once unless replace_all is true
filesarrayno1 to 50 objects {path, edits}, for several files in one atomic call
treestringno

Result for one file: {path, applied}. With files: {files: [{path, applied}], applied}, where applied is the number of edits.

patch

Apply a unified diff, git or plain -p1 format. The diff is checked first, so it is never applied in part. It takes fewer tokens than edit for a large rewrite.

ArgumentTypeRequiredRule
diffstringyes
cwdstringnoThe directory the diff's paths are relative to. Default the working directory root
treestringno

Result: {cwd, files}, the files it touched.

glob

List the files that match a glob under cwd, honoring gitignore, newest first.

ArgumentTypeRequiredRule
patternstringyes
cwdstringnoDefault the working directory root
treestringno

Result: {cwd, paths, truncated}. At most 5000 paths; truncated says there were more.

grep

Search file contents with a regex under cwd, honoring gitignore. A file over 10 MiB is skipped.

ArgumentTypeRequiredRule
patternstringyesA regex
cwdstringnoDefault the working directory root
globstringnoNarrows the files searched
modestringnocontent (default), files or count
contextintegernoLines around each match, content mode. Default 0
ignore_casebooleannoDefault false
maxintegernoMost results. Default and ceiling 2000
treestringno

Result by mode: content is {cwd, matches: [{path, line, text, context}], truncated}, with context null when context is 0. files is {cwd, files, truncated}. count is {cwd, counts: [{path, count}], truncated}.

Commands and processes

exec

Run a command as the workspace user in the working directory. A string cmd runs as sh -c (not a login shell), an array runs as an argv. Without detach the command is a job: the call waits and answers. With detach: true it becomes a process and the call answers at once.

ArgumentTypeRequiredRule
cmdstring or array of stringsyesA shell string, or an argv. An empty array is 400
cwdstringnoDefault the working directory root
envobjectnoExtra environment variables
timeout_msintegernoHow long to wait. Default 120000, maximum 600000
timeoutnumbernoThe same wait in seconds. Give this or timeout_ms
headintegernoKeep only the first N lines of each stream
tailintegernoKeep only the last N lines of each stream
quietbooleannoAnswer the exit code alone, plus the last 20 lines of stderr on failure
detachbooleannoReturn {id} and keep running
ptybooleannoNot supported in this version
treestringno

Result for a job: {exit_code, stdout, stderr, truncated, timed_out, ms}. truncated is true when output passed the 4 MiB capture limit. At the timeout, exit_code is -1, timed_out is true, and the process group is killed.

When head, tail or quiet dropped lines, trimmed: true is added. Result for detach: {id}. At most 32 processes run at once. An exited process stays listed for 10 minutes.

CLI
curl -X POST -H "Authorization: Bearer $TOKEN" $TOOLS/tools/exec \  -d '{"cmd":"npm run dev","detach":true}'# {"id":"p1"}

process_list

List every detached process.

ArgumentTypeRequiredRule
treestringno

Result: {processes: [{id, cmd, started_at, state, exit_code, failed}]}. state is running or exited. failed carries a note when the process did not start or did not start again, for example port 3000 in use.

process_output

Read a process's output since a byte offset. The server keeps the last 4 MiB of each stream.

ArgumentTypeRequiredRule
idstringyes
sinceintegernoOffset into stdout. Default 0, the start
since_errintegernoOffset into stderr. Default 0
treestringno

Result: {stdout, stderr, next, next_err, dropped, dropped_err, state, exit_code}. To page, pass back next and next_err. dropped is how much the ring had already discarded before since.

process_write

Write to a process's stdin. It fails after process_close_stdin or once the process has exited.

ArgumentTypeRequiredRule
idstringyes
datastringnoText. Give data or data_base64, not both
data_base64stringnoBytes, standard base64
treestringno

Result: {bytes}.

process_close_stdin

Close a process's stdin, so a command that reads to the end of its input (cat, a while read loop) finishes. Closing it again is not an error.

ArgumentTypeRequiredRule
idstringyes
treestringno

Result: {closed: true}.

process_wait

Wait until a process exits or the wait runs out. Once it answers exited, everything the process printed is readable with process_output.

ArgumentTypeRequiredRule
idstringyes
timeout_msintegernoDefault and maximum 20000
timeoutnumbernoThe same wait in seconds
treestringno

Result: {state, exit_code, timed_out}.

process_kill

Stop a process: TERM, then KILL after 5 seconds. The signal reaches the whole process group.

ArgumentTypeRequiredRule
idstringyes
signalstringnoTERM (default) or KILL, which skips the wait
treestringno

Result: {state}.

Watches

At most 32 watches run at once.

watch

Start a watch and return its id. Give paths or cmd.

ArgumentTypeRequiredRule
pathsarray of stringsnoEvents are file-system changes under these paths, recursively
cmdstring or array of stringsnoEvents are the output lines of this command
patternstringnoA regex that filters the lines
oncebooleannoEnd the watch at the first event
treestringno

Result: {id}.

watch_poll

Get the events since an index. The server keeps the last 1000.

ArgumentTypeRequiredRule
idstringyes
sinceintegernoIndex to read from. Default 0, the start

Result: {events, next, dropped, state}. An event is {path, kind} for a path watch, {line} for a line of a command's output, or {exit} when the command ended. Pass next back as since.

watch_stop

Stop a watch, and its command if it has one.

ArgumentTypeRequiredRule
idstringyes

Result: {state}.

Containers

Both run kl container in the working directory. The build goes to your owner's hidden builder.

container_build

Build an image and push it to your registry.

ArgumentTypeRequiredRule
tagsstring or array of stringsyesAt least one, each name[:tag]. hello:1 is pushed as <registry>/<you>/hello:1, and team/hello:1 pushes under the team
contextstringnoA path in the working directory. Default .
filestringnoThe Dockerfile, a path in the working directory
build_argsstring or array of stringsnoKEY=VALUE entries
platformstringno
no_cachebooleanno
timeout_msintegernoDefault and maximum 600000
timeoutnumbernoThe same wait in seconds
headintegernoLines kept of each stream
tailintegernoLines kept of each stream. With neither head nor tail, each stream keeps its last 40 lines
treestringno

Result: {exit_code, stdout, stderr, truncated, timed_out, ms}, with trimmed: true when lines were dropped.

container_push

Copy an image the registry already holds to more names. It does not rebuild.

ArgumentTypeRequiredRule
srcstringyesFor example hello:1
dststring or array of stringsyesAt least one, for example [hello:latest]
treestringno

Result: the same shape as container_build.

Streams

Both stream routes need the same bearer header on the upgrade request, and send one JSON object per text frame. Add X-KL-Watch: 1 to a connection that only feeds a sidebar: it does not count as activity for the idle stop.

GET /stream/process/{id}?since=&since_err= replays the ring from the two offsets (default 0), then sends every frame as it arrives, then the exit, and closes. GET /stream/process/{id}/lines sends the same objects as NDJSON, application/x-ndjson, one per line, ending after the exit.

FrameMeaning
{"stream": "stdout" or "stderr", "data": "…"}Output, as lossy UTF-8
{"dropped": n, "dropped_err": n}First frame, when an offset is behind what the ring still holds
{"dropped_frames": n}The reader fell behind and n frames were skipped
{"exit": code}The process ended. The last frame

GET /stream/watch/{id}?since= replays events from an index, then follows. Its frames are the watch's events ({path, kind}, {line}, {exit}), {"dropped_events": n}, and {"state": "stopped"} when the watch was stopped. An unknown id is 404 with the text no process {id} or no watch {id}.