NevoFlux
Headless (Docker)

Task API & interfaces

The headless HTTP task API — submitting tasks, follow-up turns, per-turn usage, cancellation — plus which interface lives on which port, session and fixed-script modes, and the full CLI-flag and env-var reference.

In service mode the daemon serves a small HTTP task API on --http-addr. All endpoints are JSON. The same port also carries the OpenAI / Anthropic and A2A front-ends; MCP, ACP and the admin API get ports of their own — see Interfaces and ports.

Endpoints

MethodPathPurpose
POST/tasksSubmit a task → {"id":"task-N"}
GET/tasks/:idStatus / result snapshot (404 for an unknown id)
GET/tasks/:id/eventsSSE — status frames until terminal
DELETE/tasks/:idCancel → 202 Accepted, or 404 for an unknown id
GET/metricsPrometheus counters
POST/session/closeSession mode: tear down the reused browser session
GET/tasks/:id/artifacts/:nameDownload a file the task left in its workspace (part of A2A)

No authentication. Nothing on this port checks credentials — not /tasks, not /session/close, not the OpenAI / Anthropic endpoints. Anyone who can reach it runs tasks with the container's provider key and browser. Keep it on a private network or behind an authenticating proxy.

Submitting a task

curl -X POST localhost:8080/tasks -H 'Content-Type: application/json' \
  -d '{"task":"open example.com and report the title","mode":"browser"}'
# → {"id":"task-0"}

The request body is the task contract:

FieldTypeDefaultMeaning
taskstring— (required)The instruction for the agent.
followupsobject[][]Further turns of the same conversation, run after task — see Follow-up turns. (v0.3.16)
modestring"browser"Agent mode.
profilestringnoneNamed base-profile to clone (login state); omit for a blank base.
policy.allow_shellboolfalseAdmit shell tools (run_command, bash).
policy.allow_fs_writeboolfalseAdmit filesystem-write tools.
policy.allow_uploadboolfalseAdmit uploadFile.
policy.domain_allowliststring[][]Restrict navigate / web_fetch to these domains (empty = any).
wall_clock_secsintnonePer-task wall-clock deadline.
token_budgetintnonePer-task token-spend cap.
idempotentboolfalseRetry even after a mutating tool ran (caller asserts idempotency).
no_retryboolfalseDisable auto-retry entirely.
end_sessionboolfalseSession mode only — tear down the shared browser after this task.
save_profileboolfalseSession mode only — persist the clone back to a base profile at teardown (implies ending the session).
save_profile_asstringthe cloned baseOptional base name to save-as.
backendstringnonePath of a script backend to run instead of the agent loop — see Choosing a backend per task.

Policy defaults to fully locked down. With no policy, a task gets no shell, no fs-write, no upload, and any domain. Grant capabilities per task, only as needed.

A full request:

{ "task": "open example.com and report the title",
  "mode": "browser",
  "profile": "base1",
  "policy": { "allow_shell": false, "allow_fs_write": false,
              "allow_upload": false, "domain_allowlist": ["example.com"] },
  "wall_clock_secs": 300, "token_budget": 200000,
  "idempotent": false, "no_retry": false }

Failed attempts are retried automatically — at most 3 attempts in total, and only if the failed attempt dispatched no mutating tool, unless idempotent is set. no_retry turns retry off.

Status / result

curl -s localhost:8080/tasks/task-0
{ "id": "task-0",
  "status": "succeeded",           // queued | running | succeeded | failed | canceled
  "attempts": 1,
  "output": "The title is Example Domain.",
  "artifacts": [],                 // file names left in the task workspace
  "session_id": "automation-task-0-1",
  "usage": [ { "main": { "input": 5120, "output": 210, "calls": 2, "cache_read": 4096 },
               "last_input": 2860, "first_token_ms": 820, "total_ms": 9400,
               "model": "claude-..." } ] }
  • canceled is its own status since v0.3.16. A cancelled task used to report failed with error: "cancelled"; it now reports canceled and keeps that error string. Clients that match exhaustively on status need updating.
  • error appears on failure; output and error are omitted when empty.
  • session_id (v0.3.16) is the session of the last attempt, the id to give nevoflux-agent session export when you want the full event log.
  • usage (v0.3.16) has one entry per agent turn that called the LLM: main (input / output tokens, call count, and the prompt-cache cache_read / cache_write portions of input when the provider reports them — Anthropic traffic uses prompt caching since v0.3.16), subagent when subagents ran, last_input (the context size of the last call), timing (first_token_ms, decode_ms, total_ms) and model. Absent when no LLM call ran — for example in fixed-script mode. estimated: true marks numbers that were estimated rather than reported by the provider.
  • turn_outputs (v0.3.16) is present when the task had follow-ups: each turn's final text, in order. output is the last of them.

