@cabane/companion 0.6.192 → 0.6.194

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/dist/cli.js CHANGED
@@ -7560,6 +7560,7 @@ function parseRunnerInput(env, stdin) {
7560
7560
  baseUrl: required(env, RUNNER_ENV.url),
7561
7561
  credential: credentials.credential,
7562
7562
  ...credentials.secrets ? { secrets: credentials.secrets } : {},
7563
+ ...credentials.grants ? { grants: credentials.grants } : {},
7563
7564
  dispatch,
7564
7565
  turnId: required(env, RUNNER_ENV.turnId),
7565
7566
  resumed: env[RUNNER_ENV.resumed] === "1",
@@ -7579,7 +7580,8 @@ function runnerEnv(input) {
7579
7580
  function runnerStdin(input) {
7580
7581
  return `${JSON.stringify({
7581
7582
  credential: input.credential,
7582
- ...input.secrets ? { secrets: input.secrets } : {}
7583
+ ...input.secrets ? { secrets: input.secrets } : {},
7584
+ ...input.grants ? { grants: input.grants } : {}
7583
7585
  })}
7584
7586
  `;
7585
7587
  }
@@ -7684,7 +7686,8 @@ var init_protocol = __esm({
7684
7686
  });
7685
7687
  credentialsSchema = z14.object({
7686
7688
  credential: z14.string().startsWith("cabrn_"),
7687
- secrets: z14.record(z14.string(), z14.string()).optional()
7689
+ secrets: z14.record(z14.string(), z14.string()).optional(),
7690
+ grants: z14.record(z14.string(), z14.record(z14.string(), z14.string())).optional()
7688
7691
  });
7689
7692
  }
7690
7693
  });
@@ -7811,7 +7814,12 @@ async function mintForRunner(opts, payload, handleOpts, signal, log2) {
7811
7814
  // The one turn the credential will operate: this run's.
7812
7815
  turnId: handleOpts.turnId
7813
7816
  });
