@cabane/companion 0.6.165 → 0.6.167

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 CHANGED
@@ -1,22 +1,22 @@
1
1
  # cabane
2
2
 
3
- Connect a **agent on your machine** to your [Cabane](https://cabane.ai) workspaces as a **responder** — it replies to messages inside Cabane while running as a full local AI client, so you can drive work against your own codebase, files, and MCP servers without putting any of it in Cabane. The model and tools run on your box; only the reply crosses back.
3
+ Connect **coding tools on your device** to your [Cabane](https://cabane.ai) workspaces as a **responder** — it replies to messages inside Cabane while running as a full local AI client, so you can drive work against your own codebase, files, and MCP servers without putting any of it in Cabane. The model and tools run on your box; only the reply crosses back.
4
4
 
5
- The Companion runs your agents through a **harness** on your machine — **[Claude Code](https://www.npmjs.com/package/@anthropic-ai/claude-code)**, **[Codex](https://developers.openai.com/codex/cli)**, or **[opencode](https://opencode.ai)** — and you expose the ones you have. Install and sign in to a harness yourself; the Companion drives it. See [Harnesses](#harnesses).
5
+ The Companion runs your agents through a **harness** on your device — **[Claude Code](https://www.npmjs.com/package/@anthropic-ai/claude-code)**, **[Codex](https://developers.openai.com/codex/cli)**, or **[opencode](https://opencode.ai)** — and you expose the ones you have. Install and sign in to a harness yourself; the Companion drives it. See [Harnesses](#harnesses).
6
6
 
7
- This is **v0**. It's a small CLI — install it from npm with one command, pair the machine once, and run. It talks to Cabane only over Cabane's public API.
7
+ This is **v0**. It's a small CLI — install it from npm with one command, pair the device once, and run. It talks to Cabane only over Cabane's public API.
8
8
 
9
- **The step-by-step guides** — **Pair a device**, **Create an agent**, **Give an agent host access** — are in Cabane's docs, with screenshots: in Cabane, open the **Account menu** and choose **Docs**. This README is the reference for _running_ the Companion on the machine.
9
+ **The step-by-step guides** — **Pair a device**, **Create an agent**, **Give an agent host access** — are in Cabane's docs, with screenshots: in Cabane, open the **Account menu** and choose **Docs**. This README is the reference for _running_ the Companion on the device.
10
10
 
11
11
  ## The model in one paragraph
12
12
 
13
- The Companion runs on a **device you pair**, running **agents you assign to it**. `cabane start` pairs the device with a short code the first time, then runs. From then on it **pulls its assignments from Cabane at runtime** — every agent you've pointed at this device, across all your workspaces — and runs each one locally. Which agents to run, and how they're configured (host access, MCP servers, model), is owned by Cabane and delivered per turn. The machine only holds the bits that _must_ be local: the device token, your secrets, and any overrides you set for this machine.
13
+ The Companion runs on a **device you pair**, running **agents you assign to it**. `cabane start` pairs the device with a short code the first time, then runs. From then on it **pulls its assignments from Cabane at runtime** — every agent you've pointed at this device, across all your workspaces — and runs each one locally. Which agents to run, and how they're configured (host access, MCP servers, model), is owned by Cabane and delivered per turn. The device only holds the bits that _must_ be local: the device token, your secrets, and any overrides you set for this device.
14
14
 
15
- > **No account password or full-account token ever touches your machine.** The short-code pairing flow delivers a **device token** (`cabdev_…`) directly to the waiting CLI — good only for pulling this device's assignments and reporting liveness. Each agent the Companion runs gets its own **workspace-bound agent token**, delivered once when the agent is assigned and scoped to that one agent in that one workspace. Renaming, deactivating and removing a device, rotating its token, and assigning agents to it all live in Cabane; the CLI only pairs the machine and runs the agents locally.
15
+ > **No account password or full-account token ever touches your device.** The short-code pairing flow delivers a **device token** (`cabdev_…`) directly to the waiting CLI — good only for pulling this device's assignments and reporting liveness. Each agent the Companion runs gets its own **workspace-bound agent token**, delivered once when the agent is assigned and scoped to that one agent in that one workspace. Renaming, deactivating and removing a device, rotating its token, and assigning agents to it all live in Cabane; the CLI only pairs the device and runs the agents locally.
16
16
 
17
17
  ## Prerequisites
18
18
 
19
- **Sign in to an agent first.** The Companion runs turns with its own bundled copies of Claude Code and Codex, so it never needs them on your PATH, only signed in. `cabane start` checks with those bundled copies and refuses to pair until one is signed in, or has an API key, printing the lines to run. The Companion never runs a sign-in itself. One agent is enough; a machine can offer several. [Harnesses](#harnesses) has the full wiring.
19
+ **Sign in to a coding tool first.** The Companion runs turns with its own bundled copies of Claude Code and Codex, so it never needs them on your PATH, only signed in. `cabane start` checks with those bundled copies and refuses to pair until one is signed in, or has an API key, printing the lines to run. The Companion never runs a sign-in itself. One coding tool is enough; a device can offer several. [Harnesses](#harnesses) has the full wiring.
20
20
 
21
21
  **Claude Code** (needs a Claude Pro or Max plan):
22
22
 
@@ -80,7 +80,7 @@ It names each agent that's signed in and asks whether to use it in Cabane. Press
80
80
 
81
81
  With nothing signed in, it pairs nothing, starts nothing, prints the sign-in lines above and exits non-zero.
82
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 machine needs no browser of its own. The device token is delivered straight to the waiting CLI. Nothing is copied or pasted.
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
84
 
85
85
  The terminal finishes by itself:
86
86
 
@@ -95,7 +95,7 @@ The terminal finishes by itself:
95
95
  All set on this side. Finish up in Cabane.
96
96
  ```
97
97
 
98
- Back in Cabane, **Name this device** asks for a **Device name**, which starts as the machine's hostname and can change later. Choose **Save and continue**. **What Runs on This Device** lists each agent on the device as a connector; choose **Continue**. The device is then listed under **Settings → Devices** as **Online**.
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
99
 
100
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.
101
101
 
@@ -115,7 +115,7 @@ A freshly paired device runs nothing yet: pairing doesn't put any agent on it. I
115
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
116
  | `--version` / `-V` | Print the Companion version. |
117
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 on each workspace's **Settings → Agents**.
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
119
 
120
120
  ## Managing the device
121
121
 
@@ -124,7 +124,7 @@ Each of these starts in Cabane, on the device's page, and three of them need a c
124
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
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
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 machine again, run `cabane logout`, then `cabane start`.
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`.
128
128
 
129
129
  If `cabane start` says the Companion is running but the device stays **Offline**, the Companion may have exited straight after starting, which happens when Cabane no longer accepts the device. `cabane status` and `cabane logs` show which.
130
130
 
@@ -140,37 +140,39 @@ Updates need the service: it's what restarts the Companion into a new version. T
140
140
 
141
141
  Run `cabane start --foreground` with `CABANE_COMPANION_SUPERVISED=1` in its environment. Configure the supervisor itself for restart and login/boot behavior, and use absolute executable paths when it does not load your interactive shell: `~/.cabane/node/bin/node` and `~/.cabane/current/dist/bin.js` don't move on an update. Without the boot guard a supervisor of your own doesn't roll back a release that fails before it starts.
142
142
 
143
- ## Configuring an agent: what's in Cabane vs. on this machine
143
+ <a id="configuring-an-agent-whats-in-cabane-vs-on-this-machine"></a>
144
144
 
145
- There's a deliberate split. **What the agent is** — its host access, working directory, MCP servers, connector, model and charter — is configured in **Cabane** (a workspace's **Settings → Agents**) 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 machine-local overrides such as a prepare-hook.
145
+ ## Configuring an agent: what's in Cabane vs. on this device
146
146
 
147
- 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 Agents 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 machine. Replacing, disconnecting or deleting a key takes effect on the agent's next turn, and nothing is revoked at the service itself.
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.
148
150
 
149
151
  ### Host access (set in Cabane)
150
152
 
151
- **Host access** is a setting in the agent's **Environment**, on its page under **Settings → Agents**. Only the owner of the agent's device can change it.
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.
152
154
 
153
155
  | Host access | What the agent can do |
154
156
  | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
155
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. |
156
158
  | Yes | All of that, plus run shell commands and read and write files on the device, starting in its working directory. ⚠️ |
157
159
 
158
- > ⚠️ **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-machine) below limits what those tools can touch.
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.
159
161
 
160
- MCP servers in the agent's Environment don't depend on host access: a stdio server listed there runs on the device and gives the agent its tools either way.
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.
161
163
 
162
164
  ### Working directory + prepare-hook
163
165
 
164
- An agent with host access starts in its **Working directory**, set in Cabane in the same **Environment** dialog as 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.
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.
165
167
 
166
- The machine 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):
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):
167
169
 
168
170
  ```jsonc
169
171
  {
170
172
  // …device identity, written when the device is paired. The one field to edit by
171
173
  // hand is `deviceToken`, after rotating the token in Cabane…
172
174
 
173
- // Per-agent machine-local overrides. Only `cwd`, `prepareHook`, `claudeCode` and
175
+ // Per-agent device-local overrides. Only `cwd`, `prepareHook`, `claudeCode` and
174
176
  // `sandbox` live here — everything else about the agent (host access, MCP
175
177
  // servers, model) is set in Cabane.
176
178
  "agents": {
@@ -212,9 +214,11 @@ Claude Code's **auto-memory** — where the agent writes notes to a local `~/.cl
212
214
  }
213
215
  ```
214
216
 
215
- ### Secrets (set on this machine)
217
+ <a id="secrets-set-on-this-machine"></a>
218
+
219
+ ### Secrets (set on this device)
216
220
 
217
- When an agent's MCP servers in Cabane need a credential, that credential is written in Cabane as a `${PLACEHOLDER}` reference — the real value never leaves your machine. The Companion resolves placeholders at dispatch time from an explicit, operator-declared store at `~/.cabane/secrets.json`, a flat `{ NAME: "value" }` map (mode 600):
221
+ When an agent's MCP servers in Cabane need a credential, that credential is written in Cabane as a `${PLACEHOLDER}` reference — the real value never leaves your device. The Companion resolves placeholders at dispatch time from an explicit, operator-declared store at `~/.cabane/secrets.json`, a flat `{ NAME: "value" }` map (mode 600):
218
222
 
219
223
  ```jsonc
220
224
  // ~/.cabane/secrets.json
@@ -226,9 +230,11 @@ When an agent's MCP servers in Cabane need a credential, that credential is writ
226
230
 
227
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.
228
232
 
229
- ### Sandbox (set on this machine)
233
+ <a id="sandbox-set-on-this-machine"></a>
234
+
235
+ ### Sandbox (set on this device)
230
236
 
231
- With `"runners": { "enabled": true, "sandbox": "os" }`, each turn's harness — the model's process and every shell it starts — runs inside an OS sandbox (bubblewrap on Linux, Seatbelt on macOS) whose network goes through a local proxy. It's a guardrail on your own machine: it stops a turn from reading the secrets named below or reaching hosts you didn't allow. It runs as your user on your kernel, so it isn't isolation. _Host access_ on the agent in Cabane decides whether the agent gets shell and file tools at all; the sandbox decides what those tools can touch. The two are independent.
237
+ With `"runners": { "enabled": true, "sandbox": "os" }`, each turn's harness — the model's process and every shell it starts — runs inside an OS sandbox (bubblewrap on Linux, Seatbelt on macOS) whose network goes through a local proxy. It's a guardrail on your own device: it stops a turn from reading the secrets named below or reaching hosts you didn't allow. It runs as your user on your kernel, so it isn't isolation. _Host access_ on the agent in Cabane decides whether the agent gets shell and file tools at all; the sandbox decides what those tools can touch. The two are independent.
232
238
 
233
239
  What the sandbox allows is `runners.policy` in `~/.cabane/config.json`. Every key is optional, and with no `policy` block you get the defaults:
234
240
 
@@ -256,7 +262,7 @@ What the sandbox allows is `runners.policy` in `~/.cabane/config.json`. Every ke
256
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. |
257
263
  | **Writes** | The working directory, the temp dir, `~/.claude`, `~/.claude.json`, `~/.codex`, plus `allowWrite`. |
258
264
 
259
- **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 machine itself, on a device whose other agents stay sandboxed:
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:
260
266
 
261
267
  ```jsonc
262
268
  {
@@ -268,7 +274,7 @@ What the sandbox allows is `runners.policy` in `~/.cabane/config.json`. Every ke
268
274
  },
269
275
  },
270
276
  "agents": {
271
- "014d18a7-6395-48e3-a2f2-313d5baa2a5e": { "sandbox": "none" }, // the ops agent, by its id: it runs with the machine; everyone else stays sandboxed
277
+ "014d18a7-6395-48e3-a2f2-313d5baa2a5e": { "sandbox": "none" }, // the ops agent, by its id: it runs with the device; everyone else stays sandboxed
272
278
  },
273
279
  }
274
280
  ```
@@ -277,7 +283,7 @@ It works the other way too: `"agents": { "<agent id>": { "sandbox": "os" } }` sa
277
283
 
278
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`.
279
285
 
280
- **Other companions on the same machine.** 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.
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.
281
287
 
282
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.
283
289
 
@@ -290,9 +296,9 @@ Paths may start with `~/` and may be globs. An invalid `policy` doesn't stop the
290
296
  1. Create a fine-grained personal access token scoped to your repositories, with Contents and Pull requests set to read and write.
291
297
  2. Put it in `~/.cabane/secrets.json` as `"GITHUB_TOKEN": "github_pat_…"`.
292
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`.
293
- 4. Use HTTPS remotes, because `~/.ssh` is unreadable in the sandbox. Rewrite existing SSH remotes once on the machine: `git config --global url."https://github.com/".insteadOf git@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:`.
294
300
 
295
- A `gh auth login` session on the machine isn't used: `~/.config/gh` is unreadable in the sandbox. Only the token in `secrets.json` is.
301
+ A `gh auth login` session on the device isn't used: `~/.config/gh` is unreadable in the sandbox. Only the token in `secrets.json` is.
296
302
 
297
303
  `cabane status` prints the effective posture: the filesystem mode, extra hosts, denied hosts and reads, the **names** of masked credentials, never their values, and each agent excepted on its row. The Companion also logs it once at start.
298
304
 
@@ -300,7 +306,7 @@ A `gh auth login` session on the machine isn't used: `~/.config/gh` is unreadabl
300
306
 
301
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.
302
308
 
303
- Which harness a turn uses is the agent's **Connector**, chosen in Cabane under **Execution** on the agent's page (**Settings → Agents**), 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.
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.
304
310
 
305
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:
306
312
 
package/dist/cli.js CHANGED
@@ -11212,7 +11212,7 @@ var init_outbox = __esm({
11212
11212
  }
11213
11213
  this.log?.warn(
11214
11214
  { workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
11215
- "Couldn't save an update on this machine. Check free disk space and access to ~/.cabane."
11215
+ "Couldn't save an update on this device. Check free disk space and access to ~/.cabane."
11216
11216
  );
11217
11217
  return;
11218
11218
  }
@@ -12083,7 +12083,7 @@ function nothingUsableLines(detection) {
12083
12083
  if (reason) lines2.push(reason);
12084
12084
  }
12085
12085
  }
12086
- lines2.push(`Nothing has been started. Run ${cabane("setup")} to set up an agent.`);
12086
+ lines2.push(`Nothing has been started. Run ${cabane("setup")} to set up a coding tool.`);
12087
12087
  return lines2;
12088
12088
  }