The task also writes result.json and a debug-bundle/ into its workspace, ws-<task-id> under NEVOFLUX_PROFILE_WORK. The image does not set that variable, so the workspace lands in /tmp/nevoflux-profiles/ — on the tmpfs, not in the /work mount. Files there can be fetched with GET /tasks/:id/artifacts/:name. If you point NEVOFLUX_PROFILE_WORK at /work to collect results on the host, note that the per-task profile clones (with their cookies) are created in the same directory while tasks run.

Live events (SSE)

curl -N localhost:8080/tasks/task-0/events
# event: status
# data: {"id":"task-0","status":"running","attempts":0,...}
# event: status
# data: {"id":"task-0","status":"succeeded","output":"...Example Domain","attempts":1,...}

A status frame is emitted on each change (plus the terminal one), with keep-alive comments in between. The stream ends when the task reaches succeeded, failed or canceled.

Cancel & metrics

curl -i -X DELETE localhost:8080/tasks/task-0   # → 202 Accepted (404 if unknown)
curl localhost:8080/metrics
# nevoflux_tasks_total / nevoflux_tasks_failed / nevoflux_browser_restarts

DELETE marks a queued or running task canceled at the queue level. It does not interrupt an attempt that is already running: that attempt runs to its own end (bounded by wall_clock_secs), and its final result can replace the canceled status. Give tasks a wall clock if you need cancellation to bound cost.

Follow-up turns

New in v0.3.16. followups runs further turns of the same conversation in one task — same session id, same browser, with the plain text of earlier turns carried as history, the way a chat carries it:

curl -X POST localhost:8080/tasks -H 'Content-Type: application/json' -d '{
  "task": "open example.com and report the title",
  "followups": [
    {"message": "now summarise the first paragraph"},
    {"message": "and list every link on the page", "delay_secs": 30}
  ]}'
  • delay_secs waits before sending that turn (for example, to let a prompt cache expire when you are measuring it).
  • Turns stop at the first failed turn — later ones would be answering a conversation that went wrong.
  • turn_outputs holds each turn's answer and usage one entry per turn.
  • Follow-ups apply to the agent loop; a script backend runs once.

Choosing a backend per task

backend names a script backend file for this one task, instead of the agent loop (or the process-wide NEVOFLUX_HEADLESS_SCRIPT). Its typical use is signing a script backend in: run a waiting script as a task with a long wall clock, log in through VNC while it waits, and save the clone as a base profile.

curl -s localhost:8080/tasks -H 'content-type: application/json' -d '{
  "task": "sign in",
  "backend": "/opt/nevoflux/scripts/login-wait.py",
  "profile": "gemini",
  "wall_clock_secs": 900,
  "save_profile": true,
  "save_profile_as": "gemini"
}'

backend is any .py path readable inside the container, and the task API is unauthenticated — so anyone who can reach /tasks can run any script that exists in the image or its mounts. Scripts run in the sandboxed interpreter, but they drive the browser without the task policy's limits. Don't mount scripts you would not let a caller run.

Interfaces and ports

Every front-end reduces a request to the same task queue. Which port serves what:

InterfaceEndpoint(s)On --http-addrDedicated flagDocs
Task API/tasks…, /metrics, /session/closeyes—this page
OpenAI Chat Completions, Responses, models/v1/chat/completions, /v1/responses, /v1/modelsyes--openai-addrOpenAI & Anthropic APIs
Anthropic Messages/v1/messagesyes--anthropic-addrOpenAI & Anthropic APIs
A2A/.well-known/agent-card.json, /a2a, /a2a/v1yes--a2a-addrA2A
MCP (Streamable HTTP)/mcpno--mcp-addrMCP endpoint & admin API
ACP (JSON-RPC 2.0)/acpno--acp-addrbelow
Admin API/admin/…no--admin-addr (+ NEVOFLUX_ADMIN_TOKEN)MCP endpoint & admin API
# everything on its own port
nevoflux-agent --daemon --headless \
  --http-addr 0.0.0.0:8080 --openai-addr 0.0.0.0:8081 \
  --mcp-addr 0.0.0.0:8082 --acp-addr 0.0.0.0:8083 \
  --a2a-addr 0.0.0.0:8084 --anthropic-addr 0.0.0.0:8085 \
  --admin-addr 127.0.0.1:8086

