@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.
@@ -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
- async function claimSetupCode(baseUrl, input) {
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.replace(/\/$/, "")}/api/device-enrollment/claim`, {
437
+ res = await fetch(`${trimBase(baseUrl)}${path}`, {
420
438
  method: "POST",
421
- headers: {
422
- "content-type": "application/json",
423
- ...input.deviceToken ? { authorization: `Bearer ${input.deviceToken}` } : {}
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
- if (res.status === 429)
439
- throw new CompanionError(
440
- "Too many attempts from this network. Wait a few minutes and paste the setup line again."
441
- );
442
- if (res.status === 404 && input.deviceToken)
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
- "That setup code is unavailable. Take a fresh setup line from your workspace\u2019s Settings \u2192 Devices."
455
+ `Couldn't reach Cabane at ${baseUrl}. Check the server URL and your connection.`
449
456
  );
450
- if (!res.ok) throw new ApiError(res.status, `Setup failed (${res.status}).`);
451
- const body = await res.json();
452
- if (!body || typeof body !== "object" || !("baseUrl" in body) || typeof body.baseUrl !== "string" || !("deviceId" in body) || typeof body.deviceId !== "string")
453
- throw new CompanionError("Cabane returned an incomplete setup response.");
454
- const token = "deviceToken" in body && typeof body.deviceToken === "string" ? body.deviceToken : input.deviceToken;
455
- if (!token) throw new CompanionError("Cabane returned no device credential.");
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
- baseUrl: body.baseUrl,
458
- deviceToken: token,
459
- deviceId: body.deviceId,
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
- claimSetupCode,
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
- const env = runnerExecEnv(launch.env ?? process.env, wrapped);
8068
- const grants = minted.deviceGrants.length > 0 ? launch.grants?.(minted.deviceGrants) ?? {} : {};
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
- ...wrapped ? { secrets: launch.secrets?.() ?? {} } : {},
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.214",
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",