reef

Contained AI agents on your servers.

Set up a team

Purpose-built agents a team shares, behind the org’s SSO. Where the single-user role optimises for a first run on a laptop, these optimise for review: one role file per purpose, each with its own egress list and its own provider credential.

The role files carry the policy, never the people. Who may reach an agent is the Access policy on its hostname, who may open a terminal is the agent’s owner, and what either can do once inside is scopes. Three separate pages, because they are three separate identities.

The roles are skeletons. The shape and the auth wiring are settled; the domain lists and hostnames are placeholders.

This is one shape, not the shape. Two agents split by purpose is what these files demonstrate, but the axes below are the point: split by whatever your org already splits by, and give each split its own role, its own egress list and its own credential.

Steps

The whole job, in order. Each step names the page that owns it.

  1. Prepare the host and install reef and msb on it.
  2. Put both provider keys in secrets.toml, below. The first fleet apply fails without them.
  3. Put your domains in roles/openclaw-marketing.toml and roles/openclaw-coding.toml, and your hostnames in fleet/openclaw-team.toml.
  4. reef role apply, then reef fleet apply. Note each agent’s published port: it is allocated once and kept for the agent’s life, which is what makes it safe to name in a tunnel config.
  5. Create one Access application per hostname, before any DNS (browser access).
  6. Run the tunnel as its own unprivileged account, one ingress rule per agent.
  7. Route DNS, last, once the connector reports registered connections.
  8. Verify from off-host that nothing answers without Access.
  9. Grant operator.admin to the people who need it (scopes).
  10. Issue SSH certificates to whoever needs a shell (terminal access).

Steps 1 to 4 are reef. Steps 5 to 8 are the proxy. Steps 9 and 10 are per person and can happen whenever.

Two axes

A fleet file is a matrix. The role is the blast radius: image, egress, secrets, resources. The owner is who may open a terminal into it. They move independently, and an owner change never touches the VM: it is an edit to the agent’s fleet entry, applied by reef fleet apply.

role: openclaw-marketing agent: marketing owner: marketing role: openclaw-coding agent: coding owner: engineering own VM, volume, port, cookie jar own VM, volume, port, cookie jar
[agents.marketing]
role = "openclaw-marketing"
owner = "marketing"

Share or split

Share an agent when the people sharing it are one trust domain. As these roles ship, a shared gateway pools nearly everything: one session list, one workspace, one credential pool, one browser cookie jar. Each person still gets a durable identity from the email Access supplies, which is what names them in the session log and on the sessions they create. Three more things hang off that identity: a personal skill library, invisible to the others until shared, a personal GitHub connection beside the system account, and a personal model account. None is a trust boundary. Library ownership governs discovery and management, not tool access, and a personal connection stops another member using it, not anyone holding the gateway’s OS account. A personal model account still has to reach its provider through the role’s egress list. Upstream is explicit that one gateway is one trusted operator domain. When the people are not in one trust domain, give each their own agent instead; they cost one role file between them.

Both roles pin tools.sessions.visibility = "agent", narrowing a default that is now all, every session on the gateway: any session on the agent reads any other. tree or self narrows it further, but neither separates the shared workspace or credential pool.

Egress and spend

Each role names only what its purpose needs, which is what a reviewer reads:

role reaches
openclaw-marketing the provider, plus the marketing stack and its asset hosts
openclaw-coding the provider, plus code hosting, its API, and the package registry

Each also names its own secret, so the two purposes hold different provider keys and the bills separate by purpose. The value is substituted host-side and never enters the guest.

Both refs have to resolve before the first fleet apply or agent creation fails. In ~/.local/state/reef/secrets.toml, which must be chmod 600:

[openclaw-marketing]
openrouter = "sk-or-..."

[openclaw-coding]
openrouter = "sk-or-..."

Inline values are plaintext at rest. To resolve them from whatever secret store the org already runs, give the store a [resolvers] command instead: reef runs it at VM create and at secret rotate, and takes its stdout as the value, so reef holds no credential for the credential store.

[resolvers]
openclaw-marketing = "op read 'op://Infra/{name}/credential' -n"

To change a key later, update it where it lives, then push it into the agents that spend it. Running agents switch without a restart:

reef secret rotate reef://openclaw-coding/openrouter

Why these fields are set

The two roles are the same image and the same seeding as the single-user OpenClaw role, shaped for a team. What differs, and why:

The config reaches the agent through [files], which writes /etc/openclaw/defaults.json into the rootfs; the role’s start script copies it to the volume only when the copy is absent. So get it right before first boot: a later role edit reaches neither.

Next: browser access with Cloudflare, the worked proxy setup.