A dedicated flag adds a listener; it does not remove the endpoint from --http-addr. In particular the main port always answers A2A and the OpenAI / Anthropic formats.

ACP

A minimal ACP endpoint: initialize, session/new and session/prompt. The prompt's text blocks are joined into a task; the answer comes back as one text block, with no streaming session/update notifications.

curl -X POST localhost:8083/acp -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"session/prompt",
       "params":{"prompt":[{"type":"text","text":"open example.com, report title"}]}}'
# → {"result":{"stopReason":"end_turn","content":[{"type":"text","text":"...Example Domain"}]}}

Env-var defaults for prompt-only interfaces

The OpenAI / Anthropic, MCP run_browser_task, ACP and A2A front-ends carry only a prompt, so mode / profile / policy / caps come from the environment. POST /tasks still takes all of these per request — the env vars are only the defaults for the interfaces that can't carry them.

Env varFieldDefault
NEVOFLUX_TASK_MODEmodebrowser
NEVOFLUX_TASK_PROFILEprofilenone
NEVOFLUX_POLICY_ALLOW_SHELLpolicy.allow_shellfalse
NEVOFLUX_POLICY_ALLOW_FS_WRITEpolicy.allow_fs_writefalse
NEVOFLUX_POLICY_ALLOW_UPLOADpolicy.allow_uploadfalse
NEVOFLUX_POLICY_DOMAIN_ALLOWLISTpolicy.domain_allowlistempty (comma-separated)
NEVOFLUX_WALL_CLOCK_SECSwall_clock_secsnone (A2A streams: 900)
NEVOFLUX_TOKEN_BUDGETtoken_budgetnone
NEVOFLUX_IDEMPOTENTidempotentfalse
NEVOFLUX_NO_RETRYno_retryfalse
NEVOFLUX_SAVE_PROFILEsave_profilefalse
NEVOFLUX_SAVE_PROFILE_ASsave_profile_asnone

Booleans accept 1 / true / TRUE / yes.

Session mode

By default every task is fully isolated: clone a fresh profile → launch a browser → run → kill the browser and delete the clone. Set NEVOFLUX_SESSION_MODE=1 to run a task-flow instead: tasks share one browser + profile clone (the active tab is soft-reset to about:blank between tasks) until you end the flow.

# NEVOFLUX_SESSION_MODE=1 in the environment
curl -X POST localhost:8080/tasks -d '{"task":"log in to example.com"}'          # uses the shared browser
curl -X POST localhost:8080/tasks -d '{"task":"go to the dashboard"}'            # reuses it (shared login)
curl -X POST localhost:8080/tasks -d '{"task":"export the report","end_session":true}'  # runs, then tears down
# or end out-of-band at any time:
curl -X POST localhost:8080/session/close        # → {"closed":true|false,"saved_to":...,"save_error":...}
  • Pre-warmed browser (v0.3.14). With session mode on, the daemon launches the shared browser at startup, so the first task (or the first MCP browser_* call) does not pay for — or fail on — a cold start. A failed pre-warm is logged and retried by the next task. (Skipped when the in-process engine serves browser tools.)
  • Drive tasks sequentially — submit, wait for it to finish, then submit the next. The shared browser is serialized by a lock, but task order is the caller's job.
  • Shared state within a flow — tasks share cookies / localStorage / login. A prompt-injected task can affect later tasks in the same flow, so use session mode only for trusted, sequential flows. Cross-flow isolation is preserved (the next flow clones a fresh base profile).
  • Crash recovery — if the shared browser crashes mid-flow, the next task relaunches it against the same clone (cookies persist on disk, so login survives).
  • Soft reset — skipped for script-backend tasks, which manage their own tabs (blanking a tab a script reuses would force a cold reload on every request). NEVOFLUX_SESSION_SOFT_RESET=0 turns it off for all tasks.

