@factiii/runner 0.11.1 → 0.11.3

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
@@ -963,7 +963,7 @@ var require_command = __commonJS({
963
963
  "node_modules/commander/lib/command.js"(exports2) {
964
964
  var EventEmitter = require("node:events").EventEmitter;
965
965
  var childProcess = require("node:child_process");
966
- var path20 = require("node:path");
966
+ var path21 = require("node:path");
967
967
  var fs15 = require("node:fs");
968
968
  var process2 = require("node:process");
969
969
  var { Argument: Argument2, humanReadableArgName } = require_argument();
@@ -1896,9 +1896,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
1896
1896
  let launchWithNode = false;
1897
1897
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1898
1898
  function findFile(baseDir, baseName) {
1899
- const localBin = path20.resolve(baseDir, baseName);
1899
+ const localBin = path21.resolve(baseDir, baseName);
1900
1900
  if (fs15.existsSync(localBin)) return localBin;
1901
- if (sourceExt.includes(path20.extname(baseName))) return void 0;
1901
+ if (sourceExt.includes(path21.extname(baseName))) return void 0;
1902
1902
  const foundExt = sourceExt.find(
1903
1903
  (ext) => fs15.existsSync(`${localBin}${ext}`)
1904
1904
  );
@@ -1916,17 +1916,17 @@ Expecting one of '${allowedValues.join("', '")}'`);
1916
1916
  } catch (err) {
1917
1917
  resolvedScriptPath = this._scriptPath;
1918
1918
  }
1919
- executableDir = path20.resolve(
1920
- path20.dirname(resolvedScriptPath),
1919
+ executableDir = path21.resolve(
1920
+ path21.dirname(resolvedScriptPath),
1921
1921
  executableDir
1922
1922
  );
1923
1923
  }
1924
1924
  if (executableDir) {
1925
1925
  let localFile = findFile(executableDir, executableFile);
1926
1926
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1927
- const legacyName = path20.basename(
1927
+ const legacyName = path21.basename(
1928
1928
  this._scriptPath,
1929
- path20.extname(this._scriptPath)
1929
+ path21.extname(this._scriptPath)
1930
1930
  );
1931
1931
  if (legacyName !== this._name) {
1932
1932
  localFile = findFile(
@@ -1937,7 +1937,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1937
1937
  }
1938
1938
  executableFile = localFile || executableFile;
1939
1939
  }
1940
- launchWithNode = sourceExt.includes(path20.extname(executableFile));
1940
+ launchWithNode = sourceExt.includes(path21.extname(executableFile));
1941
1941
  let proc;
1942
1942
  if (process2.platform !== "win32") {
1943
1943
  if (launchWithNode) {
@@ -2777,7 +2777,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2777
2777
  * @return {Command}
2778
2778
  */
2779
2779
  nameFromFilename(filename) {
2780
- this._name = path20.basename(filename, path20.extname(filename));
2780
+ this._name = path21.basename(filename, path21.extname(filename));
2781
2781
  return this;
2782
2782
  }
2783
2783
  /**
@@ -2791,9 +2791,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2791
2791
  * @param {string} [path]
2792
2792
  * @return {(string|null|Command)}
2793
2793
  */
2794
- executableDir(path21) {
2795
- if (path21 === void 0) return this._executableDir;
2796
- this._executableDir = path21;
2794
+ executableDir(path22) {
2795
+ if (path22 === void 0) return this._executableDir;
2796
+ this._executableDir = path22;
2797
2797
  return this;
2798
2798
  }
2799
2799
  /**
@@ -8428,8 +8428,8 @@ function getErrorMap() {
8428
8428
 
8429
8429
  // ../../node_modules/zod/v3/helpers/parseUtil.js
8430
8430
  var makeIssue = (params) => {
8431
- const { data, path: path20, errorMaps, issueData } = params;
8432
- const fullPath = [...path20, ...issueData.path || []];
8431
+ const { data, path: path21, errorMaps, issueData } = params;
8432
+ const fullPath = [...path21, ...issueData.path || []];
8433
8433
  const fullIssue = {
8434
8434
  ...issueData,
8435
8435
  path: fullPath
@@ -8545,11 +8545,11 @@ var errorUtil;
8545
8545
 
8546
8546
  // ../../node_modules/zod/v3/types.js
8547
8547
  var ParseInputLazyPath = class {
8548
- constructor(parent, value2, path20, key) {
8548
+ constructor(parent, value2, path21, key) {
8549
8549
  this._cachedPath = [];
8550
8550
  this.parent = parent;
8551
8551
  this.data = value2;
8552
- this._path = path20;
8552
+ this._path = path21;
8553
8553
  this._key = key;
8554
8554
  }
8555
8555
  get path() {
@@ -12659,7 +12659,7 @@ var prompts_default = {
12659
12659
  "- Write ONLY those three files under .agents/skills/deploy/.",
12660
12660
  "- Do NOT git commit or git push; the harness pushes for review.",
12661
12661
  "- Cloud CLIs on this machine (aws, vercel, wrangler, gh...) may already be signed in. You may use them READ-ONLY to verify what exists (project names, repo names, regions) so the skill is accurate - but NEVER create, modify, deploy, or delete anything, and never echo a credential or token value.",
12662
- '- Secret-bearing commands: any command needing a variable declared in required-variables.json MUST be written in the skill as `factiii-secrets run -- <command>`, with the variables referenced unexpanded and quoted (e.g. "$DATABASE_URL"). The owner authorizes each one at run time and the runner injects the values. Wrap only the commands that need a secret. NEVER include steps that check whether variables are set in the environment, and never instruct anyone to export them or put them in a .env/shell/CI config.',
12662
+ '- Secret-bearing commands: any command needing a variable declared in required-variables.json MUST be written in the skill as `factiii-secrets run --key NAME [--key NAME]... -- <command>`, naming exactly the keys THAT command reads (a wrapper script\'s keys are what the script reads, not what its command line shows), with the variables referenced unexpanded and quoted (e.g. "$DATABASE_URL"). Only the named keys are injected: a step asks for its own keys, never for every key the skill declares. The owner authorizes each one at run time and the runner injects the values. Wrap only the commands that need a secret. NEVER include steps that check whether variables are set in the environment, and never instruct anyone to export them or put them in a .env/shell/CI config.',
12663
12663
  "- No secret values in any file, ever."
12664
12664
  ],
12665
12665
  deployCreateRevise: [
@@ -12667,7 +12667,7 @@ var prompts_default = {
12667
12667
  "",
12668
12668
  "{{FEEDBACK}}",
12669
12669
  "",
12670
- "Apply them by editing .agents/skills/deploy/SKILL.md, .agents/skills/deploy/required-variables.json, and/or .agents/skills/deploy/check-environment.sh in your working directory - the same contract as before: only those three files, keys declared exactly when used, every secret-bearing command still written as `factiii-secrets run -- <command>`, no secret values anywhere, no git commit/push, and paths referenced ONLY via $WORKSPACE / $BACKUPS_DIR / $BUILDS_DIR or relative to the working directory (never absolute machine paths). Keep everything the owner did NOT ask to change untouched."
12670
+ "Apply them by editing .agents/skills/deploy/SKILL.md, .agents/skills/deploy/required-variables.json, and/or .agents/skills/deploy/check-environment.sh in your working directory - the same contract as before: only those three files, keys declared exactly when used, every secret-bearing command still written as `factiii-secrets run --key NAME ... -- <command>` naming exactly the keys that command reads, no secret values anywhere, no git commit/push, and paths referenced ONLY via $WORKSPACE / $BACKUPS_DIR / $BUILDS_DIR or relative to the working directory (never absolute machine paths). Keep everything the owner did NOT ask to change untouched."
12671
12671
  ],
12672
12672
  deployRun: [
12673
12673
  "## Deploy run protocol",
@@ -12675,9 +12675,10 @@ var prompts_default = {
12675
12675
  "",
12676
12676
  "## Secrets (read this before anything else)",
12677
12677
  "Every variable declared in required-variables.json is UNSET in your shell BY DESIGN, and will stay unset for the whole run. The values are stored encrypted on this machine, and `factiii-secrets` is how a command gets them - it is on your PATH, and the `factiii-secrets` skill describes it in full.",
12678
- "- To run a command that needs any declared variable, prefix it: `factiii-secrets run -- <command>`. The owner authorizes it with their password, the runner runs it with every declared value injected, and you get the output back with the values redacted. It exits with the command's own code, so `&&`, `||` and `set -e` behave normally. Exit 77 means the owner declined - adapt or stop, and never retry the same command.",
12678
+ "- To run a command that needs any declared variable, prefix it and NAME the keys that command reads: `factiii-secrets run --key NAME [--key NAME]... -- <command>`. The owner authorizes it with their password, the runner runs it with ONLY the keys you named injected, and you get the output back with the values redacted. A command that names no key gets no secrets, so always name them. Repeat --key per name (or comma-separate them), always BEFORE the `--`. It exits with the command's own code, so `&&`, `||` and `set -e` behave normally. Exit 77 means the owner declined - adapt or stop, and never retry the same command.",
12679
12679
  '- Quote the variables (`"$DATABASE_URL"`): they expand on the runner\'s side, not in your shell.',
12680
12680
  "- NEVER check whether these variables are set (no `env`, `printenv`, `test -n`, echo checks), never report them as missing, and never abort because your shell lacks them. Their absence tells you nothing. If the skill has a step that verifies variables are set, SKIP it - it predates this protocol.",
12681
+ "- Name what the command actually reads. For a wrapper script, that is what the SCRIPT reads, not what appears on its command line. If you are unsure of a name, run `factiii-secrets list` - it prints the key names and whether each is set, never a value. A name that is not in the store expands empty, and the runner tells you which one it was.",
12681
12682
  "- Wrap ONLY the commands that actually need a secret. Everything else runs unwrapped and needs no authorization.",
12682
12683
  "",
12683
12684
  "- Track the release with your todo/plan tool: one todo per step in ship order, updated as you go. This is the owner's live timeline.",
@@ -12909,7 +12910,7 @@ ${diff.slice(0, 8e3)}`,
12909
12910
  }
12910
12911
 
12911
12912
  // ../../shared/all/helpers/board-agent-core/engine.ts
12912
- var import_path12 = require("path");
12913
+ var import_path13 = require("path");
12913
12914
 
12914
12915
  // ../../shared/all/domains/electron.ts
12915
12916
  var SETUP_LOG_PREFIX = "log ";
@@ -13018,7 +13019,7 @@ async function ensurePrimaryClone(target, cloneUrl, branch) {
13018
13019
  await target.sh(`mkdir -p ${target.paths.worktrees}`);
13019
13020
  }
13020
13021
  async function addWorktree(target, workName, baseBranch, kind) {
13021
- const path20 = worktreeFor(target.paths, workName);
13022
+ const path21 = worktreeFor(target.paths, workName);
13022
13023
  const branch = workBranch(workName, kind);
13023
13024
  await gitInDir(
13024
13025
  target,
@@ -13028,7 +13029,7 @@ async function addWorktree(target, workName, baseBranch, kind) {
13028
13029
  baseBranch
13029
13030
  ).catch(() => {
13030
13031
  });
13031
- await removeWorktree(target, path20).catch(() => {
13032
+ await removeWorktree(target, path21).catch(() => {
13032
13033
  });
13033
13034
  await gitInDir(
13034
13035
  target,
@@ -13037,30 +13038,30 @@ async function addWorktree(target, workName, baseBranch, kind) {
13037
13038
  "add",
13038
13039
  "-B",
13039
13040
  branch,
13040
- path20,
13041
+ path21,
13041
13042
  `origin/${baseBranch}`
13042
13043
  );
13043
- return { path: path20, branch };
13044
+ return { path: path21, branch };
13044
13045
  }
13045
- async function removeWorktree(target, path20) {
13046
+ async function removeWorktree(target, path21) {
13046
13047
  await gitInDir(
13047
13048
  target,
13048
13049
  target.paths.repo,
13049
13050
  "worktree",
13050
13051
  "remove",
13051
13052
  "--force",
13052
- path20
13053
+ path21
13053
13054
  );
13054
13055
  }
13055
- async function removeWorktreeAndBranch(target, path20) {
13056
+ async function removeWorktreeAndBranch(target, path21) {
13056
13057
  const branch = await gitInDir(
13057
13058
  target,
13058
- path20,
13059
+ path21,
13059
13060
  "rev-parse",
13060
13061
  "--abbrev-ref",
13061
13062
  "HEAD"
13062
13063
  ).then((out) => out.trim()).catch(() => "");
13063
- await removeWorktree(target, path20);
13064
+ await removeWorktree(target, path21);
13064
13065
  if (branch && branch !== "HEAD") {
13065
13066
  await gitInDir(target, target.paths.repo, "branch", "-D", branch).catch(
13066
13067
  () => {
@@ -13074,8 +13075,8 @@ async function pruneWorktrees(target) {
13074
13075
  }
13075
13076
  );
13076
13077
  }
13077
- function isWorktreePath(target, path20) {
13078
- return path20.startsWith(`${target.paths.worktrees}/`);
13078
+ function isWorktreePath(target, path21) {
13079
+ return path21.startsWith(`${target.paths.worktrees}/`);
13079
13080
  }
13080
13081
  async function setGitIdentity(target, name, email, workdir = target.paths.workspace) {
13081
13082
  if (name) await gitInDir(target, workdir, "config", "user.name", name);
@@ -13101,13 +13102,13 @@ function parseStatus(raw) {
13101
13102
  const y = line[1];
13102
13103
  const rest = line.slice(3);
13103
13104
  const arrow = rest.indexOf(" -> ");
13104
- const path20 = arrow === -1 ? rest : rest.slice(arrow + 4);
13105
+ const path21 = arrow === -1 ? rest : rest.slice(arrow + 4);
13105
13106
  if (x === "?" || y === "?") {
13106
- unstaged.push({ path: path20, code: "A" });
13107
+ unstaged.push({ path: path21, code: "A" });
13107
13108
  continue;
13108
13109
  }
13109
- if (x !== " ") staged.push({ path: path20, code: x });
13110
- if (y !== " ") unstaged.push({ path: path20, code: y });
13110
+ if (x !== " ") staged.push({ path: path21, code: x });
13111
+ if (y !== " ") unstaged.push({ path: path21, code: y });
13111
13112
  }
13112
13113
  return { staged, unstaged };
13113
13114
  }
@@ -13853,6 +13854,9 @@ function createEventSink(send, spaceSlug, notify) {
13853
13854
  emitDeployEnv(payload) {
13854
13855
  send(channel("deploy-env"), payload);
13855
13856
  },
13857
+ emitSecretsAuth(payload) {
13858
+ send(channel("secrets-auth"), payload);
13859
+ },
13856
13860
  emitBareFsChange(postId, change) {
13857
13861
  send(channel(`bare-fs:${postId}`), change);
13858
13862
  },
@@ -14388,6 +14392,7 @@ var import_util5 = require("util");
14388
14392
  var import_promises2 = __toESM(require("fs/promises"));
14389
14393
  var import_net2 = __toESM(require("net"));
14390
14394
  var import_path4 = __toESM(require("path"));
14395
+ var SECRET_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
14391
14396
  function secretsSocketPath(spaceDir) {
14392
14397
  return import_path4.default.join(hostSpacePaths(spaceDir).state, "secrets.sock");
14393
14398
  }
@@ -14415,9 +14420,23 @@ async function handleRun(request, handlers, reply) {
14415
14420
  return;
14416
14421
  }
14417
14422
  const cwd = typeof request.cwd === "string" ? request.cwd : "";
14423
+ const requested = Array.isArray(request.keys) ? request.keys : [];
14424
+ const keys = [];
14425
+ for (const entry of requested) {
14426
+ if (typeof entry !== "string" || !SECRET_KEY_PATTERN.test(entry)) {
14427
+ reply({
14428
+ type: "error",
14429
+ message: `"${String(entry)}" is not a key name.`
14430
+ });
14431
+ return;
14432
+ }
14433
+ if (!keys.includes(entry)) keys.push(entry);
14434
+ }
14418
14435
  const result = await handlers.run({
14419
14436
  command,
14420
14437
  cwd,
14438
+ keys,
14439
+ token: typeof request.token === "string" ? request.token : "",
14421
14440
  status: (text) => reply({ type: "status", text })
14422
14441
  });
14423
14442
  if ("denied" in result) {
@@ -14458,7 +14477,9 @@ async function startSecretsBridge(opts) {
14458
14477
  return;
14459
14478
  }
14460
14479
  if (request.op === "list") {
14461
- void handlers.list().then((keys) => reply({ type: "keys", keys })).catch(
14480
+ void handlers.list().then(
14481
+ (entries) => reply({ type: "keys", keys: entries.map((e) => e.key), entries })
14482
+ ).catch(
14462
14483
  (err) => reply({ type: "error", message: errorText(err) })
14463
14484
  ).finally(() => conn.end());
14464
14485
  return;
@@ -15094,9 +15115,9 @@ function summarize(raw, base) {
15094
15115
  }
15095
15116
  async function rootLinkState(root) {
15096
15117
  const linkPath = import_path6.default.join(import_os2.default.homedir(), ...root.split("/"));
15097
- const stat2 = await import_promises4.default.lstat(linkPath).catch(() => null);
15098
- if (!stat2) return "missing";
15099
- if (!stat2.isSymbolicLink()) return "blocked";
15118
+ const stat = await import_promises4.default.lstat(linkPath).catch(() => null);
15119
+ if (!stat) return "missing";
15120
+ if (!stat.isSymbolicLink()) return "blocked";
15100
15121
  const target = await import_promises4.default.readlink(linkPath).catch(() => "");
15101
15122
  return target.replace(/\/+$/, "") === AGENTS_LINK_TARGET ? "linked" : "missing";
15102
15123
  }
@@ -15435,8 +15456,8 @@ async function migrateGlobal() {
15435
15456
  const moved = [];
15436
15457
  for (const root of CLI_ROOTS) {
15437
15458
  const dir = import_path6.default.join(import_os2.default.homedir(), ...root.split("/"));
15438
- const stat2 = await import_promises4.default.lstat(dir).catch(() => null);
15439
- if (!stat2 || stat2.isSymbolicLink()) continue;
15459
+ const stat = await import_promises4.default.lstat(dir).catch(() => null);
15460
+ if (!stat || stat.isSymbolicLink()) continue;
15440
15461
  const entries = await import_promises4.default.readdir(dir, { withFileTypes: true }).catch(() => []);
15441
15462
  for (const entry of entries) {
15442
15463
  if (entry.isFile() || entry.isSymbolicLink()) continue;
@@ -15556,9 +15577,9 @@ async function installBundledSkill(opts) {
15556
15577
  await write(import_path6.default.join(home, ...AGENTS_ROOT.split("/")));
15557
15578
  for (const root of CLI_ROOTS) {
15558
15579
  const linkPath = import_path6.default.join(home, ...root.split("/"));
15559
- const stat2 = await import_promises4.default.lstat(linkPath).catch(() => null);
15560
- if (stat2?.isSymbolicLink()) continue;
15561
- if (!stat2) {
15580
+ const stat = await import_promises4.default.lstat(linkPath).catch(() => null);
15581
+ if (stat?.isSymbolicLink()) continue;
15582
+ if (!stat) {
15562
15583
  await import_promises4.default.mkdir(import_path6.default.dirname(linkPath), { recursive: true });
15563
15584
  await import_promises4.default.symlink(AGENTS_LINK_TARGET, linkPath, "dir").catch(() => void 0);
15564
15585
  continue;
@@ -16116,18 +16137,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
16116
16137
  await this.core.ensureSpaceWorkspace();
16117
16138
  await assertAgentSignedIn(session.provider, this.core.target());
16118
16139
  if (hasGit) {
16119
- const { path: path20 } = await addWorktree(
16140
+ const { path: path21 } = await addWorktree(
16120
16141
  this.core.target(),
16121
16142
  session.taskId,
16122
16143
  baseBranch || config.mainBranch || "main",
16123
16144
  "card"
16124
16145
  );
16125
- session.workspacePath = path20;
16146
+ session.workspacePath = path21;
16126
16147
  await setGitIdentity(
16127
16148
  this.core.target(),
16128
16149
  config.gitName,
16129
16150
  config.gitEmail,
16130
- path20
16151
+ path21
16131
16152
  );
16132
16153
  this.emitter.addLog(session, "system", "Worktree ready.");
16133
16154
  } else {
@@ -16349,18 +16370,18 @@ var ClaudeModeEngine = class _ClaudeModeEngine {
16349
16370
  this.emitter.addLog(session, "system", "Preparing environment...");
16350
16371
  await this.core.ensureSpaceWorkspace();
16351
16372
  await assertAgentSignedIn(session.provider, this.core.target());
16352
- const { path: path20 } = await addWorktree(
16373
+ const { path: path21 } = await addWorktree(
16353
16374
  this.core.target(),
16354
16375
  session.taskId,
16355
16376
  config.mainBranch || "main",
16356
16377
  "card"
16357
16378
  );
16358
- session.workspacePath = path20;
16379
+ session.workspacePath = path21;
16359
16380
  await setGitIdentity(
16360
16381
  this.core.target(),
16361
16382
  config.gitName,
16362
16383
  config.gitEmail,
16363
- path20
16384
+ path21
16364
16385
  );
16365
16386
  await this.putCard(session);
16366
16387
  const patchDir = `${this.core.target().paths.root}/patches/${session.taskId}`;
@@ -16907,12 +16928,12 @@ var FLUSH_MS = 120;
16907
16928
  var SELF_WRITE_MS = 2e3;
16908
16929
  var dynamicImport = new Function("s", "return import(s)");
16909
16930
  var importFs = new Function("s", "return import(s)");
16910
- async function resolveReal(path20) {
16931
+ async function resolveReal(path21) {
16911
16932
  try {
16912
16933
  const { realpath } = await importFs("fs/promises");
16913
- return await realpath(path20);
16934
+ return await realpath(path21);
16914
16935
  } catch {
16915
- return path20;
16936
+ return path21;
16916
16937
  }
16917
16938
  }
16918
16939
  var loadSubscribe = null;
@@ -17026,19 +17047,19 @@ var WorkspaceWatcher = class {
17026
17047
  }
17027
17048
  record(kind, relPath) {
17028
17049
  if (this.closed) return;
17029
- const path20 = "/" + relPath;
17030
- const expiry = this.selfWrites.get(path20);
17050
+ const path21 = "/" + relPath;
17051
+ const expiry = this.selfWrites.get(path21);
17031
17052
  if (expiry !== void 0) {
17032
17053
  if (expiry > Date.now()) return;
17033
- this.selfWrites.delete(path20);
17054
+ this.selfWrites.delete(path21);
17034
17055
  }
17035
17056
  const { added, changed, removed } = this.pending;
17036
- added.delete(path20);
17037
- changed.delete(path20);
17038
- removed.delete(path20);
17039
- if (kind === "added") added.add(path20);
17040
- else if (kind === "changed") changed.add(path20);
17041
- else removed.add(path20);
17057
+ added.delete(path21);
17058
+ changed.delete(path21);
17059
+ removed.delete(path21);
17060
+ if (kind === "added") added.add(path21);
17061
+ else if (kind === "changed") changed.add(path21);
17062
+ else removed.add(path21);
17042
17063
  if (added.size + changed.size + removed.size > MAX_BATCH) {
17043
17064
  this.pending = { ...emptyPending(), resync: true };
17044
17065
  }
@@ -17057,8 +17078,8 @@ var WorkspaceWatcher = class {
17057
17078
  this.pending = emptyPending();
17058
17079
  if (!resync && !added.size && !changed.size && !removed.size) return;
17059
17080
  const now = Date.now();
17060
- for (const [path20, expiry] of this.selfWrites) {
17061
- if (expiry <= now) this.selfWrites.delete(path20);
17081
+ for (const [path21, expiry] of this.selfWrites) {
17082
+ if (expiry <= now) this.selfWrites.delete(path21);
17062
17083
  }
17063
17084
  this.emit({
17064
17085
  added: resync ? [] : [...added],
@@ -17262,8 +17283,8 @@ var TerminalModeEngine = class {
17262
17283
  /** Suppress the change event our own write is about to produce, so a save
17263
17284
  * never comes back to the editor as an outside edit. Normalized to the form
17264
17285
  * the watcher emits, since callers spell the path either way. */
17265
- expectWrite(postId, path20) {
17266
- this.watchers.get(postId)?.expectWrite(posixNormalize(path20));
17286
+ expectWrite(postId, path21) {
17287
+ this.watchers.get(postId)?.expectWrite(posixNormalize(path21));
17267
17288
  }
17268
17289
  // ── Lifecycle / ModeEngine ──
17269
17290
  start(base, args) {
@@ -17460,18 +17481,18 @@ var TerminalModeEngine = class {
17460
17481
  this.reservedNames.values()
17461
17482
  );
17462
17483
  this.reservedNames.set(session.postId, workName);
17463
- const { path: path20 } = await addWorktree(
17484
+ const { path: path21 } = await addWorktree(
17464
17485
  this.core.target(),
17465
17486
  workName,
17466
17487
  branch,
17467
17488
  "terminal"
17468
17489
  );
17469
- session.workspacePath = path20;
17490
+ session.workspacePath = path21;
17470
17491
  await setGitIdentity(
17471
17492
  this.core.target(),
17472
17493
  config.gitName,
17473
17494
  config.gitEmail,
17474
- path20
17495
+ path21
17475
17496
  );
17476
17497
  this.emitter.addLog(session, "system", "Worktree ready.");
17477
17498
  } else {
@@ -17533,11 +17554,11 @@ var TerminalModeEngine = class {
17533
17554
  }
17534
17555
  async bareFsList({
17535
17556
  postId,
17536
- path: path20
17557
+ path: path21
17537
17558
  }) {
17538
17559
  const target = this.getBareTarget(postId);
17539
17560
  const root = this.getBareRoot(postId);
17540
- const abs = this.resolveBarePath(path20, root);
17561
+ const abs = this.resolveBarePath(path21, root);
17541
17562
  const relRoot = abs.slice(root.length) || "/";
17542
17563
  const out = await target.run([
17543
17564
  "sh",
@@ -17587,30 +17608,30 @@ var TerminalModeEngine = class {
17587
17608
  }
17588
17609
  async bareFsRead({
17589
17610
  postId,
17590
- path: path20
17611
+ path: path21
17591
17612
  }) {
17592
17613
  const target = this.getBareTarget(postId);
17593
- const abs = this.resolveBarePath(path20, this.getBareRoot(postId));
17614
+ const abs = this.resolveBarePath(path21, this.getBareRoot(postId));
17594
17615
  const b64 = (await target.sh(`base64 < ${shEscape(abs)} | tr -d '
17595
17616
  '`)).trim();
17596
17617
  return { content: b64, encoding: "base64" };
17597
17618
  }
17598
17619
  async bareFsWrite({
17599
17620
  postId,
17600
- path: path20,
17621
+ path: path21,
17601
17622
  content,
17602
17623
  encoding
17603
17624
  }) {
17604
17625
  const target = this.getBareTarget(postId);
17605
- const abs = this.resolveBarePath(path20, this.getBareRoot(postId));
17626
+ const abs = this.resolveBarePath(path21, this.getBareRoot(postId));
17606
17627
  const b64 = encoding === "base64" ? content : Buffer.from(content, "utf-8").toString("base64");
17607
17628
  if (!/^[A-Za-z0-9+/=\n\r]*$/.test(b64)) {
17608
17629
  throw new Error("Invalid base64 payload.");
17609
17630
  }
17610
- this.expectWrite(String(postId), path20);
17631
+ this.expectWrite(String(postId), path21);
17611
17632
  await writeBase64File(target, abs, b64);
17612
17633
  const session = this.sessions.get(String(postId));
17613
- if (session) delete session.buffers[path20];
17634
+ if (session) delete session.buffers[path21];
17614
17635
  }
17615
17636
  /** Append one chunk of a dropped image to the per-space drops dir
17616
17637
  * (`paths.root`, OUTSIDE the card worktree so it never touches the repo).
@@ -17660,39 +17681,39 @@ var TerminalModeEngine = class {
17660
17681
  }
17661
17682
  bareOpenFile({
17662
17683
  postId: rawPostId,
17663
- path: path20
17684
+ path: path21
17664
17685
  }) {
17665
17686
  const session = this.sessions.get(String(rawPostId));
17666
17687
  if (!session || session.mode !== "bare") return;
17667
- if (!session.openedFiles.includes(path20)) {
17668
- session.openedFiles.push(path20);
17688
+ if (!session.openedFiles.includes(path21)) {
17689
+ session.openedFiles.push(path21);
17669
17690
  }
17670
17691
  }
17671
17692
  bareCloseFile({
17672
17693
  postId: rawPostId,
17673
- path: path20
17694
+ path: path21
17674
17695
  }) {
17675
17696
  const session = this.sessions.get(String(rawPostId));
17676
17697
  if (!session || session.mode !== "bare") return;
17677
- session.openedFiles = session.openedFiles.filter((p) => p !== path20);
17678
- delete session.buffers[path20];
17698
+ session.openedFiles = session.openedFiles.filter((p) => p !== path21);
17699
+ delete session.buffers[path21];
17679
17700
  }
17680
17701
  bareBufferSet({
17681
17702
  postId: rawPostId,
17682
- path: path20,
17703
+ path: path21,
17683
17704
  content
17684
17705
  }) {
17685
17706
  const session = this.sessions.get(String(rawPostId));
17686
17707
  if (!session || session.mode !== "bare") return;
17687
- session.buffers[path20] = content;
17708
+ session.buffers[path21] = content;
17688
17709
  }
17689
17710
  async bareFsDelete({
17690
17711
  postId,
17691
- path: path20
17712
+ path: path21
17692
17713
  }) {
17693
17714
  const target = this.getBareTarget(postId);
17694
17715
  const root = this.getBareRoot(postId);
17695
- const abs = this.resolveBarePath(path20, root);
17716
+ const abs = this.resolveBarePath(path21, root);
17696
17717
  if (abs === root) {
17697
17718
  throw new Error("Refusing to delete the repo root.");
17698
17719
  }
@@ -17700,10 +17721,10 @@ var TerminalModeEngine = class {
17700
17721
  }
17701
17722
  async bareFsMkdir({
17702
17723
  postId,
17703
- path: path20
17724
+ path: path21
17704
17725
  }) {
17705
17726
  const target = this.getBareTarget(postId);
17706
- const abs = this.resolveBarePath(path20, this.getBareRoot(postId));
17727
+ const abs = this.resolveBarePath(path21, this.getBareRoot(postId));
17707
17728
  await target.sh(`mkdir -p ${shEscape(abs)}`);
17708
17729
  }
17709
17730
  async bareReview({
@@ -18150,13 +18171,13 @@ var ClaudeLoginFlow = class {
18150
18171
  async onClose() {
18151
18172
  if (this.settled) return;
18152
18173
  const errLine = this.output.slice(Math.max(this.submitMark, 0)).match(/(?:Login failed|OAuth error):[^\r\n]+/)?.[0];
18153
- const tail2 = errLine ? `${errLine.trim()} Try signing in again.` : this.output.trim().slice(-300);
18174
+ const tail3 = errLine ? `${errLine.trim()} Try signing in again.` : this.output.trim().slice(-300);
18154
18175
  if (this.urlRejecter) {
18155
- this.fail(tail2 || "Claude sign-in ended unexpectedly.");
18176
+ this.fail(tail3 || "Claude sign-in ended unexpectedly.");
18156
18177
  return;
18157
18178
  }
18158
18179
  this.settled = true;
18159
- await this.verifyAndComplete(tail2);
18180
+ await this.verifyAndComplete(tail3);
18160
18181
  }
18161
18182
  fail(message) {
18162
18183
  if (this.settled) return;
@@ -18286,13 +18307,13 @@ var CodexLoginFlow = class {
18286
18307
  }
18287
18308
  async onClose() {
18288
18309
  if (this.settled) return;
18289
- const tail2 = this.output.trim().slice(-300);
18310
+ const tail3 = this.output.trim().slice(-300);
18290
18311
  if (this.startRejecter) {
18291
- this.fail(tail2 || "Codex sign-in ended unexpectedly.");
18312
+ this.fail(tail3 || "Codex sign-in ended unexpectedly.");
18292
18313
  return;
18293
18314
  }
18294
18315
  this.settled = true;
18295
- await this.verifyAndComplete(tail2);
18316
+ await this.verifyAndComplete(tail3);
18296
18317
  }
18297
18318
  fail(message) {
18298
18319
  if (this.settled) return;
@@ -18385,8 +18406,8 @@ async function pollGithubDeviceCode(clientId, deviceCode) {
18385
18406
  };
18386
18407
  }
18387
18408
  }
18388
- async function githubApi(token, path20) {
18389
- const res = await fetch(`${API_BASE}${path20}`, {
18409
+ async function githubApi(token, path21) {
18410
+ const res = await fetch(`${API_BASE}${path21}`, {
18390
18411
  headers: {
18391
18412
  Authorization: `Bearer ${token}`,
18392
18413
  Accept: "application/vnd.github+json",
@@ -18395,7 +18416,7 @@ async function githubApi(token, path20) {
18395
18416
  });
18396
18417
  if (!res.ok) {
18397
18418
  throw new Error(
18398
- `GitHub ${path20} failed (${res.status}): ${await res.text()}`
18419
+ `GitHub ${path21} failed (${res.status}): ${await res.text()}`
18399
18420
  );
18400
18421
  }
18401
18422
  return await res.json();
@@ -18440,11 +18461,103 @@ async function listGithubBranches(token, fullName) {
18440
18461
  }
18441
18462
 
18442
18463
  // ../../shared/all/helpers/board-agent-core/host-run.ts
18464
+ var import_path10 = __toESM(require("path"));
18465
+
18466
+ // ../../shared/all/helpers/board-agent-core/secrets-gate.ts
18467
+ var import_crypto2 = __toESM(require("crypto"));
18468
+ var DEPLOY_ORIGIN = "Deploy run";
18469
+ var UNATTRIBUTED = "This runner did not start the session that asked, so the request was refused. `factiii-secrets` only works inside a session the runner spawned.";
18470
+ var SecretsGate = class {
18471
+ constructor() {
18472
+ /** token -> origin label. Lives as long as the session that holds it. */
18473
+ this.tokens = /* @__PURE__ */ new Map();
18474
+ this.waiting = null;
18475
+ /** Notified on appearance, replacement AND clearing, so a modal that opened
18476
+ * on one request closes when it is answered elsewhere. More than one,
18477
+ * because the space publishes the change and the deploy run separately
18478
+ * projects its own requests onto the release timeline. */
18479
+ this.listeners = /* @__PURE__ */ new Set();
18480
+ }
18481
+ subscribe(listener) {
18482
+ this.listeners.add(listener);
18483
+ return () => this.listeners.delete(listener);
18484
+ }
18485
+ onChange() {
18486
+ for (const listener of this.listeners) listener();
18487
+ }
18488
+ // ── Who is asking ──
18489
+ /** Mint a token for one session, labelled with what the owner should see on
18490
+ * the prompt - the label is most of the decision, so name the session. */
18491
+ issueToken(origin) {
18492
+ const token = import_crypto2.default.randomBytes(24).toString("hex");
18493
+ this.tokens.set(token, origin);
18494
+ return token;
18495
+ }
18496
+ /** Drop a session's token when it ends, so a leaked one stops working. */
18497
+ revokeToken(token) {
18498
+ this.tokens.delete(token);
18499
+ }
18500
+ /** The origin for a token, or null when this runner never issued it. */
18501
+ originFor(token) {
18502
+ return this.tokens.get(token) ?? null;
18503
+ }
18504
+ // ── The pending request ──
18505
+ pending() {
18506
+ return this.waiting?.request ?? null;
18507
+ }
18508
+ /** Open the gate for one command and block until the owner answers. A second
18509
+ * request denies the older one rather than dropping it: that caller is
18510
+ * blocked on the promise and would otherwise hang forever. */
18511
+ request(opts) {
18512
+ this.waiting?.resolve({ denied: true });
18513
+ return new Promise((resolve2) => {
18514
+ this.waiting = {
18515
+ request: {
18516
+ id: import_crypto2.default.randomUUID(),
18517
+ command: opts.command,
18518
+ keys: opts.keys,
18519
+ origin: opts.origin,
18520
+ error: opts.error ?? "",
18521
+ requestedAt: Date.now()
18522
+ },
18523
+ resolve: resolve2
18524
+ };
18525
+ this.onChange();
18526
+ });
18527
+ }
18528
+ /** Answer the pending request. `id` must match what the client was shown: a
18529
+ * wrong-password re-prompt replaces the request, and an answer aimed at the
18530
+ * previous one must not resolve its successor with a stale password. */
18531
+ answer(id, result) {
18532
+ const waiting = this.waiting;
18533
+ if (!waiting || waiting.request.id !== id) return { success: false };
18534
+ this.waiting = null;
18535
+ waiting.resolve(result);
18536
+ this.onChange();
18537
+ return { success: true };
18538
+ }
18539
+ /** Deny whatever is waiting, without needing to know its id. The run was
18540
+ * cancelled, or the space is going down: nothing may stay blocked. */
18541
+ cancel() {
18542
+ const waiting = this.waiting;
18543
+ if (!waiting) return;
18544
+ this.waiting = null;
18545
+ waiting.resolve({ denied: true });
18546
+ this.onChange();
18547
+ }
18548
+ destroy() {
18549
+ this.cancel();
18550
+ this.tokens.clear();
18551
+ this.listeners.clear();
18552
+ }
18553
+ };
18554
+
18555
+ // ../../shared/all/helpers/board-agent-core/secrets-run.ts
18443
18556
  var import_path9 = __toESM(require("path"));
18444
18557
 
18445
18558
  // ../../shared/all/helpers/board-agent-core/host-secrets.ts
18446
18559
  var import_child_process4 = require("child_process");
18447
- var import_crypto2 = __toESM(require("crypto"));
18560
+ var import_crypto3 = __toESM(require("crypto"));
18448
18561
  var import_promises6 = __toESM(require("fs/promises"));
18449
18562
  var import_path8 = __toESM(require("path"));
18450
18563
  var import_util6 = require("util");
@@ -18472,7 +18585,7 @@ async function assertBinaries() {
18472
18585
  }
18473
18586
  }
18474
18587
  function deriveKey(password, salt) {
18475
- return import_crypto2.default.scryptSync(password, salt, 32, {
18588
+ return import_crypto3.default.scryptSync(password, salt, 32, {
18476
18589
  N: SCRYPT_N,
18477
18590
  maxmem: 64 * 1024 * 1024
18478
18591
  });
@@ -18501,9 +18614,9 @@ async function initSecrets(configDir, password) {
18501
18614
  if (!identity || !recipient) {
18502
18615
  throw new Error("age-keygen returned no usable keypair.");
18503
18616
  }
18504
- const salt = import_crypto2.default.randomBytes(16);
18505
- const iv = import_crypto2.default.randomBytes(12);
18506
- const cipher = import_crypto2.default.createCipheriv(
18617
+ const salt = import_crypto3.default.randomBytes(16);
18618
+ const iv = import_crypto3.default.randomBytes(12);
18619
+ const cipher = import_crypto3.default.createCipheriv(
18507
18620
  "aes-256-gcm",
18508
18621
  deriveKey(password, salt),
18509
18622
  iv
@@ -18528,7 +18641,7 @@ async function initSecrets(configDir, password) {
18528
18641
  return { success: true };
18529
18642
  }
18530
18643
  function decryptIdentity(box, password) {
18531
- const decipher = import_crypto2.default.createDecipheriv(
18644
+ const decipher = import_crypto3.default.createDecipheriv(
18532
18645
  "aes-256-gcm",
18533
18646
  deriveKey(password, Buffer.from(box.salt, "base64")),
18534
18647
  Buffer.from(box.iv, "base64")
@@ -18614,6 +18727,18 @@ async function writeEnv(configDir, spaceDir, identity, values) {
18614
18727
  await import_promises6.default.rm(tmp, { force: true });
18615
18728
  }
18616
18729
  }
18730
+ async function storedSecretKeys(spaceDir) {
18731
+ const raw = await import_promises6.default.readFile(envPath(spaceDir), "utf-8").catch(() => "");
18732
+ const keys = [];
18733
+ for (const line of raw.split("\n")) {
18734
+ const eq = line.indexOf("=");
18735
+ if (eq <= 0 || line.startsWith("#")) continue;
18736
+ const key = line.slice(0, eq);
18737
+ if (key.startsWith("sops_")) continue;
18738
+ keys.push(key);
18739
+ }
18740
+ return keys;
18741
+ }
18617
18742
  async function listSecretKeys(configDir, spaceDir, password) {
18618
18743
  const identity = await identityFor(configDir, password);
18619
18744
  return Object.keys(await readEnv(spaceDir, identity));
@@ -18641,31 +18766,10 @@ async function secretsForInjection(configDir, spaceDir, password) {
18641
18766
  return readEnv(spaceDir, identity);
18642
18767
  }
18643
18768
 
18644
- // ../../shared/all/helpers/board-agent-core/host-run.ts
18645
- var HUMAN_MARKER = "AWAITING_INPUT:";
18646
- var SECRETS_MARKER = "NEED_SECRETS:";
18647
- var COMPLETE_MARKER = "DEPLOY_COMPLETE:";
18648
- var FAILED_MARKER = "DEPLOY_FAILED:";
18649
- var MAX_MARKERLESS_TURNS = 3;
18769
+ // ../../shared/all/helpers/board-agent-core/secrets-run.ts
18650
18770
  var COMMAND_IDLE_TIMEOUT_MS = 45 * 6e4;
18651
18771
  var COMMAND_TIMEOUT_MS = 6 * 60 * 6e4;
18652
18772
  var tail = (s, max = 8e3) => s.length > max ? s.slice(-max) : s;
18653
- function tailMarker(text) {
18654
- const zone = text.slice(-2e3);
18655
- let best = null;
18656
- for (const marker of [
18657
- SECRETS_MARKER,
18658
- HUMAN_MARKER,
18659
- COMPLETE_MARKER,
18660
- FAILED_MARKER
18661
- ]) {
18662
- const at = zone.lastIndexOf(marker);
18663
- if (at !== -1 && (!best || at > best.at)) best = { marker, at };
18664
- }
18665
- if (!best) return null;
18666
- const value2 = zone.slice(best.at + best.marker.length).trim();
18667
- return value2 ? { marker: best.marker, value: value2 } : null;
18668
- }
18669
18773
  function containedCwd(cwd, spaceDir) {
18670
18774
  if (!cwd) return null;
18671
18775
  const resolved = import_path9.default.resolve(cwd);
@@ -18673,46 +18777,90 @@ function containedCwd(cwd, spaceDir) {
18673
18777
  return resolved === root || resolved.startsWith(root + import_path9.default.sep) ? resolved : null;
18674
18778
  }
18675
18779
  async function authorizeAndRun(opts) {
18676
- const { command, hooks } = opts;
18677
- opts.status?.("waiting for the owner to authorize this command");
18678
- let secrets = null;
18679
- let authError = "";
18680
- while (!secrets) {
18681
- const auth = await hooks.requestAuth(command, authError);
18780
+ const { command, gate } = opts;
18781
+ const scope = opts.keys ?? opts.declared;
18782
+ opts.status?.(
18783
+ scope.length ? `waiting for the owner to authorize this command (${scope.join(", ")})` : "waiting for the owner to authorize this command (no secrets requested)"
18784
+ );
18785
+ let store = null;
18786
+ let error = "";
18787
+ while (!store) {
18788
+ const auth = await gate.request({
18789
+ command,
18790
+ keys: scope,
18791
+ origin: opts.origin,
18792
+ error
18793
+ });
18682
18794
  if ("denied" in auth) return { denied: true };
18683
18795
  try {
18684
- secrets = await secretsForInjection(
18796
+ store = await secretsForInjection(
18685
18797
  opts.configDir,
18686
18798
  opts.spaceDir,
18687
18799
  auth.password
18688
18800
  );
18689
18801
  } catch (err) {
18690
- authError = err instanceof Error ? err.message : String(err);
18691
- opts.status?.(`authorization failed: ${authError}`);
18802
+ error = err instanceof Error ? err.message : String(err);
18803
+ opts.status?.(`authorization failed: ${error}`);
18692
18804
  }
18693
18805
  }
18694
- const redact = redactor(Object.values(secrets));
18806
+ const injected = {};
18807
+ const missing = [];
18808
+ for (const key of scope) {
18809
+ if (key in store) injected[key] = store[key];
18810
+ else missing.push(key);
18811
+ }
18812
+ if (missing.length && opts.keys) {
18813
+ const note = `${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} not in the store, so the command sees ${missing.length === 1 ? "it" : "them"} empty.`;
18814
+ opts.status?.(note);
18815
+ opts.sendLog?.({ type: "system", content: note });
18816
+ }
18817
+ const redact = redactor(Object.values(store));
18818
+ const names = Object.keys(injected);
18695
18819
  opts.status?.("authorized, running");
18696
- hooks.sendLog({
18820
+ opts.sendLog?.({
18697
18821
  type: "system",
18698
- content: "Authorized. Running the command with secrets injected..."
18822
+ content: `Authorized. Running the command with ${names.length ? `${names.join(", ")} injected` : "no secrets injected"}...`
18699
18823
  });
18700
18824
  const res = await execLogin(command, {
18701
18825
  cwd: opts.cwd,
18702
- env: { ...process.env, ...spaceEnv(opts.spaceDir), ...secrets },
18826
+ env: { ...process.env, ...spaceEnv(opts.spaceDir), ...injected },
18703
18827
  timeout: COMMAND_TIMEOUT_MS,
18704
18828
  idleTimeout: COMMAND_IDLE_TIMEOUT_MS,
18705
18829
  maxBuffer: 32 * 1024 * 1024
18706
18830
  });
18707
18831
  const output = redact(res.output);
18708
- hooks.sendLog({ type: "tool", content: `$ ${command}
18832
+ opts.sendLog?.({ type: "tool", content: `$ ${command}
18709
18833
  ${tail(output)}` });
18710
18834
  return { code: res.code, output };
18711
18835
  }
18836
+
18837
+ // ../../shared/all/helpers/board-agent-core/host-run.ts
18838
+ var HUMAN_MARKER = "AWAITING_INPUT:";
18839
+ var SECRETS_MARKER = "NEED_SECRETS:";
18840
+ var COMPLETE_MARKER = "DEPLOY_COMPLETE:";
18841
+ var FAILED_MARKER = "DEPLOY_FAILED:";
18842
+ var MAX_MARKERLESS_TURNS = 3;
18843
+ var tail2 = (s, max = 8e3) => s.length > max ? s.slice(-max) : s;
18844
+ function tailMarker(text) {
18845
+ const zone = text.slice(-2e3);
18846
+ let best = null;
18847
+ for (const marker of [
18848
+ SECRETS_MARKER,
18849
+ HUMAN_MARKER,
18850
+ COMPLETE_MARKER,
18851
+ FAILED_MARKER
18852
+ ]) {
18853
+ const at = zone.lastIndexOf(marker);
18854
+ if (at !== -1 && (!best || at > best.at)) best = { marker, at };
18855
+ }
18856
+ if (!best) return null;
18857
+ const value2 = zone.slice(best.at + best.marker.length).trim();
18858
+ return value2 ? { marker: best.marker, value: value2 } : null;
18859
+ }
18712
18860
  async function startDeployRun(opts) {
18713
18861
  const { hooks } = opts;
18714
- const workspace = import_path9.default.join(opts.spaceDir, "workspace");
18715
- const configDir = import_path9.default.dirname(opts.spaceDir);
18862
+ const workspace = import_path10.default.join(opts.spaceDir, "workspace");
18863
+ const configDir = import_path10.default.dirname(opts.spaceDir);
18716
18864
  const status = await checkDeployStatus(opts);
18717
18865
  if (!status.hasSkill || !status.hasVariables || !status.hasChecker) {
18718
18866
  throw new Error(
@@ -18747,26 +18895,8 @@ async function startDeployRun(opts) {
18747
18895
  let sessionId = "";
18748
18896
  let markerless = 0;
18749
18897
  let todos = [];
18750
- const bridge = await startSecretsBridge({
18751
- spaceDir: opts.spaceDir,
18752
- handlers: {
18753
- run: ({ command, cwd, status: report }) => authorizeAndRun({
18754
- configDir,
18755
- spaceDir: opts.spaceDir,
18756
- // A skill legitimately cds into subdirectories, so the CLI's own cwd
18757
- // is the right place to run - but only while it is still inside the
18758
- // space. Anywhere else is a confused caller, not a step, and the
18759
- // workspace is the safe reading of what it meant.
18760
- cwd: containedCwd(cwd, opts.spaceDir) ?? workspace,
18761
- command,
18762
- hooks,
18763
- status: report
18764
- }),
18765
- // Names only. The contract on the default branch already declares them
18766
- // in public, so this reveals nothing the repo does not.
18767
- list: () => Promise.resolve(status.variables.map((v) => v.key))
18768
- }
18769
- });
18898
+ const declared = status.variables.map((v) => v.key);
18899
+ const deployToken = opts.gate.issueToken(DEPLOY_ORIGIN);
18770
18900
  try {
18771
18901
  for (; ; ) {
18772
18902
  if (hooks.isCancelled()) throw new Error("Cancelled by user.");
@@ -18784,7 +18914,10 @@ async function startDeployRun(opts) {
18784
18914
  includePartialMessages: true,
18785
18915
  resumeSessionId: sessionId || void 0,
18786
18916
  appendSystemPrompt: sessionId ? void 0 : contract,
18787
- env: spaceEnv(opts.spaceDir),
18917
+ env: {
18918
+ ...spaceEnv(opts.spaceDir),
18919
+ FACTIII_SECRETS_TOKEN: deployToken
18920
+ },
18788
18921
  callbacks: {
18789
18922
  initialTodos: todos,
18790
18923
  onLog: (entry) => hooks.sendLog(entry),
@@ -18808,14 +18941,22 @@ async function startDeployRun(opts) {
18808
18941
  spaceDir: opts.spaceDir,
18809
18942
  cwd: workspace,
18810
18943
  command,
18811
- hooks
18944
+ // The marker carries a command and nothing else, so a skill speaking
18945
+ // it cannot name its keys. null takes the declared contract - still
18946
+ // a narrowing from the whole store, and the most this protocol can
18947
+ // say. New skills name their keys and get less.
18948
+ keys: null,
18949
+ declared,
18950
+ origin: DEPLOY_ORIGIN,
18951
+ gate: opts.gate,
18952
+ sendLog: hooks.sendLog
18812
18953
  });
18813
18954
  if ("denied" in res) {
18814
18955
  prompt2 = "The owner declined to run that command. Adapt or stop and summarize where the release stands.";
18815
18956
  continue;
18816
18957
  }
18817
18958
  markerless = 0;
18818
- const clean = tail(res.output);
18959
+ const clean = tail2(res.output);
18819
18960
  prompt2 = `The authorized command finished (exit ${res.code}). Redacted output:
18820
18961
 
18821
18962
  ${clean}`;
@@ -18845,7 +18986,7 @@ ${clean}`;
18845
18986
  prompt2 = "Your turn ended with no protocol marker. If the release fully shipped, end your message with DEPLOY_COMPLETE: followed by your run report; if you are stopping without shipping, end with DEPLOY_FAILED: followed by the report. Otherwise CONTINUE executing the skill from where you left off. If you had background tasks running, they were killed when your turn ended - rerun them, and reap every background task before ending a turn.";
18846
18987
  }
18847
18988
  } finally {
18848
- await bridge.close().catch(() => void 0);
18989
+ opts.gate.revokeToken(deployToken);
18849
18990
  }
18850
18991
  }
18851
18992
 
@@ -18939,37 +19080,37 @@ async function getDriveRoot(accessToken) {
18939
19080
 
18940
19081
  // ../../shared/all/helpers/board-agent-core/space-core.ts
18941
19082
  var import_promises8 = require("fs/promises");
18942
- var import_path11 = __toESM(require("path"));
19083
+ var import_path12 = __toESM(require("path"));
18943
19084
 
18944
19085
  // ../../shared/all/helpers/board-agent-core/plugin.ts
18945
- var import_crypto3 = __toESM(require("crypto"));
19086
+ var import_crypto4 = __toESM(require("crypto"));
18946
19087
  var import_fs3 = require("fs");
18947
19088
  var import_promises7 = require("fs/promises");
18948
19089
  var import_os3 = __toESM(require("os"));
18949
- var import_path10 = __toESM(require("path"));
19090
+ var import_path11 = __toESM(require("path"));
18950
19091
  var MARKETPLACE_NAME = "factiii-runner";
18951
19092
  var PLUGIN_ID = `factiii@${MARKETPLACE_NAME}`;
18952
19093
  var SECRETS_BIN = "factiii-secrets";
18953
19094
  function candidates() {
18954
19095
  return [
18955
- import_path10.default.join(__dirname, "..", "plugin"),
18956
- import_path10.default.join(__dirname, "..", "..", "..", "..", "apps", "runner", "plugin"),
18957
- import_path10.default.join(process.cwd(), "apps", "runner", "plugin")
19096
+ import_path11.default.join(__dirname, "..", "plugin"),
19097
+ import_path11.default.join(__dirname, "..", "..", "..", "..", "apps", "runner", "plugin"),
19098
+ import_path11.default.join(process.cwd(), "apps", "runner", "plugin")
18958
19099
  ];
18959
19100
  }
18960
19101
  function pluginRoot() {
18961
19102
  for (const dir of candidates()) {
18962
- if ((0, import_fs3.existsSync)(import_path10.default.join(dir, ".claude-plugin", "marketplace.json"))) {
19103
+ if ((0, import_fs3.existsSync)(import_path11.default.join(dir, ".claude-plugin", "marketplace.json"))) {
18963
19104
  return dir;
18964
19105
  }
18965
19106
  }
18966
19107
  return null;
18967
19108
  }
18968
19109
  function renderedPluginRoot() {
18969
- return import_path10.default.join(import_os3.default.homedir(), ".factiii-runner", "plugin");
19110
+ return import_path11.default.join(import_os3.default.homedir(), ".factiii-runner", "plugin");
18970
19111
  }
18971
19112
  async function runnerVersion(source) {
18972
- const pkg = import_path10.default.join(source, "..", "package.json");
19113
+ const pkg = import_path11.default.join(source, "..", "package.json");
18973
19114
  const raw = await (0, import_promises7.readFile)(pkg, "utf8").catch(() => null);
18974
19115
  if (!raw) return FALLBACK_VERSION;
18975
19116
  try {
@@ -18984,16 +19125,16 @@ async function treeFiles(dir) {
18984
19125
  const entries = await (0, import_promises7.readdir)(dir, { withFileTypes: true });
18985
19126
  const files = [];
18986
19127
  for (const entry of entries) {
18987
- const full = import_path10.default.join(dir, entry.name);
19128
+ const full = import_path11.default.join(dir, entry.name);
18988
19129
  if (entry.isDirectory()) files.push(...await treeFiles(full));
18989
19130
  else if (entry.isFile()) files.push(full);
18990
19131
  }
18991
19132
  return files.sort();
18992
19133
  }
18993
19134
  async function fingerprint(source, url2, version = "") {
18994
- const hash = import_crypto3.default.createHash("sha256").update(url2).update(version);
19135
+ const hash = import_crypto4.default.createHash("sha256").update(url2).update(version);
18995
19136
  for (const file of await treeFiles(source)) {
18996
- hash.update(import_path10.default.relative(source, file));
19137
+ hash.update(import_path11.default.relative(source, file));
18997
19138
  hash.update(await (0, import_promises7.readFile)(file));
18998
19139
  }
18999
19140
  return hash.digest("hex");
@@ -19019,10 +19160,10 @@ async function renderPluginImpl(boardMcpUrl) {
19019
19160
  const want = await fingerprint(source, boardMcpUrl, version);
19020
19161
  const have = await (0, import_promises7.readFile)(stampPath(dest), "utf8").catch(() => null);
19021
19162
  if (have === want) return dest;
19022
- await (0, import_promises7.mkdir)(import_path10.default.dirname(dest), { recursive: true });
19163
+ await (0, import_promises7.mkdir)(import_path11.default.dirname(dest), { recursive: true });
19023
19164
  await (0, import_promises7.cp)(source, dest, { recursive: true, force: true });
19024
19165
  await (0, import_promises7.writeFile)(
19025
- import_path10.default.join(dest, "factiii", ".mcp.json"),
19166
+ import_path11.default.join(dest, "factiii", ".mcp.json"),
19026
19167
  `${JSON.stringify(boardMcpConfig(boardMcpUrl), null, 2)}
19027
19168
  `
19028
19169
  );
@@ -19031,7 +19172,7 @@ async function renderPluginImpl(boardMcpUrl) {
19031
19172
  return dest;
19032
19173
  }
19033
19174
  async function stampVersion(dest, version) {
19034
- const manifest = import_path10.default.join(dest, "factiii", ".claude-plugin", "plugin.json");
19175
+ const manifest = import_path11.default.join(dest, "factiii", ".claude-plugin", "plugin.json");
19035
19176
  const raw = await (0, import_promises7.readFile)(manifest, "utf8").catch(() => null);
19036
19177
  if (!raw) return;
19037
19178
  const parsed = JSON.parse(raw);
@@ -19257,6 +19398,17 @@ var SpaceCore = class {
19257
19398
  // Positive-only cache per provider: true sticks until sign-out, false is
19258
19399
  // re-probed (an in-terminal login can flip it).
19259
19400
  this.loggedIn = {};
19401
+ /** The space's authorization gate, and the registry of which sessions may
19402
+ * ask at all (see secrets-gate.ts). Here rather than on the engine because
19403
+ * every session type has a SpaceCore and needs a token minted for it. */
19404
+ this.gate = new SecretsGate();
19405
+ /** The live deploy run's log sink, so a command it authorizes still lands on
19406
+ * the release timeline now that the bridge is not the run's own. Set by the
19407
+ * engine for the length of a run. */
19408
+ this.deployLog = null;
19409
+ this.secretsBridge = null;
19410
+ this.bridgeStarting = null;
19411
+ this.spaceSecretsToken = "";
19260
19412
  // ── Persistent per-space workspace: clone, worktrees, redis state ──
19261
19413
  // Just a directory, so a restart needs no reattach.
19262
19414
  this.execTarget = null;
@@ -19271,10 +19423,101 @@ var SpaceCore = class {
19271
19423
  this.mcp = options.mcp;
19272
19424
  this.requestSudo = options.requestSudo;
19273
19425
  this.activity = new ActivityTracker(this.emitter);
19426
+ this.gate.subscribe(() => {
19427
+ const pending = this.gate.pending();
19428
+ if (pending) {
19429
+ this.activity.set(
19430
+ "secrets-auth",
19431
+ "awaiting-input",
19432
+ "Needs your store password",
19433
+ `${pending.origin}: ${pending.command}`
19434
+ );
19435
+ } else {
19436
+ this.activity.clear("secrets-auth");
19437
+ }
19438
+ this.emitter.emitSecretsAuth({ pending: pending !== null });
19439
+ });
19274
19440
  }
19275
19441
  setLoggedIn(provider, value2) {
19276
19442
  this.loggedIn[provider] = value2;
19277
19443
  }
19444
+ /** Serve `factiii-secrets` for this space, once. Lazy rather than
19445
+ * constructor-time: the socket lives under `state`, which does not exist
19446
+ * until the workspace is provisioned. Provisioning and reattach both call
19447
+ * it, so the socket is up before anything can reach for it. */
19448
+ async ensureSecretsBridge() {
19449
+ if (this.secretsBridge) return;
19450
+ this.bridgeStarting ??= this.startSecretsBridgeOnce().finally(() => {
19451
+ this.bridgeStarting = null;
19452
+ });
19453
+ return this.bridgeStarting;
19454
+ }
19455
+ async startSecretsBridgeOnce() {
19456
+ const spaceDir = this.spaceDir();
19457
+ this.secretsBridge = await startSecretsBridge({
19458
+ spaceDir,
19459
+ handlers: {
19460
+ run: async ({ command, cwd, keys, token, status }) => {
19461
+ const origin = token ? this.gate.originFor(token) : null;
19462
+ if (!origin) {
19463
+ status(UNATTRIBUTED);
19464
+ return { code: 3, output: "" };
19465
+ }
19466
+ return authorizeAndRun({
19467
+ configDir: import_path12.default.dirname(spaceDir),
19468
+ spaceDir,
19469
+ // A step legitimately cds into subdirectories, so the caller's own
19470
+ // cwd is right - but only inside the space. Anywhere else is a
19471
+ // confused caller, and the workspace is the safe reading.
19472
+ cwd: containedCwd(cwd, spaceDir) ?? this.target().paths.workspace,
19473
+ command,
19474
+ keys,
19475
+ // Only the legacy marker needs a declared fallback, and that never
19476
+ // arrives here - a bridge caller always names its own keys.
19477
+ declared: [],
19478
+ origin,
19479
+ gate: this.gate,
19480
+ // A deploy-origin command belongs on the release timeline; every
19481
+ // other origin has no log of its own and reads the CLI's stderr.
19482
+ sendLog: this.deployLog ?? void 0
19483
+ });
19484
+ },
19485
+ list: () => this.secretsKeyInfo()
19486
+ }
19487
+ });
19488
+ }
19489
+ /** Key NAMES and status, no password. The contract declares its keys in
19490
+ * public on the default branch, and sops leaves names in cleartext, so this
19491
+ * reveals nothing the repo and the file do not already say. */
19492
+ async secretsKeyInfo() {
19493
+ const stored = new Set(await storedSecretKeys(this.spaceDir()));
19494
+ const config = this.readConfig();
19495
+ const declared = new Set(
19496
+ config.repoUrl && config.githubToken ? await checkDeployStatus({
19497
+ spaceDir: this.spaceDir(),
19498
+ repoUrl: config.repoUrl,
19499
+ mainBranch: config.mainBranch,
19500
+ githubToken: config.githubToken
19501
+ }).then((status) => status.variables.map((v) => v.key)).catch(() => []) : []
19502
+ );
19503
+ return [.../* @__PURE__ */ new Set([...declared, ...stored])].sort().map((key) => ({
19504
+ key,
19505
+ stored: stored.has(key),
19506
+ declared: declared.has(key)
19507
+ }));
19508
+ }
19509
+ /**
19510
+ * The token every session this space spawns carries, so `factiii-secrets`
19511
+ * inside one is attributable and everything else is refused.
19512
+ *
19513
+ * One per space, not per session: `extraEnv` is the only place every spawn
19514
+ * passes through and it cannot tell which session it is building for. A run
19515
+ * that DOES know itself - the deploy run - mints its own and overrides this.
19516
+ */
19517
+ secretsToken() {
19518
+ this.spaceSecretsToken ||= this.gate.issueToken("A session on this board");
19519
+ return this.spaceSecretsToken;
19520
+ }
19278
19521
  readConfig() {
19279
19522
  return this.configProvider.readConfig(this.spaceSlug);
19280
19523
  }
@@ -19286,6 +19529,7 @@ var SpaceCore = class {
19286
19529
  for (const lane of this.mcpLanes()) env[lane.tokenEnv] = lane.mcp.token;
19287
19530
  const board = this.boardMcp();
19288
19531
  if (board) env[BOARD_MCP_TOKEN_ENV] = board.token;
19532
+ env.FACTIII_SECRETS_TOKEN = this.secretsToken();
19289
19533
  return env;
19290
19534
  });
19291
19535
  return this.execTarget;
@@ -19461,6 +19705,7 @@ var SpaceCore = class {
19461
19705
  `mkdir -p ${paths.root} ${paths.worktrees} ${paths.state} ${paths.backups} ${paths.builds} ${paths.bin}`
19462
19706
  );
19463
19707
  await allocateSpacePorts(this.spaceDir());
19708
+ await this.ensureSecretsBridge().catch(() => void 0);
19464
19709
  await ensureRedis(target);
19465
19710
  await Promise.all([
19466
19711
  this.provisionGitLane(status),
@@ -19557,14 +19802,14 @@ var SpaceCore = class {
19557
19802
  const source = pluginRoot();
19558
19803
  if (!source) return;
19559
19804
  const { bin } = this.target().paths;
19560
- const from = import_path11.default.join(source, "factiii", "bin", SECRETS_BIN);
19561
- const to = import_path11.default.join(bin, SECRETS_BIN);
19805
+ const from = import_path12.default.join(source, "factiii", "bin", SECRETS_BIN);
19806
+ const to = import_path12.default.join(bin, SECRETS_BIN);
19562
19807
  try {
19563
19808
  const [src, staged] = await Promise.all([
19564
- (0, import_promises8.stat)(from),
19565
- (0, import_promises8.stat)(to).catch(() => null)
19809
+ (0, import_promises8.readFile)(from),
19810
+ (0, import_promises8.readFile)(to).catch(() => null)
19566
19811
  ]);
19567
- if (staged?.size === src.size) return;
19812
+ if (staged?.equals(src)) return;
19568
19813
  await (0, import_promises8.mkdir)(bin, { recursive: true });
19569
19814
  await (0, import_promises8.copyFile)(from, to);
19570
19815
  await (0, import_promises8.chmod)(to, 493);
@@ -19623,10 +19868,12 @@ var SpaceCore = class {
19623
19868
  } catch {
19624
19869
  return false;
19625
19870
  }
19871
+ await this.ensureSecretsBridge().catch(() => void 0);
19626
19872
  await ensureRedis(target).catch(() => {
19627
19873
  });
19628
19874
  await pruneWorktrees(target).catch(() => {
19629
19875
  });
19876
+ await Promise.all([this.provisionPluginLane(), this.provisionBinLane()]);
19630
19877
  await this.provisionMcpLane();
19631
19878
  this.ready = true;
19632
19879
  return true;
@@ -19651,6 +19898,9 @@ var SpaceCore = class {
19651
19898
  destroy() {
19652
19899
  this.ready = false;
19653
19900
  this.setupPromise = null;
19901
+ this.gate.destroy();
19902
+ void this.secretsBridge?.close();
19903
+ this.secretsBridge = null;
19654
19904
  this.activity.destroy();
19655
19905
  }
19656
19906
  };
@@ -19701,6 +19951,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19701
19951
  logs: [],
19702
19952
  question: "",
19703
19953
  pendingCommand: "",
19954
+ pendingKeys: [],
19704
19955
  error: "",
19705
19956
  summary: "",
19706
19957
  startedAt: 0,
@@ -19722,6 +19973,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19722
19973
  this.emitter = options.emitter;
19723
19974
  this.claude = new ClaudeModeEngine(this.core, this.emitter);
19724
19975
  this.terminal = new TerminalModeEngine(this.core, this.emitter);
19976
+ this.gate.subscribe(() => this.projectGateOntoRun());
19725
19977
  }
19726
19978
  /** Everything the runner does for this space that isn't a card session, so
19727
19979
  * the Running jobs panel can report the whole machine. Owned by the core,
@@ -19729,6 +19981,39 @@ var BoardAgentEngine = class _BoardAgentEngine {
19729
19981
  get activity() {
19730
19982
  return this.core.activity;
19731
19983
  }
19984
+ /** The space's gate (space-core.ts). Reached through here because most of
19985
+ * what uses it - the bridge, the deploy run - lives on the engine. */
19986
+ get gate() {
19987
+ return this.core.gate;
19988
+ }
19989
+ /** Mirror the gate onto the deploy run's phase, for clients older than the
19990
+ * global modal - they know a password is needed only from `awaiting-auth`. */
19991
+ projectGateOntoRun() {
19992
+ const pending = this.gate.pending();
19993
+ const runLive = this.runState.phase === "running" || this.runState.phase === "awaiting-auth";
19994
+ if (runLive && pending?.origin === DEPLOY_ORIGIN) {
19995
+ this.setRun({
19996
+ phase: "awaiting-auth",
19997
+ pendingCommand: pending.command,
19998
+ pendingKeys: pending.keys,
19999
+ error: pending.error
20000
+ });
20001
+ } else if (this.runState.phase === "awaiting-auth") {
20002
+ this.setRun({ phase: "running", pendingCommand: "", pendingKeys: [] });
20003
+ }
20004
+ }
20005
+ // ── Secrets authorization (see secrets-gate.ts + .specs/deploy.md) ──
20006
+ /** What the global authorization modal renders. Null when nothing is
20007
+ * waiting, which is also how an answered prompt tells clients to close. */
20008
+ secretsAuthState() {
20009
+ return { pending: this.gate.pending() };
20010
+ }
20011
+ secretsAuthAnswer(payload) {
20012
+ return this.gate.answer(
20013
+ payload.id,
20014
+ payload.password === void 0 ? { denied: true } : { password: payload.password }
20015
+ );
20016
+ }
19732
20017
  /** How this host protects stored credentials. Read-only: the daemon answers
19733
20018
  * unlock and password changes itself (SECURE_ACTIONS in daemon.ts). */
19734
20019
  secureStatus() {
@@ -19939,7 +20224,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19939
20224
  * sit on `awaiting-input` for hours, which is exactly the state worth
19940
20225
  * seeing without opening the modal. */
19941
20226
  syncRunActivity() {
19942
- const { phase, question, pendingCommand, error } = this.runState;
20227
+ const { phase, question, pendingCommand, pendingKeys, error } = this.runState;
19943
20228
  switch (phase) {
19944
20229
  case "running":
19945
20230
  return this.activity.set("deploy-run", "working", "Deploying");
@@ -19955,7 +20240,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19955
20240
  "deploy-run",
19956
20241
  "awaiting-input",
19957
20242
  "Deploy needs authorization",
19958
- pendingCommand
20243
+ pendingKeys.length ? `${pendingCommand} (${pendingKeys.join(", ")})` : pendingCommand
19959
20244
  );
19960
20245
  case "failed":
19961
20246
  return this.activity.set("deploy-run", "error", "Deploy failed", error);
@@ -19977,6 +20262,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
19977
20262
  logs: [],
19978
20263
  question: "",
19979
20264
  pendingCommand: "",
20265
+ pendingKeys: [],
19980
20266
  error: "",
19981
20267
  summary: "",
19982
20268
  startedAt: Date.now(),
@@ -20001,6 +20287,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
20001
20287
  }
20002
20288
  sendLog(entry);
20003
20289
  };
20290
+ this.core.deployLog = bufferLog;
20004
20291
  void startDeployRun({
20005
20292
  spaceDir: opts.spaceDir,
20006
20293
  repoUrl: opts.repoUrl,
@@ -20008,6 +20295,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
20008
20295
  githubToken: opts.githubToken,
20009
20296
  provider: opts.provider,
20010
20297
  releaseBranch,
20298
+ gate: this.gate,
20011
20299
  hooks: {
20012
20300
  sendLog: bufferLog,
20013
20301
  onTodos: (todos) => this.setRun({ todos }),
@@ -20015,19 +20303,13 @@ var BoardAgentEngine = class _BoardAgentEngine {
20015
20303
  this.runResolvers.human = resolve2;
20016
20304
  this.setRun({ phase: "awaiting-input", question });
20017
20305
  }),
20018
- requestAuth: (command, errorNote) => new Promise((resolve2) => {
20019
- this.runResolvers.auth = resolve2;
20020
- this.setRun({
20021
- phase: "awaiting-auth",
20022
- pendingCommand: command,
20023
- error: errorNote ?? ""
20024
- });
20025
- }),
20026
20306
  isCancelled: () => this.runCancelled,
20027
20307
  registerProc: (proc) => {
20028
20308
  this.runProc = proc;
20029
20309
  }
20030
20310
  }
20311
+ }).finally(() => {
20312
+ this.core.deployLog = null;
20031
20313
  }).then(({ shipped, summary }) => {
20032
20314
  if (shipped) {
20033
20315
  this.setRun({ phase: "success", summary, finishedAt: Date.now() });
@@ -20060,31 +20342,28 @@ var BoardAgentEngine = class _BoardAgentEngine {
20060
20342
  }
20061
20343
  // The password arrives RUNNER-DIRECT here and only feeds decryption; it
20062
20344
  // never enters the agent session (see .specs/deploy.md).
20345
+ //
20346
+ // Both predate the global modal, when a deploy run was the only thing that
20347
+ // could ask, and are kept so a client too old to send `secretsAuthAnswer`
20348
+ // still works.
20063
20349
  deployRunAuthorize(payload) {
20064
- const resolve2 = this.runResolvers.auth;
20065
- if (!resolve2 || this.runState.phase !== "awaiting-auth") {
20066
- throw new Error("No command is awaiting authorization.");
20067
- }
20068
- this.runResolvers.auth = void 0;
20069
- this.setRun({ phase: "running", pendingCommand: "", error: "" });
20070
- resolve2({ password: payload.password });
20071
- return { success: true };
20350
+ return this.answerPendingAuth({ password: payload.password });
20072
20351
  }
20073
20352
  deployRunDeny() {
20074
- const resolve2 = this.runResolvers.auth;
20075
- if (!resolve2 || this.runState.phase !== "awaiting-auth") {
20076
- throw new Error("No command is awaiting authorization.");
20353
+ return this.answerPendingAuth({ denied: true });
20354
+ }
20355
+ answerPendingAuth(result) {
20356
+ const pending = this.gate.pending();
20357
+ if (pending?.origin !== DEPLOY_ORIGIN) {
20358
+ throw new Error("No deploy command is awaiting authorization.");
20077
20359
  }
20078
- this.runResolvers.auth = void 0;
20079
- this.setRun({ phase: "running", pendingCommand: "" });
20080
- resolve2({ denied: true });
20081
- return { success: true };
20360
+ return this.gate.answer(pending.id, result);
20082
20361
  }
20083
20362
  deployRunCancel() {
20084
20363
  this.runCancelled = true;
20085
20364
  if (this.runProc) killTree(this.runProc, "SIGTERM");
20086
20365
  this.runResolvers.human?.("cancel");
20087
- this.runResolvers.auth?.({ denied: true });
20366
+ if (this.gate.pending()?.origin === DEPLOY_ORIGIN) this.gate.cancel();
20088
20367
  this.runResolvers = {};
20089
20368
  return { success: true };
20090
20369
  }
@@ -20101,6 +20380,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
20101
20380
  logs: [],
20102
20381
  question: "",
20103
20382
  pendingCommand: "",
20383
+ pendingKeys: [],
20104
20384
  error: "",
20105
20385
  summary: "",
20106
20386
  startedAt: 0,
@@ -20248,7 +20528,7 @@ var BoardAgentEngine = class _BoardAgentEngine {
20248
20528
  // ── Runner secrets store (secrets.sh contract; see host-secrets.ts) ──
20249
20529
  secretsDirs() {
20250
20530
  const spaceDir = this.core.spaceDir();
20251
- return { configDir: (0, import_path12.dirname)(spaceDir), spaceDir };
20531
+ return { configDir: (0, import_path13.dirname)(spaceDir), spaceDir };
20252
20532
  }
20253
20533
  async deploySecretsStatus() {
20254
20534
  return {
@@ -21132,6 +21412,12 @@ ${c.content.trim() || "(no description)"}`
21132
21412
  return this.deployRunDismiss();
21133
21413
  case "deployApproveSkill":
21134
21414
  return await this.deployApproveSkill();
21415
+ case "secretsAuthState":
21416
+ return this.secretsAuthState();
21417
+ case "secretsAuthAnswer":
21418
+ return this.secretsAuthAnswer(
21419
+ payload
21420
+ );
21135
21421
  case "deploySecretsStatus":
21136
21422
  return await this.deploySecretsStatus();
21137
21423
  case "deploySecretsInit":
@@ -21312,11 +21598,11 @@ ${c.content.trim() || "(no description)"}`
21312
21598
 
21313
21599
  // ../../shared/all/helpers/board-agent-core/index-service.ts
21314
21600
  var import_child_process5 = require("child_process");
21315
- var import_crypto4 = require("crypto");
21601
+ var import_crypto5 = require("crypto");
21316
21602
  var import_fs4 = require("fs");
21317
21603
  var import_promises9 = __toESM(require("fs/promises"));
21318
21604
  var import_net3 = require("net");
21319
- var import_path13 = require("path");
21605
+ var import_path14 = require("path");
21320
21606
  var import_util7 = require("util");
21321
21607
  var execFileAsync4 = (0, import_util7.promisify)(import_child_process5.execFile);
21322
21608
  var IndexService = class {
@@ -21330,7 +21616,7 @@ var IndexService = class {
21330
21616
  /** Per-space bearer for MCP + file, derived from the control token (the
21331
21617
  * service computes the same). */
21332
21618
  bearerToken(spaceSlug) {
21333
- return (0, import_crypto4.createHmac)("sha256", this.opts.controlToken).update(`mcp:${spaceSlug}`).digest("hex");
21619
+ return (0, import_crypto5.createHmac)("sha256", this.opts.controlToken).update(`mcp:${spaceSlug}`).digest("hex");
21334
21620
  }
21335
21621
  /** MCP bridge for a space. Null until the service is up (which it kicks
21336
21622
  * off), so a bad :0 URL is never baked in; provisionMcpLane re-runs. */
@@ -21375,7 +21661,7 @@ var IndexService = class {
21375
21661
  const log = await this.openLog();
21376
21662
  this.proc = (0, import_child_process5.spawn)(
21377
21663
  process.execPath,
21378
- [(0, import_path13.join)(this.opts.indexDir, "index.js")],
21664
+ [(0, import_path14.join)(this.opts.indexDir, "index.js")],
21379
21665
  {
21380
21666
  cwd: this.opts.indexDir,
21381
21667
  env: {
@@ -21404,7 +21690,7 @@ var IndexService = class {
21404
21690
  async openLog() {
21405
21691
  try {
21406
21692
  await import_promises9.default.mkdir(this.opts.dataDir, { recursive: true });
21407
- return (0, import_fs4.openSync)((0, import_path13.join)(this.opts.dataDir, "index.log"), "a");
21693
+ return (0, import_fs4.openSync)((0, import_path14.join)(this.opts.dataDir, "index.log"), "a");
21408
21694
  } catch {
21409
21695
  return null;
21410
21696
  }
@@ -21413,10 +21699,10 @@ var IndexService = class {
21413
21699
  * platform needing node-gyp fails here rather than at query time. */
21414
21700
  async installDeps() {
21415
21701
  const dir = this.opts.indexDir;
21416
- if ((0, import_fs4.existsSync)((0, import_path13.join)(dir, "node_modules"))) return;
21702
+ if ((0, import_fs4.existsSync)((0, import_path14.join)(dir, "node_modules"))) return;
21417
21703
  await import_promises9.default.copyFile(
21418
- (0, import_path13.join)(dir, "image-package.json"),
21419
- (0, import_path13.join)(dir, "package.json")
21704
+ (0, import_path14.join)(dir, "image-package.json"),
21705
+ (0, import_path14.join)(dir, "package.json")
21420
21706
  );
21421
21707
  await execFileAsync4(
21422
21708
  "npm",
@@ -21428,7 +21714,7 @@ var IndexService = class {
21428
21714
  * CLIs' persistent MCP config, so a fresh port each start would leave every
21429
21715
  * registration pointing at a dead one. */
21430
21716
  async stablePort() {
21431
- const file = (0, import_path13.join)(this.opts.dataDir, ".port");
21717
+ const file = (0, import_path14.join)(this.opts.dataDir, ".port");
21432
21718
  const saved = Number(await import_promises9.default.readFile(file, "utf-8").catch(() => ""));
21433
21719
  if (Number.isInteger(saved) && saved > 0 && await isFree(saved)) {
21434
21720
  return saved;
@@ -21605,22 +21891,22 @@ async function pairWithBrowser(serverUrl) {
21605
21891
  // src/config.ts
21606
21892
  var import_fs8 = __toESM(require("fs"));
21607
21893
  var import_os6 = __toESM(require("os"));
21608
- var import_path18 = __toESM(require("path"));
21894
+ var import_path19 = __toESM(require("path"));
21609
21895
 
21610
21896
  // src/secureFile.ts
21611
- var import_crypto7 = __toESM(require("crypto"));
21897
+ var import_crypto8 = __toESM(require("crypto"));
21612
21898
  var import_fs7 = __toESM(require("fs"));
21613
- var import_path17 = __toESM(require("path"));
21899
+ var import_path18 = __toESM(require("path"));
21614
21900
 
21615
21901
  // src/keychain.ts
21616
21902
  var import_child_process7 = require("child_process");
21617
- var import_crypto5 = __toESM(require("crypto"));
21903
+ var import_crypto6 = __toESM(require("crypto"));
21618
21904
  var import_fs5 = __toESM(require("fs"));
21619
21905
  var import_os4 = __toESM(require("os"));
21620
- var import_path14 = __toESM(require("path"));
21906
+ var import_path15 = __toESM(require("path"));
21621
21907
  var SERVICE = "factiii-runner";
21622
21908
  var ACCOUNT = "config-encryption-key";
21623
- var DPAPI_KEY_FILE = import_path14.default.join(
21909
+ var DPAPI_KEY_FILE = import_path15.default.join(
21624
21910
  import_os4.default.homedir(),
21625
21911
  ".factiii-runner",
21626
21912
  "config-key.dpapi"
@@ -21726,7 +22012,7 @@ function winWrite(key) {
21726
22012
  "-Command",
21727
22013
  `ConvertTo-SecureString -String '${key.toString("base64")}' -AsPlainText -Force | ConvertFrom-SecureString`
21728
22014
  ]);
21729
- import_fs5.default.mkdirSync(import_path14.default.dirname(DPAPI_KEY_FILE), { recursive: true });
22015
+ import_fs5.default.mkdirSync(import_path15.default.dirname(DPAPI_KEY_FILE), { recursive: true });
21730
22016
  import_fs5.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
21731
22017
  `, { mode: 384 });
21732
22018
  }
@@ -21866,7 +22152,7 @@ function resolve() {
21866
22152
  cached = { key: null, status: diagnose() };
21867
22153
  return cached;
21868
22154
  }
21869
- const fresh = import_crypto5.default.randomBytes(32);
22155
+ const fresh = import_crypto6.default.randomBytes(32);
21870
22156
  writeKey(fresh);
21871
22157
  const verify = readKey();
21872
22158
  if (verify.state !== "found" || !verify.key.equals(fresh)) {
@@ -21888,23 +22174,23 @@ function keychainStatus() {
21888
22174
  }
21889
22175
 
21890
22176
  // src/secureStore.ts
21891
- var import_crypto6 = __toESM(require("crypto"));
22177
+ var import_crypto7 = __toESM(require("crypto"));
21892
22178
  var import_fs6 = __toESM(require("fs"));
21893
- var import_path16 = __toESM(require("path"));
22179
+ var import_path17 = __toESM(require("path"));
21894
22180
 
21895
22181
  // src/paths.ts
21896
22182
  var import_os5 = __toESM(require("os"));
21897
- var import_path15 = __toESM(require("path"));
21898
- var CONFIG_DIR = import_path15.default.join(import_os5.default.homedir(), ".factiii-runner");
22183
+ var import_path16 = __toESM(require("path"));
22184
+ var CONFIG_DIR = import_path16.default.join(import_os5.default.homedir(), ".factiii-runner");
21899
22185
  function safeSlug(spaceSlug) {
21900
22186
  return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
21901
22187
  }
21902
22188
  function spaceDirPath(spaceSlug) {
21903
- return import_path15.default.join(CONFIG_DIR, safeSlug(spaceSlug));
22189
+ return import_path16.default.join(CONFIG_DIR, safeSlug(spaceSlug));
21904
22190
  }
21905
22191
 
21906
22192
  // src/secureStore.ts
21907
- var VAULT_KEY_FILE = import_path16.default.join(CONFIG_DIR, "vault-key.json");
22193
+ var VAULT_KEY_FILE = import_path17.default.join(CONFIG_DIR, "vault-key.json");
21908
22194
  var SCRYPT_N2 = 1 << 15;
21909
22195
  var MIN_PASSWORD_LENGTH = 8;
21910
22196
  var SecureStoreError = class extends Error {
@@ -21931,7 +22217,7 @@ function requireKeychainKey() {
21931
22217
  );
21932
22218
  }
21933
22219
  function passwordKey(password, salt) {
21934
- return import_crypto6.default.scryptSync(password, salt, 32, {
22220
+ return import_crypto7.default.scryptSync(password, salt, 32, {
21935
22221
  N: SCRYPT_N2,
21936
22222
  maxmem: 64 * 1024 * 1024
21937
22223
  });
@@ -21939,7 +22225,7 @@ function passwordKey(password, salt) {
21939
22225
  function wrapKey(machineKey, pwKey) {
21940
22226
  const ikm = pwKey ? Buffer.concat([machineKey, pwKey]) : machineKey;
21941
22227
  return Buffer.from(
21942
- import_crypto6.default.hkdfSync(
22228
+ import_crypto7.default.hkdfSync(
21943
22229
  "sha256",
21944
22230
  ikm,
21945
22231
  Buffer.alloc(0),
@@ -21949,8 +22235,8 @@ function wrapKey(machineKey, pwKey) {
21949
22235
  );
21950
22236
  }
21951
22237
  function seal(plaintext, key) {
21952
- const iv = import_crypto6.default.randomBytes(12);
21953
- const cipher = import_crypto6.default.createCipheriv("aes-256-gcm", key, iv);
22238
+ const iv = import_crypto7.default.randomBytes(12);
22239
+ const cipher = import_crypto7.default.createCipheriv("aes-256-gcm", key, iv);
21954
22240
  const data = Buffer.concat([cipher.update(plaintext), cipher.final()]);
21955
22241
  return {
21956
22242
  iv: iv.toString("base64"),
@@ -21960,7 +22246,7 @@ function seal(plaintext, key) {
21960
22246
  }
21961
22247
  function unseal(box, key) {
21962
22248
  try {
21963
- const decipher = import_crypto6.default.createDecipheriv(
22249
+ const decipher = import_crypto7.default.createDecipheriv(
21964
22250
  "aes-256-gcm",
21965
22251
  key,
21966
22252
  Buffer.from(box.iv, "base64")
@@ -21993,7 +22279,7 @@ function writeVaultFile(file) {
21993
22279
  }
21994
22280
  var unlockedDek = null;
21995
22281
  function createVault(machineKey) {
21996
- const dek = import_crypto6.default.randomBytes(32);
22282
+ const dek = import_crypto7.default.randomBytes(32);
21997
22283
  writeVaultFile({
21998
22284
  v: 1,
21999
22285
  passwordEnabled: false,
@@ -22088,7 +22374,7 @@ function enablePassword(password) {
22088
22374
  );
22089
22375
  }
22090
22376
  const dek = dataKey();
22091
- const salt = import_crypto6.default.randomBytes(16);
22377
+ const salt = import_crypto7.default.randomBytes(16);
22092
22378
  writeVaultFile({
22093
22379
  v: 1,
22094
22380
  passwordEnabled: true,
@@ -22116,7 +22402,7 @@ function changePassword(current, next) {
22116
22402
  "The current password is not correct."
22117
22403
  );
22118
22404
  }
22119
- const salt = import_crypto6.default.randomBytes(16);
22405
+ const salt = import_crypto7.default.randomBytes(16);
22120
22406
  writeVaultFile({
22121
22407
  v: 1,
22122
22408
  passwordEnabled: true,
@@ -22193,8 +22479,8 @@ function storeStatus() {
22193
22479
  // src/secureFile.ts
22194
22480
  var MAGIC = "FACTIII1";
22195
22481
  function encrypt(plain, key, version) {
22196
- const iv = import_crypto7.default.randomBytes(12);
22197
- const cipher = import_crypto7.default.createCipheriv("aes-256-gcm", key, iv);
22482
+ const iv = import_crypto8.default.randomBytes(12);
22483
+ const cipher = import_crypto8.default.createCipheriv("aes-256-gcm", key, iv);
22198
22484
  const data = Buffer.concat([cipher.update(plain), cipher.final()]);
22199
22485
  const envelope = {
22200
22486
  v: version,
@@ -22205,7 +22491,7 @@ function encrypt(plain, key, version) {
22205
22491
  return `${MAGIC}${JSON.stringify(envelope)}`;
22206
22492
  }
22207
22493
  function decrypt(envelope, key) {
22208
- const decipher = import_crypto7.default.createDecipheriv(
22494
+ const decipher = import_crypto8.default.createDecipheriv(
22209
22495
  "aes-256-gcm",
22210
22496
  key,
22211
22497
  Buffer.from(envelope.iv, "base64")
@@ -22226,7 +22512,7 @@ function parseEnvelope(raw) {
22226
22512
  }
22227
22513
  }
22228
22514
  function atomicWrite(filePath, body) {
22229
- import_fs7.default.mkdirSync(import_path17.default.dirname(filePath), { recursive: true });
22515
+ import_fs7.default.mkdirSync(import_path18.default.dirname(filePath), { recursive: true });
22230
22516
  const tmp = `${filePath}.${process.pid}.tmp`;
22231
22517
  import_fs7.default.writeFileSync(tmp, body, { mode: 384 });
22232
22518
  import_fs7.default.renameSync(tmp, filePath);
@@ -22341,8 +22627,8 @@ function writeBootJson(filePath, value2) {
22341
22627
  }
22342
22628
 
22343
22629
  // src/config.ts
22344
- var CONFIG_DIR2 = import_path18.default.join(import_os6.default.homedir(), ".factiii-runner");
22345
- var CONFIG_FILE = import_path18.default.join(CONFIG_DIR2, "config.json");
22630
+ var CONFIG_DIR2 = import_path19.default.join(import_os6.default.homedir(), ".factiii-runner");
22631
+ var CONFIG_FILE = import_path19.default.join(CONFIG_DIR2, "config.json");
22346
22632
  function readRunnerConfig() {
22347
22633
  return readBootJson(CONFIG_FILE);
22348
22634
  }
@@ -22511,10 +22797,10 @@ function makeChunkReassembler() {
22511
22797
  }
22512
22798
 
22513
22799
  // src/daemon.ts
22514
- var import_crypto8 = require("crypto");
22800
+ var import_crypto9 = require("crypto");
22515
22801
  var import_fs10 = __toESM(require("fs"));
22516
22802
  var import_node_datachannel = require("node-datachannel");
22517
- var import_path20 = __toESM(require("path"));
22803
+ var import_path21 = __toESM(require("path"));
22518
22804
 
22519
22805
  // ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
22520
22806
  var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
@@ -23694,12 +23980,12 @@ function parse2(str) {
23694
23980
  uri.queryKey = queryKey(uri, uri["query"]);
23695
23981
  return uri;
23696
23982
  }
23697
- function pathNames(obj, path20) {
23698
- const regx = /\/{2,9}/g, names = path20.replace(regx, "/").split("/");
23699
- if (path20.slice(0, 1) == "/" || path20.length === 0) {
23983
+ function pathNames(obj, path21) {
23984
+ const regx = /\/{2,9}/g, names = path21.replace(regx, "/").split("/");
23985
+ if (path21.slice(0, 1) == "/" || path21.length === 0) {
23700
23986
  names.splice(0, 1);
23701
23987
  }
23702
- if (path20.slice(-1) == "/") {
23988
+ if (path21.slice(-1) == "/") {
23703
23989
  names.splice(names.length - 1, 1);
23704
23990
  }
23705
23991
  return names;
@@ -24318,7 +24604,7 @@ var protocol2 = Socket.protocol;
24318
24604
  // ../../node_modules/socket.io-client/build/esm-debug/url.js
24319
24605
  var import_debug7 = __toESM(require_src(), 1);
24320
24606
  var debug7 = (0, import_debug7.default)("socket.io-client:url");
24321
- function url(uri, path20 = "", loc) {
24607
+ function url(uri, path21 = "", loc) {
24322
24608
  let obj = uri;
24323
24609
  loc = loc || typeof location !== "undefined" && location;
24324
24610
  if (null == uri)
@@ -24352,7 +24638,7 @@ function url(uri, path20 = "", loc) {
24352
24638
  obj.path = obj.path || "/";
24353
24639
  const ipv6 = obj.host.indexOf(":") !== -1;
24354
24640
  const host = ipv6 ? "[" + obj.host + "]" : obj.host;
24355
- obj.id = obj.protocol + "://" + host + ":" + obj.port + path20;
24641
+ obj.id = obj.protocol + "://" + host + ":" + obj.port + path21;
24356
24642
  obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
24357
24643
  return obj;
24358
24644
  }
@@ -25986,8 +26272,8 @@ function lookup(uri, opts) {
25986
26272
  const parsed = url(uri, opts.path || "/socket.io");
25987
26273
  const source = parsed.source;
25988
26274
  const id = parsed.id;
25989
- const path20 = parsed.path;
25990
- const sameNamespace = cache[id] && path20 in cache[id]["nsps"];
26275
+ const path21 = parsed.path;
26276
+ const sameNamespace = cache[id] && path21 in cache[id]["nsps"];
25991
26277
  const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
25992
26278
  let io;
25993
26279
  if (newConnection) {
@@ -26029,8 +26315,8 @@ function cardContextPath(target, postId) {
26029
26315
  const safe = postId.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80) || "card";
26030
26316
  return `${target.paths.root}/.cards/${safe}.md`;
26031
26317
  }
26032
- function cardContextPrompt(path20) {
26033
- return `Read ${path20} - it is the Factiii card this session was opened from. Use it as the context and start working on it.`;
26318
+ function cardContextPrompt(path21) {
26319
+ return `Read ${path21} - it is the Factiii card this session was opened from. Use it as the context and start working on it.`;
26034
26320
  }
26035
26321
  var CLAUDE_PERMISSION_MODE = `--permission-mode auto`;
26036
26322
  var RESUME_SEED = "You are now in an interactive terminal, not the board UI. Respond in plain text and use your normal tools; do not use openui-lang or component schemas anymore.";
@@ -26195,10 +26481,10 @@ var TerminalManager = class {
26195
26481
 
26196
26482
  // src/agent-adapter.ts
26197
26483
  var import_fs9 = __toESM(require("fs"));
26198
- var import_path19 = __toESM(require("path"));
26199
- var RUNNER_CONFIG_PATH = import_path19.default.join(CONFIG_DIR, "runner-config.json");
26484
+ var import_path20 = __toESM(require("path"));
26485
+ var RUNNER_CONFIG_PATH = import_path20.default.join(CONFIG_DIR, "runner-config.json");
26200
26486
  function connectionPath(spaceSlug) {
26201
- return import_path19.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
26487
+ return import_path20.default.join(CONFIG_DIR, `space-${safeSlug(spaceSlug)}.onedrive.json`);
26202
26488
  }
26203
26489
  function writeJson(filePath, value2) {
26204
26490
  writeSecureJson(filePath, value2);
@@ -26232,7 +26518,7 @@ function migrateLegacyConfigs() {
26232
26518
  for (const name of legacy) {
26233
26519
  const slug = name.slice("space-".length, -".json".length);
26234
26520
  const parsed = readSecureJson(
26235
- import_path19.default.join(CONFIG_DIR, name)
26521
+ import_path20.default.join(CONFIG_DIR, name)
26236
26522
  );
26237
26523
  if (!parsed) continue;
26238
26524
  if (!file.defaultSlug) {
@@ -26257,7 +26543,7 @@ function encryptStrayPlaintextConfigs() {
26257
26543
  return;
26258
26544
  }
26259
26545
  for (const name of names) {
26260
- const filePath = import_path19.default.join(CONFIG_DIR, name);
26546
+ const filePath = import_path20.default.join(CONFIG_DIR, name);
26261
26547
  try {
26262
26548
  const raw = import_fs9.default.readFileSync(filePath, "utf-8");
26263
26549
  if (!raw.startsWith("{")) continue;
@@ -26386,7 +26672,7 @@ var localConfigProvider = {
26386
26672
  "claude",
26387
26673
  "codex"
26388
26674
  ]) {
26389
- import_fs9.default.mkdirSync(import_path19.default.join(dir, sub), { recursive: true });
26675
+ import_fs9.default.mkdirSync(import_path20.default.join(dir, sub), { recursive: true });
26390
26676
  }
26391
26677
  return dir;
26392
26678
  }
@@ -26560,15 +26846,15 @@ async function startDaemon(config) {
26560
26846
  }
26561
26847
  );
26562
26848
  const indexCandidates = [
26563
- import_path20.default.join(__dirname, "..", "index"),
26564
- import_path20.default.join(__dirname, "..", "..", "index", "image")
26849
+ import_path21.default.join(__dirname, "..", "index"),
26850
+ import_path21.default.join(__dirname, "..", "..", "index", "image")
26565
26851
  ];
26566
26852
  const indexDir = indexCandidates.find((p) => import_fs10.default.existsSync(p)) ?? indexCandidates[0];
26567
26853
  const odIndex = new IndexService({
26568
- controlToken: (0, import_crypto8.createHash)("sha256").update(config.authToken).digest("hex"),
26854
+ controlToken: (0, import_crypto9.createHash)("sha256").update(config.authToken).digest("hex"),
26569
26855
  embedUrl: () => config.serverUrl,
26570
26856
  indexDir,
26571
- dataDir: import_path20.default.join(CONFIG_DIR, "index-data")
26857
+ dataDir: import_path21.default.join(CONFIG_DIR, "index-data")
26572
26858
  });
26573
26859
  void odIndex.ensureRunning().catch((err) => {
26574
26860
  console.error(
@@ -27182,11 +27468,27 @@ ${err.fix}`);
27182
27468
  console.error("");
27183
27469
  process.exit(1);
27184
27470
  }
27471
+ var MAX_SUPPORTED_NODE_MAJOR = 24;
27472
+ function assertSupportedNode() {
27473
+ const major = Number(process.versions.node.split(".")[0]);
27474
+ if (Number.isNaN(major) || major <= MAX_SUPPORTED_NODE_MAJOR) return;
27475
+ console.error(
27476
+ `
27477
+ factiii-runner needs Node 20\u2013${MAX_SUPPORTED_NODE_MAJOR} (pinned 24.11.1); you are on Node ${process.versions.node}.
27478
+ The bundled OneDrive index compiles better-sqlite3 from source, and that build fails on Node 25+.
27479
+
27480
+ Switch with nvm: nvm install 24.11.1 && nvm use 24.11.1
27481
+ then relaunch: factiii-runner start
27482
+ `
27483
+ );
27484
+ process.exit(1);
27485
+ }
27185
27486
  import_node_dns.default.setDefaultResultOrder("ipv4first");
27186
27487
  var program2 = new Command();
27187
27488
  program2.name("factiii-runner").description("Factiii Runner - run Board AI agents on remote servers").version(currentVersion());
27188
27489
  program2.command("setup").description("Verify the host toolchain and pair this runner").option("-s, --server <url>", "API server URL", "https://api.factiii.com").action(async (opts) => {
27189
27490
  try {
27491
+ assertSupportedNode();
27190
27492
  await setup(opts.server);
27191
27493
  } catch (err) {
27192
27494
  console.error(
@@ -27215,6 +27517,7 @@ program2.command("connect").description("Pair this runner with a Factiii account
27215
27517
  }
27216
27518
  });
27217
27519
  program2.command("start").description("Start the runner daemon").action(async () => {
27520
+ assertSupportedNode();
27218
27521
  if (!configExists()) {
27219
27522
  console.error('No config found. Run "factiii-runner setup" first.');
27220
27523
  process.exit(1);
@@ -27260,6 +27563,9 @@ program2.command("status").description("Show current runner configuration").acti
27260
27563
  console.log(`Token: \xB7\xB7\xB7\xB7${config.authToken.slice(-4)}`);
27261
27564
  });
27262
27565
  program2.command("doctor").description("Show where each host tool resolves, for a failed environment").action(async () => {
27566
+ const nodeMajor = Number(process.versions.node.split(".")[0]);
27567
+ const nodeNote = nodeMajor > MAX_SUPPORTED_NODE_MAJOR ? " (UNSUPPORTED \u2014 use 24.11.1)" : "";
27568
+ console.log(`${"node".padEnd(14)} ${process.execPath} v${process.versions.node}${nodeNote}`);
27263
27569
  const report = await reportToolchain(hostTarget(CONFIG_DIR));
27264
27570
  for (const tool of report.tools) {
27265
27571
  console.log(`${tool.bin.padEnd(14)} ${tool.location || "NOT FOUND"}`);