7814
- return { ok: true, credential: minted.credential, runnerId: minted.runnerId };
7817
+ return {
7818
+ ok: true,
7819
+ credential: minted.credential,
7820
+ runnerId: minted.runnerId,
7821
+ deviceGrants: minted.deviceGrants ?? []
7822
+ };
7815
7823
  } catch (err) {
7816
7824
  if (signal.aborted) return { ok: false, reason: "cancelled" };
7817
7825
  const status2 = err instanceof ApiError ? err.status : 0;
@@ -7906,10 +7914,12 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
7906
7914
  try {
7907
7915
  const boxed = launch.boxed === true;
7908
7916
  const env = runnerExecEnv(launch.env ?? process.env, boxed);
7917
+ const grants = minted.deviceGrants.length > 0 ? launch.grants?.(minted.deviceGrants) ?? {} : {};
7909
7918
  const input = {
7910
7919
  baseUrl: opts.baseUrl,
7911
7920
  credential: minted.credential,
7912
7921
  ...boxed ? { secrets: launch.secrets?.() ?? {} } : {},
7922
+ ...Object.keys(grants).length > 0 ? { grants } : {},
7913
7923
  dispatch: payload,
7914
7924
  turnId,
7915
7925
  resumed: handleOpts.resumed,
@@ -8072,11 +8082,101 @@ var init_launch = __esm({
8072
8082
  }
8073
8083
  });
8074
8084
 
8085
+ // src/grants.ts
8086
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
8087
+ import { join as join14 } from "path";
8088
+ import { z as z15 } from "zod";
8089
+ function allowedGrantVariable(name) {
8090
+ return name.length <= 100 && VARIABLE_NAME_RE.test(name) && !name.startsWith("CABANE_");
8091
+ }
8092
+ function environmentSafe(value) {
8093
+ return !value.includes("\0");
8094
+ }
8095
+ function grantsPath() {
8096
+ return join14(cabaneDir(), "grants.json");
8097
+ }
8098
+ function readGrants(keys, report) {
8099
+ if (keys.length === 0) return {};
8100
+ const file = loadGrantsFile(report);
8101
+ const out = {};
8102
+ for (const key of keys) {
8103
+ if (!allowedGrantVariable(key) || !Object.hasOwn(file, key)) continue;
8104
+ const entry = {};
8105
+ for (const [variable, value] of Object.entries(file[key])) {
8106
+ if (allowedGrantVariable(variable) && environmentSafe(value)) entry[variable] = value;
8107
+ else report.refusedVariable(key, variable);
8108
+ }
8109
+ out[key] = entry;
8110
+ }
8111
+ return out;
8112
+ }
8113
+ function loadGrantsFile(report) {
8114
+ const path = grantsPath();
8115
+ if (!existsSync9(path)) return {};
8116
+ let raw;
8117
+ try {
8118
+ raw = readFileSync7(path, "utf8");
8119
+ } catch (err) {
8120
+ const code = err.code ?? "unknown error";
8121
+ report.unreadable(`couldn't read ${path}: ${code}`);
8122
+ return {};
8123
+ }
8124
+ if (raw.trim().length === 0) return {};
8125
+ let parsed;
8126
+ try {
8127
+ parsed = JSON.parse(raw);
8128
+ } catch {
8129
+ report.unreadable(`${path} is not valid JSON.`);
8130
+ return {};
8131
+ }
8132
+ const result = grantsFileSchema.safeParse(parsed);
8133
+ if (!result.success) {
8134
+ report.unreadable(
8135
+ `${path} must map a secret's variable name to an object of "VARIABLE": "value" pairs (e.g. { "HAS_PROD_ACCESS": { "RENDER_API_KEY": "rnd_\u2026" } }).`
8136
+ );
8137
+ return {};
8138
+ }
8139
+ return result.data;
8140
+ }
8141
+ function harnessSecretEnv(parts) {
8142
+ const env = {};
8143
+ const dropped = [];
8144
+ for (const [name, value] of Object.entries({
8145
+ ...parts.deviceDefaults,
8146
+ ...parts.granted,
8147
+ ...parts.issued
8148
+ })) {
8149
+ if (environmentSafe(value)) env[name] = value;
8150
+ else dropped.push(name);
8151
+ }
8152
+ return { env, dropped };
8153
+ }
8154
+ function grantEnv(sent, issuedBySecret, issued) {
8155
+ const env = {};
8156
+ for (const [key, entry] of Object.entries(sent ?? {})) {
8157
+ if (!Object.hasOwn(issuedBySecret, key)) continue;
8158
+ for (const [variable, value] of Object.entries(entry)) {
8159
+ if (Object.hasOwn(issued, variable) || Object.hasOwn(env, variable)) continue;
8160
+ env[variable] = value;
8161
+ }
8162
+ }
8163
+ return env;
8164
+ }
8165
+ var VARIABLE_NAME_RE, grantsFileSchema;
8166
+ var init_grants = __esm({
8167
+ "src/grants.ts"() {
8168
+ "use strict";
8169
+ init_config();
8170
+ VARIABLE_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
8171
+ grantsFileSchema = z15.record(z15.string(), z15.record(z15.string(), z15.string()));
8172
+ }
8173
+ });
8174
+
8075
8175
  // src/work-dir.ts
8076
8176
  import { statSync as statSync3 } from "fs";
8077
- import { join as join14 } from "path";
8177
+ import { join as join15 } from "path";
8078
8178
  function pairDirectory(root, pair) {
8079
- return join14(root, pair.workspaceSlug, pair.agentUsername, pair.conversationId);
8179
+ return join15(root, pair.workspaceSlug, pair.agentUsername, pair.conversationId);
8080
8180
  }
8081
8181
  function chooseStandingDirectory(named, pairDir, isDirectory = directoryExists) {
8082
8182
  const skipped = [];
@@ -8101,7 +8201,7 @@ var init_work_dir = __esm({
8101
8201
 
8102
8202
  // src/turn-control-tools.ts
8103
8203
  import { randomUUID as randomUUID2 } from "crypto";
8104
- import { z as z15 } from "zod";
8204
+ import { z as z16 } from "zod";
8105
8205
  function createSkipState() {
8106
8206
  return { skipped: false, reason: null };
8107
8207
  }
@@ -8152,7 +8252,7 @@ function createTurnControlMcpServer(acts, skipState) {
8152
8252
  SKIP_TURN_TOOL,
8153
8253
  TURN_CONTROL_TOOL_DESCRIPTIONS.skip_turn,
8154
8254
  {
8155
- reason: z15.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
8255
+ reason: z16.string().min(1).max(MAX_SKIP_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.skip_turn.reason)
8156
8256
  },
8157
8257
  async (args) => {
8158
8258
  skipState.skipped = true;
@@ -8168,22 +8268,22 @@ function createTurnControlMcpServer(acts, skipState) {
8168
8268
  ASK_TOOL,
8169
8269
  TURN_CONTROL_TOOL_DESCRIPTIONS.ask,
8170
8270
  {
8171
- targetUserId: z15.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
8172
- question: z15.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
8173
- headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
8174
- options: z15.array(z15.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
8175
- questions: z15.array(
8176
- z15.object({
8177
- headline: z15.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
8178
- body: z15.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
8179
- options: z15.array(
8180
- z15.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)
8271
+ targetUserId: z16.string().regex(ASK_TARGET_PATTERN, { error: ASK_TARGET_INVALID }).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.targetUserId),
8272
+ question: z16.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.question),
8273
+ headline: z16.string().min(1).max(MAX_ASK_HEADLINE_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.headline),
8274
+ options: z16.array(z16.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.options),
8275
+ questions: z16.array(
8276
+ z16.object({
8277
+ headline: z16.string().min(1).max(MAX_ASK_HEADLINE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.headline"]),
8278
+ body: z16.string().min(1).max(MAX_ASK_QUESTION_BYTES).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.body"]),
8279
+ options: z16.array(
8280
+ z16.string({ error: ASK_OPTION_NOT_A_STRING }).min(1).max(MAX_ASK_OPTION_BYTES)
8181
8281
  ).min(MIN_ASK_OPTIONS).max(MAX_ASK_OPTIONS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["questions.options"])
8182
8282
  })
8183
8283
  ).min(1).max(MAX_ASK_ITEMS).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.questions),
8184
- replaces: z15.object({
8185
- askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
8186
- reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
8284
+ replaces: z16.object({
8285
+ askId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.askId"]),
8286
+ reason: z16.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask["replaces.reason"])
8187
8287
  }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.ask.replaces)
8188
8288
  },
8189
8289
  async (args) => {
@@ -8224,9 +8324,9 @@ function createTurnControlMcpServer(acts, skipState) {
8224
8324
  WAKE_ME_TOOL,
8225
8325
  TURN_CONTROL_TOOL_DESCRIPTIONS.wake_me,
8226
8326
  {
8227
- afterSeconds: z15.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
8228
- at: z15.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
8229
- note: z15.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
8327
+ afterSeconds: z16.number().int().positive().optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.afterSeconds),
8328
+ at: z16.string().datetime({ offset: true }).optional().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.at),
8329
+ note: z16.string().min(1).max(MAX_WAKE_NOTE_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.wake_me.note)
8230
8330
  },
8231
8331
  async (args) => {
8232
8332
  const hasAfter = args.afterSeconds !== void 0;
@@ -8260,8 +8360,8 @@ function createTurnControlMcpServer(acts, skipState) {
8260
8360
  WITHDRAW_ASK_TOOL,
8261
8361
  TURN_CONTROL_TOOL_DESCRIPTIONS.withdraw_ask,
8262
8362
  {
8263
- askId: z15.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
8264
- reason: z15.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
8363
+ askId: z16.string().uuid().describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.askId),
8364
+ reason: z16.string().min(1).max(MAX_WITHDRAW_REASON_BYTES).describe(TURN_CONTROL_FIELD_DESCRIPTIONS.withdraw_ask.reason)
8265
8365
  },
8266
8366
  async (args) => performAct(acts, {
8267
8367
  kind: "withdraw_ask",
@@ -8359,10 +8459,10 @@ var init_build_options = __esm({
8359
8459
  // src/codex-config.ts
8360
8460
  import { readFile as readFile2 } from "fs/promises";
8361
8461
  import { homedir as homedir7 } from "os";
8362
- import { join as join15 } from "path";
8462
+ import { join as join16 } from "path";
8363
8463
  import { parse } from "smol-toml";
8364
8464
  function codexConfigPath(env = process.env, home = homedir7()) {
8365
- return join15(env.CODEX_HOME || join15(home, ".codex"), "config.toml");
8465
+ return join16(env.CODEX_HOME || join16(home, ".codex"), "config.toml");
8366
8466
  }
8367
8467
  async function readCodexConfigSetsSandboxMode(path = codexConfigPath()) {
8368
8468
  let text;
@@ -8389,13 +8489,13 @@ var init_codex_config = __esm({
8389
8489
  // src/codex-images.ts
8390
8490
  import { mkdtemp, rm, writeFile } from "fs/promises";
8391
8491
  import { tmpdir } from "os";
8392
- import { join as join16 } from "path";
8492
+ import { join as join17 } from "path";
8393
8493
  function createCodexImagePreparer(opts) {
8394
8494
  const doFetch = opts.fetch ?? fetch;
8395
8495
  const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
8396
8496
  return async (urls, signal) => {
8397
8497
  signal.throwIfAborted();
8398
- const dir2 = await mkdtemp(join16(tmpdir(), `cabane-codex-images-${opts.turnId}-`));
8498
+ const dir2 = await mkdtemp(join17(tmpdir(), `cabane-codex-images-${opts.turnId}-`));
8399
8499
  const cleanup = async () => {
8400
8500
  try {
8401
8501
  await rm(dir2, { recursive: true, force: true });
@@ -8416,7 +8516,7 @@ function createCodexImagePreparer(opts) {
8416
8516
  const ext = EXTENSIONS[type];
8417
8517
  if (!ext) throw new Error(`not a vision image type: ${type || "none"}`);
8418
8518
  const bytes = new Uint8Array(await res.arrayBuffer());
8419
- const path = join16(dir2, `image-${index + 1}.${ext}`);
8519
+ const path = join17(dir2, `image-${index + 1}.${ext}`);
8420
8520
  await writeFile(path, bytes, { mode: 384 });
8421
8521
  return path;
8422
8522
  } catch (err) {
@@ -8458,9 +8558,9 @@ var init_codex_images = __esm({
8458
8558
  import { createReadStream } from "fs";
8459
8559
  import { readdir } from "fs/promises";
8460
8560
  import { homedir as homedir8 } from "os";
8461
- import { join as join17 } from "path";
8561
+ import { join as join18 } from "path";
8462
8562
  import { createInterface as createInterface2 } from "readline";
8463
- function createCodexCompactionCounter(codexHome = process.env.CODEX_HOME || join17(homedir8(), ".codex")) {
8563
+ function createCodexCompactionCounter(codexHome = process.env.CODEX_HOME || join18(homedir8(), ".codex")) {
8464
8564
  return async (threadId) => {
8465
8565
  try {
8466
8566
  const rollout = await findRollout(codexHome, threadId);
@@ -8477,7 +8577,7 @@ async function findRollout(codexHome, threadId) {
8477
8577
  const suffix = `-${threadId}.jsonl`;
8478
8578
  for (const offset of [0, -DAY_MS, DAY_MS]) {
8479
8579
  const day = new Date(createdAt + offset);
8480
- const dir2 = join17(
8580
+ const dir2 = join18(
8481
8581
  codexHome,
8482
8582
  "sessions",
8483
8583
  String(day.getUTCFullYear()),
@@ -8491,7 +8591,7 @@ async function findRollout(codexHome, threadId) {
8491
8591
  continue;
8492
8592
  }
8493
8593
  const name = names.find((n) => n.startsWith("rollout-") && n.endsWith(suffix));
8494
- if (name) return join17(dir2, name);
8594
+ if (name) return join18(dir2, name);
8495
8595
  }
8496
8596
  return null;
8497
8597
  }
@@ -8623,22 +8723,22 @@ var init_prepare_failure = __esm({
8623
8723
  });
8624
8724
 
8625
8725
  // src/prepared.ts
8626
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync9 } from "fs";
8627
- import { join as join18 } from "path";
8726
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
8727
+ import { join as join19 } from "path";
8628
8728
  function dirFor(workspaceId) {
8629
- return join18(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8729
+ return join19(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8630
8730
  }
8631
8731
  function conversationDir(workspaceId, conversationId) {
8632
- return join18(dirFor(workspaceId), encodeURIComponent(conversationId));
8732
+ return join19(dirFor(workspaceId), encodeURIComponent(conversationId));
8633
8733
  }
8634
8734
  function pathFor2(workspaceId, conversationId, agentId) {
8635
- return join18(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
8735
+ return join19(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
8636
8736
  }
8637
8737
  function readPrepared(workspaceId, conversationId, agentId) {
8638
8738
  const path = pathFor2(workspaceId, conversationId, agentId);
8639
- if (!existsSync9(path)) return null;
8739
+ if (!existsSync10(path)) return null;
8640
8740
  try {
8641
- const parsed = JSON.parse(readFileSync7(path, "utf8"));
8741
+ const parsed = JSON.parse(readFileSync8(path, "utf8"));
8642
8742
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
8643
8743
  return {
8644
8744
  cwd: parsed.cwd,
@@ -8669,18 +8769,18 @@ var init_prepared = __esm({
8669
8769
  });
8670
8770
 
8671
8771
  // src/secrets.ts
8672
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
8673
- import { join as join19 } from "path";
8674
- import { z as z16 } from "zod";
8772
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
8773
+ import { join as join20 } from "path";
8774
+ import { z as z17 } from "zod";
8675
8775
  function secretsPath() {
8676
- return join19(cabaneDir(), "secrets.json");
8776
+ return join20(cabaneDir(), "secrets.json");
8677
8777
  }
8678
8778
  function loadSecretValues() {
8679
8779
  const path = secretsPath();
8680
- if (!existsSync10(path)) return {};
8780
+ if (!existsSync11(path)) return {};
8681
8781
  let raw;
8682
8782
  try {
8683
- raw = readFileSync8(path, "utf8");
8783
+ raw = readFileSync9(path, "utf8");
8684
8784
  } catch (err) {
8685
8785
  throw new ConfigError(
8686
8786
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -8798,7 +8898,7 @@ var init_secrets = __esm({
8798
8898
  init_config();
8799
8899
  init_errors();
8800
8900
  PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
8801
- secretStoreSchema = z16.record(z16.string(), z16.string());
8901
+ secretStoreSchema = z17.record(z17.string(), z17.string());
8802
8902
  SECRET_ENV_NAME = /^CABANE_MCP_(\d+)$/;
8803
8903
  }
8804
8904
  });
@@ -8830,9 +8930,9 @@ var init_turn_env = __esm({
8830
8930
 
8831
8931
  // src/transcript-writer.ts
8832
8932
  import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
8833
- import { basename as basename3, dirname as dirname7, join as join20 } from "path";
8933
+ import { basename as basename3, dirname as dirname7, join as join21 } from "path";
8834
8934
  function transcriptsDir() {
8835
- return join20(cabaneDir(), "transcripts");
8935
+ return join21(cabaneDir(), "transcripts");
8836
8936
  }
8837
8937
  function fileName(meta) {
8838
8938
  const d = new Date(meta.ts);
@@ -8858,7 +8958,7 @@ function pruneOld(dir2, retain) {
8858
8958
  const drop = files.sort().slice(0, files.length - retain);
8859
8959
  for (const f of drop) {
8860
8960
  try {
8861
- rmSync6(join20(dir2, f), { force: true });
8961
+ rmSync6(join21(dir2, f), { force: true });
8862
8962
  } catch {
8863
8963
  }
8864
8964
  }
@@ -8876,7 +8976,7 @@ var init_transcript_writer = __esm({
8876
8976
  onWarn;
8877
8977
  constructor(dir2, meta, onWarn) {
8878
8978
  this.onWarn = onWarn;
8879
- this.path = join20(dir2, fileName(meta));
8979
+ this.path = join21(dir2, fileName(meta));
8880
8980
  try {
8881
8981
  mkdirSync8(dir2, { recursive: true });
8882
8982
  try {
@@ -8905,10 +9005,10 @@ var init_transcript_writer = __esm({
8905
9005
  preserveAnomaly() {
8906
9006
  if (this.broken) return;
8907
9007
  try {
8908
- const dir2 = join20(dirname7(this.path), "anomalies");
9008
+ const dir2 = join21(dirname7(this.path), "anomalies");
8909
9009
  mkdirSync8(dir2, { recursive: true, mode: 448 });
8910
9010
  chmodSync4(dir2, 448);
8911
- const target = join20(dir2, basename3(this.path));
9011
+ const target = join21(dir2, basename3(this.path));
8912
9012
  copyFileSync(this.path, target);
8913
9013
  chmodSync4(target, 384);
8914
9014
  pruneOld(dir2, ANOMALY_RETAIN);
@@ -9204,6 +9304,7 @@ var init_turn_execution = __esm({
9204
9304
  init_cabane_command();
9205
9305
  init_boot_id();
9206
9306
  init_src();
9307
+ init_grants();
9207
9308
  init_work_dir();
9208
9309
  init_build_options();
9209
9310
  init_bundled_binaries();
@@ -9575,7 +9676,19 @@ var init_turn_execution = __esm({
9575
9676
  throw this.concluded(reason, reason);
9576
9677
  }
9577
9678
  this.resolvedMcpServers = resolvedMcpServers;
9578
- this.secretEnv = { ...secretEnv, ...harnessLoginEnv(secretStore, this.turnContext.runtime) };
9679
+ const granted = grantEnv(this.opts.deviceGrants, this.turnContext.secretEnv ?? {}, secretEnv);
9680
+ const harness = harnessSecretEnv({
9681
+ deviceDefaults: harnessLoginEnv(secretStore, this.turnContext.runtime),
9682
+ granted,
9683
+ issued: secretEnv
9684
+ });
9685
+ if (harness.dropped.length > 0) {
9686
+ turnLog.warn(
9687
+ { variables: harness.dropped },
9688
+ "Left out a secret value this turn: it contains a NUL character, which no environment can hold."
9689
+ );
9690
+ }
9691
+ this.secretEnv = harness.env;
9579
9692
  this.secretStore = secretStore;
9580
9693
  }
9581
9694
  async prepareEnvironment() {
@@ -10346,11 +10459,11 @@ var init_turn_execution = __esm({
10346
10459
 
10347
10460
  // src/dispatcher.ts
10348
10461
  import { randomUUID as randomUUID4 } from "crypto";
10349
- import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
10350
- import { join as join21 } from "path";
10462
+ import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
10463
+ import { join as join22 } from "path";
10351
10464
  function checkoutState(cwd) {
10352
10465
  if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
10353
- if (!existsSync11(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
10466
+ if (!existsSync12(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
10354
10467
  let entries;
10355
10468
  try {
10356
10469
  entries = readdirSync2(cwd);
@@ -10360,15 +10473,15 @@ function checkoutState(cwd) {
10360
10473
  if (entries.length === 0) {
10361
10474
  return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
10362
10475
  }
10363
- const gitPath = join21(cwd, ".git");
10364
- if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
10476
+ const gitPath = join22(cwd, ".git");
10477
+ if (!existsSync12(gitPath)) return { ok: true, reason: "usable" };
10365
10478
  let stat;
10366
10479
  try {
10367
10480
  stat = statSync4(gitPath);
10368
10481
  } catch (error) {
10369
10482
  return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
10370
10483
  }
10371
- if (stat.isDirectory() && !existsSync11(join21(gitPath, "HEAD")))
10484
+ if (stat.isDirectory() && !existsSync12(join22(gitPath, "HEAD")))
10372
10485
  return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
10373
10486
  return { ok: true, reason: "usable" };
10374
10487
  }
@@ -10476,15 +10589,15 @@ var init_dispatcher = __esm({
10476
10589
 
10477
10590
  // src/outbox.ts
10478
10591
  import {
10479
- existsSync as existsSync13,
10592
+ existsSync as existsSync14,
10480
10593
  mkdirSync as mkdirSync10,
10481
10594
  readdirSync as readdirSync3,
10482
- readFileSync as readFileSync10,
10595
+ readFileSync as readFileSync11,
10483
10596
  renameSync as renameSync5,
10484
10597
  rmSync as rmSync7,
10485
10598
  writeFileSync as writeFileSync6
10486
10599
  } from "fs";
10487
- import { join as join22 } from "path";
10600
+ import { join as join23 } from "path";
10488
10601
  var MAX_ENTRIES, MAX_AGE_MS, Outbox;
10489
10602
  var init_outbox = __esm({
10490
10603
  "src/outbox.ts"() {
@@ -10502,10 +10615,10 @@ var init_outbox = __esm({
10502
10615
  // Resolved lazily (per call) so tests that swap HOME between
10503
10616
  // cases route writes at the right tmpdir.
10504
10617
  dir() {
10505
- return join22(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
10618
+ return join23(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
10506
10619
  }
10507
10620
  fileFor(turnId, seq) {
10508
- return join22(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
10621
+ return join23(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
10509
10622
  }
10510
10623
  // Persist a commit for later draining. Atomic (temp file + rename) so a
10511
10624
  // concurrent `list()` never reads a half-written entry, then enforces the
@@ -10537,7 +10650,7 @@ var init_outbox = __esm({
10537
10650
  // wedging the drain.
10538
10651
  list() {
10539
10652
  const dir2 = this.dir();
10540
- if (!existsSync13(dir2)) return [];
10653
+ if (!existsSync14(dir2)) return [];
10541
10654
  let names;
10542
10655
  try {
10543
10656
  names = readdirSync3(dir2);
@@ -10547,9 +10660,9 @@ var init_outbox = __esm({
10547
10660
  const entries = [];
10548
10661
  for (const name of names) {
10549
10662
  if (!name.endsWith(".json")) continue;
10550
- const full = join22(dir2, name);
10663
+ const full = join23(dir2, name);
10551
10664
  try {
10552
- const parsed = JSON.parse(readFileSync10(full, "utf8"));
10665
+ const parsed = JSON.parse(readFileSync11(full, "utf8"));
10553
10666
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
10554
10667
  entries.push(parsed);
10555
10668
  } else {
@@ -10583,7 +10696,7 @@ var init_outbox = __esm({
10583
10696
  // different: it is positive evidence that nothing was ever queued here.
10584
10697
  maxSeqForTurn(turnId) {
10585
10698
  const dir2 = this.dir();
10586
- if (!existsSync13(dir2)) return 0;
10699
+ if (!existsSync14(dir2)) return 0;
10587
10700
  const names = readdirSync3(dir2);
10588
10701
  const prefix = `${encodeURIComponent(turnId)}__`;
10589
10702
  let max = 0;
@@ -10603,7 +10716,7 @@ var init_outbox = __esm({
10603
10716
  }
10604
10717
  size() {
10605
10718
  const dir2 = this.dir();
10606
- if (!existsSync13(dir2)) return 0;
10719
+ if (!existsSync14(dir2)) return 0;
10607
10720
  try {
10608
10721
  return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
10609
10722
  } catch {
@@ -10654,7 +10767,7 @@ var init_outbox = __esm({
10654
10767
 
10655
10768
  // src/runner/exec-shim.ts
10656
10769
  import { chmodSync as chmodSync6, mkdirSync as mkdirSync18, mkdtempSync, rmSync as rmSync13, writeFileSync as writeFileSync11 } from "fs";
10657
- import { join as join32 } from "path";
10770
+ import { join as join33 } from "path";
10658
10771
  function shellQuote(value) {
10659
10772
  return `'${value.replace(/'/g, `'\\''`)}'`;
10660
10773
  }
@@ -10664,10 +10777,10 @@ function createShimDir() {
10664
10777
  return {
10665
10778
  write(argv) {
10666
10779
  if (!dir2) {
10667
- mkdirSync18(join32(cabaneDir(), "runner"), { recursive: true });
10668
- dir2 = mkdtempSync(join32(cabaneDir(), "runner", "turn-"));
10780
+ mkdirSync18(join33(cabaneDir(), "runner"), { recursive: true });
10781
+ dir2 = mkdtempSync(join33(cabaneDir(), "runner", "turn-"));
10669
10782
  }
10670
- const path = join32(dir2, `exec-${++count}`);
10783
+ const path = join33(dir2, `exec-${++count}`);
10671
10784
  writeFileSync11(path, `#!/bin/sh
10672
10785
  exec ${argv.map(shellQuote).join(" ")} "$@"
10673
10786
  `);
@@ -10815,6 +10928,8 @@ function runnerTurnOptions(input, log2, emit, extras = {}) {
10815
10928
  // The launcher's copy of the device's secrets, when it sent one: a runner
10816
10929
  // in a box has no `secrets.json` to read.
10817
10930
  ...input.secrets ? { secretStore: makeStore(input.secrets) } : {},
10931
+ // The grants-file entries the launcher read for this dispatch's turn.
10932
+ ...input.grants ? { deviceGrants: input.grants } : {},
10818
10933
  log: log2,
10819
10934
  ...queryFn ? { queryFn } : {},
10820
10935
  ...spec.opencodeServerUrl ? { opencodeServerUrl: spec.opencodeServerUrl } : {},
@@ -12905,7 +13020,7 @@ var ConnectorHealthStore = class {
12905
13020
  init_dispatcher();
12906
13021
 
12907
13022
  // src/host-report.ts
12908
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
13023
+ import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
12909
13024
  import { userInfo as userInfo2 } from "os";
12910
13025
  function inContainer(signals) {
12911
13026
  if (signals.dockerenv) return true;
@@ -12916,11 +13031,11 @@ function inContainer(signals) {
12916
13031
  function readContainerSignals() {
12917
13032
  let cgroup = null;
12918
13033
  try {
12919
- cgroup = readFileSync9("/proc/1/cgroup", "utf8");
13034
+ cgroup = readFileSync10("/proc/1/cgroup", "utf8");
12920
13035
  } catch {
12921
13036
  cgroup = null;
12922
13037
  }
12923
- return { dockerenv: existsSync12("/.dockerenv"), cgroup, env: process.env };
13038
+ return { dockerenv: existsSync13("/.dockerenv"), cgroup, env: process.env };
12924
13039
  }
12925
13040
  function osUser() {
12926
13041
  try {
@@ -13207,6 +13322,7 @@ var DispatchClaims = class {
13207
13322
  init_outbox();
13208
13323
  init_run_config();
13209
13324
  init_secrets();
13325
+ init_grants();
13210
13326
 
13211
13327
  // src/sse.ts
13212
13328
  init_errors();
@@ -13332,20 +13448,20 @@ init_transcript_writer();
13332
13448
 
13333
13449
  // src/updater.ts
13334
13450
  import { spawn as spawn6 } from "child_process";
13335
- import { copyFileSync as copyFileSync2, existsSync as existsSync15, mkdirSync as mkdirSync14, renameSync as renameSync9, rmSync as rmSync10 } from "fs";
13336
- import { dirname as dirname11, join as join26 } from "path";
13451
+ import { copyFileSync as copyFileSync2, existsSync as existsSync16, mkdirSync as mkdirSync14, renameSync as renameSync9, rmSync as rmSync10 } from "fs";
13452
+ import { dirname as dirname11, join as join27 } from "path";
13337
13453
 
13338
13454
  // src/boot-rule.ts
13339
13455
  import {
13340
13456
  mkdirSync as mkdirSync11,
13341
- readFileSync as readFileSync11,
13457
+ readFileSync as readFileSync12,
13342
13458
  realpathSync as realpathSync4,
13343
13459
  renameSync as renameSync6,
13344
13460
  rmSync as rmSync8,
13345
13461
  symlinkSync,
13346
13462
  writeFileSync as writeFileSync7
13347
13463
  } from "fs";
13348
- import { dirname as dirname8, join as join23 } from "path";
13464
+ import { dirname as dirname8, join as join24 } from "path";
13349
13465
  var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
13350
13466
  var MAX_STAGE_ATTEMPTS = 3;
13351
13467
  function superseded(pending, ...running) {
@@ -13369,42 +13485,42 @@ function rolledBackState(state, pending, reason, now) {
13369
13485
  // src/install-layout.ts
13370
13486
  init_config();
13371
13487
  import {
13372
- existsSync as existsSync14,
13488
+ existsSync as existsSync15,
13373
13489
  lstatSync,
13374
13490
  mkdirSync as mkdirSync12,
13375
- readFileSync as readFileSync12,
13491
+ readFileSync as readFileSync13,
13376
13492
  readdirSync as readdirSync4,
13377
13493
  realpathSync as realpathSync5,
13378
13494
  renameSync as renameSync7,
13379
13495
  rmSync as rmSync9,
13380
13496
  symlinkSync as symlinkSync2
13381
13497
  } from "fs";
13382
- import { dirname as dirname9, join as join24 } from "path";
13498
+ import { dirname as dirname9, join as join25 } from "path";
13383
13499
  import { fileURLToPath as fileURLToPath5 } from "url";
13384
13500
  function versionsDir() {
13385
- return join24(cabaneDir(), "versions");
13501
+ return join25(cabaneDir(), "versions");
13386
13502
  }
13387
13503
  function currentLink() {
13388
- return join24(cabaneDir(), "current");
13504
+ return join25(cabaneDir(), "current");
13389
13505
  }
13390
13506
  function bootGuardPath() {
13391
- return join24(cabaneDir(), "boot-guard.mjs");
13507
+ return join25(cabaneDir(), "boot-guard.mjs");
13392
13508
  }
13393
13509
  function pinnedNodeDir() {
13394
- return join24(cabaneDir(), "node");
13510
+ return join25(cabaneDir(), "node");
13395
13511
  }
13396
13512
  function pinnedNodePath() {
13397
- return join24(pinnedNodeDir(), "bin", "node");
13513
+ return join25(pinnedNodeDir(), "bin", "node");
13398
13514
  }
13399
13515
  function versionPackageRoot(version) {
13400
- return join24(versionsDir(), version, "node_modules", "@cabane", "companion");
13516
+ return join25(versionsDir(), version, "node_modules", "@cabane", "companion");
13401
13517
  }
13402
13518
  function ownPackageRoot(moduleUrl = import.meta.url) {
13403
13519
  return dirname9(dirname9(fileURLToPath5(moduleUrl)));
13404
13520
  }
13405
13521
  function packageVersion(root) {
13406
13522
  try {
13407
- const pkg2 = JSON.parse(readFileSync12(join24(root, "package.json"), "utf8"));
13523
+ const pkg2 = JSON.parse(readFileSync13(join25(root, "package.json"), "utf8"));
13408
13524
  return typeof pkg2.version === "string" ? pkg2.version : null;
13409
13525
  } catch {
13410
13526
  return null;
@@ -13413,7 +13529,7 @@ function packageVersion(root) {
13413
13529
  function currentRoot() {
13414
13530
  try {
13415
13531
  const root = realpathSync5(currentLink());
13416
- return existsSync14(join24(root, "dist", "cli.js")) ? root : null;
13532
+ return existsSync15(join25(root, "dist", "cli.js")) ? root : null;
13417
13533
  } catch {
13418
13534
  return null;
13419
13535
  }
@@ -13432,11 +13548,11 @@ function pointCurrentAt(root) {
13432
13548
  }
13433
13549
  function pruneVersions(keep) {
13434
13550
  const dir2 = versionsDir();
13435
- if (!existsSync14(dir2)) return [];
13551
+ if (!existsSync15(dir2)) return [];
13436
13552
  const removed = [];
13437
13553
  for (const name of readdirSync4(dir2)) {
13438
13554
  if (keep.includes(name)) continue;
13439
- rmSync9(join24(dir2, name), { recursive: true, force: true });
13555
+ rmSync9(join25(dir2, name), { recursive: true, force: true });
13440
13556
  removed.push(name);
13441
13557
  }
13442
13558
  return removed;
@@ -13455,36 +13571,36 @@ function compareVersions(a, b) {
13455
13571
 
13456
13572
  // src/update-state.ts
13457
13573
  init_config();
13458
- import { mkdirSync as mkdirSync13, readFileSync as readFileSync13, renameSync as renameSync8, writeFileSync as writeFileSync8 } from "fs";
13459
- import { dirname as dirname10, join as join25 } from "path";
13460
- import { z as z17 } from "zod";
13461
- var resultSchema = z17.object({
13462
- kind: z17.enum(["updated", "rolled_back", "failed"]),
13463
- from: z17.string(),
13464
- to: z17.string(),
13465
- at: z17.string(),
13466
- reason: z17.string().optional()
13574
+ import { mkdirSync as mkdirSync13, readFileSync as readFileSync14, renameSync as renameSync8, writeFileSync as writeFileSync8 } from "fs";
13575
+ import { dirname as dirname10, join as join26 } from "path";
13576
+ import { z as z18 } from "zod";
13577
+ var resultSchema = z18.object({
13578
+ kind: z18.enum(["updated", "rolled_back", "failed"]),
13579
+ from: z18.string(),
13580
+ to: z18.string(),
13581
+ at: z18.string(),
13582
+ reason: z18.string().optional()
13467
13583
  });
13468
- var pendingSchema = z17.object({
13469
- from: z17.string(),
13584
+ var pendingSchema = z18.object({
13585
+ from: z18.string(),
13470
13586
  // The package root `current` pointed at before the swap, so a rollback can point it back.
13471
- fromRoot: z17.string(),
13472
- to: z17.string(),
13473
- at: z17.string(),
13474
- boots: z17.number().int().nonnegative()
13587
+ fromRoot: z18.string(),
13588
+ to: z18.string(),
13589
+ at: z18.string(),
13590
+ boots: z18.number().int().nonnegative()
13475
13591
  });
13476
- var stateSchema = z17.object({
13477
- server: z17.object({ latest: z17.string(), minimum: z17.string(), at: z17.string() }).optional(),
13592
+ var stateSchema = z18.object({
13593
+ server: z18.object({ latest: z18.string(), minimum: z18.string(), at: z18.string() }).optional(),
13478
13594
  pending: pendingSchema.optional(),
13479
13595
  lastResult: resultSchema.optional(),
13480
- failed: z17.object({ version: z17.string(), attempts: z17.number().int(), at: z17.string() }).optional()
13596
+ failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
13481
13597
  });
13482
13598
  function updateStatePath(dir2 = cabaneDir()) {
13483
- return join25(dir2, "update.json");
13599
+ return join26(dir2, "update.json");
13484
13600
  }
13485
13601
  function readUpdateState(path = updateStatePath()) {
13486
13602
  try {
13487
- const parsed = stateSchema.safeParse(JSON.parse(readFileSync13(path, "utf8")));
13603
+ const parsed = stateSchema.safeParse(JSON.parse(readFileSync14(path, "utf8")));
13488
13604
  return parsed.success ? parsed.data : {};
13489
13605
  } catch {
13490
13606
  return {};
@@ -13525,12 +13641,12 @@ function versionToStage(input) {
13525
13641
  return latest;
13526
13642
  }
13527
13643
  function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
13528
- if (existsSync15(pending.fromRoot)) pointCurrentAt(pending.fromRoot);
13644
+ if (existsSync16(pending.fromRoot)) pointCurrentAt(pending.fromRoot);
13529
13645
  return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
13530
13646
  }
13531
13647
  function writeBootGuard(root = ownPackageRoot()) {
13532
- const built = join26(root, "dist", "boot-guard.js");
13533
- if (!existsSync15(built)) return false;
13648
+ const built = join27(root, "dist", "boot-guard.js");
13649
+ if (!existsSync16(built)) return false;
13534
13650
  const target = bootGuardPath();
13535
13651
  mkdirSync14(dirname11(target), { recursive: true });
13536
13652
  const tmp = `${target}.${process.pid}.tmp`;
@@ -13569,17 +13685,17 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
13569
13685
  async function stageVersion(version, harnesses, deps = {}) {
13570
13686
  const run = deps.run ?? runProcess;
13571
13687
  const node = deps.execPath ?? process.execPath;
13572
- const target = join26(versionsDir(), version);
13688
+ const target = join27(versionsDir(), version);
13573
13689
  const placed = versionPackageRoot(version);
13574
- if (existsSync15(placed)) {
13690
+ if (existsSync16(placed)) {
13575
13691
  if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
13576
13692
  return { ok: true, root: placed };
13577
13693
  }
13578
- const npm = join26(dirname11(node), "npm");
13579
- if (!existsSync15(npm))
13694
+ const npm = join27(dirname11(node), "npm");
13695
+ if (!existsSync16(npm))
13580
13696
  return { ok: false, reason: `there is no npm beside Node at ${dirname11(node)}` };
13581
13697
  mkdirSync14(versionsDir(), { recursive: true });
13582
- const staging = join26(versionsDir(), `.${version}.staging-${process.pid}`);
13698
+ const staging = join27(versionsDir(), `.${version}.staging-${process.pid}`);
13583
13699
  rmSync10(staging, { recursive: true, force: true });
13584
13700
  const installed = await run(
13585
13701
  npm,
@@ -13608,7 +13724,7 @@ async function stageVersion(version, harnesses, deps = {}) {
13608
13724
  return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
13609
13725
  return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
13610
13726
  }
13611
- const root = join26(staging, "node_modules", "@cabane", "companion");
13727
+ const root = join27(staging, "node_modules", "@cabane", "companion");
13612
13728
  const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
13613
13729
  if (verdict) {
13614
13730
  rmSync10(staging, { recursive: true, force: true });
@@ -13620,7 +13736,7 @@ async function stageVersion(version, harnesses, deps = {}) {
13620
13736
  }
13621
13737
  async function verifyInstall(root, version, harnesses, deps) {
13622
13738
  if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
13623
- const answered = await deps.run(deps.execPath, [join26(root, "dist", "bin.js"), "--version"], {
13739
+ const answered = await deps.run(deps.execPath, [join27(root, "dist", "bin.js"), "--version"], {
13624
13740
  timeoutMs: 6e4,
13625
13741
  env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
13626
13742
  });
@@ -13628,7 +13744,7 @@ async function verifyInstall(root, version, harnesses, deps) {
13628
13744
  return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
13629
13745
  }
13630
13746
  if (harnesses.length === 0) return null;
13631
- const probed = await deps.run(deps.execPath, [join26(root, "dist", "postinstall.js"), "--json"], {
13747
+ const probed = await deps.run(deps.execPath, [join27(root, "dist", "postinstall.js"), "--json"], {
13632
13748
  timeoutMs: 6e4
13633
13749
  });
13634
13750
  let resolutions = {};
@@ -14625,6 +14741,14 @@ var CompanionSupervisor = class {
14625
14741
  (m) => this.log.warn({ err: m }, "Could not read the secrets file for a boxed turn.")
14626
14742
  )
14627
14743
  } : {},
14744
+ // Read per dispatch, for the secret variables its mint named and no others.
14745
+ grants: (keys) => readGrants(keys, {
14746
+ refusedVariable: (key, variable) => this.log.warn(
14747
+ { key, variable },
14748
+ "Skipped a variable in ~/.cabane/grants.json: its name must use capital letters, digits and underscores, not starting with CABANE_, and its value cannot contain a NUL character."
14749
+ ),
14750
+ unreadable: (message2) => this.log.warn({ err: message2 }, "Could not read the grants file.")
14751
+ }),
14628
14752
  ...this.runnerEnv ? { env: this.runnerEnv } : {},
14629
14753
  reportRunnerGone: (report) => this.reportRunnerGone(report),
14630
14754
  connectorHealth: this.connectorHealth,
@@ -15317,10 +15441,10 @@ function handleUncaught(log2, err, origin) {
15317
15441
 
15318
15442
  // src/crash-marker.ts
15319
15443
  init_config();
15320
- import { existsSync as existsSync16, mkdirSync as mkdirSync15, readFileSync as readFileSync14, rmSync as rmSync11, writeFileSync as writeFileSync9 } from "fs";
15321
- import { join as join27 } from "path";
15444
+ import { existsSync as existsSync17, mkdirSync as mkdirSync15, readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync9 } from "fs";
15445
+ import { join as join28 } from "path";
15322
15446
  function crashMarkerPath() {
15323
- return join27(cabaneDir(), "last-error.json");
15447
+ return join28(cabaneDir(), "last-error.json");
15324
15448
  }
15325
15449
  function recordCrash(rec2) {
15326
15450
  try {
@@ -15332,7 +15456,7 @@ function recordCrash(rec2) {
15332
15456
  function clearCrash() {
15333
15457
  try {
15334
15458
  const path = crashMarkerPath();
15335
- if (existsSync16(path)) rmSync11(path, { force: true });
15459
+ if (existsSync17(path)) rmSync11(path, { force: true });
15336
15460
  } catch {
15337
15461
  }
15338
15462
  }
@@ -15340,8 +15464,8 @@ function clearCrash() {
15340
15464
  // src/stray-companion.ts
15341
15465
  init_config();
15342
15466
  import { spawnSync } from "child_process";
15343
- import { readFileSync as readFileSync15 } from "fs";
15344
- import { join as join28 } from "path";
15467
+ import { readFileSync as readFileSync16 } from "fs";
15468
+ import { join as join29 } from "path";
15345
15469
  var COMPANION_ENTRY = /@cabane\/companion\/|cabane-companion|(^|[\s/])cabane\s|\.cabane\/(versions|current|boot-guard)/;
15346
15470
  var START = /\sstart(\s|$)/;
15347
15471
  function companionStarts(psOutput) {
@@ -15358,7 +15482,7 @@ function companionStarts(psOutput) {
15358
15482
  }
15359
15483
  function stateDirOf(env) {
15360
15484
  const home = env.CABANE_COMPANION_HOME || env.HOME;
15361
- return home ? join28(home, ".cabane") : null;
15485
+ return home ? join29(home, ".cabane") : null;
15362
15486
  }
15363
15487
  function envFromPsE(output, command) {
15364
15488
  const line = output.replace(/\n$/, "");
@@ -15373,7 +15497,7 @@ function envFromPsE(output, command) {
15373
15497
  function envFromProc(pid) {
15374
15498
  try {
15375
15499
  const env = {};
15376
- for (const entry of readFileSync15(`/proc/${pid}/environ`, "utf8").split("\0")) {
15500
+ for (const entry of readFileSync16(`/proc/${pid}/environ`, "utf8").split("\0")) {
15377
15501
  const eq = entry.indexOf("=");
15378
15502
  if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
15379
15503
  }
@@ -15625,15 +15749,15 @@ import { createHash as createHash3 } from "crypto";
15625
15749
  import {
15626
15750
  chmodSync as chmodSync5,
15627
15751
  cpSync,
15628
- existsSync as existsSync17,
15752
+ existsSync as existsSync18,
15629
15753
  mkdirSync as mkdirSync16,
15630
- readFileSync as readFileSync16,
15754
+ readFileSync as readFileSync17,
15631
15755
  renameSync as renameSync10,
15632
15756
  rmSync as rmSync12,
15633
15757
  writeFileSync as writeFileSync10
15634
15758
  } from "fs";
15635
15759
  import { homedir as homedir9, userInfo as userInfo3 } from "os";
15636
- import { dirname as dirname12, join as join29, sep as sep3 } from "path";
15760
+ import { dirname as dirname12, join as join30, sep as sep3 } from "path";
15637
15761
  function servicePlatform(platform = process.platform) {
15638
15762
  if (platform === "linux") return "systemd";
15639
15763
  if (platform === "darwin") return "launchd";
@@ -15648,18 +15772,18 @@ function launchdLabel(env = process.env) {
15648
15772
  return serviceName(env).replace(/^cabane-companion/, "ai.cabane.companion").replace("-", ".");
15649
15773
  }
15650
15774
  function serviceNode(execPath = process.execPath) {
15651
- return existsSync17(pinnedNodePath()) ? pinnedNodePath() : execPath;
15775
+ return existsSync18(pinnedNodePath()) ? pinnedNodePath() : execPath;
15652
15776
  }
15653
15777
  function servicePaths(platform, env = process.env, home = homedir9(), node = serviceNode()) {
15654
- const unitFile = platform === "systemd" ? join29(
15655
- env.XDG_CONFIG_HOME || join29(home, ".config"),
15778
+ const unitFile = platform === "systemd" ? join30(
15779
+ env.XDG_CONFIG_HOME || join30(home, ".config"),
15656
15780
  "systemd",
15657
15781
  "user",
15658
15782
  `${serviceName(env)}.service`
15659
- ) : join29(home, "Library", "LaunchAgents", `${launchdLabel(env)}.plist`);
15783
+ ) : join30(home, "Library", "LaunchAgents", `${launchdLabel(env)}.plist`);
15660
15784
  return {
15661
15785
  unitFile,
15662
- envFile: join29(cabaneDir(), "service.env"),
15786
+ envFile: join30(cabaneDir(), "service.env"),
15663
15787
  node,
15664
15788
  entry: bootGuardPath()
15665
15789
  };
@@ -15726,7 +15850,7 @@ function renderLaunchdPlist(paths, env = process.env, servicePath) {
15726
15850
  " <key>ThrottleInterval</key><integer>5</integer>",
15727
15851
  // The companion writes its own log; the console copy would only grow a second one.
15728
15852
  " <key>StandardOutPath</key><string>/dev/null</string>",
15729
- ` <key>StandardErrorPath</key><string>${xml(join29(cabaneDir(), "service-error.log"))}</string>`,
15853
+ ` <key>StandardErrorPath</key><string>${xml(join30(cabaneDir(), "service-error.log"))}</string>`,
15730
15854
  "</dict>",
15731
15855
  "</plist>",
15732
15856
  ""
@@ -15782,7 +15906,7 @@ async function installService(deps = {}) {
15782
15906
  const paths = servicePaths(platform, env, deps.home, node);
15783
15907
  const servicePath = await loginShellPath(run, env);
15784
15908
  mkdirSync16(dirname12(paths.envFile), { recursive: true });
15785
- const existing = existsSync17(paths.envFile) ? readFileSync16(paths.envFile, "utf8") : "";
15909
+ const existing = existsSync18(paths.envFile) ? readFileSync17(paths.envFile, "utf8") : "";
15786
15910
  writeFileSync10(paths.envFile, withServicePath(existing, servicePath), { mode: 384 });
15787
15911
  chmodSync5(paths.envFile, 384);
15788
15912
  mkdirSync16(dirname12(paths.unitFile), { recursive: true });
@@ -15838,7 +15962,7 @@ async function uninstallService(deps = {}) {
15838
15962
  const platform = servicePlatform(deps.platform);
15839
15963
  if (!platform) return { ok: true, lines: [] };
15840
15964
  const paths = servicePaths(platform, env, deps.home);
15841
- if (!existsSync17(paths.unitFile)) return { ok: true, lines: ["No Cabane service is installed."] };
15965
+ if (!existsSync18(paths.unitFile)) return { ok: true, lines: ["No Cabane service is installed."] };
15842
15966
  await removeUnit(platform, paths, env, run);
15843
15967
  return { ok: true, lines: [] };
15844
15968
  }
@@ -15865,16 +15989,16 @@ async function seedManagedInstall(deps) {
15865
15989
  return { ok: true, root: own };
15866
15990
  }
15867
15991
  const target = versionPackageRoot(version);
15868
- if (!existsSync17(target)) {
15869
- const staging = join29(versionsDir(), `.${version}.seed-${process.pid}`);
15992
+ if (!existsSync18(target)) {
15993
+ const staging = join30(versionsDir(), `.${version}.seed-${process.pid}`);
15870
15994
  rmSync12(staging, { recursive: true, force: true });
15871
15995
  try {
15872
- cpSync(own, join29(staging, "node_modules", "@cabane", "companion"), {
15996
+ cpSync(own, join30(staging, "node_modules", "@cabane", "companion"), {
15873
15997
  recursive: true,
15874
15998
  verbatimSymlinks: true
15875
15999
  });
15876
- rmSync12(join29(versionsDir(), version), { recursive: true, force: true });
15877
- renameSync10(staging, join29(versionsDir(), version));
16000
+ rmSync12(join30(versionsDir(), version), { recursive: true, force: true });
16001
+ renameSync10(staging, join30(versionsDir(), version));
15878
16002
  } catch (err) {
15879
16003
  rmSync12(staging, { recursive: true, force: true });
15880
16004
  return {
@@ -15885,7 +16009,7 @@ async function seedManagedInstall(deps) {
15885
16009
  }
15886
16010
  const unrunnable = await verifyInstall(target, version, [], deps);
15887
16011
  if (unrunnable) {
15888
- rmSync12(join29(versionsDir(), version), { recursive: true, force: true });
16012
+ rmSync12(join30(versionsDir(), version), { recursive: true, force: true });
15889
16013
  return { ok: false, reason: `The copy of Cabane for the service can't run: ${unrunnable}.` };
15890
16014
  }
15891
16015
  pointCurrentAt(target);
@@ -15897,7 +16021,7 @@ function tildeDir() {
15897
16021
  function unitNode(platform, unitFile) {
15898
16022
  let text;
15899
16023
  try {
15900
- text = readFileSync16(unitFile, "utf8");
16024
+ text = readFileSync17(unitFile, "utf8");
15901
16025
  } catch {
15902
16026
  return null;
15903
16027
  }
@@ -15919,9 +16043,9 @@ async function serviceState(deps = {}) {
15919
16043
  };
15920
16044
  if (!platform) return none;
15921
16045
  const paths = servicePaths(platform, env, deps.home);
15922
- if (!existsSync17(paths.unitFile)) return none;
16046
+ if (!existsSync18(paths.unitFile)) return none;
15923
16047
  const node = unitNode(platform, paths.unitFile);
15924
- const nodeOk = node === null || existsSync17(node);
16048
+ const nodeOk = node === null || existsSync18(node);
15925
16049
  if (platform === "systemd") {
15926
16050
  const r2 = await run(
15927
16051
  "systemctl",
@@ -15953,7 +16077,7 @@ async function serviceState(deps = {}) {
15953
16077
  function serviceInstalled(deps = {}) {
15954
16078
  const platform = servicePlatform(deps.platform);
15955
16079
  if (!platform) return false;
15956
- return existsSync17(servicePaths(platform, deps.env ?? process.env, deps.home).unitFile);
16080
+ return existsSync18(servicePaths(platform, deps.env ?? process.env, deps.home).unitFile);
15957
16081
  }
15958
16082
  function writeAtomic(path, content) {
15959
16083
  const tmp = `${path}.${process.pid}.tmp`;
@@ -15967,12 +16091,12 @@ function userName() {
15967
16091
  return process.env.USER ?? "";
15968
16092
  }
15969
16093
  }
15970
- function rewriteScriptWrappers(dir2 = join29(cabaneDir(), "bin")) {
16094
+ function rewriteScriptWrappers(dir2 = join30(cabaneDir(), "bin")) {
15971
16095
  for (const name of ["cabane", "cabane-companion"]) {
15972
- const path = join29(dir2, name);
16096
+ const path = join30(dir2, name);
15973
16097
  let text;
15974
16098
  try {
15975
- text = readFileSync16(path, "utf8");
16099
+ text = readFileSync17(path, "utf8");
15976
16100
  } catch {
15977
16101
  continue;
15978
16102
  }
@@ -16641,7 +16765,7 @@ function writeCompletedPairing(raw) {
16641
16765
  }
16642
16766
 
16643
16767
  // src/cli.ts
16644
- import { readFileSync as readFileSync18 } from "fs";
16768
+ import { readFileSync as readFileSync19 } from "fs";
16645
16769
 
16646
16770
  // src/commands/logs.ts
16647
16771
  init_cabane_command();
@@ -16726,9 +16850,9 @@ async function logs(opts = {}) {
16726
16850
  init_cabane_command();
16727
16851
  init_install_kind();
16728
16852
  init_cabane_command();
16729
- import { readdirSync as readdirSync5, realpathSync as realpathSync6, existsSync as existsSync18 } from "fs";
16853
+ import { readdirSync as readdirSync5, realpathSync as realpathSync6, existsSync as existsSync19 } from "fs";
16730
16854
  import { homedir as homedir10 } from "os";
16731
- import { dirname as dirname13, join as join30 } from "path";
16855
+ import { dirname as dirname13, join as join31 } from "path";
16732
16856
  init_config();
16733
16857
  init_secrets();
16734
16858
  init_term();
@@ -17014,7 +17138,7 @@ function versionLine() {
17014
17138
  const install = companionInstall();
17015
17139
  const own = installedCommandPath({ install });
17016
17140
  if (install === "script") {
17017
- return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(join30(homedir10(), ".cabane"))})`;
17141
+ return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(join31(homedir10(), ".cabane"))})`;
17018
17142
  }
17019
17143
  if (own)
17020
17144
  return `Cabane ${COMPANION_VERSION} \xB7 installed with npm (${tildePath(dirname13(dirname13(own)))})`;
@@ -17040,7 +17164,7 @@ function otherInstall(install, own) {
17040
17164
  }
17041
17165
  };
17042
17166
  if (install === "npm") {
17043
- const script = join30(
17167
+ const script = join31(
17044
17168
  homedir10(),
17045
17169
  ".cabane",
17046
17170
  "node",
@@ -17049,11 +17173,11 @@ function otherInstall(install, own) {
17049
17173
  "@cabane",
17050
17174
  "companion"
17051
17175
  );
17052
- return existsSync18(script) ? join30(homedir10(), ".cabane") : null;
17176
+ return existsSync19(script) ? join31(homedir10(), ".cabane") : null;
17053
17177
  }
17054
17178
  const found = firstOnPath("cabane-companion", process.env.PATH);
17055
17179
  if (!found) return null;
17056
- const ownAlias = own ? real(join30(dirname13(own), "cabane-companion")) : null;
17180
+ const ownAlias = own ? real(join31(dirname13(own), "cabane-companion")) : null;
17057
17181
  return real(found) === ownAlias ? null : dirname13(found);
17058
17182
  }
17059
17183
  function runningMode(running, service2) {
@@ -17259,8 +17383,8 @@ init_cabane_command();
17259
17383
  init_errors();
17260
17384
  init_src();
17261
17385
  init_transcript_writer();
17262
- import { existsSync as existsSync19, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "fs";
17263
- import { basename as basename4, isAbsolute as isAbsolute2, join as join31 } from "path";
17386
+ import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as readdirSync6 } from "fs";
17387
+ import { basename as basename4, isAbsolute as isAbsolute2, join as join32 } from "path";
17264
17388
  async function transcript(opts = {}) {
17265
17389
  const dir2 = transcriptsDir();
17266
17390
  if (opts.follow) {
@@ -17277,7 +17401,7 @@ async function transcript(opts = {}) {
17277
17401
  process.stdout.write(emptyMessage(dir2));
17278
17402
  return;
17279
17403
  }
17280
- process.stdout.write(renderFile(join31(dir2, newest)) + "\n");
17404
+ process.stdout.write(renderFile(join32(dir2, newest)) + "\n");
17281
17405
  return;
17282
17406
  }
17283
17407
  printList(dir2);
@@ -17360,7 +17484,7 @@ function isComplete(content) {
17360
17484
  async function followTranscripts(dir2) {
17361
17485
  const follower = new TranscriptFollower({
17362
17486
  listFiles: () => listFiles(dir2),
17363
- read: (f) => readFileSync17(join31(dir2, f), "utf8"),
17487
+ read: (f) => readFileSync18(join32(dir2, f), "utf8"),
17364
17488
  write: (s) => process.stdout.write(s),
17365
17489
  // CSI: cursor up `n` lines, then erase from cursor to end of screen.
17366
17490
  clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
@@ -17396,7 +17520,7 @@ function listTranscriptFiles(dir2) {
17396
17520
  if (!entry.isDirectory() || entry.name === "anomalies") continue;
17397
17521
  let inner;
17398
17522
  try {
17399
- inner = readdirSync6(join31(dir2, entry.name));
17523
+ inner = readdirSync6(join32(dir2, entry.name));
17400
17524
  } catch {
17401
17525
  continue;
17402
17526
  }
@@ -17415,7 +17539,7 @@ function printList(dir2) {
17415
17539
 
17416
17540
  `);
17417
17541
  for (const f of files.slice(0, 20)) {
17418
- const { meta, outcome } = peek(join31(dir2, f));
17542
+ const { meta, outcome } = peek(join32(dir2, f));
17419
17543
  const when = fmtTime(rec(meta)?.ts);
17420
17544
  const ws = str2(rec(meta)?.workspaceSlug);
17421
17545
  const o = rec(outcome);
@@ -17437,7 +17561,7 @@ function peek(path) {
17437
17561
  let meta;
17438
17562
  let outcome;
17439
17563
  try {
17440
- for (const line of readFileSync17(path, "utf8").split("\n")) {
17564
+ for (const line of readFileSync18(path, "utf8").split("\n")) {
17441
17565
  if (!line.trim()) continue;
17442
17566
  const o = safeParse(line);
17443
17567
  const t = str2(rec(o)?.type);
@@ -17450,14 +17574,14 @@ function peek(path) {
17450
17574
  }
17451
17575
  function resolveTarget(dir2, target) {
17452
17576
  if (isAbsolute2(target) || target.includes("/")) {
17453
- if (existsSync19(target)) return target;
17454
- if (!isAbsolute2(target) && existsSync19(join31(dir2, target))) return join31(dir2, target);
17577
+ if (existsSync20(target)) return target;
17578
+ if (!isAbsolute2(target) && existsSync20(join32(dir2, target))) return join32(dir2, target);
17455
17579
  throw new CompanionError(`no transcript at ${target}.`);
17456
17580
  }
17457
- const exact = join31(dir2, target);
17458
- if (existsSync19(exact)) return exact;
17581
+ const exact = join32(dir2, target);
17582
+ if (existsSync20(exact)) return exact;
17459
17583
  const matches = listFiles(dir2).filter((f) => f.includes(target));
17460
- if (matches.length === 1) return join31(dir2, matches[0]);
17584
+ if (matches.length === 1) return join32(dir2, matches[0]);
17461
17585
  if (matches.length === 0) {
17462
17586
  throw new CompanionError(
17463
17587
  `no transcript matching "${target}" in ${dir2}. Run \`${cabane("transcript")}\` to list them.`
@@ -17471,7 +17595,7 @@ function resolveTarget(dir2, target) {
17471
17595
  function renderFile(path) {
17472
17596
  let content;
17473
17597
  try {
17474
- content = readFileSync17(path, "utf8");
17598
+ content = readFileSync18(path, "utf8");
17475
17599
  } catch (err) {
17476
17600
  throw new CompanionError(
17477
17601
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -17644,7 +17768,7 @@ program.command("setup [code]").description("set up this device: find your agent
17644
17768
  });
17645
17769
  });
17646
17770
  program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
17647
- writeCompletedPairing(readFileSync18(0, "utf8"));
17771
+ writeCompletedPairing(readFileSync19(0, "utf8"));
17648
17772
  });
17649
17773
  program.command("start").description("run Cabane on this device in the background.").option("--foreground", "run attached in this terminal (for terminals and supervisors)").option("--daemon", "run in the background (the default; kept for compatibility)").addOption(new Option("--server <url>").hideHelp()).addOption(
17650
17774
  new Option("--log-level <level>", "info (default), warn, or debug").choices([