Persist login back to a base. Set save_profile: true on a task (or POST /session/close with {"save": true, "as": "<name>"}) to write the session's clone back to a base profile at teardown (save_profile_as / as picks a different base name). This requires the base-profiles mount to be writable (drop the :ro).

A2A contexts use the same task-flow machinery whether or not NEVOFLUX_SESSION_MODE is set, and POST /session/close also releases a bound A2A context.

Fixed-script mode (no LLM)

For a deterministic browser-use pipeline that needs no LLM provider, point the daemon at a Python script:

NEVOFLUX_HEADLESS_SCRIPT=/opt/nevoflux/fixed-flow.py

When set, every headless task runs that script instead of the LLM agent loop (unless a request picks another backend — see model routing and backend). The script defines def run(task): ...; the daemon calls it with the interface's task string. Whatever run returns (or prints) becomes the output; a raised exception → status: "failed" with the error. It uses the same browser tools as the agent, in the sandboxed Monty interpreter, driving the bound headless browser — but with zero LLM calls and no API key (and no GBrain needed). A script may instead define def chat(request) to receive the full script-backend contract.

def run(task):
    nav = browser_navigate(url="https://example.com/search")
    tab = nav["tab_id"]                              # navigate opens a NEW tab
    browser_fill(selector="#q", value=task, tab_id=tab)
    browser_click(selector="button[type=submit]", tab_id=tab)
    browser_wait_for(selector="#results", tab_id=tab, timeout_ms=15000)
    return browser_get_markdown(tab_id=tab)["markdown"]

Gotchas:

  1. browser_navigate opens a new, inactive tab and returns {"tab_id": N} — thread that tab_id into every later call, or tools hit "No active web tab found".
  2. Tool results are structured dicts, not strings — e.g. browser_get_markdown(...) → {"markdown","title","url","success"}. Index the field.
  3. Monty is a Python subset, upgraded in v0.3.14 (it now runs much more ordinary Python than before). Unsupported constructs fail loudly — decorators, for instance, are rejected rather than silently ignored.
  4. The script's time budget is the task wall clock minus a 10-second margin (metadata.budget_secs in a chat() request). Monty has no clock, so bound polling loops by round count.

Mount the script and set the env var (Compose):

environment:
  NEVOFLUX_HEADLESS_SCRIPT: /opt/nevoflux/fixed-flow.py
volumes:
  - ./fixed-flow.py:/opt/nevoflux/fixed-flow.py:ro

Template scripts ship at deploy/headless/examples/fixed-flow.py (basic navigate → fill → click → read) and fixed-flow-advanced.py (multi-step pagination

  • try/except, always returns a structured {ok, ...} dict).

CLI flags reference

The headless deployment flags on the nevoflux-agent binary:

FlagMeaning
--daemonRun as the core daemon. Required for every mode on this page.
--headlessHeadless automation mode: spawn a browser per task and serve the interfaces below.
--remote-controlRun a phone-driven head with no HTTP surface — see Remote control (container). (v0.3.14)
--http-addr <addr>Task API; also OpenAI, Anthropic and A2A routes.
--openai-addr <addr>OpenAI Chat Completions, Responses and /v1/models on a dedicated port.
--anthropic-addr <addr>Anthropic Messages on a dedicated port. (v0.3.16)
--a2a-addr <addr>A2A card + JSON-RPC paths on a dedicated port. (v0.3.16)
--mcp-addr <addr>MCP over Streamable HTTP (/mcp). Only served here.
--acp-addr <addr>ACP over HTTP (POST /acp). Only served here.
--admin-addr <addr>Token-protected admin API; not mounted without NEVOFLUX_ADMIN_TOKEN. Bind it to a private interface. (v0.3.14)

To use a config.toml somewhere other than ~/.config/nevoflux/, set NEVOFLUX_CONFIG=<path> (v0.3.16); it takes precedence over the default location, and an empty value counts as unset. (The binary also accepts a --config flag, but as of v0.3.16 the daemon does not read it — use the environment variable.)

Not to be confused with --mcp (no --daemon), which runs the separate stdio MCP server for Claude Code integration — a different mode from the headless MCP-over-HTTP interface above.

On this page