# OctoBus runs Node.js services under a Go binary, and a capset decides what an agent may call

> A single-binary local gateway that supervises pluggable Node.js gRPC services, keeps their state in one SQLite file, and republishes the methods you bind as gRPC, Connect RPC or MCP on a single port. The interesting parts are the runtime sandbox and the awkward gap between streaming and everything else.

**chaitin/OctoBus** — A secure local gateway for AI agents to reliably call approved enterprise APIs, tools, and services.

- Repository: https://github.com/chaitin/OctoBus
- Website: https://chaitin.github.io/OctoBus/
- Stars: 202 · Forks: 91
- Language: JavaScript
- License: GPL-3.0
- Published: 2026-08-26 · Updated: 2026-08-26 · Language: en
- Canonical page: https://hysenlabs.com/projects/chaitin-octobus

## One port carries the admin API, gRPC, Connect, MCP and reflection

The daemon listens on a single address, `127.0.0.1:9000` by default, and everything is dispatched through it. The admin API, gRPC, Connect RPC, MCP and gRPC reflection all share that one port, which means there is no second listener to firewall and no port map to keep in sync. You can bind somewhere else explicitly with `--addr`, for example `0.0.0.0:9000`, and the project is direct about the consequence: when you expose OctoBus remotely, network access control is your responsibility. There is no built-in authentication story in that sentence, so a remote bind is a decision to secure at another layer. A related choice worth knowing: the CLI performs its management operations through the admin API by default and does not write the SQLite file directly, so the database is not a second interface you can accidentally corrupt with a hand-edited row.

## A capset is the unit of permission, and it chains down to a single method

The core model has four nouns and they compose in one direction: capset, then service, then instance, then method. A capset is a deterministic set of capabilities for an agent or a use case, built from those bindings, and a method binding is the gRPC method actually selected and exposed inside a capset. Deterministic is the important word. An agent does not discover what exists at runtime and decide what to call; it is handed a set that someone declared, and the set resolves to specific methods on specific instances. Below that, a service is a service root inside an importable Node.js package, holding `service.json`, proto files and a gRPC implementation, and an instance is one running copy of that service with its own config and workdir. Long-running instances also get logs and a local listen port, which is the difference that matters for streaming.

## Streaming methods work over gRPC only, and never on the on-demand path

The protocol split is uneven, and this is the sharpest limitation in the project. Unary methods can be called three ways: through gRPC, through Connect RPC, and through MCP. Streaming methods can be called one way: gRPC, and only for long-running services. They are not available through Connect RPC, not available through MCP, and not available on the on-demand invocation path. That last exclusion is not an oversight but a consequence of how on-demand works, since an on-demand instance is not prestarted and has no persistent process to hold a stream open. If your integration budget assumes that anything exposed to an agent works over MCP, that assumption breaks here. A capability you want an agent to use as a stream has to be spoken to over gRPC by something that is not an MCP client, and a service that must be reachable that way has to be a long-running one.

## Long-running services get a resident process, on-demand ones get a subprocess per request

Packages declare how they want to run, and the default is a resident process. After an instance is created or started, OctoBus launches a long-running Node.js gRPC subprocess that stays up, which is what makes streaming possible and what gives the instance its logs and listen port. A package can instead declare `"runtime":{"mode":"on-demand"}` in its `service.json`. Such instances are not prestarted, they store no PID and no listen address, and for each incoming request OctoBus starts one short-lived `invoke` subprocess. The trade is direct: on-demand removes idle resident processes and the state that comes with them, at the cost of a process launch per call and no streaming. Two of the example packages in the repository are named for exactly this distinction, one long-running and one on-demand, and a third is named for streaming, which is a quick way to see the three shapes the project supports.

## Runtime hardening is off by default, and the node level is a real sandbox

The default posture is permissive: a service runtime inherits the daemon's environment and can reach anything the daemon user can. The opt-in is a flag or its environment equivalent:

```bash
./bin/octobus serve --runtime-hardening=node
```

At the `node` level the restrictions are enforced by the Node.js process itself, and the list is longer than a typical allowlist. A runtime receives only an allowlist of environment variables plus the `OCTOBUS_*` context, with other daemon variables and any inherited `NODE_OPTIONS` dropped. Its home and temp variables are redirected into its own instance directory, and temporary files left there are not cleaned up automatically. It runs under the Node permission model, so it can read its own artifacts and its own instance directory and cannot read other files, which explicitly includes `octobus.db` and other instances, start child processes, use worker threads, or load native addons. Its V8 old generation is capped at 512 MB, and on Unix it runs in its own process group so stopping an instance stops what it started.

## The sandbox blocks egress by patching the network calls themselves

