@cabane/companion 0.6.175 → 0.6.176
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -319
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,248 +1,124 @@
|
|
|
1
1
|
# cabane
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Cabane Companion runs your Cabane agents on your own device. Install it, paste one setup line, and the agents you assign to this device answer in Cabane while the model, the shell and the files stay on your device. Only the reply crosses back.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It drives a **harness** you already have signed in: [Claude Code](https://www.npmjs.com/package/@anthropic-ai/claude-code), [Codex](https://developers.openai.com/codex/cli) or [OpenCode](https://opencode.ai). The Companion never installs a harness and never runs a sign-in; it finds the ones that are usable and reports them.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Step-by-step guides with screenshots are in Cabane: open the **Account menu** and choose **Docs**. This README is the reference for the device side.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## The model
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Three things, one relation each.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
- **A device** is a machine running the Companion. It belongs to your account and can be joined to any of your workspaces. Cabane shows it as online, offline or deactivated.
|
|
14
|
+
- **A harness** is a coding tool on the device with a login behind it. The Companion detects each one and reports its state: connected, needs sign-in, limited, problem, off or gone. A device offers exactly the harnesses it has.
|
|
15
|
+
- **An agent** is assigned to one device and runs through one of that device's harnesses, with a model from it. Assign in as many workspaces as you like: one Companion serves them all.
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
**Cabane decides what an agent is; the device decides what it can touch.** Everything about the agent lives on its page in Cabane and is delivered with each turn, so an edit there lands on the next reply with no restart: its charter, its harness and model, its MCP servers, its host access and its starting folder. The device holds only what can't travel from a server: the device token, your secrets, the sandbox, and local overrides for this device.
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
How a turn runs, top to bottom:
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
| Layer | Holds |
|
|
22
|
+
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| **Cabane** | The agent's reach: **host access** off (workspace only, the default) or on (shell and files on the device, starting in a folder). Only the agent's owner changes it. A conversation can't widen it. |
|
|
24
|
+
| **Companion** | The listener on the device. Holds the pairing and the harness logins. Sets the device's ceiling: the sandbox and what it allows, which no agent setting can override. |
|
|
25
|
+
| **Runner** | A child process per turn with a one-turn credential, when runners are on. Under Docker, a container per conversation and agent. |
|
|
26
|
+
| **Sandbox** | An OS wrapper around the whole harness process (bubblewrap on Linux, Seatbelt on macOS), or the container itself. Everything the harness spawns is inside it: subprocesses, stdio MCP servers, the lot. |
|
|
27
|
+
| **Harness** | Claude Code, Codex or OpenCode with its normal tools. Nothing is removed by name; the sandbox is enforced by the operating system around the process, never by a list of tool names. Nothing asks for permission mid-task. |
|
|
20
28
|
|
|
21
|
-
|
|
29
|
+
> **An agent with host access acts on the device as the user account the Companion runs under, with no approval prompts.** Turn it on for an agent whose work needs the device, on a device where that reach is acceptable, and put the sandbox under it. An agent that reads untrusted input (public web, inbound mail, outside members) stays sandboxed, whatever the device.
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
curl -fsSL https://claude.ai/install.sh | bash
|
|
25
|
-
~/.local/bin/claude auth login
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
The full path works in the same window even when Claude's installer leaves `~/.local/bin` off your PATH. With an Anthropic API key instead, `export ANTHROPIC_API_KEY=…` in the window that runs `cabane start`: the Companion reports it as **API key configured**. A key lives in that window's environment, so one exported later in another window isn't seen by a running Companion; `cabane connect` says so. A plan sign-in is stored on disk and never needs a restart.
|
|
29
|
-
|
|
30
|
-
**Codex** — `codex login`, or `export CODEX_API_KEY=…` in the window that runs `cabane start` (Cabane never sees the key).
|
|
31
|
-
|
|
32
|
-
**opencode** — authenticate a provider through its own flow and leave its server running (`opencode serve --port 4096`). A reachable server counts as usable; for one anywhere else, connect it with `cabane connect opencode --url <url>`.
|
|
33
|
-
|
|
34
|
-
You also need **a Cabane account.** A device belongs to your account, not to one workspace: once paired, it is available in every workspace the account is in.
|
|
31
|
+
**No account password or full-account token ever touches the device.** The setup code delivers a device token (`cabdev_…`) good only for pulling this device's assignments and reporting liveness. Each assigned agent gets its own workspace-bound credential, delivered once. Renaming, deactivating, removing a device and rotating its token happen in Cabane.
|
|
35
32
|
|
|
36
33
|
## Install
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
**The install script** brings its own Node (22, checksum-checked, into `~/.cabane`), needs no sudo, and runs `cabane start` in the same window:
|
|
35
|
+
In a workspace, open **Settings → Devices** and choose **Add a device**. The page mints a setup line with a code in it, good for 15 minutes. Paste it in a terminal on the device. One of:
|
|
41
36
|
|
|
42
37
|
```sh
|
|
43
|
-
curl -fsSL https://cabane.ai/install.sh | bash
|
|
38
|
+
curl -fsSL https://cabane.ai/install.sh | bash -s -- CODE
|
|
44
39
|
```
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
The script brings its own Node (checksum-checked, into `~/.cabane`) and needs no sudo. Or, with Node 22 or newer already there (the only route on Windows):
|
|
47
42
|
|
|
48
43
|
```sh
|
|
49
|
-
npm i -g @cabane/companion
|
|
50
|
-
cabane start
|
|
44
|
+
npm i -g @cabane/companion && cabane setup CODE
|
|
51
45
|
```
|
|
52
46
|
|
|
53
|
-
Either
|
|
54
|
-
|
|
55
|
-
To **update**, run `cabane update`, whichever way you installed: it installs the version Cabane names. Under the service (below) the Companion restarts into it once its turns finish; without the service, the next `cabane start` runs it, and `cabane stop` before that ends any turn in progress. Re-running the install script changes nothing while a Companion is running.
|
|
47
|
+
Either way `cabane setup CODE` pairs the device to your account, joins it to that workspace, detects your harnesses, and starts the Companion: as a service on macOS and Linux, in the background elsewhere. No confirm, no name screen, no browser on the device. The same line on a device that is already paired joins it to the workspace and mints nothing.
|
|
56
48
|
|
|
57
|
-
## First run
|
|
58
|
-
|
|
59
|
-
```sh
|
|
60
|
-
cabane start
|
|
61
49
|
```
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
50
|
+
✓ Claude Code signed in (2.1.289) — connected
|
|
51
|
+
Codex not signed in — run codex login
|
|
52
|
+
OpenCode not running
|
|
53
|
+
All set here. Back to the browser.
|
|
65
54
|
```
|
|
66
|
-
Claude Code is signed in on this computer (2.1.289).
|
|
67
|
-
Use it in Cabane? [Y/n]
|
|
68
|
-
✓ Claude Code — available once this computer is paired.
|
|
69
55
|
|
|
70
|
-
|
|
56
|
+
With nothing signed in, the device still pairs and the Companion still runs; the rows say which harness needs sign-in, and the device shows **Needs attention** in Cabane until one is usable. Sign in, and the next check finds it: no restart.
|
|
71
57
|
|
|
72
|
-
|
|
58
|
+
A fresh device runs nothing yet. In a workspace, open an agent's page and assign it to the device; the Companion picks the assignment up on its next poll.
|
|
73
59
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
https://app.cabane.ai/pair/enter-code?code=4HSP-FPWB
|
|
77
|
-
|
|
78
|
-
Waiting for you to confirm in Cabane… (the code is good for 15 minutes)
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
With nothing signed in, it pairs nothing, starts nothing, prints the sign-in lines above and exits non-zero.
|
|
82
|
-
|
|
83
|
-
In Cabane, open the **Account menu** at the bottom left and choose **Settings**, then **Devices**, then **Pair a device** and **Enter pairing code**. Type the eight characters and choose **Pair device**. Or open the link from the terminal, which fills the code in. Any browser signed in to the account will do, so a headless device needs no browser of its own. The device token is delivered straight to the waiting CLI. Nothing is copied or pasted.
|
|
84
|
-
|
|
85
|
-
The terminal finishes by itself:
|
|
86
|
-
|
|
87
|
-
```
|
|
88
|
-
✓ Paired with Maya Finch's Cabane account as maya-mbp.
|
|
89
|
-
✓ Claude Code connected.
|
|
90
|
-
✓ Cabane is running in the background. You can close this window.
|
|
91
|
-
Stop: cabane stop Logs: cabane logs
|
|
92
|
-
|
|
93
|
-
! It won't restart on its own after a reboot — run cabane start again.
|
|
94
|
-
|
|
95
|
-
All set on this side. Finish up in Cabane.
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Back in Cabane, **Name this device** asks for a **Device name**, which starts as the device's hostname and can change later. Choose **Save and continue**. **What Runs on This Device** lists each coding tool on the device as a connector; choose **Continue**. The device is then listed under **Settings → Devices** as **Online**.
|
|
99
|
-
|
|
100
|
-
A freshly paired device runs nothing yet: pairing doesn't put any agent on it. In a workspace, open **Settings**, then **Agents**, choose an agent, and under **Assignment** choose **Assign a device**, pick this device, and choose **Assign device**. Then, under **Execution**, choose its **Connector** and model and choose **Save execution settings**. Until those are saved, the agent's row in the list still says "Not configured". Only the device's own owner can assign an agent to it. The Create an agent guide in Cabane's docs walks these screens. Assign agents in as many workspaces as the account is in: one running Companion serves them all.
|
|
60
|
+
If `cabane` isn't found afterwards, open a new terminal, or run the `source` line setup printed.
|
|
101
61
|
|
|
102
62
|
## Commands
|
|
103
63
|
|
|
104
|
-
| Command
|
|
105
|
-
|
|
|
106
|
-
| `
|
|
107
|
-
| `start [--foreground]
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `logout [-y] [--purge]` | Remove the device token and cached agent credentials. `--purge` removes the whole local config. Neither removes the device from Cabane. |
|
|
116
|
-
| `--version` / `-V` | Print the Companion version. |
|
|
117
|
-
|
|
118
|
-
Everything else about a device is done in Cabane, not the CLI: **rename**, **deactivate**, **rotate token** and **remove** are on the device's page under the account's **Settings → Devices**, and agents are assigned from each agent's page in Cabane, under **Where it runs**.
|
|
119
|
-
|
|
120
|
-
## Managing the device
|
|
121
|
-
|
|
122
|
-
Each of these starts in Cabane, on the device's page, and three of them need a command on the device afterwards.
|
|
123
|
-
|
|
124
|
-
- **After a reboot, or whenever the Companion has stopped**, run `cabane start`. Running it while the Companion is already running changes nothing. In Cabane, a device whose Companion has stopped turns to **Offline** about two minutes later, and back to **Online** once it is up.
|
|
125
|
-
- **Deactivate** turns the device off without removing it. The Companion on the device exits. To bring it back, choose **Reactivate** in Cabane, then run `cabane start` on the device. The Companion's log says to log out and pair again; that isn't needed.
|
|
126
|
-
- **Rotate token** shows a new device token (`cabdev_…`) once, and the old one stops working at once. On the device, run `cabane stop`, replace the value of `deviceToken` in `~/.cabane/config.json` with the new token, then run `cabane start`. Stop first: a Companion that is still running keeps the old token, and `start` does nothing while one is running.
|
|
127
|
-
- **Remove device** can't be undone. The Companion on the device exits. To pair the same device again, run `cabane logout`, then `cabane start`.
|
|
64
|
+
| Command | What it does |
|
|
65
|
+
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| `setup [CODE]` | With a code: pair or join, detect harnesses, start. Without: a re-runnable checklist that reports harnesses and starts what isn't running. |
|
|
67
|
+
| `start [--foreground]` | Run in the background. Never asks. Unpaired, it names `setup`. `--foreground` stays attached, for a supervisor of your own. |
|
|
68
|
+
| `stop` | Stop a running Companion cleanly. Ends any turn in progress. |
|
|
69
|
+
| `status` | Version and install, the paired device by its Cabane name, whether it's running, each harness's state, the sandbox posture, declared secret names, and local overrides. |
|
|
70
|
+
| `update [--auto on\|off]` | Install the version Cabane names now; with `--auto`, set whether the Companion keeps itself up to date. |
|
|
71
|
+
| `service install\|uninstall\|status` | Run as a systemd user service or a LaunchAgent that starts at boot and restarts if it stops; remove it; show its state. |
|
|
72
|
+
| `logs [-n N] [-f]` | Recent log lines, or follow. |
|
|
73
|
+
| `transcript [file] [--last] [-f]` | Read back a full agent turn, tool calls and results included. |
|
|
74
|
+
| `logout [--purge]` | Remove the device token and cached agent credentials. Doesn't remove the device from Cabane. |
|
|
128
75
|
|
|
129
|
-
|
|
76
|
+
Anything else about a device is done in Cabane, on the device's page.
|
|
130
77
|
|
|
131
|
-
##
|
|
78
|
+
## Keeping it running
|
|
132
79
|
|
|
133
|
-
|
|
80
|
+
Under the service the Companion restarts if it stops, comes back after a reboot, and follows the version Cabane names: it installs the new version under `~/.cabane/versions`, checks it starts, and swaps between turns. A version that can't start is rolled back by the boot guard. `cabane update --auto off` stops updates; without the service, `cabane update` installs and the next `cabane start` runs it.
|
|
134
81
|
|
|
135
|
-
|
|
82
|
+
Other supervisors: run `cabane start --foreground` with `CABANE_COMPANION_SUPERVISED=1` set, using the absolute paths `~/.cabane/node/bin/node` and `~/.cabane/current/dist/bin.js`, which don't move on an update.
|
|
136
83
|
|
|
137
|
-
|
|
84
|
+
A device whose Companion has stopped shows **Offline** in Cabane about two minutes later. Deactivating a device in Cabane stops its Companion; reactivate there, then `cabane start` on the device. After **Rotate token**: `cabane stop`, replace `deviceToken` in `~/.cabane/config.json`, `cabane start`.
|
|
138
85
|
|
|
139
|
-
|
|
86
|
+
## What's on this device
|
|
140
87
|
|
|
141
|
-
|
|
88
|
+
All of it in `~/.cabane/`, mode 600, no database. Cabane is the source of truth for which agents run and how.
|
|
142
89
|
|
|
143
|
-
<a id="configuring-an-agent-whats-in-cabane-vs-on-this-machine"></a>
|
|
144
|
-
|
|
145
|
-
## Configuring an agent: what's in Cabane vs. on this device
|
|
146
|
-
|
|
147
|
-
There's a deliberate split. **What the agent is** — its host access, working directory, MCP servers, connector, model and charter — is configured in **Cabane** (each agent's page) and delivered to the Companion per turn, so an edit in Cabane lands on the agent's next reply with **no restart**. One exception: a conversation given Custom execution settings keeps its own connector, model and tuning when the agent's defaults change. The Companion holds only the bits that can't travel from a server: your **secrets**, and device-local overrides such as a prepare-hook.
|
|
148
|
-
|
|
149
|
-
An agent can reach MCP servers two ways, and they don't overlap. **The agent's own MCP servers** are part of what the agent is: set on its page, they go with it into every room. **Connections** belong to a person instead: you add one under a workspace's **Settings → Connections** (an endpoint and a pasted key, stored encrypted in Cabane), then add it to a room from the room's menu and pick which agents there may use it. On each turn in that room, Cabane attaches the room's connections for that agent and sends the key with the turn, into the harness's environment and never onto its command line. Nothing is configured on this device. Replacing, disconnecting or deleting a key takes effect on the agent's next turn, and nothing is revoked at the service itself.
|
|
150
|
-
|
|
151
|
-
### Host access (set in Cabane)
|
|
152
|
-
|
|
153
|
-
**Host access** is a setting under **What it can reach**, on the agent's page in Cabane. Only the agent's owner can change it.
|
|
154
|
-
|
|
155
|
-
| Host access | What the agent can do |
|
|
156
|
-
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
157
|
-
| No | Work in the workspace: read and write the workspace's files and take part in conversations. No shell or file tools on the device. The default. |
|
|
158
|
-
| Yes | All of that, plus run shell commands and read and write files on the device, starting in its working directory. ⚠️ |
|
|
159
|
-
|
|
160
|
-
> ⚠️ **An agent with host access acts on the device as the user account the Companion runs under, with no approval prompts.** With Claude Code as the connector, it runs every command and writes every file it is asked to, and it isn't restricted to its working directory. Turn it on for an agent whose work needs the device, on a device where that reach is acceptable. The [sandbox](#sandbox-set-on-this-device) below limits what those tools can touch.
|
|
161
|
-
|
|
162
|
-
The agent's MCP servers don't depend on host access: a stdio server listed there runs on the device and gives the agent its tools either way.
|
|
163
|
-
|
|
164
|
-
### Working directory + prepare-hook
|
|
165
|
-
|
|
166
|
-
An agent with host access starts in the folder set under **Starts in**, in Cabane's **What it can reach** dialog alongside host access. Left empty, or naming a folder that isn't on the device, the agent starts in the folder the Companion was started from; a missing folder is reported only in the Companion's log.
|
|
167
|
-
|
|
168
|
-
The device can override it. A `cwd` in `~/.cabane/config.json` wins over the directory set in Cabane, and a prepare-hook wins over both. Add an `agents` map keyed by the agent's id, its `@username`, or `workspace-slug/username` (all three show up in `cabane status`, copy-pasteable):
|
|
169
|
-
|
|
170
|
-
```jsonc
|
|
171
|
-
{
|
|
172
|
-
// …device identity, written when the device is paired. The one field to edit by
|
|
173
|
-
// hand is `deviceToken`, after rotating the token in Cabane…
|
|
174
|
-
|
|
175
|
-
// Per-agent device-local overrides. Only `cwd`, `prepareHook`, `claudeCode` and
|
|
176
|
-
// `sandbox` live here — everything else about the agent (host access, MCP
|
|
177
|
-
// servers, model) is set in Cabane.
|
|
178
|
-
"agents": {
|
|
179
|
-
"my-coding-agent": {
|
|
180
|
-
"cwd": "/Users/you/code/project", // overrides the working directory set in Cabane
|
|
181
|
-
"prepareHook": { "command": "/Users/you/bin/prepare-turn" }, // optional: resolves the cwd before each turn
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
}
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
A malformed `agents` block fails `cabane start` immediately with a message pointing at the exact field — it won't silently fall back.
|
|
188
|
-
|
|
189
|
-
**A hook that is the same for every agent goes at the top level instead.** If your hook works out the directory itself — from the conversation and agent ids the Companion hands it — then listing every agent by name is a roster you have to remember to update, and an agent you forget gets no hook at all and starts its turn wherever the Companion happens to be. So `prepareHook` can sit beside `agents` rather than inside it:
|
|
190
|
-
|
|
191
|
-
```jsonc
|
|
192
|
-
{
|
|
193
|
-
"prepareHook": { "command": "/Users/you/bin/prepare-turn" }, // every agent on this device
|
|
194
|
-
"agents": {
|
|
195
|
-
"special-case": { "prepareHook": { "command": "/Users/you/bin/other-hook" } }, // wins for this one
|
|
196
|
-
},
|
|
197
|
-
}
|
|
198
90
|
```
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
```jsonc
|
|
207
|
-
{
|
|
208
|
-
"agents": {
|
|
209
|
-
"my-coding-agent": {
|
|
210
|
-
"cwd": "/Users/you/code/project",
|
|
211
|
-
"claudeCode": { "autoMemory": true }, // opt back into local Claude Code auto-memory
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
}
|
|
91
|
+
~/.cabane/
|
|
92
|
+
├── config.json # pairing, local overrides, sandbox policy
|
|
93
|
+
├── credentials.json # per-agent workspace-bound credentials, delivered once
|
|
94
|
+
├── secrets.json # { NAME: "value" } for ${NAME} placeholders
|
|
95
|
+
├── transcripts/ # one JSONL per turn, newest ~200 kept, local only
|
|
96
|
+
├── outbox/ # a reply survives a transient API outage
|
|
97
|
+
└── companion.log
|
|
215
98
|
```
|
|
216
99
|
|
|
217
|
-
|
|
100
|
+
### Secrets
|
|
218
101
|
|
|
219
|
-
|
|
102
|
+
An MCP server configured in Cabane names its credential as `${NAME}`; the value never leaves the device. The Companion resolves it from `secrets.json` at dispatch, never from your shell environment, and a name that isn't declared fails the turn loudly. It reports the declared names (never values) on its heartbeat, so Cabane can warn before a turn runs.
|
|
220
103
|
|
|
221
|
-
|
|
104
|
+
### Local overrides
|
|
222
105
|
|
|
223
|
-
|
|
224
|
-
// ~/.cabane/secrets.json
|
|
225
|
-
{
|
|
226
|
-
"GITHUB_TOKEN": "ghp_…",
|
|
227
|
-
"LINEAR_API_KEY": "lin_api_…",
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
A `${VAR}` the store doesn't declare fails the turn loudly — it is **never** read from your shell environment, so a config can't smuggle out an ambient credential. The Companion reports the declared **names** (never values) to Cabane on its heartbeat, so the settings UI can warn "this agent needs `${GITHUB_TOKEN}`, this device doesn't expose it" before a turn ever runs.
|
|
232
|
-
|
|
233
|
-
<a id="sandbox-set-on-this-machine"></a>
|
|
106
|
+
`config.json` can carry, per agent (keyed by id, `@username` or `workspace-slug/username`) or for the whole device:
|
|
234
107
|
|
|
235
|
-
|
|
108
|
+
- `cwd`: wins over the agent's **Starts in** folder set in Cabane.
|
|
109
|
+
- `prepareHook`: a command that resolves the working directory before each turn; wins over both. A device-level hook applies to every agent that doesn't name its own.
|
|
110
|
+
- `harnesses`: an allowlist such as `["claude-code"]`; absent means all three.
|
|
111
|
+
- `claudeCode.autoMemory`: Claude Code's local memory directory is off by default on every Companion, because notes belong in the workspace where others can read them. `true` hands it back.
|
|
236
112
|
|
|
237
|
-
|
|
113
|
+
A malformed block fails `cabane start` with the field named.
|
|
238
114
|
|
|
239
|
-
|
|
115
|
+
### Sandbox
|
|
240
116
|
|
|
241
117
|
```jsonc
|
|
242
118
|
{
|
|
243
119
|
"runners": {
|
|
244
120
|
"enabled": true,
|
|
245
|
-
"sandbox": "os",
|
|
121
|
+
"sandbox": "os", // or "provider": "docker", where the container is the sandbox
|
|
246
122
|
"policy": {
|
|
247
123
|
"hosts": { "allow": ["gitlab.example.com"], "deny": [], "defaults": true },
|
|
248
124
|
"filesystem": { "mode": "blocklist", "denyRead": [], "allowRead": [], "allowWrite": [] },
|
|
@@ -255,142 +131,30 @@ What the sandbox allows is `runners.policy` in `~/.cabane/config.json`. Every ke
|
|
|
255
131
|
}
|
|
256
132
|
```
|
|
257
133
|
|
|
258
|
-
|
|
259
|
-
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
260
|
-
| **Hosts**, always reachable | Cabane, and the model provider of the turn's harness (Anthropic for Claude Code, OpenAI for Codex). `allow` and `defaults` never remove these; a `deny` entry naming one does, and blocks the turn from reaching it. |
|
|
261
|
-
| **Hosts**, `defaults` | `github.com`, `*.github.com`, `*.githubusercontent.com`, `registry.npmjs.org`, `pypi.org`, `files.pythonhosted.org`, `crates.io`, `static.crates.io`, `index.crates.io`. `allow` adds to them, `deny` removes a host (and wins), `defaults: false` drops the list. |
|
|
262
|
-
| **Reads**, `blocklist` mode (the default) | Everything is readable except every companion's store (`~/.cabane`, and any `.cabane` under your home or beside this companion's home; see below), the `.claude` and `.codex` of the other companion homes those stores sit in, `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.config/gh`, `~/.git-credentials`, `~/.netrc`, `~/.npmrc`, `~/.pypirc`, `~/.docker/config.json`, `~/.kube`, `~/.config/gcloud`, `~/.azure`, `~/.config/op`, `~/.password-store`, and your `denyRead` paths. `allowRead` reopens a path inside a denied one. |
|
|
263
|
-
| **Writes** | The working directory, the temp dir, `~/.claude`, `~/.claude.json`, `~/.codex`, plus `allowWrite`. |
|
|
264
|
-
|
|
265
|
-
**One agent can be excepted.** The sandbox is set per device, and one agent's row in `agents` can say otherwise — for example an ops agent whose job is the device itself, on a device whose other agents stay sandboxed:
|
|
266
|
-
|
|
267
|
-
```jsonc
|
|
268
|
-
{
|
|
269
|
-
"runners": {
|
|
270
|
-
"enabled": true,
|
|
271
|
-
"sandbox": "os",
|
|
272
|
-
"policy": {
|
|
273
|
-
/* unchanged */
|
|
274
|
-
},
|
|
275
|
-
},
|
|
276
|
-
"agents": {
|
|
277
|
-
"014d18a7-6395-48e3-a2f2-313d5baa2a5e": { "sandbox": "none" }, // the ops agent, by its id: it runs with the device; everyone else stays sandboxed
|
|
278
|
-
},
|
|
279
|
-
}
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
It works the other way too: `"agents": { "<agent id>": { "sandbox": "os" } }` sandboxes one agent on a device that otherwise isn't. **A sandbox row is keyed by the agent's id**, the last part of the agent's settings page URL in Cabane (`/<workspace>/settings/agents/<agent id>`). A username, or `workspace-slug/username`, can match more than one agent: slugs are unique only per workspace owner, so two workspaces this device serves can share one. A `sandbox` row keyed any other way is refused at start, with the field named. `cwd` and `prepareHook` rows still take all three keys. The agent's row wins, then `runners.sandbox`, then no sandbox. The policy stays the device's. Under the `docker` provider the container is the sandbox, so a row's `sandbox` is ignored, and the log says so at start. The exception is set only here: Cabane shows it on the device page and can't change it.
|
|
283
|
-
|
|
284
|
-
Paths may start with `~/` and may be globs. An invalid `policy` doesn't stop the Companion from starting. Instead, every sandboxed turn is refused and the conversation names the field that's wrong, for example `runners.policy.filesystem.mode`.
|
|
285
|
-
|
|
286
|
-
**Other companions on the same device.** A second companion (another account, a test device) keeps its own secrets in its own `.cabane`, so a turn can't read any companion's store, not only this device's. Every turn denies this companion's store and `~/.cabane` (both, when the Companion runs from a home set with `CABANE_COMPANION_HOME`), plus any `.cabane` up to two levels under your home or beside this companion's home, where companion homes conventionally sit. Stores anywhere deeper are found by a search of your home with `find`, along with the `.claude` and `.codex` beside each one, which hold that companion's harness logins. That search takes seconds over a large home, so the Companion runs it in the background: when it starts, and again whenever a turn starts more than 5 minutes after the last search finished. Only the first turn after a start waits for it. **A store created deeper than two levels is denied within about 5 minutes plus one search, not instantly.** If a search can't finish (`find` is missing, runs past 60 seconds, or hits a directory it can't list), the stores it did find are still denied, and the log says so. The search runs in strict mode too, because strict mode reopens the working directory, and a store inside it must stay hidden.
|
|
287
|
-
|
|
288
|
-
**`.env` files are readable.** Beyond the credential locations above, nothing guesses which of your files hold secrets, so `.env` files, in the working directory or any other project, are readable unless you deny them. To hide them, list the roots that hold them, for example `"denyRead": ["~/work/**/.env", "~/work/**/.env.*"]`. Each glob is expanded when the turn starts, which costs a walk of that tree on every turn and misses files created during the turn, so keep the roots narrow. When other projects' secrets really matter, strict mode is the better answer, because it hides them all without a walk.
|
|
289
|
-
|
|
290
|
-
**Strict mode.** `"filesystem": { "mode": "strict" }` hides your whole home directory except the working directory, the harness state (`~/.claude`, `~/.codex`), what the Companion and its harnesses run from, and the paths you list in `allowRead`. Toolchains live all over a home directory (`~/.nvm`, `~/.local/share/pnpm`, `~/.cargo`, `~/.pyenv`, mise or asdf shims), so list yours, or builds will fail with "No such file or directory" on a path you didn't expect. On Linux a hidden path looks missing rather than denied, so the Companion log prints the readable list at the start of each strict turn. Compare it against the path the build couldn't find.
|
|
134
|
+
Every key under `policy` is optional. The defaults:
|
|
291
135
|
|
|
292
|
-
**
|
|
136
|
+
- **Network**: Cabane and the turn's model provider always; GitHub, npm, PyPI and crates.io by default; `allow` adds hosts, `deny` removes them and wins. Nothing else is reachable.
|
|
137
|
+
- **Reads**: everything except known credential locations (`~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.config/gh`, `~/.netrc`, `~/.npmrc`, `~/.kube`, cloud CLI configs, every Companion's own `~/.cabane` and the harness logins beside it) plus your `denyRead` globs. `.env` files are readable unless you deny them. `"mode": "strict"` hides the whole home except the working directory, the harness state and your `allowRead` list; name your toolchains there.
|
|
138
|
+
- **Writes**: the working directory, the temp dir, the harness's own state, plus `allowWrite`.
|
|
139
|
+
- **Credentials**: an entry sets the variable to a placeholder inside the sandbox and the proxy swaps the real value in only on requests to the hosts named. The harness never holds the token. A `git` entry does the same for HTTPS remotes (`~/.ssh` is unreadable, so use HTTPS).
|
|
293
140
|
|
|
294
|
-
|
|
141
|
+
One agent can be excepted, keyed by its id: `"agents": { "<agent id>": { "sandbox": "none" } }`, for an ops agent whose job is the device itself. It works the other way too. The agent's row wins, then `runners.sandbox`. Cabane shows the exception on the device page and can't change it.
|
|
295
142
|
|
|
296
|
-
|
|
297
|
-
2. Put it in `~/.cabane/secrets.json` as `"GITHUB_TOKEN": "github_pat_…"`.
|
|
298
|
-
3. Add both credential entries from the example above: `env.GH_TOKEN` with `"hosts": ["api.github.com"]` for `gh`, and a `git` entry for `github.com`.
|
|
299
|
-
4. Use HTTPS remotes, because `~/.ssh` is unreadable in the sandbox. Rewrite existing SSH remotes once on the device: `git config --global url."https://github.com/".insteadOf git@github.com:`.
|
|
143
|
+
A policy the sandbox can't enforce, or a sandbox that fails to start for a turn holding credentials, refuses the turn rather than running it unsandboxed. On Linux the OS sandbox needs `bwrap`, `socat` and `rg` on the Companion's PATH. `cabane status` prints the effective posture.
|
|
300
144
|
|
|
301
|
-
|
|
145
|
+
## Debugging a turn
|
|
302
146
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
## Harnesses
|
|
306
|
-
|
|
307
|
-
A **harness** is the local tool that runs a model — **[Claude Code](https://www.npmjs.com/package/@anthropic-ai/claude-code)**, **[Codex](https://developers.openai.com/codex/cli)**, or **[opencode](https://opencode.ai)** — together with the credential behind it. A harness connected to Cabane on this device is a **Connector**, and one device runs as many Connectors as it has harnesses. None is privileged: a device advertises exactly the harnesses it actually has.
|
|
308
|
-
|
|
309
|
-
Which harness a turn uses is the agent's **Connector**, chosen in Cabane under **How it thinks** on the agent's page, together with a model from that connector. The list holds the connectors on the agent's device, so set the harness up first, then choose it for the agent.
|
|
310
|
-
|
|
311
|
-
What "exposed" means differs per harness, because each is discoverable in a different way. Bring your own install and your own login in all three cases — the Companion never installs a binary and never drives a login:
|
|
312
|
-
|
|
313
|
-
| Harness | How the device exposes it |
|
|
314
|
-
| --------------- | -------------------------------------------------------------------------------------------------- |
|
|
315
|
-
| **Claude Code** | A `claudeCode` block in `~/.cabane/config.json`, **and** the Agent SDK's bundled binary installed. |
|
|
316
|
-
| **Codex** | A `codex` block in `~/.cabane/config.json`, **and** the Codex SDK's vendored binary installed. |
|
|
317
|
-
| **opencode** | An `opencode.serverUrl` in `~/.cabane/config.json` pointing at a reachable `opencode serve`. |
|
|
318
|
-
|
|
319
|
-
Claude Code and Codex each run a native binary their SDK ships, installed alongside the Companion as an optional npm dependency. Those are what a turn launches — not the `claude` / `codex` CLIs on your PATH, which are for setup and login. If one didn't install, the Companion says so on startup and on the device's page in Cabane, with the commands that repair it: `npm i -g @cabane/companion --include=optional` for an npm install, `cabane stop` then the install script for a script install, and `cabane stop` then `cabane update` for a copy Cabane installed itself when it updated or became a service. Stopping ends any turn in progress.
|
|
320
|
-
|
|
321
|
-
**You usually don't hand-edit the config.** Interactive `cabane start` offers each signed-in agent, and `cabane connect codex` (or `claude-code` / `opencode`) can connect one later. Editing `~/.cabane/config.json` yourself is the fallback and what a headless device wants:
|
|
322
|
-
|
|
323
|
-
```jsonc
|
|
324
|
-
{
|
|
325
|
-
// …device identity, written when the device is paired…
|
|
326
|
-
"claudeCode": { "enabled": true },
|
|
327
|
-
"codex": { "enabled": true },
|
|
328
|
-
"opencode": { "serverUrl": "http://127.0.0.1:4096" },
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
Every block is optional and independent — set only the harnesses you have. Claude Code and Codex each take a flag and no URL, because the SDK bundled with the Companion spawns its own native binary per turn; opencode is a long-lived server addressed by URL, so run one `opencode serve` per Companion process. A hand-edit needs a restart (`cabane stop && cabane start`); the `connect` command updates a running Companion over its control socket.
|
|
333
|
-
|
|
334
|
-
Everything else about a Companion-run agent is the same whichever harness runs it — same assignment, same working directory, same secrets. A harness is a way to execute a turn, not a different way to run the Companion.
|
|
335
|
-
|
|
336
|
-
## Files on disk
|
|
337
|
-
|
|
338
|
-
```
|
|
339
|
-
~/.cabane/
|
|
340
|
-
├── config.json # device identity: baseUrl + device token (cabdev_…) + device id/label,
|
|
341
|
-
│ # plus optional per-agent `agents` overrides (cwd / prepareHook), the
|
|
342
|
-
│ # optional harness blocks (`claudeCode` / `codex` enabled,
|
|
343
|
-
│ # `opencode` serverUrl), and
|
|
344
|
-
│ # legacy local prefs. mode 600.
|
|
345
|
-
│ # No account password, no full-account token.
|
|
346
|
-
├── credentials.json # agentId → per-agent workspace-bound token, cached on first assignment
|
|
347
|
-
│ # pull (delivered once, then never re-sent). mode 600.
|
|
348
|
-
├── secrets.json # operator-declared { NAME: "value" } store for ${PLACEHOLDER} resolution. mode 600.
|
|
349
|
-
├── cursors/<workspace_id> # last-seen SSE event id per workspace
|
|
350
|
-
├── outbox/<workspace_id>/ # durable per-agent commit queue — a reply survives a transient API outage
|
|
351
|
-
├── runtime.json # written while `start` runs: control socket + pid (swept on exit)
|
|
352
|
-
├── transcripts/ # one JSONL file per dispatch — the full agent turn (see below)
|
|
353
|
-
└── companion.log # readable event log (JSON opt-in)
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
No SQLite, no embedded DB. Cabane is the source of truth for which agents to run and how they're configured.
|
|
357
|
-
|
|
358
|
-
## Logs
|
|
359
|
-
|
|
360
|
-
`cabane logs` shows the last 50 lines. Use `-n 20` for twenty lines or
|
|
361
|
-
`-f` to follow new lines until Ctrl-C. The initial read is capped at 1 MiB.
|
|
362
|
-
The file is `~/.cabane/companion.log` and is human-readable by default: local
|
|
363
|
-
`HH:MM:SS`, a level word for warnings and errors, then a sentence. Turns include
|
|
364
|
-
the agent's name and an eight-character conversation id. Failed turns include
|
|
365
|
-
a reason, the next step and the transcript path.
|
|
366
|
-
|
|
367
|
-
- `info` (default): startup, connection, assigned agents, and turn outcomes.
|
|
368
|
-
- `warn`: degraded operation, such as reconnecting or updates waiting to arrive.
|
|
369
|
-
- `debug`: internal diagnostics for investigating a problem. Errors are always shown.
|
|
370
|
-
|
|
371
|
-
Choose with `cabane start --log-level debug` or `"logLevel": "debug"`
|
|
372
|
-
in `~/.cabane/config.json`. Config changes apply on reload; a start flag wins.
|
|
373
|
-
Use `--log-format json` or `"logFormat": "json"` for machine-readable JSON with
|
|
374
|
-
full ids. The default is `human`; the terminal always stays human-readable.
|
|
375
|
-
Log files append across starts. Transcripts remain JSONL in either mode.
|
|
376
|
-
|
|
377
|
-
## Debugging a turn: transcripts
|
|
378
|
-
|
|
379
|
-
Cabane shows the agent's final reply, but not _how_ it got there. When a turn misbehaves — an agent that "couldn't read the file", a tool that errored, MCP tools that didn't load, a missing secret — the full picture is on disk. Every dispatch — on whichever harness ran it — writes the complete turn stream (the system init with its tool list, every tool call **and its result**, the assistant text, the outcome) to a JSONL file under `~/.cabane/transcripts/`. On a failed turn, the Companion log also prints the exact path.
|
|
380
|
-
|
|
381
|
-
Read one back in a readable form:
|
|
147
|
+
Cabane shows the reply, not how the agent got there. Every turn writes its full stream to `~/.cabane/transcripts/`: the tool list, every tool call and its result, the text, the outcome. A failed turn's log line names its transcript.
|
|
382
148
|
|
|
383
149
|
```sh
|
|
384
|
-
cabane transcript --last
|
|
385
|
-
cabane transcript
|
|
386
|
-
cabane transcript
|
|
387
|
-
cabane
|
|
150
|
+
cabane transcript --last # render the most recent turn
|
|
151
|
+
cabane transcript # list recent turns
|
|
152
|
+
cabane transcript -f # live-watch turns as they land
|
|
153
|
+
cabane logs -f # follow the log
|
|
388
154
|
```
|
|
389
155
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
It renders the conversation step by step — `🔧 tool(input) → ok/ERROR: result` — so you can see exactly where a turn went wrong. The raw JSONL stays on disk for `jq`. Transcripts can contain whatever the agent read (file contents in tool results), so they're kept local-only in a `700` dir; the newest ~200 are retained.
|
|
156
|
+
`cabane start --log-level debug` or `"logLevel": "debug"` in `config.json` for more; `--log-format json` for machine-readable lines.
|
|
393
157
|
|
|
394
|
-
##
|
|
158
|
+
## Not yet
|
|
395
159
|
|
|
396
|
-
Windows has no service install and no self-update. There
|
|
160
|
+
Windows has no service install and no self-update. There is no per-tool permissioning for an agent with host access: the sandbox is the boundary, and it's all or nothing per agent.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.176",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect an agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
|
|
6
6
|
"license": "UNLICENSED",
|