@cabane/companion 0.6.238 → 0.6.239
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 +2 -0
- package/dist/cli.js +55 -8
- package/dist/launch.js +38 -6
- package/dist/postinstall.js +38 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,8 @@ Enter the code in Cabane, signed in as yourself: from Your settings → Devices
|
|
|
38
38
|
|
|
39
39
|
Without a terminal, setup prints the same lines once and waits, so a container's logs carry the code. An expired code is replaced by a fresh one, up to three; Ctrl-C stops cleanly. The link is on the Cabane the device pairs with. There is no default: setup pairs with the Cabane named by `--server <url>`, by `CABANE_BASE_URL`, or by the device's config, and stops if none names one. Once a Cabane has issued a code, setup keeps its address, so after a cancelled or expired pairing a bare `cabane setup` pairs with the same one.
|
|
40
40
|
|
|
41
|
+
The address must be https, or plain http to this machine (`localhost`, `127.0.0.1`). A Companion that reaches its Cabane across a private network where the server has no certificate, such as a container on a Docker network it shares only with the server, can be allowed plain http to that one host with `CABANE_ALLOW_PLAIN_HTTP_HOST`: for example `CABANE_ALLOW_PLAIN_HTTP_HOST=app` with `CABANE_BASE_URL=http://app:8788`. The value must be a name with no dot, which only the local network's resolver answers, or a private address (`10.x.x.x`, `172.16.x.x`–`172.31.x.x`, `192.168.x.x`, or IPv6 `fc00::/7`); anything else is ignored. A Companion that pairs over plain http to that host keeps that address, instead of the public one Cabane names, since that is the only way it reaches Cabane. Over plain http, anything else on that network can read the device token and the agents' work, so use it only on a network nothing else joins.
|
|
42
|
+
|
|
41
43
|
From an interactive terminal, setup then starts the Companion in the background, waits for its first check-in and reports the device's state. Without an interactive terminal, setup stays attached to the running Companion.
|
|
42
44
|
|
|
43
45
|
If no harness is usable, interactive setup asks which one you want to set up. Follow the complete CLI installation and sign-in steps for [Mac setup](https://cabane.ai/docs/mac-setup#2-sign-in), [Windows setup](https://cabane.ai/docs/windows-setup#2-sign-in) or [Server and VPS setup](https://cabane.ai/docs/server-and-vps-setup#2-sign-in-without-a-browser), then check `cabane status`.
|
package/dist/cli.js
CHANGED
|
@@ -279,7 +279,7 @@ var init_errors = __esm({
|
|
|
279
279
|
});
|
|
280
280
|
|
|
281
281
|
// src/pairing.ts
|
|
282
|
-
function isAllowedBaseUrl(raw) {
|
|
282
|
+
function isAllowedBaseUrl(raw, env = process.env) {
|
|
283
283
|
let url;
|
|
284
284
|
try {
|
|
285
285
|
url = new URL(raw);
|
|
@@ -289,13 +289,57 @@ function isAllowedBaseUrl(raw) {
|
|
|
289
289
|
if (url.protocol === "https:") return true;
|
|
290
290
|
if (url.protocol === "http:") {
|
|
291
291
|
const host = url.hostname.toLowerCase();
|
|
292
|
-
|
|
292
|
+
if (host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "::1" || host === "[::1]")
|
|
293
|
+
return true;
|
|
294
|
+
const allowed = plainHttpHost(env);
|
|
295
|
+
return allowed !== null && host === allowed;
|
|
293
296
|
}
|
|
294
297
|
return false;
|
|
295
298
|
}
|
|
299
|
+
function plainHttpHost(env = process.env) {
|
|
300
|
+
const raw = env[ALLOW_PLAIN_HTTP_HOST_ENV]?.trim().toLowerCase();
|
|
301
|
+
if (!raw) return null;
|
|
302
|
+
if (isSingleLabel(raw) || isPrivateIpv4(raw)) return raw;
|
|
303
|
+
const bare = raw.startsWith("[") && raw.endsWith("]") ? raw.slice(1, -1) : raw;
|
|
304
|
+
return privateIpv6(bare);
|
|
305
|
+
}
|
|
306
|
+
function isSingleLabel(host) {
|
|
307
|
+
return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(host) && !/^\d+$/.test(host);
|
|
308
|
+
}
|
|
309
|
+
function isPrivateIpv4(host) {
|
|
310
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
311
|
+
if (!m) return false;
|
|
312
|
+
const [a, b, c, d] = m.slice(1).map(Number);
|
|
313
|
+
if ([a, b, c, d].some((n) => n > 255)) return false;
|
|
314
|
+
return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
|
|
315
|
+
}
|
|
316
|
+
function privateIpv6(host) {
|
|
317
|
+
if (!host.includes(":")) return null;
|
|
318
|
+
let bracketed;
|
|
319
|
+
try {
|
|
320
|
+
bracketed = new URL(`http://[${host}]/`).hostname;
|
|
321
|
+
} catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
const first = Number.parseInt(bracketed.slice(1).split(":")[0] || "0", 16);
|
|
325
|
+
return (first & 65024) === 64512 ? bracketed : null;
|
|
326
|
+
}
|
|
327
|
+
function addressToKeep(pairedOver, cabaneNamed, env = process.env) {
|
|
328
|
+
const allowed = plainHttpHost(env);
|
|
329
|
+
if (allowed === null) return cabaneNamed;
|
|
330
|
+
let url;
|
|
331
|
+
try {
|
|
332
|
+
url = new URL(pairedOver);
|
|
333
|
+
} catch {
|
|
334
|
+
return cabaneNamed;
|
|
335
|
+
}
|
|
336
|
+
return url.protocol === "http:" && url.hostname.toLowerCase() === allowed ? pairedOver.replace(/\/+$/, "") : cabaneNamed;
|
|
337
|
+
}
|
|
338
|
+
var ALLOW_PLAIN_HTTP_HOST_ENV;
|
|
296
339
|
var init_pairing = __esm({
|
|
297
340
|
"src/pairing.ts"() {
|
|
298
341
|
"use strict";
|
|
342
|
+
ALLOW_PLAIN_HTTP_HOST_ENV = "CABANE_ALLOW_PLAIN_HTTP_HOST";
|
|
299
343
|
}
|
|
300
344
|
});
|
|
301
345
|
|
|
@@ -712,10 +756,10 @@ var init_config = __esm({
|
|
|
712
756
|
}).strict();
|
|
713
757
|
agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema);
|
|
714
758
|
companionConfigSchema = z2.object({
|
|
715
|
-
// https-enforced (loopback
|
|
716
|
-
// device token and prompt on a plaintext channel.
|
|
717
|
-
baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
|
|
718
|
-
message:
|
|
759
|
+
// https-enforced (loopback, and the one private host a person names, exempt), so a
|
|
760
|
+
// hand-edited config can't put the device token and prompt on a plaintext channel.
|
|
761
|
+
baseUrl: z2.string().url().refine((url) => isAllowedBaseUrl(url), {
|
|
762
|
+
message: `baseUrl must be https (plain http only to this machine, or to the private host ${ALLOW_PLAIN_HTTP_HOST_ENV} names)`
|
|
719
763
|
}),
|
|
720
764
|
// The `cabdev_` device token plaintext — the companion's one durable credential,
|
|
721
765
|
// presented as `Authorization: Bearer <token>` on the device-facing pull +
|
|
@@ -10287,6 +10331,9 @@ function openLink(link) {
|
|
|
10287
10331
|
});
|
|
10288
10332
|
}
|
|
10289
10333
|
|
|
10334
|
+
// src/commands/setup.ts
|
|
10335
|
+
init_pairing();
|
|
10336
|
+
|
|
10290
10337
|
// src/commands/start.ts
|
|
10291
10338
|
init_cabane_command();
|
|
10292
10339
|
init_config();
|
|
@@ -10302,7 +10349,7 @@ function resolvePairBaseUrl(server) {
|
|
|
10302
10349
|
if (!raw) throw new NoPairAddressError();
|
|
10303
10350
|
if (!isAllowedBaseUrl(raw)) {
|
|
10304
10351
|
throw new CompanionError(
|
|
10305
|
-
`invalid server URL "${raw}": must be an https URL (
|
|
10352
|
+
`invalid server URL "${raw}": must be an https URL (plain http only to this machine, or to the private host ${ALLOW_PLAIN_HTTP_HOST_ENV} names). Pass it as \`${cabane("setup --server https://your-cabane")}\`.`
|
|
10306
10353
|
);
|
|
10307
10354
|
}
|
|
10308
10355
|
return raw;
|
|
@@ -15124,7 +15171,7 @@ async function setupScript(opts, deps) {
|
|
|
15124
15171
|
onCode: () => rememberPairAddress(baseUrl)
|
|
15125
15172
|
});
|
|
15126
15173
|
if (!device) return;
|
|
15127
|
-
writePairedConfig(device);
|
|
15174
|
+
writePairedConfig({ ...device, baseUrl: addressToKeep(baseUrl, device.baseUrl) });
|
|
15128
15175
|
justPaired = true;
|
|
15129
15176
|
}
|
|
15130
15177
|
const detection = await start(
|
package/dist/launch.js
CHANGED
|
@@ -45,7 +45,8 @@ import { dirname as dirname3, isAbsolute, join as join4 } from "path";
|
|
|
45
45
|
import { z as z2 } from "zod";
|
|
46
46
|
|
|
47
47
|
// src/pairing.ts
|
|
48
|
-
|
|
48
|
+
var ALLOW_PLAIN_HTTP_HOST_ENV = "CABANE_ALLOW_PLAIN_HTTP_HOST";
|
|
49
|
+
function isAllowedBaseUrl(raw, env = process.env) {
|
|
49
50
|
let url;
|
|
50
51
|
try {
|
|
51
52
|
url = new URL(raw);
|
|
@@ -55,10 +56,41 @@ function isAllowedBaseUrl(raw) {
|
|
|
55
56
|
if (url.protocol === "https:") return true;
|
|
56
57
|
if (url.protocol === "http:") {
|
|
57
58
|
const host = url.hostname.toLowerCase();
|
|
58
|
-
|
|
59
|
+
if (host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "::1" || host === "[::1]")
|
|
60
|
+
return true;
|
|
61
|
+
const allowed = plainHttpHost(env);
|
|
62
|
+
return allowed !== null && host === allowed;
|
|
59
63
|
}
|
|
60
64
|
return false;
|
|
61
65
|
}
|
|
66
|
+
function plainHttpHost(env = process.env) {
|
|
67
|
+
const raw = env[ALLOW_PLAIN_HTTP_HOST_ENV]?.trim().toLowerCase();
|
|
68
|
+
if (!raw) return null;
|
|
69
|
+
if (isSingleLabel(raw) || isPrivateIpv4(raw)) return raw;
|
|
70
|
+
const bare = raw.startsWith("[") && raw.endsWith("]") ? raw.slice(1, -1) : raw;
|
|
71
|
+
return privateIpv6(bare);
|
|
72
|
+
}
|
|
73
|
+
function isSingleLabel(host) {
|
|
74
|
+
return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(host) && !/^\d+$/.test(host);
|
|
75
|
+
}
|
|
76
|
+
function isPrivateIpv4(host) {
|
|
77
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
78
|
+
if (!m) return false;
|
|
79
|
+
const [a, b, c, d] = m.slice(1).map(Number);
|
|
80
|
+
if ([a, b, c, d].some((n) => n > 255)) return false;
|
|
81
|
+
return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
|
|
82
|
+
}
|
|
83
|
+
function privateIpv6(host) {
|
|
84
|
+
if (!host.includes(":")) return null;
|
|
85
|
+
let bracketed;
|
|
86
|
+
try {
|
|
87
|
+
bracketed = new URL(`http://[${host}]/`).hostname;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const first = Number.parseInt(bracketed.slice(1).split(":")[0] || "0", 16);
|
|
92
|
+
return (first & 65024) === 64512 ? bracketed : null;
|
|
93
|
+
}
|
|
62
94
|
|
|
63
95
|
// src/prepare-hook.ts
|
|
64
96
|
import { spawn } from "child_process";
|
|
@@ -121,10 +153,10 @@ function isRootPath(value) {
|
|
|
121
153
|
return isAbsolute(value) || value === "~" || value.startsWith("~/");
|
|
122
154
|
}
|
|
123
155
|
var companionConfigSchema = z2.object({
|
|
124
|
-
// https-enforced (loopback
|
|
125
|
-
// device token and prompt on a plaintext channel.
|
|
126
|
-
baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
|
|
127
|
-
message:
|
|
156
|
+
// https-enforced (loopback, and the one private host a person names, exempt), so a
|
|
157
|
+
// hand-edited config can't put the device token and prompt on a plaintext channel.
|
|
158
|
+
baseUrl: z2.string().url().refine((url) => isAllowedBaseUrl(url), {
|
|
159
|
+
message: `baseUrl must be https (plain http only to this machine, or to the private host ${ALLOW_PLAIN_HTTP_HOST_ENV} names)`
|
|
128
160
|
}),
|
|
129
161
|
// The `cabdev_` device token plaintext — the companion's one durable credential,
|
|
130
162
|
// presented as `Authorization: Bearer <token>` on the device-facing pull +
|
package/dist/postinstall.js
CHANGED
|
@@ -92,7 +92,8 @@ var ConfigError = class extends CompanionError {
|
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
// src/pairing.ts
|
|
95
|
-
|
|
95
|
+
var ALLOW_PLAIN_HTTP_HOST_ENV = "CABANE_ALLOW_PLAIN_HTTP_HOST";
|
|
96
|
+
function isAllowedBaseUrl(raw, env = process.env) {
|
|
96
97
|
let url;
|
|
97
98
|
try {
|
|
98
99
|
url = new URL(raw);
|
|
@@ -102,10 +103,41 @@ function isAllowedBaseUrl(raw) {
|
|
|
102
103
|
if (url.protocol === "https:") return true;
|
|
103
104
|
if (url.protocol === "http:") {
|
|
104
105
|
const host = url.hostname.toLowerCase();
|
|
105
|
-
|
|
106
|
+
if (host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "::1" || host === "[::1]")
|
|
107
|
+
return true;
|
|
108
|
+
const allowed = plainHttpHost(env);
|
|
109
|
+
return allowed !== null && host === allowed;
|
|
106
110
|
}
|
|
107
111
|
return false;
|
|
108
112
|
}
|
|
113
|
+
function plainHttpHost(env = process.env) {
|
|
114
|
+
const raw = env[ALLOW_PLAIN_HTTP_HOST_ENV]?.trim().toLowerCase();
|
|
115
|
+
if (!raw) return null;
|
|
116
|
+
if (isSingleLabel(raw) || isPrivateIpv4(raw)) return raw;
|
|
117
|
+
const bare = raw.startsWith("[") && raw.endsWith("]") ? raw.slice(1, -1) : raw;
|
|
118
|
+
return privateIpv6(bare);
|
|
119
|
+
}
|
|
120
|
+
function isSingleLabel(host) {
|
|
121
|
+
return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(host) && !/^\d+$/.test(host);
|
|
122
|
+
}
|
|
123
|
+
function isPrivateIpv4(host) {
|
|
124
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
125
|
+
if (!m) return false;
|
|
126
|
+
const [a, b, c, d] = m.slice(1).map(Number);
|
|
127
|
+
if ([a, b, c, d].some((n) => n > 255)) return false;
|
|
128
|
+
return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
|
|
129
|
+
}
|
|
130
|
+
function privateIpv6(host) {
|
|
131
|
+
if (!host.includes(":")) return null;
|
|
132
|
+
let bracketed;
|
|
133
|
+
try {
|
|
134
|
+
bracketed = new URL(`http://[${host}]/`).hostname;
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const first = Number.parseInt(bracketed.slice(1).split(":")[0] || "0", 16);
|
|
139
|
+
return (first & 65024) === 64512 ? bracketed : null;
|
|
140
|
+
}
|
|
109
141
|
|
|
110
142
|
// src/prepare-hook.ts
|
|
111
143
|
import { spawn } from "child_process";
|
|
@@ -171,10 +203,10 @@ function isRootPath(value) {
|
|
|
171
203
|
return isAbsolute(value) || value === "~" || value.startsWith("~/");
|
|
172
204
|
}
|
|
173
205
|
var companionConfigSchema = z2.object({
|
|
174
|
-
// https-enforced (loopback
|
|
175
|
-
// device token and prompt on a plaintext channel.
|
|
176
|
-
baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
|
|
177
|
-
message:
|
|
206
|
+
// https-enforced (loopback, and the one private host a person names, exempt), so a
|
|
207
|
+
// hand-edited config can't put the device token and prompt on a plaintext channel.
|
|
208
|
+
baseUrl: z2.string().url().refine((url) => isAllowedBaseUrl(url), {
|
|
209
|
+
message: `baseUrl must be https (plain http only to this machine, or to the private host ${ALLOW_PLAIN_HTTP_HOST_ENV} names)`
|
|
178
210
|
}),
|
|
179
211
|
// The `cabdev_` device token plaintext — the companion's one durable credential,
|
|
180
212
|
// presented as `Authorization: Bearer <token>` on the device-facing pull +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.239",
|
|
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",
|