contenox
Browse docs/

contenox serve: the standing host

The three shapes contenox runs in are partitioned by one question: who is accountable for the machine.

In contenox beam that is you, at the keyboard, on a device you own. In contenox run it is whoever wrote the script. contenox serve is the third answer: an organization is accountable for a host that nobody is sitting at.

contenox serve

That difference is not administrative. It decides what the host is allowed to be, and the rest of this page follows from it.

One workspace, fixed at launch

A host serves exactly one workspace, chosen when you start it and immutable until you stop it:

contenox serve              # your home directory
contenox serve .            # the directory you are standing in
contenox serve ~/src/api    # one project

With no path the host serves your home directory, not the directory you started it from. A host outlives the shell that launched it and is reached from a device that knows nothing about where that shell was standing, so scoping it to the launch directory would make its scope depend on an invisible detail. contenox serve . is how you ask for the narrow scope, and it says so where the next person can read it.

There is no workspace selection anywhere in the system — no picker in beam, in an editor, or in the app. A client does not choose where a session runs; it discovers instances and the sessions they are already holding, and the workspace is whatever the instance was launched with. Serving a second workspace means starting a second instance, which is also how you say, in the process list, that it is a different thing.

No filesystem. No terminal. Ever.

A host has no local_fs and no local_shell, under any policy, on any run. This is not a strict preset — those tools are absent from the shape.

They are absent because the client that would carry them is absent. local_fs and local_shell are forwarded to a connected client’s fs/* and terminal/* capabilities: beam performs them on your machine, an editor performs them in the project you have open. A standing host has no such client, and a host that performed them itself would be an agent with a shell on a shared box that nobody is watching.

So every capability a host has is an MCP server you attached, or an HTTP service wrapped from an OpenAPI spec:

contenox mcp add notion https://mcp.notion.com/mcp --auth-type oauth
contenox tools add erp_billing --url https://erp.internal.example.com --spec ./billing-subset.yaml

That constraint is what makes the host reviewable. The set of things it can touch is a list you wrote and can read back, each entry a named service with its own credentials, rather than “whatever is on that disk”. Everything else is unchanged: the same declarations, the same envelope, the same durable asks.

What drives it

Nobody types into a host. Work reaches it two ways.

Through the relay. A paired machine holds a connection open, and the contenox app attaches over it — reading a transcript, answering an ask, starting a session against the workspace the host was launched with. See Pairing a machine with a relay.

Through triggers. Internal domain events land in a durable log, and operator-authored trigger-*.json files fire chains from them — a mission report, a status change, an ask that has been waiting too long. Opt-in and beta; see Events & triggers.

Either way the envelope is what bounds it. A host is the shape where that matters most, because there is no human in the loop by default — only the ones your policy stops for.

What it prints

The host checks its own setup before serving and then reports what the process actually is:

  Setup        ready
  Workspace    /home/you
  ID           8d7db1ed-329d-4300-83ac-ad325c2e8d75
  Model        vertex-google/gemini-2.5-pro
  Relay        attached to https://relay.contenox.com
  Instance     ca2e8376-99ab-4669-9ca4-b32e5605bb4d
  App          https://app.contenox.com
  Logs         ~/.contenox/logs/serve-2026-08-15.log
               new part at 50MB · keep 4 files · 7 days

  Running. Press Ctrl-C to stop.

Every line is a fact about this process rather than a claim about what should be true. The setup row runs the same readiness check as contenox doctor; a host with no model configured says so and still serves, because being reachable is worth something even when nothing can run yet.

An unpaired host prints the steps to pair it and keeps running — it is simply reachable on that machine only.

The instance token is never printed.

When one host is not enough

State lives in a local SQLite file by default, which is the right answer for one host. Three environment variables move the store, the message bus and the key-value cache onto infrastructure you already run — Postgres, NATS and Valkey — so several hosts share one control plane. Unset, they are absent and the file is all there is. See External backends for state.

Logs

A host is meant to run for days, so its structured logs go to files rather than over the top of the status screen. They live in <data-dir>/logs — override with --log-dir — and are organised by date:

~/.contenox/logs/
  serve-2026-08-14.log
  serve-2026-08-15.log
  serve-2026-08-15.2.log

A day that outgrows its size bound continues in a numbered part, so “what did this host do on Tuesday?” stays a question about filenames. Parts restart at one each day, and restarting a host continues the current part rather than opening a file per launch.

Retention is bounded in both directions, because either bound alone leaves a gap: a file count never retires a quiet host’s logs, and an age limit never bounds a busy one’s disk.

contenox config set log-max-size 50MB      # new part at this size
contenox config set log-max-files 4        # kept across every date and part
contenox config set log-max-age-days 7     # retired by date

Sizes take an optional unit (10MB, 512KB, 1GB, or plain bytes). Either retention key accepts 0 for “no limit”. The defaults — 10MB parts, 14 files, 14 days — bound an unconfigured host without being asked. Full descriptions are in the configuration keys table.

Two things the host will not do: delete a file it does not own (only its own serve-<date>.log names are ever candidates, so a log directory shared with something else is safe), and delete the file it is currently writing, however tight the bound.

Host logs are separate from telemetry.log, so turning telemetry on or off never changes whether a host has somewhere to write its diagnostics.

Stopping it

Ctrl-C. The host stops dialling, in-flight work is torn down, and the machine becomes unreachable from the app until a host or a session runs again. Stopping a host does not unpair it — the machine stays attached to the relay and is reachable again the moment something starts. To detach the machine entirely, use contenox unpair.

A run that was waiting on an ask when you stopped the host is not lost. The ask is a durable row, and the run checkpoints beside it on the way out; both outlive the process, so answering the ask later resumes the run — in the next host to start, or in the terminal that answers it.

See also

Esc to close