12089
12089
  function indent(line) {
@@ -14402,7 +14402,10 @@ async function setupScript(opts, deps) {
14402
14402
  const detection = await detect(cfg, running, deps);
14403
14403
  ui.intro("Cabane setup");
14404
14404
  const usable = anyUsable(detection);
14405
- ui.step(["Agents on this device", ...agentLines(detection, { signInHint: paired && !usable })]);
14405
+ ui.step([
14406
+ "Coding tools on this device",
14407
+ ...agentLines(detection, { signInHint: paired && !usable })
14408
+ ]);
14406
14409
  if (paired) ui.step([PAIRED_LINE]);
14407
14410
  if (paired && running) {
14408
14411
  ui.step([runningLine(serviceInstalled())]);
@@ -17320,6 +17323,10 @@ var CompanionSupervisor = class {
17320
17323
  heartbeated = false;
17321
17324
  recoveryCursor;
17322
17325
  recoveredTurns = /* @__PURE__ */ new Set();
17326
+ // Agents assigned here, as `workspaceId|agentId`, whose sign-in to Cabane is missing and
17327
+ // couldn't be had back on the last pull. A dispatch for one is dropped as `missing_credential`,
17328
+ // so the room names that cause rather than a roster miss. Rebuilt on every pull.
17329
+ uncredentialed = /* @__PURE__ */ new Set();
17323
17330
  heartbeatTimer = null;
17324
17331
  inFlightHeartbeat = null;
17325
17332
  pollTimer = null;
@@ -17615,6 +17622,9 @@ var CompanionSupervisor = class {
17615
17622
  if (it.credential) setCredential(device.id, it.agentId, it.credential);
17616
17623
  }
17617
17624
  items = await this.restoreMissingCredentials(device.id, items);
17625
+ this.uncredentialed = new Set(
17626
+ items.filter((it) => !it.credential && !getCredential(device.id, it.agentId)).map((it) => `${it.workspaceId}|${it.agentId}`)
17627
+ );
17618
17628
  const byWorkspace = /* @__PURE__ */ new Map();
17619
17629
  for (const it of items) {
17620
17630
  const list = byWorkspace.get(it.workspaceId) ?? [];
@@ -17723,7 +17733,7 @@ var CompanionSupervisor = class {
17723
17733
  if (!credential) {
17724
17734
  this.log.error(
17725
17735
  { workspaceId, agentId: it.agentId, username: it.agentUsername },
17726
- `Cannot run ${it.agentDisplayName}: its sign-in to Cabane is missing. Reassign it to this device in Cabane.`
17736
+ `Cannot run ${it.agentDisplayName}: its sign-in to Cabane is missing and Cabane didn't send a new one. Asking again on the next check.`
17727
17737
  );
17728
17738
  this.removeAgent(wr, it.agentId);
17729
17739
  this.hub.setAgent(workspaceId, {
@@ -18209,7 +18219,8 @@ var CompanionSupervisor = class {
18209
18219
  conversationId: payload.conversationId,
18210
18220
  agentId: payload.agentId,
18211
18221
  messageId: payload.messageId,
18212
- reason: "agent_not_on_device"
18222
+ reason: "agent_not_on_device",
18223
+ ...this.uncredentialed.has(`${workspaceId}|${payload.agentId}`) ? { detail: "missing_credential" } : {}
18213
18224
  });
18214
18225
  this.log.debug(
18215
18226
  {
@@ -18961,7 +18972,7 @@ async function startScript(opts, interactive) {
18961
18972
  }
18962
18973
  if (!isDevicePaired()) {
18963
18974
  refuse([
18964
- `This device isn't set up yet. Run ${cabane("setup")} to connect an agent and pair it.`
18975
+ `This device isn't set up yet. Run ${cabane("setup")} to set up a coding tool and pair it.`
18965
18976
  ]);
18966
18977
  return;
18967
18978
  }
package/dist/runtime.js CHANGED
@@ -13014,7 +13014,7 @@ var Outbox = class {
13014
13014
  }
13015
13015
  this.log?.warn(
13016
13016
  { workspaceId: this.workspaceId, err: err instanceof Error ? err.message : String(err) },
13017
- "Couldn't save an update on this machine. Check free disk space and access to ~/.cabane."
13017
+ "Couldn't save an update on this device. Check free disk space and access to ~/.cabane."
13018
13018
  );
13019
13019
  return;
13020
13020
  }
@@ -13886,6 +13886,10 @@ var CompanionSupervisor = class {
13886
13886
  heartbeated = false;
13887
13887
  recoveryCursor;
13888
13888
  recoveredTurns = /* @__PURE__ */ new Set();
13889
+ // Agents assigned here, as `workspaceId|agentId`, whose sign-in to Cabane is missing and
13890
+ // couldn't be had back on the last pull. A dispatch for one is dropped as `missing_credential`,
13891
+ // so the room names that cause rather than a roster miss. Rebuilt on every pull.
13892
+ uncredentialed = /* @__PURE__ */ new Set();
13889
13893
  heartbeatTimer = null;
13890
13894
  inFlightHeartbeat = null;
13891
13895
  pollTimer = null;
@@ -14181,6 +14185,9 @@ var CompanionSupervisor = class {
14181
14185
  if (it.credential) setCredential(device.id, it.agentId, it.credential);
14182
14186
  }
14183
14187
  items = await this.restoreMissingCredentials(device.id, items);
14188
+ this.uncredentialed = new Set(
14189
+ items.filter((it) => !it.credential && !getCredential(device.id, it.agentId)).map((it) => `${it.workspaceId}|${it.agentId}`)
14190
+ );
14184
14191
  const byWorkspace = /* @__PURE__ */ new Map();
14185
14192
  for (const it of items) {
14186
14193
  const list = byWorkspace.get(it.workspaceId) ?? [];
@@ -14289,7 +14296,7 @@ var CompanionSupervisor = class {
14289
14296
  if (!credential) {
14290
14297
  this.log.error(
14291
14298
  { workspaceId, agentId: it.agentId, username: it.agentUsername },
14292
- `Cannot run ${it.agentDisplayName}: its sign-in to Cabane is missing. Reassign it to this device in Cabane.`
14299
+ `Cannot run ${it.agentDisplayName}: its sign-in to Cabane is missing and Cabane didn't send a new one. Asking again on the next check.`
14293
14300
  );
14294
14301
  this.removeAgent(wr, it.agentId);
14295
14302
  this.hub.setAgent(workspaceId, {
@@ -14775,7 +14782,8 @@ var CompanionSupervisor = class {
14775
14782
  conversationId: payload.conversationId,
14776
14783
  agentId: payload.agentId,
14777
14784
  messageId: payload.messageId,
14778
- reason: "agent_not_on_device"
14785
+ reason: "agent_not_on_device",
14786
+ ...this.uncredentialed.has(`${workspaceId}|${payload.agentId}`) ? { detail: "missing_credential" } : {}
14779
14787
  });
14780
14788
  this.log.debug(
14781
14789
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.165",
3
+ "version": "0.6.167",
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",