The network restriction is the most interesting implementation detail in the project, because it is enforced by interception rather than by a firewall. A runtime at the `node` level refuses to dial loopback, link-local, unspecified and cloud metadata addresses, the addresses of its own host, or a unix socket path. It does that by patching `dns.lookup`, `fetch`, `net.connect` and `net.Socket.prototype.connect`. The consequence of patching those four is that a hostname is judged on the address it resolves to at the moment the connection is made, rather than on the name, which closes the usual gap where a name resolves to a forbidden address. Each redirect is judged again for the same reason, so a permitted first hop cannot walk the connection somewhere blocked. The blocking of cloud metadata addresses is worth calling out, since that is the address an agent with a shell would reach for first in a cloud deployment, and the sandbox refuses it without needing a rule the operator writes.

## The node hardening level refuses to start on old Node, and on version manager shims

The `node` level requires Node.js 22.13 or newer, 23.5 or newer, or 24 or newer, and the daemon enforces that at startup rather than at first use. What it does is write the egress rules into its data directory, check the version of the `node` on `PATH`, then launch `node` once with the same preparation a real runtime gets, meaning the environment, the `NODE_OPTIONS` containing the rules, the temp directory and the process group, with a temporary directory standing in for the service and instance directories. The daemon refuses to start if either check fails. The documented failure mode is a version manager shim: something that needs variables outside the allowlist, with Volta and asdf given as the examples, fails this check, and the fix is to put a real `node` binary first on `PATH`. One more detail has teeth: the check runs only at startup, so changing your Node installation afterwards does not get picked up until you restart the daemon.

## npm ships a launcher and a platform binary, and still leaves four tools for you to install

The npm route is the quick one, and it is worth reading precisely because the package does not contain everything:

```bash
npm install -g @chaitin-ai/octobus
octobus serve --dev
```

The main package installs a small Node.js launcher and pulls the matching native Go binary through platform-specific optional dependencies such as `@chaitin-ai/octobus-linux-x64`, so the platform matrix is carried by the dependency set rather than by the package itself. The project then says plainly what the package does not do: it installs the `octobus` binary only, and normal service import and runtime flows still require `node`, `npm`, `protoc` and `git`. The Docker route is the one that carries those dependencies, since the image includes the runtime dependencies used for normal service import and instance startup:

```bash
docker run --rm \
  -p 9000:9000 \
  -e OCTOBUS_BOOTSTRAP_ADMIN_TOKEN=... \
  -v octobus-data:/var/lib/octobus \
  ghcr.io/chaitin/octobus:latest
```

The container listens on `0.0.0.0:9000` rather than loopback, and stores its state under `/var/lib/octobus`, which is why the volume in that command is not optional for anything you want to keep.

## Conclusion

Use OctoBus if you are wrapping internal services into one agent-facing surface and you want the set of callable methods declared in advance rather than discovered at runtime, with a sandbox that stops a service package from reading the rest of the disk. Skip it if you need streaming over MCP or Connect, or if you want the gateway to face the network for you, because neither is what it does. Before you deploy it, read the hardening section carefully, confirm your Node version satisfies the requirement for the level you intend to use, keep the default loopback bind unless you have put your own access control in front, and decide early whether long-running or on-demand services fit your traffic, since streaming methods only exist for the former.

## FAQ

### What is OctoBus and what does the daemon do?

OctoBus is a locally running single-binary gateway for managing pluggable Node.js service packages. The Go-built octobus binary runs the daemon and CLI, exposes selected methods as gRPC plus unary methods as Connect RPC and MCP streamable HTTP, and records services, instances, capsets, bindings and runtime state in SQLite.

### Can OctoBus expose streaming methods to an MCP client?

No. Unary methods can be called through gRPC, Connect RPC and MCP, but streaming methods support gRPC calls for long-running services only and are not available through Connect RPC, MCP or the on-demand invocation path.

### What is a capset in OctoBus?

A capset is a deterministic set of capabilities for an agent or use case, composed of capset to service to instance to method bindings. The method binding is the gRPC method actually selected and exposed inside that capset.

### Does OctoBus sandbox service runtimes by default?

No. By default a service runtime inherits the daemon's environment and can access anything the daemon user can. Pass --runtime-hardening=node, or set OCTOBUS_RUNTIME_HARDENING=node, to restrict each runtime, and that level requires Node.js 22.13 or newer.

## Sources

- [Official documentation](https://chaitin.github.io/OctoBus/)
- [Official README](https://github.com/chaitin/OctoBus#readme)
- [Project repository](https://github.com/chaitin/OctoBus)
- [Release notes](https://github.com/chaitin/OctoBus/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/chaitin-octobus
