@cabane/companion 0.6.237 → 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 +78 -253
- package/dist/launch.js +41 -10
- package/dist/postinstall.js +41 -10
- package/package.json +1 -3
- package/dist/pairing-config.js +0 -591
- package/dist/runtime.js +0 -9663
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 +
|
|
@@ -133,7 +165,7 @@ var companionConfigSchema = z2.object({
|
|
|
133
165
|
// the rest of the config; `pair` always writes one.
|
|
134
166
|
deviceToken: z2.string().optional(),
|
|
135
167
|
// Identity hints, learned from the device flow and refreshed on the first
|
|
136
|
-
// assignments pull. Cosmetic — used for `status
|
|
168
|
+
// assignments pull. Cosmetic — used for `status` display only.
|
|
137
169
|
deviceId: z2.string().optional(),
|
|
138
170
|
deviceLabel: z2.string().optional(),
|
|
139
171
|
// Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
|
|
@@ -145,9 +177,8 @@ var companionConfigSchema = z2.object({
|
|
|
145
177
|
// silent roster: an agent nobody listed gets no hook and no error, and starts
|
|
146
178
|
// in the operator's `$HOME`.
|
|
147
179
|
prepareHook: prepareHookSchema.optional(),
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
// startup. `logLevel` is pino's level, live-editable.
|
|
180
|
+
// Nothing reads either dashboard setting any more; both stay parseable so an
|
|
181
|
+
// older config file still loads. `logLevel` is pino's level, live-editable.
|
|
151
182
|
dashboardPort: z2.number().int().min(1).max(65535).optional(),
|
|
152
183
|
autoOpen: z2.boolean().optional(),
|
|
153
184
|
logLevel: z2.enum(["warn", "info", "debug"]).optional(),
|
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 +
|
|
@@ -183,7 +215,7 @@ var companionConfigSchema = z2.object({
|
|
|
183
215
|
// the rest of the config; `pair` always writes one.
|
|
184
216
|
deviceToken: z2.string().optional(),
|
|
185
217
|
// Identity hints, learned from the device flow and refreshed on the first
|
|
186
|
-
// assignments pull. Cosmetic — used for `status
|
|
218
|
+
// assignments pull. Cosmetic — used for `status` display only.
|
|
187
219
|
deviceId: z2.string().optional(),
|
|
188
220
|
deviceLabel: z2.string().optional(),
|
|
189
221
|
// Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
|
|
@@ -195,9 +227,8 @@ var companionConfigSchema = z2.object({
|
|
|
195
227
|
// silent roster: an agent nobody listed gets no hook and no error, and starts
|
|
196
228
|
// in the operator's `$HOME`.
|
|
197
229
|
prepareHook: prepareHookSchema.optional(),
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
// startup. `logLevel` is pino's level, live-editable.
|
|
230
|
+
// Nothing reads either dashboard setting any more; both stay parseable so an
|
|
231
|
+
// older config file still loads. `logLevel` is pino's level, live-editable.
|
|
201
232
|
dashboardPort: z2.number().int().min(1).max(65535).optional(),
|
|
202
233
|
autoOpen: z2.boolean().optional(),
|
|
203
234
|
logLevel: z2.enum(["warn", "info", "debug"]).optional(),
|
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",
|
|
@@ -16,8 +16,6 @@
|
|
|
16
16
|
"main": "./dist/cli.js",
|
|
17
17
|
"exports": {
|
|
18
18
|
".": "./dist/cli.js",
|
|
19
|
-
"./runtime": "./dist/runtime.js",
|
|
20
|
-
"./pairing": "./dist/pairing-config.js",
|
|
21
19
|
"./credential-broker": "./dist/credential-broker.js",
|
|
22
20
|
"./package.json": "./package.json",
|
|
23
21
|
"./*": "./*"
|