Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/irchaosclub/FANGS/llms.txt

Use this file to discover all available pages before exploring further.

Runner lifecycle in FANGS is explicit: a runner process registers itself once at startup, then repeatedly polls for work and sends heartbeats to keep its registration alive. Three endpoints manage this lifecycle. Registration validates the protocol version and returns timing parameters that govern the rest of the connection. Heartbeats carry optional in-flight status so the orchestrator can surface “what’s running where” without a separate polling mechanism. Job polling uses HTTP long-polling — the server holds the connection open for up to 25 seconds before responding 204 No Content, letting runners avoid tight polling loops.

POST /v1/runners/register

Registers a runner with the orchestrator. The runner calls this once at startup, before polling for jobs. If a runner with the same runner_id already exists in the registry, the previous record is replaced — this handles runner restarts transparently. Request body
{
  "runner_id": "prod-runner-1",
  "hostname": "worker-node-42.internal",
  "capabilities": ["sensor", "sandbox.docker"],
  "kernel_version": "6.8.0-51-generic",
  "proto_version": 1
}
runner_id
string
required
Stable operator-supplied identifier. Defaults to the runner’s hostname when not explicitly set. Used as the path parameter in all subsequent runner endpoints.
hostname
string
The machine hostname, for display in the UI and logs.
capabilities
array
Advertised capabilities, e.g. ["sensor", "sandbox.docker"]. The orchestrator logs these but does not yet use them for job routing — all registered runners are currently equivalent.
kernel_version
string
Output of uname -r. Logged at registration time; useful for debugging eBPF compatibility issues across kernel versions.
proto_version
number
required
Must equal 1 (the current CurrentProtoVersion). The orchestrator returns HTTP 400 if this does not match, preventing silent version-skew bugs when the wire format evolves.
Response — 200 OK
{
  "ok": true,
  "orchestrator_id": "fangs-orchestrator",
  "job_poll_interval": 5000000000,
  "heartbeat_interval": 30000000000
}
ok
boolean
true when registration succeeded.
orchestrator_id
string
The orchestrator’s stable identifier. The runner logs this for cross-referencing in multi-orchestrator deployments.
job_poll_interval
number
How frequently the runner should call GET /v1/runners/{id}/jobs when idle, in nanoseconds. Currently 5000000000 (5 seconds).
heartbeat_interval
number
How frequently the runner should call POST /v1/runners/{id}/heartbeat, in nanoseconds. Currently 30000000000 (30 seconds). Runners not seen for 3× this interval (90 seconds) are evicted.
Error responses
StatusCondition
400runner_id is missing or empty
400proto_version does not match the orchestrator’s CurrentProtoVersion
Re-registering an already-known runner_id silently replaces the existing record. This is the intended recovery path when a runner restarts or the orchestrator restarts with existing runners.

POST /v1/runners//heartbeat

Keeps a runner’s registration alive and optionally reports in-flight status. The orchestrator updates the runner’s LastSeen timestamp on every heartbeat. Runners not seen for more than 90 seconds are pruned from the in-memory registry by the background pruner. Path parameter
id
string
required
The runner_id supplied during registration.
Request body The body is optional. If Content-Length is zero the orchestrator still processes the heartbeat and only updates LastSeen.
{
  "runner_id": "prod-runner-1",
  "active_run_id": "a1b2c3d4e5f60708090a0b0c0d0e0f10",
  "status": "running",
  "events_queued": 32
}
runner_id
string
Echo of the runner’s own ID. Informational — the path parameter is authoritative.
active_run_id
string
Hex-encoded run ID of the job currently being processed. Omitted or empty string when the runner is idle. The orchestrator stores this for the UI’s “what is this runner doing” display.
status
string
Runner’s self-reported status. Valid values: "idle", "running", "draining". Omitted when idle.
events_queued
number
Depth of the runner’s internal outbound event buffer. A consistently high value indicates the runner is producing events faster than it can flush them to the orchestrator.
Response — 200 OK
{"ok": true}
If the runner is not in the orchestrator’s registry (e.g. after an orchestrator restart):
{"ok": false, "unknown_runner": true}
ok
boolean
true when the runner is recognized and the heartbeat was recorded. false when the runner is unknown.
unknown_runner
boolean
Present and true when ok is false. The runner must call POST /v1/runners/register to rejoin before it can receive jobs.
When unknown_runner: true is received, the runner must re-register immediately. This happens after an orchestrator restart, since runner state is held in memory and not persisted across restarts.

GET /v1/runners//jobs

Long-polls for the next job assigned to this runner. The server holds the connection open for up to 25 seconds. If a job becomes available within that window, it is returned immediately with HTTP 200. If the wait expires with no work, the server responds HTTP 204 No Content and the runner polls again after job_poll_interval. The 25-second cap is deliberate: it stays safely below common load-balancer and proxy idle-connection timeouts while still dramatically reducing polling frequency compared to a naïve short-poll. Path parameter
id
string
required
The runner_id supplied during registration.
Response — 200 OK (job available)
{
  "run_id": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  "kind": "sandbox_scan",
  "package_name": "axios",
  "version": "1.7.9",
  "cgroup_path": "",
  "watched_paths": [
    {"prefix": "/etc/", "cred_tagged": false},
    {"prefix": "/etc/shadow", "cred_tagged": true},
    {"prefix": "/root/.ssh/", "cred_tagged": true},
    {"prefix": "/tmp/"}
  ],
  "duration": 60000000000,
  "dispatched_at": "2025-01-15T10:23:00Z",
  "sandbox": {
    "image": "node:20-slim",
    "command": ["sh", "-c", "cd /tmp && mkdir -p test && cd test && npm init -y >/dev/null 2>&1 && npm install axios@1.7.9 2>&1 | tail -3; sleep 2"],
    "network_mode": "bridge",
    "pull_policy": "missing",
    "user": "0:0",
    "grace_period": 2000000000,
    "cgroup_parent": "/sys/fs/cgroup/fangs/"
  }
}
run_id
array
16-byte binary run identifier encoded as a JSON array of 16 unsigned integer values (0–255). Use the hex string returned by POST /v1/scans for display and database lookups.
kind
string
Job type. Two shapes are supported:
package_name
string
npm package name. Used as the baseline-keying identifier — all runs for the same package share a baseline fingerprint table.
version
string
Package version string, e.g. "1.7.9".
cgroup_path
string
Relevant only for sensor_only jobs. Ignored for sandbox_scan.
watched_paths
array
Paths the eBPF sensor should observe for file-access events. Each entry has:
duration
number
How long the runner should let the sandbox run, in nanoseconds. Default is 10 seconds for sensor_only, set by the orchestrator when the job’s duration was zero.
dispatched_at
string
RFC 3339 timestamp stamped by SubmitScan when the job was enqueued.
sandbox
object
Present only for sandbox_scan jobs. Describes the container the runner must spawn.
Response — 204 No Content (no work available) Empty body. The runner should wait job_poll_interval before polling again. Response — 404 Not Found
{"error": "runner not registered; POST /v1/runners/register first"}
The runner must register before polling for jobs.
The default watched-path set — /etc/, /root/, /tmp/, /usr/, credential-tagged paths like /etc/shadow and /root/.ssh/ — is stamped by the orchestrator onto every job whose watched_paths field is empty. You can override it by explicitly populating watched_paths in a POST /v1/scans body.

Build docs developers (and LLMs) love