@cabane/companion 0.6.214 → 0.6.216
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 +41 -7
- package/dist/cli.js +410 -190
- package/dist/credential-broker.js +653 -0
- package/dist/launch.js +4 -1
- package/dist/pairing-config.js +100 -37
- package/dist/runtime.js +44 -8
- package/package.json +10 -1
package/dist/pairing-config.js
CHANGED
|
@@ -210,7 +210,10 @@ var companionConfigSchema = z2.object({
|
|
|
210
210
|
// credential and secrets on its stdin (`runner/launch.ts`, `wrapped`). The README lists
|
|
211
211
|
// what a wrapper can rely on.
|
|
212
212
|
runner: z2.object({
|
|
213
|
-
command: z2.array(z2.string().min(1)).min(1)
|
|
213
|
+
command: z2.array(z2.string().min(1)).min(1),
|
|
214
|
+
// The external wrapper supplies only broker placeholders and removes
|
|
215
|
+
// credential mounts. Do not hand provider logins to that runner on stdin.
|
|
216
|
+
brokeredCredentials: z2.boolean().optional()
|
|
214
217
|
}).strict().optional()
|
|
215
218
|
});
|
|
216
219
|
var RETIRED_ROW_KEYS = ["sandbox"];
|
|
@@ -413,55 +416,112 @@ function saveConfig(cfg) {
|
|
|
413
416
|
}
|
|
414
417
|
|
|
415
418
|
// src/enrollment.ts
|
|
416
|
-
|
|
419
|
+
var EnrollmentExpiredError = class extends CompanionError {
|
|
420
|
+
constructor() {
|
|
421
|
+
super("this pairing code expired before it was confirmed.");
|
|
422
|
+
this.name = "EnrollmentExpiredError";
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
var EnrollmentCancelledError = class extends CompanionError {
|
|
426
|
+
constructor() {
|
|
427
|
+
super("pairing was cancelled.");
|
|
428
|
+
this.name = "EnrollmentCancelledError";
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
function trimBase(baseUrl) {
|
|
432
|
+
return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
433
|
+
}
|
|
434
|
+
async function postJson(baseUrl, path, body, signal) {
|
|
417
435
|
let res;
|
|
418
436
|
try {
|
|
419
|
-
res = await fetch(`${baseUrl
|
|
437
|
+
res = await fetch(`${trimBase(baseUrl)}${path}`, {
|
|
420
438
|
method: "POST",
|
|
421
|
-
headers: {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
},
|
|
425
|
-
body: JSON.stringify({
|
|
426
|
-
userCode: input.userCode,
|
|
427
|
-
label: input.label,
|
|
428
|
-
platform: process.platform,
|
|
429
|
-
arch: process.arch,
|
|
430
|
-
nodeMajor: Number.parseInt(process.versions.node.split(".")[0], 10)
|
|
431
|
-
})
|
|
439
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
440
|
+
body: JSON.stringify(body),
|
|
441
|
+
...signal ? { signal } : {}
|
|
432
442
|
});
|
|
433
443
|
} catch {
|
|
444
|
+
if (signal?.aborted) throw new EnrollmentCancelledError();
|
|
434
445
|
throw new CompanionError(
|
|
435
446
|
`Couldn't reach Cabane at ${baseUrl}. Check the server URL and your connection.`
|
|
436
447
|
);
|
|
437
448
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
throw new CompanionError(
|
|
444
|
-
"The setup code or saved device credential is unavailable. If this device was removed, run cabane logout, then take a fresh setup line from your workspace\u2019s Settings \u2192 Devices."
|
|
445
|
-
);
|
|
446
|
-
if (res.status === 404 || res.status === 410)
|
|
449
|
+
let raw;
|
|
450
|
+
try {
|
|
451
|
+
raw = await res.text();
|
|
452
|
+
} catch {
|
|
453
|
+
if (signal?.aborted) throw new EnrollmentCancelledError();
|
|
447
454
|
throw new CompanionError(
|
|
448
|
-
|
|
455
|
+
`Couldn't reach Cabane at ${baseUrl}. Check the server URL and your connection.`
|
|
449
456
|
);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
457
|
+
}
|
|
458
|
+
let parsed = void 0;
|
|
459
|
+
if (raw.length > 0) {
|
|
460
|
+
try {
|
|
461
|
+
parsed = JSON.parse(raw);
|
|
462
|
+
} catch {
|
|
463
|
+
parsed = raw;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (res.status >= 400) {
|
|
467
|
+
if (res.status === 429)
|
|
468
|
+
throw new CompanionError(
|
|
469
|
+
"Too many pairing requests from this network. Wait a few minutes and run cabane setup again."
|
|
470
|
+
);
|
|
471
|
+
if (res.status === 404 && path.endsWith("/code"))
|
|
472
|
+
throw new CompanionError(
|
|
473
|
+
`No pairing endpoint at ${baseUrl}. Either that server is too old for this companion, or it isn't a Cabane API origin: the hosted app is https://app.cabane.ai (pass \`--server <url>\` for your own instance).`
|
|
474
|
+
);
|
|
475
|
+
const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
|
|
476
|
+
throw new ApiError(res.status, `${res.status} ${msg}`, parsed);
|
|
477
|
+
}
|
|
478
|
+
return parsed;
|
|
479
|
+
}
|
|
480
|
+
function requestEnrollmentCode(baseUrl, opts = {}) {
|
|
481
|
+
return postJson(
|
|
482
|
+
baseUrl,
|
|
483
|
+
"/api/device-enrollment/code",
|
|
484
|
+
{ ...opts.label ? { label: opts.label } : {}, ...hostFacts() },
|
|
485
|
+
opts.signal
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
function hostFacts() {
|
|
489
|
+
const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "", 10);
|
|
456
490
|
return {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
..."workspace" in body && body.workspace && typeof body.workspace === "object" && "name" in body.workspace && typeof body.workspace.name === "string" && "slug" in body.workspace && typeof body.workspace.slug === "string" ? { workspace: { name: body.workspace.name, slug: body.workspace.slug } } : {},
|
|
461
|
-
..."devicesUrl" in body && typeof body.devicesUrl === "string" ? { devicesUrl: body.devicesUrl } : {},
|
|
462
|
-
..."deviceLabel" in body && typeof body.deviceLabel === "string" ? { deviceLabel: body.deviceLabel } : {}
|
|
491
|
+
platform: process.platform,
|
|
492
|
+
arch: process.arch,
|
|
493
|
+
...Number.isInteger(nodeMajor) ? { nodeMajor } : {}
|
|
463
494
|
};
|
|
464
495
|
}
|
|
496
|
+
function pollOnce(baseUrl, deviceCode, signal) {
|
|
497
|
+
return postJson(baseUrl, "/api/device-enrollment/poll", { deviceCode }, signal);
|
|
498
|
+
}
|
|
499
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
500
|
+
async function awaitEnrollment(baseUrl, code, opts = {}) {
|
|
501
|
+
const now = opts.now ?? (() => Date.now());
|
|
502
|
+
const wait = opts.sleepMs ?? sleep;
|
|
503
|
+
const deadline = now() + code.expiresIn * 1e3;
|
|
504
|
+
let intervalMs = Math.max(1, code.interval) * 1e3;
|
|
505
|
+
while (now() < deadline) {
|
|
506
|
+
await wait(intervalMs);
|
|
507
|
+
if (opts.signal?.aborted) throw new EnrollmentCancelledError();
|
|
508
|
+
const res = await pollOnce(baseUrl, code.deviceCode, opts.signal);
|
|
509
|
+
if (opts.signal?.aborted) throw new EnrollmentCancelledError();
|
|
510
|
+
if (res.status === "complete") {
|
|
511
|
+
if (!res.deviceToken || !res.baseUrl)
|
|
512
|
+
throw new CompanionError("Cabane reported the pairing complete but returned no token.");
|
|
513
|
+
return {
|
|
514
|
+
baseUrl: res.baseUrl,
|
|
515
|
+
deviceToken: res.deviceToken,
|
|
516
|
+
...res.deviceId ? { deviceId: res.deviceId } : {},
|
|
517
|
+
...res.deviceLabel ? { deviceLabel: res.deviceLabel } : {}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (res.status === "expired") throw new EnrollmentExpiredError();
|
|
521
|
+
if (res.status === "slow_down") intervalMs += 1e3;
|
|
522
|
+
}
|
|
523
|
+
throw new EnrollmentExpiredError();
|
|
524
|
+
}
|
|
465
525
|
|
|
466
526
|
// src/pairing-config.ts
|
|
467
527
|
var DEFAULT_BASE_URL = "https://app.cabane.ai";
|
|
@@ -496,8 +556,11 @@ function isDevicePaired() {
|
|
|
496
556
|
export {
|
|
497
557
|
ApiError,
|
|
498
558
|
CompanionError,
|
|
499
|
-
|
|
559
|
+
EnrollmentCancelledError,
|
|
560
|
+
EnrollmentExpiredError,
|
|
561
|
+
awaitEnrollment,
|
|
500
562
|
isDevicePaired,
|
|
563
|
+
requestEnrollmentCode,
|
|
501
564
|
resolvePairBaseUrl,
|
|
502
565
|
writePairedConfig
|
|
503
566
|
};
|
package/dist/runtime.js
CHANGED
|
@@ -294,7 +294,10 @@ var companionConfigSchema = z2.object({
|
|
|
294
294
|
// credential and secrets on its stdin (`runner/launch.ts`, `wrapped`). The README lists
|
|
295
295
|
// what a wrapper can rely on.
|
|
296
296
|
runner: z2.object({
|
|
297
|
-
command: z2.array(z2.string().min(1)).min(1)
|
|
297
|
+
command: z2.array(z2.string().min(1)).min(1),
|
|
298
|
+
// The external wrapper supplies only broker placeholders and removes
|
|
299
|
+
// credential mounts. Do not hand provider logins to that runner on stdin.
|
|
300
|
+
brokeredCredentials: z2.boolean().optional()
|
|
298
301
|
}).strict().optional()
|
|
299
302
|
});
|
|
300
303
|
function workRoot(cfg) {
|
|
@@ -7801,6 +7804,30 @@ function decodeRunnerEvent(line) {
|
|
|
7801
7804
|
return value;
|
|
7802
7805
|
}
|
|
7803
7806
|
|
|
7807
|
+
// src/credential-broker/client-config.ts
|
|
7808
|
+
import { parse, stringify } from "smol-toml";
|
|
7809
|
+
var BROKERED_CREDENTIAL_NAMES = /* @__PURE__ */ new Set([
|
|
7810
|
+
"ANTHROPIC_API_KEY",
|
|
7811
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
7812
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
7813
|
+
"CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
|
|
7814
|
+
"CLAUDE_CODE_SESSION_ACCESS_TOKEN",
|
|
7815
|
+
"CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR",
|
|
7816
|
+
"CLAUDE_CODE_HOST_CREDS_FILE",
|
|
7817
|
+
"CODEX_API_KEY",
|
|
7818
|
+
"OPENAI_API_KEY",
|
|
7819
|
+
"OPENAI_ACCESS_TOKEN",
|
|
7820
|
+
"GH_TOKEN",
|
|
7821
|
+
"GITHUB_TOKEN",
|
|
7822
|
+
"GH_ENTERPRISE_TOKEN",
|
|
7823
|
+
"GITHUB_ENTERPRISE_TOKEN"
|
|
7824
|
+
]);
|
|
7825
|
+
function withoutBrokeredCredentials(values) {
|
|
7826
|
+
return Object.fromEntries(
|
|
7827
|
+
Object.entries(values).filter(([name]) => !BROKERED_CREDENTIAL_NAMES.has(name))
|
|
7828
|
+
);
|
|
7829
|
+
}
|
|
7830
|
+
|
|
7804
7831
|
// src/runner/launch.ts
|
|
7805
7832
|
function gzipBodiesSignal() {
|
|
7806
7833
|
let on = false;
|
|
@@ -8064,12 +8091,22 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
|
|
|
8064
8091
|
}
|
|
8065
8092
|
try {
|
|
8066
8093
|
const wrapped = launch.wrapped === true;
|
|
8067
|
-
|
|
8068
|
-
|
|
8094
|
+
if (launch.brokeredCredentials && !wrapped)
|
|
8095
|
+
throw new Error("credential broker requires a wrapped runner");
|
|
8096
|
+
const inherited = runnerExecEnv(launch.env ?? process.env, wrapped);
|
|
8097
|
+
const env = launch.brokeredCredentials ? withoutBrokeredCredentials(inherited) : inherited;
|
|
8098
|
+
const directGrants = minted.deviceGrants.length > 0 ? launch.grants?.(minted.deviceGrants) ?? {} : {};
|
|
8099
|
+
const grants = launch.brokeredCredentials ? Object.fromEntries(
|
|
8100
|
+
Object.entries(directGrants).map(([key, values]) => [
|
|
8101
|
+
key,
|
|
8102
|
+
withoutBrokeredCredentials(values)
|
|
8103
|
+
])
|
|
8104
|
+
) : directGrants;
|
|
8105
|
+
const secrets = wrapped ? launch.secrets?.() ?? {} : void 0;
|
|
8069
8106
|
const input = {
|
|
8070
8107
|
baseUrl: opts.baseUrl,
|
|
8071
8108
|
credential: minted.credential,
|
|
8072
|
-
...
|
|
8109
|
+
...secrets ? { secrets: launch.brokeredCredentials ? withoutBrokeredCredentials(secrets) : secrets } : {},
|
|
8073
8110
|
...Object.keys(grants).length > 0 ? { grants } : {},
|
|
8074
8111
|
dispatch: payload,
|
|
8075
8112
|
turnId,
|
|
@@ -8319,7 +8356,7 @@ var CANCEL_WAKE_TOOL_NAME = `mcp__${COMPANION_LOCAL_MCP_SERVER}__${CANCEL_WAKE_T
|
|
|
8319
8356
|
import { readFile as readFile2 } from "fs/promises";
|
|
8320
8357
|
import { homedir as homedir6 } from "os";
|
|
8321
8358
|
import { join as join15 } from "path";
|
|
8322
|
-
import { parse } from "smol-toml";
|
|
8359
|
+
import { parse as parse2 } from "smol-toml";
|
|
8323
8360
|
|
|
8324
8361
|
// src/codex-images.ts
|
|
8325
8362
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
@@ -10201,9 +10238,7 @@ var CompanionSupervisor = class {
|
|
|
10201
10238
|
this.detectTimer = setInterval(() => void this.detect(), DETECT_INTERVAL_MS);
|
|
10202
10239
|
this.detectTimer.unref?.();
|
|
10203
10240
|
if (!this.deviceApi || !this.config.deviceToken) {
|
|
10204
|
-
this.log.warn(
|
|
10205
|
-
"This device isn't paired. Get a setup line from a workspace's Settings \u2192 Devices."
|
|
10206
|
-
);
|
|
10241
|
+
this.log.warn(`This device isn't paired. Run ${cabane("setup")} on it.`);
|
|
10207
10242
|
return;
|
|
10208
10243
|
}
|
|
10209
10244
|
this.deviceSub = new SseSubscriber({
|
|
@@ -10763,6 +10798,7 @@ var CompanionSupervisor = class {
|
|
|
10763
10798
|
),
|
|
10764
10799
|
...this.config.runner?.command ? {
|
|
10765
10800
|
wrapped: true,
|
|
10801
|
+
brokeredCredentials: this.config.runner.brokeredCredentials,
|
|
10766
10802
|
secrets: () => loadSecretValuesTolerant(
|
|
10767
10803
|
(m) => this.log.warn({ err: m }, "Could not read the secrets file for a wrapped turn.")
|
|
10768
10804
|
)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.216",
|
|
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",
|
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
"cabane-companion": "./dist/bin.js"
|
|
15
15
|
},
|
|
16
16
|
"main": "./dist/cli.js",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./dist/cli.js",
|
|
19
|
+
"./runtime": "./dist/runtime.js",
|
|
20
|
+
"./pairing": "./dist/pairing-config.js",
|
|
21
|
+
"./credential-broker": "./dist/credential-broker.js",
|
|
22
|
+
"./package.json": "./package.json",
|
|
23
|
+
"./*": "./*"
|
|
24
|
+
},
|
|
17
25
|
"files": [
|
|
18
26
|
"dist"
|
|
19
27
|
],
|
|
@@ -37,6 +45,7 @@
|
|
|
37
45
|
"commander": "12.1.0",
|
|
38
46
|
"eventsource-parser": "3.0.8",
|
|
39
47
|
"hono": "4.12.18",
|
|
48
|
+
"graphql": "16.11.0",
|
|
40
49
|
"picocolors": "1.1.1",
|
|
41
50
|
"pino": "10.3.1",
|
|
42
51
|
"smol-toml": "1.8.0",
|