From 28d8af72728b195c734e8b1a21d7c03c185abf81 Mon Sep 17 00:00:00 2001 From: Hai Huang Date: Fri, 30 Oct 2026 10:28:01 -0400 Subject: [PATCH 1/3] Docs: Running Cortex on a remote machine Cortex stays loopback-bound and unauthenticated; an encrypted tunnel (ssh -L, kubectl port-forward, oc port-forward with one project per user) is the only way in, and agentop connects with --endpoint. Verified end to end on a Lima VM: systemd user unit, session API, erase through the tunnel, agents-local traffic through the remote forward proxy and TLS bridge, agentop observe against the remote endpoint. Assisted-By: Claude (Anthropic AI) Signed-off-by: Hai Huang --- README.md | 4 ++ docs/README.md | 1 + docs/remote.md | 118 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 docs/remote.md diff --git a/README.md b/README.md index 33929aa59..56890fefa 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,10 @@ The same binary runs as a Kubernetes sidecar: a verifiable identity per workload right credentials for each downstream call (**AuthBridge**), guardrails, egress control and spend caps. Start with [Running Cortex in Kubernetes](./docs/kubernetes.md). +The same binary also runs on a remote machine — a VM, a home server, a pod — with +`agentop` connecting over an encrypted tunnel. See +[Running Cortex on a remote machine](./docs/remote.md). + ## Learn more [All docs](./docs/README.md) · [agentop reference](./cmd/agentop/README.md) · diff --git a/docs/README.md b/docs/README.md index 8a5205f19..87edf2613 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ holds the repo-level pieces; this file is the index. | Install and run Cortex on a laptop | [root README](../README.md) | | Route a specific agent through Cortex | [`agents/`](agents/) — one guide per agent: [claude-code](agents/claude-code.md), [codex](agents/codex.md), [opencode](agents/opencode.md), [IBM Bob](agents/bob.md), [Pi](agents/pi.md), [any other agent](agents/any-agent.md) | | Run the service, group sessions into sessions, troubleshoot | [`laptop-service.md`](laptop-service.md) | +| Run Cortex on a remote machine and connect from anywhere | [`remote.md`](remote.md) | | Cut token cost with tool pruning | [`laptop-token-savings.md`](laptop-token-savings.md) | | Customize Cortex with a small Python or shell script | [`script-plugin.md`](script-plugin.md) | | Run an agent on a local model, or switch inference servers | [`laptop-local-model.md`](laptop-local-model.md) | diff --git a/docs/remote.md b/docs/remote.md new file mode 100644 index 000000000..a1b4f3fa6 --- /dev/null +++ b/docs/remote.md @@ -0,0 +1,118 @@ +# Running Cortex on a remote machine + +Cortex on a laptop is one process bound to loopback. On a remote machine — a VM, a +home server, a cluster pod — it is the same process with the same binding, and the +machine is reached over an encrypted tunnel that carries loopback across the network. +Your SSH key or cluster identity is the authentication; Cortex never learns it is +remote, and nothing about its security model changes. + +## The rule + +**Every Cortex port stays on loopback, and the tunnel is the only way in.** Never +expose a port directly, and never put one behind an ingress: + +- The session API (`:47601`) is unauthenticated by design and serves raw prompts, + completions and tool results. +- The forward proxy (`:47600`) carries your agent's decrypted traffic and its + credentials. + +A tunnel — `ssh -L`, `kubectl port-forward`, SSH over Tailscale — gives you both +encryption and "only me" without Cortex growing an auth subsystem. It also keeps the +session API's browser guards passing: a tunnelled request arrives with a loopback +`Host`, so erasing sessions (`DELETE /v1/sessions`) keeps working from afar. + +## Ports + +A local-style install binds, on the remote machine's loopback: + +| Port | What it is | +|---|---| +| `47600` | Forward proxy — where agents point `HTTPS_PROXY` | +| `47601` | Session API — what `agentop` reads | +| `47602` | Stats — `/reload/status`, live config, pprof | +| `47604` | Health | + +## Deploying + +Cortex needs a writable `HOME` (it writes `~/.cortex`) and nothing else — no +Keycloak, no SPIRE, no cluster. Run the release binary with `--local`, which writes +and watches `~/.cortex/config.yaml`. + +As a systemd user service (verified on Ubuntu 24.04, aarch64): + +```ini +# ~/.config/systemd/user/cortex.service +[Unit] +Description=Cortex + +[Service] +ExecStart=%h/cortex --local +Restart=on-failure + +[Install] +WantedBy=default.target +``` + +```sh +systemctl --user enable --now cortex.service +loginctl enable-linger "$USER" # keep it running after you log out +``` + +`agentop service` can manage the same unit if you install agentop on the server. In a +container, run the binary with `HOME` pointed at a volume. + +## Connecting agentop + +Forward the session API and point agentop at your end of the tunnel: + +```sh +ssh -N -L 47601:localhost:47601 you@server +agentop observe --endpoint http://localhost:47601 +``` + +Everything works through the tunnel: the session list, the live event stream, the +timeline, cost — and erasing sessions, because the request arrives with a loopback +`Host`. If you also run a local Cortex on the same machine, forward to a free port +(`-L 48601:localhost:47601`) and name it in `--endpoint`. + +## Where the agents run + +**On the remote box, beside Cortex** — nothing more to do. The agents use +`http://127.0.0.1:47600` as their proxy, exactly as on a laptop; see the per-agent +guides in [`agents/`](agents/). + +**On your machine, through the remote Cortex** — forward the proxy port too and +point the agent at it: + +```sh +ssh -N -L 47600:localhost:47600 you@server +``` + +Then set `HTTPS_PROXY=http://localhost:47600` for the agent, and trust the remote +bridge CA — copy it over and export it: + +```sh +scp you@server:.cortex/ca/ca.crt ~/.cortex/remote-ca.pem +export NODE_EXTRA_CA_CERTS=~/.cortex/remote-ca.pem # or SSL_CERT_FILE, per agent +``` + +Two caveats: + +- `agentop exec` assumes a local Cortex: pointed at a remote stats URL + (`--cortex-stats-url`) it fetches the remote config but prints the *server's* + proxy address and CA path. Set the two variables by hand for this shape, or run + `exec` on the server itself. +- Every model call now crosses the tunnel. Fine on a LAN or over Tailscale; over a + slow WAN link, expect the latency of the link on every request. + +## Other targets + +| Target | Tunnel | +|---|---| +| VM or server | `ssh -L` (above); `autossh` or SSH-over-Tailscale for a permanent one | +| Kubernetes | `kubectl port-forward`, or agentop's Namespaces → Pods picker — see [`kubernetes.md`](kubernetes.md) | +| OpenShift | one project per user, `oc port-forward` — project membership is the boundary; in a shared project, `edit` reaches every pod, so don't share | + +On OpenShift, note that Kata / sandboxed containers silently break `port-forward` +(the runtime's implementation is a stub) — run the plain runc runtime for a pod you +want to reach this way. From f660ba896716b1784c81cedaf87b7ad389efe641 Mon Sep 17 00:00:00 2001 From: Hai Huang Date: Fri, 30 Oct 2026 10:35:26 -0400 Subject: [PATCH 2/3] Docs: Say which remote CA file the trust-store-replacing variables need MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NODE_EXTRA_CA_CERTS adds to Node's roots, so the bare remote ca.crt is right for it — but the aside sent SSL_CERT_FILE readers at the same file, which replaces the whole store and would break every direct TLS call. The server's bundle.crt is no substitute (it carries the server's roots); point at building a local bundle and at agents/any-agent.md's add/replace table. Assisted-By: Claude (Anthropic AI) Signed-off-by: Hai Huang --- docs/remote.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/remote.md b/docs/remote.md index a1b4f3fa6..18e3d99fd 100644 --- a/docs/remote.md +++ b/docs/remote.md @@ -93,9 +93,18 @@ bridge CA — copy it over and export it: ```sh scp you@server:.cortex/ca/ca.crt ~/.cortex/remote-ca.pem -export NODE_EXTRA_CA_CERTS=~/.cortex/remote-ca.pem # or SSL_CERT_FILE, per agent +export NODE_EXTRA_CA_CERTS=~/.cortex/remote-ca.pem ``` +`NODE_EXTRA_CA_CERTS` *adds* to Node's own roots, so the bare remote CA is the right +file for it. Variables that *replace* the trust store (`SSL_CERT_FILE`, +`GIT_SSL_CAINFO`, `CURL_CA_BUNDLE`, `REQUESTS_CA_BUNDLE`) must not get it — they +would trust one private CA and nothing else — and the server's `bundle.crt` is no +substitute either, since it carries the *server's* public roots. Give those a file +holding your machine's roots plus the remote CA, e.g. your local `bundle.crt` with +`remote-ca.pem` appended. [`agents/any-agent.md`](agents/any-agent.md) lists which +variables add and which replace. + Two caveats: - `agentop exec` assumes a local Cortex: pointed at a remote stats URL From d440843877885becc2b3a264ac692e9b9e8f4e8a Mon Sep 17 00:00:00 2001 From: Hai Huang Date: Fri, 30 Oct 2026 10:42:20 -0400 Subject: [PATCH 3/3] =?UTF-8?q?Docs:=20Address=20review=20=E2=80=94=20shar?= =?UTF-8?q?ed-host=20caveat,=20ExecStart=20path,=20agentop=20service=20not?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-By: Claude (Anthropic AI) Signed-off-by: Hai Huang --- docs/remote.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/remote.md b/docs/remote.md index 18e3d99fd..cbdf4bc58 100644 --- a/docs/remote.md +++ b/docs/remote.md @@ -16,6 +16,11 @@ expose a port directly, and never put one behind an ingress: - The forward proxy (`:47600`) carries your agent's decrypted traffic and its credentials. +One assumption carries over from the laptop: loopback is *yours*. On a shared +host every local account reaches both ports without the tunnel, so this model +assumes a machine you are the only user of — or one whose every account you +trust, the same caveat as a shared OpenShift project below. + A tunnel — `ssh -L`, `kubectl port-forward`, SSH over Tailscale — gives you both encryption and "only me" without Cortex growing an auth subsystem. It also keeps the session API's browser guards passing: a tunnelled request arrives with a loopback @@ -46,7 +51,7 @@ As a systemd user service (verified on Ubuntu 24.04, aarch64): Description=Cortex [Service] -ExecStart=%h/cortex --local +ExecStart=%h/.local/bin/cortex --local Restart=on-failure [Install] @@ -58,8 +63,11 @@ systemctl --user enable --now cortex.service loginctl enable-linger "$USER" # keep it running after you log out ``` -`agentop service` can manage the same unit if you install agentop on the server. In a -container, run the binary with `HOME` pointed at a volume. +Or skip the hand-written unit: `agentop service install` (with agentop installed on +the server) writes and supervises an equivalent one, with quoted absolute paths and +restart backoff. It writes to the same `~/.config/systemd/user/cortex.service`, so +running it replaces a hand-written unit. In a container, run the binary with `HOME` +pointed at a volume. ## Connecting agentop