@solongate/proxy 0.83.10 → 0.83.11

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.
@@ -15,7 +15,7 @@ import { resolve, join, dirname } from "path";
15
15
  import { homedir } from "os";
16
16
  import { fileURLToPath } from "url";
17
17
  import { createInterface } from "readline";
18
- import { execFileSync } from "child_process";
18
+ import { execFileSync, spawn } from "child_process";
19
19
  var __dirname = dirname(fileURLToPath(import.meta.url));
20
20
  var HOOKS_DIR = resolve(__dirname, "..", "hooks");
21
21
  function globalPaths() {
@@ -25,6 +25,7 @@ function globalPaths() {
25
25
  const claudeDir = join(home, ".claude");
26
26
  const antigravityDir = join(home, ".gemini", "config");
27
27
  const codexDir = process.env["CODEX_HOME"] ? resolve(process.env["CODEX_HOME"]) : join(home, ".codex");
28
+ const opencodeDir = join(process.env["XDG_CONFIG_HOME"] ? resolve(process.env["XDG_CONFIG_HOME"]) : join(home, ".config"), "opencode");
28
29
  return {
29
30
  home,
30
31
  sgDir,
@@ -32,6 +33,7 @@ function globalPaths() {
32
33
  claudeDir,
33
34
  antigravityDir,
34
35
  codexDir,
36
+ opencodeDir,
35
37
  settingsPath: join(claudeDir, "settings.json"),
36
38
  backupPath: join(claudeDir, "settings.solongate.bak"),
37
39
  configPath: join(sgDir, "cloud-guard.json"),
@@ -45,7 +47,14 @@ function globalPaths() {
45
47
  // the user's TOML, which also holds the hook TRUST state Codex manages).
46
48
  codexHooksPath: join(codexDir, "hooks.json"),
47
49
  codexBackupPath: join(codexDir, "hooks.solongate.bak"),
48
- codexConfigPath: join(codexDir, "config.toml")
50
+ codexConfigPath: join(codexDir, "config.toml"),
51
+ // OpenCode scans its plugin folder at startup and loads every module in it.
52
+ // Measured on 1.18.10: BOTH `plugin/` and `plugins/` are scanned, so the
53
+ // docs and the field reports are each half right. We write the documented
54
+ // one. There is nothing to register anywhere — dropping the file IS the
55
+ // installation, which also means deleting the file IS the uninstall.
56
+ opencodePluginDir: join(opencodeDir, "plugins"),
57
+ opencodePluginPath: join(opencodeDir, "plugins", "solongate.js")
49
58
  };
50
59
  }
51
60
  function isOurCodexGroup(group) {
@@ -267,7 +276,7 @@ async function request(method, path, opts = {}) {
267
276
  }
268
277
 
269
278
  // src/api-client/device-login.ts
270
- import { spawn } from "child_process";
279
+ import { spawn as spawn2 } from "child_process";
271
280
 
272
281
  // src/api-client/auth.ts
273
282
  var auth_exports = {};
@@ -22,6 +22,7 @@ export declare function globalPaths(): {
22
22
  claudeDir: string;
23
23
  antigravityDir: string;
24
24
  codexDir: string;
25
+ opencodeDir: string;
25
26
  settingsPath: string;
26
27
  backupPath: string;
27
28
  configPath: string;
@@ -30,6 +31,8 @@ export declare function globalPaths(): {
30
31
  codexHooksPath: string;
31
32
  codexBackupPath: string;
32
33
  codexConfigPath: string;
34
+ opencodePluginDir: string;
35
+ opencodePluginPath: string;
33
36
  };
34
37
  export declare function clearGuardUpdateCheck(): boolean;
35
38
  /** Is our guard currently registered in Codex's hooks.json on THIS device? */
@@ -48,6 +51,10 @@ export declare function codexHooksStatus(): {
48
51
  trusted: boolean;
49
52
  disabled: boolean;
50
53
  };
54
+ export declare function isOpencodeGuardInstalled(): boolean;
55
+ /** True when OpenCode looks present on this device, so callers can decide
56
+ * whether the OpenCode line is worth showing at all. */
57
+ export declare function opencodeDetected(): boolean;
51
58
  export declare function runGlobalRestore(): void;
52
59
  /**
53
60
  * TUI-safe (re)install — the dataroom's one-key "install / update guard" action.
@@ -4,7 +4,7 @@ import { resolve, join, dirname } from "path";
4
4
  import { homedir } from "os";
5
5
  import { fileURLToPath } from "url";
6
6
  import { createInterface } from "readline";
7
- import { execFileSync } from "child_process";
7
+ import { execFileSync, spawn } from "child_process";
8
8
  var __dirname = dirname(fileURLToPath(import.meta.url));
9
9
  var HOOKS_DIR = resolve(__dirname, "..", "hooks");
10
10
  function lockFile(file) {
@@ -89,7 +89,11 @@ function protectedTargets() {
89
89
  p.configPath,
90
90
  p.settingsPath,
91
91
  p.antigravityHooksPath,
92
- p.codexHooksPath
92
+ p.codexHooksPath,
93
+ // The OpenCode plugin IS the registration — delete it and the guard is gone
94
+ // from that client with nothing left to notice the absence, so it belongs
95
+ // under the same OS lock as every other protection file.
96
+ p.opencodePluginPath
93
97
  ];
94
98
  }
95
99
  function lockProtected() {
@@ -121,6 +125,7 @@ function globalPaths() {
121
125
  const claudeDir = join(home, ".claude");
122
126
  const antigravityDir = join(home, ".gemini", "config");
123
127
  const codexDir = process.env["CODEX_HOME"] ? resolve(process.env["CODEX_HOME"]) : join(home, ".codex");
128
+ const opencodeDir = join(process.env["XDG_CONFIG_HOME"] ? resolve(process.env["XDG_CONFIG_HOME"]) : join(home, ".config"), "opencode");
124
129
  return {
125
130
  home,
126
131
  sgDir,
@@ -128,6 +133,7 @@ function globalPaths() {
128
133
  claudeDir,
129
134
  antigravityDir,
130
135
  codexDir,
136
+ opencodeDir,
131
137
  settingsPath: join(claudeDir, "settings.json"),
132
138
  backupPath: join(claudeDir, "settings.solongate.bak"),
133
139
  configPath: join(sgDir, "cloud-guard.json"),
@@ -141,7 +147,14 @@ function globalPaths() {
141
147
  // the user's TOML, which also holds the hook TRUST state Codex manages).
142
148
  codexHooksPath: join(codexDir, "hooks.json"),
143
149
  codexBackupPath: join(codexDir, "hooks.solongate.bak"),
144
- codexConfigPath: join(codexDir, "config.toml")
150
+ codexConfigPath: join(codexDir, "config.toml"),
151
+ // OpenCode scans its plugin folder at startup and loads every module in it.
152
+ // Measured on 1.18.10: BOTH `plugin/` and `plugins/` are scanned, so the
153
+ // docs and the field reports are each half right. We write the documented
154
+ // one. There is nothing to register anywhere — dropping the file IS the
155
+ // installation, which also means deleting the file IS the uninstall.
156
+ opencodePluginDir: join(opencodeDir, "plugins"),
157
+ opencodePluginPath: join(opencodeDir, "plugins", "solongate.js")
145
158
  };
146
159
  }
147
160
  function clearGuardUpdateCheck() {
@@ -341,6 +354,47 @@ function codexHooksStatus() {
341
354
  }
342
355
  return { registered, trusted, disabled };
343
356
  }
357
+ function installOpencodeGuard(p) {
358
+ mkdirSync(p.opencodePluginDir, { recursive: true });
359
+ const src = readHook("opencode-plugin.mjs").replace("__SOLONGATE_NODE__", process.execPath.replace(/\\/g, "\\\\"));
360
+ writeProtectedFile(p.opencodePluginPath, src);
361
+ }
362
+ function removeOpencodeGuard(p) {
363
+ try {
364
+ unlockFile(p.opencodePluginPath);
365
+ rmSync(p.opencodePluginPath, { force: true });
366
+ } catch {
367
+ }
368
+ }
369
+ function warmPolicyCache(hooksDir, agents) {
370
+ for (const agent of agents) {
371
+ try {
372
+ const child = spawn(process.execPath, [join(hooksDir, "guard.mjs"), agent, "--sg-refresh-policy"], {
373
+ detached: true,
374
+ stdio: "ignore",
375
+ windowsHide: true
376
+ });
377
+ child.on("error", () => {
378
+ });
379
+ child.unref();
380
+ } catch {
381
+ }
382
+ }
383
+ }
384
+ function isOpencodeGuardInstalled() {
385
+ try {
386
+ return readFileSync(globalPaths().opencodePluginPath, "utf-8").includes("tool.execute.before");
387
+ } catch {
388
+ return false;
389
+ }
390
+ }
391
+ function opencodeDetected() {
392
+ try {
393
+ return existsSync(globalPaths().opencodeDir);
394
+ } catch {
395
+ return false;
396
+ }
397
+ }
344
398
  function ask(question) {
345
399
  const rl = createInterface({ input: process.stdin, output: process.stderr });
346
400
  return new Promise((res) => rl.question(question, (a) => {
@@ -357,6 +411,10 @@ function runGlobalRestore() {
357
411
  removeCodexGuard(p);
358
412
  } catch {
359
413
  }
414
+ try {
415
+ removeOpencodeGuard(p);
416
+ } catch {
417
+ }
360
418
  if (existsSync(p.backupPath)) {
361
419
  writeFileSync(p.settingsPath, readFileSync(p.backupPath, "utf-8"));
362
420
  console.log(` Restored ${p.settingsPath} from backup.`);
@@ -383,7 +441,8 @@ function repairQuiet() {
383
441
  line("cloud credential", has(p.configPath), "present", "MISSING"),
384
442
  line("Claude Code settings", isGuardInstalled(), "guard registered", "guard NOT registered"),
385
443
  line("Antigravity hooks", has(p.antigravityHooksPath), "present", "MISSING"),
386
- line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered")
444
+ line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered"),
445
+ line("OpenCode plugin", isOpencodeGuardInstalled(), "installed", "MISSING")
387
446
  ];
388
447
  const r = installGlobalQuiet();
389
448
  if (!r.ok) return { ok: false, message: r.message, before, after: [], notes: [] };
@@ -391,7 +450,8 @@ function repairQuiet() {
391
450
  { label: "guard hook file", ok: true, detail: `present (v${installedGuardVersion() ?? "?"})` },
392
451
  line("Claude Code settings", isGuardInstalled(), "guard registered", "NOT registered"),
393
452
  line("Antigravity hooks", has(p.antigravityHooksPath), "present", "MISSING"),
394
- line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered")
453
+ line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered"),
454
+ line("OpenCode plugin", isOpencodeGuardInstalled(), "installed", "MISSING")
395
455
  ];
396
456
  const notes = [];
397
457
  const cx = codexHooksStatus();
@@ -494,6 +554,11 @@ function installGlobalQuiet() {
494
554
  installCodexGuard(p, p.hooksDir);
495
555
  } catch {
496
556
  }
557
+ try {
558
+ installOpencodeGuard(p);
559
+ } catch {
560
+ }
561
+ warmPolicyCache(p.hooksDir, ["claude-code", "codex", "antigravity", "opencode"]);
497
562
  if (process.env["SOLONGATE_NO_OS_LOCK"] !== "1") lockProtected();
498
563
  return { ok: true, message: "guard installed (open a new session)" };
499
564
  } catch (e) {
@@ -541,6 +606,10 @@ function uninstallGlobalQuiet() {
541
606
  removeCodexGuard(p);
542
607
  } catch {
543
608
  }
609
+ try {
610
+ removeOpencodeGuard(p);
611
+ } catch {
612
+ }
544
613
  if (!existsSync(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
545
614
  const s = JSON.parse(readFileSync(p.settingsPath, "utf-8"));
546
615
  delete s.hooks;
@@ -703,6 +772,13 @@ async function runGlobalInstall(opts = {}) {
703
772
  }
704
773
  } catch {
705
774
  }
775
+ try {
776
+ installOpencodeGuard(p);
777
+ console.log(` Installed OpenCode plugin \u2192 ${p.opencodePluginPath}`);
778
+ console.log(" OpenCode: `opencode --pure` runs without external plugins and");
779
+ console.log(" therefore without the guard. That is its own escape hatch.");
780
+ } catch {
781
+ }
706
782
  if (process.env["SOLONGATE_NO_OS_LOCK"] !== "1") {
707
783
  lockProtected();
708
784
  console.log(" Locked protection files (OS-level read-only/immutable).");
@@ -723,7 +799,9 @@ export {
723
799
  installedGuardVersion,
724
800
  isCodexGuardInstalled,
725
801
  isGuardInstalled,
802
+ isOpencodeGuardInstalled,
726
803
  lockProtected,
804
+ opencodeDetected,
727
805
  removeClaudeShim,
728
806
  repairQuiet,
729
807
  runGlobalInstall,
package/dist/index.js CHANGED
@@ -6201,7 +6201,9 @@ __export(global_install_exports, {
6201
6201
  installedGuardVersion: () => installedGuardVersion,
6202
6202
  isCodexGuardInstalled: () => isCodexGuardInstalled,
6203
6203
  isGuardInstalled: () => isGuardInstalled,
6204
+ isOpencodeGuardInstalled: () => isOpencodeGuardInstalled,
6204
6205
  lockProtected: () => lockProtected,
6206
+ opencodeDetected: () => opencodeDetected,
6205
6207
  removeClaudeShim: () => removeClaudeShim,
6206
6208
  repairQuiet: () => repairQuiet,
6207
6209
  runGlobalInstall: () => runGlobalInstall,
@@ -6216,7 +6218,7 @@ import { resolve as resolve3, join as join4, dirname } from "path";
6216
6218
  import { homedir as homedir2 } from "os";
6217
6219
  import { fileURLToPath } from "url";
6218
6220
  import { createInterface } from "readline";
6219
- import { execFileSync as execFileSync2 } from "child_process";
6221
+ import { execFileSync as execFileSync2, spawn } from "child_process";
6220
6222
  function lockFile(file) {
6221
6223
  if (!existsSync3(file)) return;
6222
6224
  try {
@@ -6299,7 +6301,11 @@ function protectedTargets() {
6299
6301
  p.configPath,
6300
6302
  p.settingsPath,
6301
6303
  p.antigravityHooksPath,
6302
- p.codexHooksPath
6304
+ p.codexHooksPath,
6305
+ // The OpenCode plugin IS the registration — delete it and the guard is gone
6306
+ // from that client with nothing left to notice the absence, so it belongs
6307
+ // under the same OS lock as every other protection file.
6308
+ p.opencodePluginPath
6303
6309
  ];
6304
6310
  }
6305
6311
  function lockProtected() {
@@ -6331,6 +6337,7 @@ function globalPaths() {
6331
6337
  const claudeDir = join4(home, ".claude");
6332
6338
  const antigravityDir = join4(home, ".gemini", "config");
6333
6339
  const codexDir = process.env["CODEX_HOME"] ? resolve3(process.env["CODEX_HOME"]) : join4(home, ".codex");
6340
+ const opencodeDir = join4(process.env["XDG_CONFIG_HOME"] ? resolve3(process.env["XDG_CONFIG_HOME"]) : join4(home, ".config"), "opencode");
6334
6341
  return {
6335
6342
  home,
6336
6343
  sgDir,
@@ -6338,6 +6345,7 @@ function globalPaths() {
6338
6345
  claudeDir,
6339
6346
  antigravityDir,
6340
6347
  codexDir,
6348
+ opencodeDir,
6341
6349
  settingsPath: join4(claudeDir, "settings.json"),
6342
6350
  backupPath: join4(claudeDir, "settings.solongate.bak"),
6343
6351
  configPath: join4(sgDir, "cloud-guard.json"),
@@ -6351,7 +6359,14 @@ function globalPaths() {
6351
6359
  // the user's TOML, which also holds the hook TRUST state Codex manages).
6352
6360
  codexHooksPath: join4(codexDir, "hooks.json"),
6353
6361
  codexBackupPath: join4(codexDir, "hooks.solongate.bak"),
6354
- codexConfigPath: join4(codexDir, "config.toml")
6362
+ codexConfigPath: join4(codexDir, "config.toml"),
6363
+ // OpenCode scans its plugin folder at startup and loads every module in it.
6364
+ // Measured on 1.18.10: BOTH `plugin/` and `plugins/` are scanned, so the
6365
+ // docs and the field reports are each half right. We write the documented
6366
+ // one. There is nothing to register anywhere — dropping the file IS the
6367
+ // installation, which also means deleting the file IS the uninstall.
6368
+ opencodePluginDir: join4(opencodeDir, "plugins"),
6369
+ opencodePluginPath: join4(opencodeDir, "plugins", "solongate.js")
6355
6370
  };
6356
6371
  }
6357
6372
  function clearGuardUpdateCheck() {
@@ -6548,6 +6563,47 @@ function codexHooksStatus() {
6548
6563
  }
6549
6564
  return { registered, trusted, disabled };
6550
6565
  }
6566
+ function installOpencodeGuard(p) {
6567
+ mkdirSync3(p.opencodePluginDir, { recursive: true });
6568
+ const src = readHook("opencode-plugin.mjs").replace("__SOLONGATE_NODE__", process.execPath.replace(/\\/g, "\\\\"));
6569
+ writeProtectedFile(p.opencodePluginPath, src);
6570
+ }
6571
+ function removeOpencodeGuard(p) {
6572
+ try {
6573
+ unlockFile(p.opencodePluginPath);
6574
+ rmSync2(p.opencodePluginPath, { force: true });
6575
+ } catch {
6576
+ }
6577
+ }
6578
+ function warmPolicyCache(hooksDir, agents) {
6579
+ for (const agent of agents) {
6580
+ try {
6581
+ const child = spawn(process.execPath, [join4(hooksDir, "guard.mjs"), agent, "--sg-refresh-policy"], {
6582
+ detached: true,
6583
+ stdio: "ignore",
6584
+ windowsHide: true
6585
+ });
6586
+ child.on("error", () => {
6587
+ });
6588
+ child.unref();
6589
+ } catch {
6590
+ }
6591
+ }
6592
+ }
6593
+ function isOpencodeGuardInstalled() {
6594
+ try {
6595
+ return readFileSync4(globalPaths().opencodePluginPath, "utf-8").includes("tool.execute.before");
6596
+ } catch {
6597
+ return false;
6598
+ }
6599
+ }
6600
+ function opencodeDetected() {
6601
+ try {
6602
+ return existsSync3(globalPaths().opencodeDir);
6603
+ } catch {
6604
+ return false;
6605
+ }
6606
+ }
6551
6607
  function ask(question) {
6552
6608
  const rl = createInterface({ input: process.stdin, output: process.stderr });
6553
6609
  return new Promise((res) => rl.question(question, (a) => {
@@ -6564,6 +6620,10 @@ function runGlobalRestore() {
6564
6620
  removeCodexGuard(p);
6565
6621
  } catch {
6566
6622
  }
6623
+ try {
6624
+ removeOpencodeGuard(p);
6625
+ } catch {
6626
+ }
6567
6627
  if (existsSync3(p.backupPath)) {
6568
6628
  writeFileSync3(p.settingsPath, readFileSync4(p.backupPath, "utf-8"));
6569
6629
  console.log(` Restored ${p.settingsPath} from backup.`);
@@ -6590,7 +6650,8 @@ function repairQuiet() {
6590
6650
  line("cloud credential", has(p.configPath), "present", "MISSING"),
6591
6651
  line("Claude Code settings", isGuardInstalled(), "guard registered", "guard NOT registered"),
6592
6652
  line("Antigravity hooks", has(p.antigravityHooksPath), "present", "MISSING"),
6593
- line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered")
6653
+ line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered"),
6654
+ line("OpenCode plugin", isOpencodeGuardInstalled(), "installed", "MISSING")
6594
6655
  ];
6595
6656
  const r = installGlobalQuiet();
6596
6657
  if (!r.ok) return { ok: false, message: r.message, before, after: [], notes: [] };
@@ -6598,7 +6659,8 @@ function repairQuiet() {
6598
6659
  { label: "guard hook file", ok: true, detail: `present (v${installedGuardVersion() ?? "?"})` },
6599
6660
  line("Claude Code settings", isGuardInstalled(), "guard registered", "NOT registered"),
6600
6661
  line("Antigravity hooks", has(p.antigravityHooksPath), "present", "MISSING"),
6601
- line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered")
6662
+ line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered"),
6663
+ line("OpenCode plugin", isOpencodeGuardInstalled(), "installed", "MISSING")
6602
6664
  ];
6603
6665
  const notes = [];
6604
6666
  const cx = codexHooksStatus();
@@ -6701,6 +6763,11 @@ function installGlobalQuiet() {
6701
6763
  installCodexGuard(p, p.hooksDir);
6702
6764
  } catch {
6703
6765
  }
6766
+ try {
6767
+ installOpencodeGuard(p);
6768
+ } catch {
6769
+ }
6770
+ warmPolicyCache(p.hooksDir, ["claude-code", "codex", "antigravity", "opencode"]);
6704
6771
  if (process.env["SOLONGATE_NO_OS_LOCK"] !== "1") lockProtected();
6705
6772
  return { ok: true, message: "guard installed (open a new session)" };
6706
6773
  } catch (e) {
@@ -6748,6 +6815,10 @@ function uninstallGlobalQuiet() {
6748
6815
  removeCodexGuard(p);
6749
6816
  } catch {
6750
6817
  }
6818
+ try {
6819
+ removeOpencodeGuard(p);
6820
+ } catch {
6821
+ }
6751
6822
  if (!existsSync3(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
6752
6823
  const s = JSON.parse(readFileSync4(p.settingsPath, "utf-8"));
6753
6824
  delete s.hooks;
@@ -6908,6 +6979,13 @@ async function runGlobalInstall(opts = {}) {
6908
6979
  }
6909
6980
  } catch {
6910
6981
  }
6982
+ try {
6983
+ installOpencodeGuard(p);
6984
+ console.log(` Installed OpenCode plugin \u2192 ${p.opencodePluginPath}`);
6985
+ console.log(" OpenCode: `opencode --pure` runs without external plugins and");
6986
+ console.log(" therefore without the guard. That is its own escape hatch.");
6987
+ } catch {
6988
+ }
6911
6989
  if (process.env["SOLONGATE_NO_OS_LOCK"] !== "1") {
6912
6990
  lockProtected();
6913
6991
  console.log(" Locked protection files (OS-level read-only/immutable).");
@@ -6949,7 +7027,7 @@ __export(self_update_exports, {
6949
7027
  tuiUpdateFlow: () => tuiUpdateFlow,
6950
7028
  updateNow: () => updateNow
6951
7029
  });
6952
- import { execFile, execFileSync as execFileSync3, spawn } from "child_process";
7030
+ import { execFile, execFileSync as execFileSync3, spawn as spawn2 } from "child_process";
6953
7031
  import { access, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4, constants as FS } from "fs";
6954
7032
  import { homedir as homedir3 } from "os";
6955
7033
  import { dirname as dirname2, join as join5, sep } from "path";
@@ -7023,7 +7101,7 @@ function spawnGlobalInstall(version) {
7023
7101
  try {
7024
7102
  mkdirSync4(join5(homedir3(), ".solongate"), { recursive: true });
7025
7103
  const log3 = openSync(LOG_FILE, "a");
7026
- const p = spawn("npm", ["install", "-g", `${PKG}@${version}`], {
7104
+ const p = spawn2("npm", ["install", "-g", `${PKG}@${version}`], {
7027
7105
  stdio: ["ignore", log3, log3],
7028
7106
  detached: true,
7029
7107
  windowsHide: true,
@@ -7297,7 +7375,7 @@ __export(logs_server_daemon_exports, {
7297
7375
  startLogsServerDaemon: () => startLogsServerDaemon,
7298
7376
  stopLogsServerDaemon: () => stopLogsServerDaemon
7299
7377
  });
7300
- import { spawn as spawn2 } from "child_process";
7378
+ import { spawn as spawn3 } from "child_process";
7301
7379
  import { mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
7302
7380
  import { homedir as homedir4 } from "os";
7303
7381
  import { dirname as dirname3, join as join6 } from "path";
@@ -7344,7 +7422,7 @@ function startLogsServerDaemon() {
7344
7422
  mkdirSync5(DIR, { recursive: true });
7345
7423
  const log3 = openSync2(LOG_FILE2, "a");
7346
7424
  const cli = join6(dirname3(fileURLToPath3(import.meta.url)), "index.js");
7347
- const p = spawn2(process.execPath, [cli, "logs-server"], {
7425
+ const p = spawn3(process.execPath, [cli, "logs-server"], {
7348
7426
  detached: true,
7349
7427
  stdio: ["ignore", log3, log3],
7350
7428
  windowsHide: true,
@@ -7995,12 +8073,12 @@ var init_types = __esm({
7995
8073
  });
7996
8074
 
7997
8075
  // src/api-client/device-login.ts
7998
- import { spawn as spawn3 } from "child_process";
8076
+ import { spawn as spawn4 } from "child_process";
7999
8077
  function openBrowser(url) {
8000
8078
  try {
8001
8079
  const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
8002
8080
  const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
8003
- const child = spawn3(cmd, args, { stdio: "ignore", detached: true });
8081
+ const child = spawn4(cmd, args, { stdio: "ignore", detached: true });
8004
8082
  child.on("error", () => {
8005
8083
  });
8006
8084
  child.unref();
@@ -8353,24 +8431,24 @@ var init_api_client = __esm({
8353
8431
  });
8354
8432
 
8355
8433
  // src/tui/notify.ts
8356
- import { spawn as spawn4 } from "child_process";
8434
+ import { spawn as spawn5 } from "child_process";
8357
8435
  function desktopNotify(title, msg) {
8358
8436
  try {
8359
8437
  if (process.platform === "linux") {
8360
- const p = spawn4("notify-send", ["-a", "SolonGate", title, msg], { stdio: "ignore", detached: true });
8438
+ const p = spawn5("notify-send", ["-a", "SolonGate", title, msg], { stdio: "ignore", detached: true });
8361
8439
  p.on("error", () => {
8362
8440
  });
8363
8441
  p.unref();
8364
8442
  } else if (process.platform === "win32") {
8365
8443
  const q = (s) => s.replace(/'/g, "''");
8366
8444
  const ps = `Add-Type -AssemblyName System.Windows.Forms;Add-Type -AssemblyName System.Drawing;$n=New-Object System.Windows.Forms.NotifyIcon;$n.Icon=[System.Drawing.SystemIcons]::Information;$n.Visible=$true;$n.ShowBalloonTip(6000,'${q(title)}','${q(msg)}',[System.Windows.Forms.ToolTipIcon]::Warning);Start-Sleep -Milliseconds 6500;$n.Dispose()`;
8367
- const p = spawn4("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], { stdio: "ignore", detached: true, windowsHide: true });
8445
+ const p = spawn5("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], { stdio: "ignore", detached: true, windowsHide: true });
8368
8446
  p.on("error", () => {
8369
8447
  });
8370
8448
  p.unref();
8371
8449
  } else if (process.platform === "darwin") {
8372
8450
  const esc = (s) => s.replace(/"/g, '\\"');
8373
- const p = spawn4("osascript", ["-e", `display notification "${esc(msg)}" with title "${esc(title)}"`], { stdio: "ignore", detached: true });
8451
+ const p = spawn5("osascript", ["-e", `display notification "${esc(msg)}" with title "${esc(title)}"`], { stdio: "ignore", detached: true });
8374
8452
  p.on("error", () => {
8375
8453
  });
8376
8454
  p.unref();
package/dist/tui/index.js CHANGED
@@ -14,7 +14,7 @@ import { resolve, join as join3, dirname } from "path";
14
14
  import { homedir as homedir3 } from "os";
15
15
  import { fileURLToPath } from "url";
16
16
  import { createInterface } from "readline";
17
- import { execFileSync } from "child_process";
17
+ import { execFileSync, spawn } from "child_process";
18
18
  function lockFile(file) {
19
19
  if (!existsSync2(file)) return;
20
20
  try {
@@ -97,7 +97,11 @@ function protectedTargets() {
97
97
  p.configPath,
98
98
  p.settingsPath,
99
99
  p.antigravityHooksPath,
100
- p.codexHooksPath
100
+ p.codexHooksPath,
101
+ // The OpenCode plugin IS the registration — delete it and the guard is gone
102
+ // from that client with nothing left to notice the absence, so it belongs
103
+ // under the same OS lock as every other protection file.
104
+ p.opencodePluginPath
101
105
  ];
102
106
  }
103
107
  function lockProtected() {
@@ -129,6 +133,7 @@ function globalPaths() {
129
133
  const claudeDir = join3(home, ".claude");
130
134
  const antigravityDir = join3(home, ".gemini", "config");
131
135
  const codexDir = process.env["CODEX_HOME"] ? resolve(process.env["CODEX_HOME"]) : join3(home, ".codex");
136
+ const opencodeDir = join3(process.env["XDG_CONFIG_HOME"] ? resolve(process.env["XDG_CONFIG_HOME"]) : join3(home, ".config"), "opencode");
132
137
  return {
133
138
  home,
134
139
  sgDir,
@@ -136,6 +141,7 @@ function globalPaths() {
136
141
  claudeDir,
137
142
  antigravityDir,
138
143
  codexDir,
144
+ opencodeDir,
139
145
  settingsPath: join3(claudeDir, "settings.json"),
140
146
  backupPath: join3(claudeDir, "settings.solongate.bak"),
141
147
  configPath: join3(sgDir, "cloud-guard.json"),
@@ -149,7 +155,14 @@ function globalPaths() {
149
155
  // the user's TOML, which also holds the hook TRUST state Codex manages).
150
156
  codexHooksPath: join3(codexDir, "hooks.json"),
151
157
  codexBackupPath: join3(codexDir, "hooks.solongate.bak"),
152
- codexConfigPath: join3(codexDir, "config.toml")
158
+ codexConfigPath: join3(codexDir, "config.toml"),
159
+ // OpenCode scans its plugin folder at startup and loads every module in it.
160
+ // Measured on 1.18.10: BOTH `plugin/` and `plugins/` are scanned, so the
161
+ // docs and the field reports are each half right. We write the documented
162
+ // one. There is nothing to register anywhere — dropping the file IS the
163
+ // installation, which also means deleting the file IS the uninstall.
164
+ opencodePluginDir: join3(opencodeDir, "plugins"),
165
+ opencodePluginPath: join3(opencodeDir, "plugins", "solongate.js")
153
166
  };
154
167
  }
155
168
  function readHook(filename) {
@@ -338,6 +351,40 @@ function codexHooksStatus() {
338
351
  }
339
352
  return { registered, trusted, disabled };
340
353
  }
354
+ function installOpencodeGuard(p) {
355
+ mkdirSync(p.opencodePluginDir, { recursive: true });
356
+ const src = readHook("opencode-plugin.mjs").replace("__SOLONGATE_NODE__", process.execPath.replace(/\\/g, "\\\\"));
357
+ writeProtectedFile(p.opencodePluginPath, src);
358
+ }
359
+ function removeOpencodeGuard(p) {
360
+ try {
361
+ unlockFile(p.opencodePluginPath);
362
+ rmSync(p.opencodePluginPath, { force: true });
363
+ } catch {
364
+ }
365
+ }
366
+ function warmPolicyCache(hooksDir, agents) {
367
+ for (const agent of agents) {
368
+ try {
369
+ const child = spawn(process.execPath, [join3(hooksDir, "guard.mjs"), agent, "--sg-refresh-policy"], {
370
+ detached: true,
371
+ stdio: "ignore",
372
+ windowsHide: true
373
+ });
374
+ child.on("error", () => {
375
+ });
376
+ child.unref();
377
+ } catch {
378
+ }
379
+ }
380
+ }
381
+ function isOpencodeGuardInstalled() {
382
+ try {
383
+ return readFileSync3(globalPaths().opencodePluginPath, "utf-8").includes("tool.execute.before");
384
+ } catch {
385
+ return false;
386
+ }
387
+ }
341
388
  function repairQuiet() {
342
389
  const p = globalPaths();
343
390
  const has = (f) => existsSync2(f);
@@ -348,7 +395,8 @@ function repairQuiet() {
348
395
  line("cloud credential", has(p.configPath), "present", "MISSING"),
349
396
  line("Claude Code settings", isGuardInstalled(), "guard registered", "guard NOT registered"),
350
397
  line("Antigravity hooks", has(p.antigravityHooksPath), "present", "MISSING"),
351
- line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered")
398
+ line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered"),
399
+ line("OpenCode plugin", isOpencodeGuardInstalled(), "installed", "MISSING")
352
400
  ];
353
401
  const r = installGlobalQuiet();
354
402
  if (!r.ok) return { ok: false, message: r.message, before, after: [], notes: [] };
@@ -356,7 +404,8 @@ function repairQuiet() {
356
404
  { label: "guard hook file", ok: true, detail: `present (v${installedGuardVersion() ?? "?"})` },
357
405
  line("Claude Code settings", isGuardInstalled(), "guard registered", "NOT registered"),
358
406
  line("Antigravity hooks", has(p.antigravityHooksPath), "present", "MISSING"),
359
- line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered")
407
+ line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered"),
408
+ line("OpenCode plugin", isOpencodeGuardInstalled(), "installed", "MISSING")
360
409
  ];
361
410
  const notes = [];
362
411
  const cx = codexHooksStatus();
@@ -425,6 +474,11 @@ function installGlobalQuiet() {
425
474
  installCodexGuard(p, p.hooksDir);
426
475
  } catch {
427
476
  }
477
+ try {
478
+ installOpencodeGuard(p);
479
+ } catch {
480
+ }
481
+ warmPolicyCache(p.hooksDir, ["claude-code", "codex", "antigravity", "opencode"]);
428
482
  if (process.env["SOLONGATE_NO_OS_LOCK"] !== "1") lockProtected();
429
483
  return { ok: true, message: "guard installed (open a new session)" };
430
484
  } catch (e) {
@@ -472,6 +526,10 @@ function uninstallGlobalQuiet() {
472
526
  removeCodexGuard(p);
473
527
  } catch {
474
528
  }
529
+ try {
530
+ removeOpencodeGuard(p);
531
+ } catch {
532
+ }
475
533
  if (!existsSync2(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
476
534
  const s = JSON.parse(readFileSync3(p.settingsPath, "utf-8"));
477
535
  delete s.hooks;
@@ -1146,12 +1204,12 @@ async function request(method, path, opts = {}) {
1146
1204
  }
1147
1205
 
1148
1206
  // src/api-client/device-login.ts
1149
- import { spawn } from "child_process";
1207
+ import { spawn as spawn2 } from "child_process";
1150
1208
  function openBrowser(url) {
1151
1209
  try {
1152
1210
  const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
1153
1211
  const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
1154
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
1212
+ const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
1155
1213
  child.on("error", () => {
1156
1214
  });
1157
1215
  child.unref();
@@ -1433,24 +1491,24 @@ function list4() {
1433
1491
  var api = { auth: auth_exports, policies: policies_exports, settings: settings_exports, stats: stats_exports, audit: audit_exports, agents: agents_exports, keys: keys_exports, mcp: mcp_exports };
1434
1492
 
1435
1493
  // src/tui/notify.ts
1436
- import { spawn as spawn2 } from "child_process";
1494
+ import { spawn as spawn3 } from "child_process";
1437
1495
  function desktopNotify(title, msg) {
1438
1496
  try {
1439
1497
  if (process.platform === "linux") {
1440
- const p = spawn2("notify-send", ["-a", "SolonGate", title, msg], { stdio: "ignore", detached: true });
1498
+ const p = spawn3("notify-send", ["-a", "SolonGate", title, msg], { stdio: "ignore", detached: true });
1441
1499
  p.on("error", () => {
1442
1500
  });
1443
1501
  p.unref();
1444
1502
  } else if (process.platform === "win32") {
1445
1503
  const q = (s) => s.replace(/'/g, "''");
1446
1504
  const ps = `Add-Type -AssemblyName System.Windows.Forms;Add-Type -AssemblyName System.Drawing;$n=New-Object System.Windows.Forms.NotifyIcon;$n.Icon=[System.Drawing.SystemIcons]::Information;$n.Visible=$true;$n.ShowBalloonTip(6000,'${q(title)}','${q(msg)}',[System.Windows.Forms.ToolTipIcon]::Warning);Start-Sleep -Milliseconds 6500;$n.Dispose()`;
1447
- const p = spawn2("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], { stdio: "ignore", detached: true, windowsHide: true });
1505
+ const p = spawn3("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], { stdio: "ignore", detached: true, windowsHide: true });
1448
1506
  p.on("error", () => {
1449
1507
  });
1450
1508
  p.unref();
1451
1509
  } else if (process.platform === "darwin") {
1452
1510
  const esc = (s) => s.replace(/"/g, '\\"');
1453
- const p = spawn2("osascript", ["-e", `display notification "${esc(msg)}" with title "${esc(title)}"`], { stdio: "ignore", detached: true });
1511
+ const p = spawn3("osascript", ["-e", `display notification "${esc(msg)}" with title "${esc(title)}"`], { stdio: "ignore", detached: true });
1454
1512
  p.on("error", () => {
1455
1513
  });
1456
1514
  p.unref();
@@ -4446,7 +4504,7 @@ async function collectChecks() {
4446
4504
  }
4447
4505
 
4448
4506
  // src/logs-server-daemon.ts
4449
- import { spawn as spawn3 } from "child_process";
4507
+ import { spawn as spawn4 } from "child_process";
4450
4508
  import { mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
4451
4509
  import { homedir as homedir8 } from "os";
4452
4510
  import { dirname as dirname2, join as join8 } from "path";
@@ -4494,7 +4552,7 @@ function startLogsServerDaemon() {
4494
4552
  mkdirSync5(DIR, { recursive: true });
4495
4553
  const log = openSync2(LOG_FILE, "a");
4496
4554
  const cli = join8(dirname2(fileURLToPath2(import.meta.url)), "index.js");
4497
- const p = spawn3(process.execPath, [cli, "logs-server"], {
4555
+ const p = spawn4(process.execPath, [cli, "logs-server"], {
4498
4556
  detached: true,
4499
4557
  stdio: ["ignore", log, log],
4500
4558
  windowsHide: true,
@@ -4524,7 +4582,7 @@ function stopLogsServerDaemon() {
4524
4582
  }
4525
4583
 
4526
4584
  // src/self-update.ts
4527
- import { execFile, execFileSync as execFileSync2, spawn as spawn4 } from "child_process";
4585
+ import { execFile, execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
4528
4586
  import { access, mkdirSync as mkdirSync6, openSync as openSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync7, constants as FS } from "fs";
4529
4587
  import { homedir as homedir9 } from "os";
4530
4588
  import { dirname as dirname3, join as join9, sep } from "path";
@@ -6536,7 +6536,7 @@ import { resolve, join, dirname, isAbsolute } from "node:path";
6536
6536
  import { homedir } from "node:os";
6537
6537
  import { gunzipSync } from "node:zlib";
6538
6538
  import { createHash } from "node:crypto";
6539
- var HOOK_VERSION = 75;
6539
+ var HOOK_VERSION = 76;
6540
6540
  function localLogsOnly(security) {
6541
6541
  if (security !== void 0) {
6542
6542
  const l = security && security.localLogs;
@@ -6925,6 +6925,20 @@ var CLIENTS = {
6925
6925
  redactsOutput: false
6926
6926
  // has a post-tool stage, but it rejects output rewrites
6927
6927
  },
6928
+ // OpenCode: no subprocess hook contract at all — a plugin runs in-process and
6929
+ // refuses a call by throwing. The shim that does the throwing (see
6930
+ // hooks/opencode-plugin.mjs) spawns this guard with a flat Claude-shaped
6931
+ // payload and reads the Claude dialect back, so both halves are reused as-is
6932
+ // and only the identity differs.
6933
+ opencode: {
6934
+ parse: parseFlatPayload,
6935
+ emit: emitHookSpecific,
6936
+ // tool.execute.after does hand the plugin the tool's result, but whether
6937
+ // writing to it changes what the model sees is untested. Claiming the
6938
+ // capability we have not proven would let a secret through masked-in-name-
6939
+ // only; false makes any masking we cannot apply a block instead.
6940
+ redactsOutput: false
6941
+ },
6928
6942
  // Antigravity CLI: nested payload, and a decision dialect of its own.
6929
6943
  antigravity: {
6930
6944
  redactsOutput: false,
package/hooks/guard.mjs CHANGED
@@ -32,7 +32,7 @@ import { createHash } from 'node:crypto';
32
32
  // the installed hook self-updates when the cloud version is higher (see
33
33
  // maybeSelfUpdate). This is what makes guard fixes propagate without a manual
34
34
  // reinstall — the same trust model as the OPA WASM this hook already runs.
35
- const HOOK_VERSION = 75;
35
+ const HOOK_VERSION = 76;
36
36
 
37
37
  // True when local log storage is ON. In that mode logs are kept LOCAL ONLY and
38
38
  // nothing is sent to the cloud audit log — local and cloud are one or the
@@ -617,6 +617,21 @@ const CLIENTS = {
617
617
  redactsOutput: false, // has a post-tool stage, but it rejects output rewrites
618
618
  },
619
619
 
620
+ // OpenCode: no subprocess hook contract at all — a plugin runs in-process and
621
+ // refuses a call by throwing. The shim that does the throwing (see
622
+ // hooks/opencode-plugin.mjs) spawns this guard with a flat Claude-shaped
623
+ // payload and reads the Claude dialect back, so both halves are reused as-is
624
+ // and only the identity differs.
625
+ opencode: {
626
+ parse: parseFlatPayload,
627
+ emit: emitHookSpecific,
628
+ // tool.execute.after does hand the plugin the tool's result, but whether
629
+ // writing to it changes what the model sees is untested. Claiming the
630
+ // capability we have not proven would let a secret through masked-in-name-
631
+ // only; false makes any masking we cannot apply a block instead.
632
+ redactsOutput: false,
633
+ },
634
+
620
635
  // Antigravity CLI: nested payload, and a decision dialect of its own.
621
636
  antigravity: {
622
637
  redactsOutput: false, // no post-tool stage at all (its output is ignored)
@@ -0,0 +1,151 @@
1
+ /**
2
+ * SolonGate plugin for OpenCode.
3
+ *
4
+ * OpenCode is the odd one out among the guarded clients: it has no subprocess
5
+ * hook contract. Plugins are JavaScript modules Bun loads INTO the running
6
+ * process, and a tool call is refused by throwing from `tool.execute.before`.
7
+ *
8
+ * So this file is a shim, not an engine. It spawns the same guard every other
9
+ * client runs and turns its answer into the shape OpenCode wants — which keeps
10
+ * one implementation of policy, DLP, rate limiting, ghost paths and auditing
11
+ * instead of a second one that drifts.
12
+ *
13
+ * Measured against opencode 1.18.10, because none of this is documented:
14
+ * tool.execute.before(input, output)
15
+ * input = { tool, sessionID, callID }
16
+ * output = { args } ← mutable, so a rewrite lands here
17
+ * throwing blocks the call, and the Error message is handed to the model as
18
+ * the tool result (it reads it and reports it, so the reason is visible)
19
+ * tool.execute.after(input, output)
20
+ * input = { tool, sessionID, callID, args }
21
+ * output = { title, metadata, output, attachments }
22
+ *
23
+ * Verified on the same version: subagent calls spawned through the `task` tool
24
+ * DO reach this hook (in their own sessionID), and so do MCP tool calls (named
25
+ * `<server>_<tool>`). Both were once bypasses; both are closed.
26
+ *
27
+ * Not covered, and it cannot be: `opencode --pure` runs with external plugins
28
+ * disabled, which switches the guard off. That is the client's design and there
29
+ * is no hook that survives it.
30
+ */
31
+ import { spawn } from 'node:child_process';
32
+ import { existsSync } from 'node:fs';
33
+ import { homedir } from 'node:os';
34
+ import { join } from 'node:path';
35
+
36
+ const HOOKS_DIR = join(homedir(), '.solongate', 'hooks');
37
+ const GUARD = join(HOOKS_DIR, 'guard.mjs');
38
+ const AUDIT = join(HOOKS_DIR, 'audit.mjs');
39
+
40
+ // The node binary, written in by the installer.
41
+ //
42
+ // NOT process.execPath: OpenCode runs plugins inside its own Bun-based binary,
43
+ // so in here process.execPath is `.../opencode-ai/bin/opencode.exe`. Spawning
44
+ // THAT with the guard as an argument does not run the guard — it re-runs
45
+ // opencode with nonsense arguments, which exits non-2, which this shim reads as
46
+ // "allowed". The result was a guard that never guarded and never said so.
47
+ // Measured: execPath=/…/opencode-ai/bin/opencode.exe, versions.bun=1.3.14.
48
+ const NODE_BAKED = '__SOLONGATE_NODE__';
49
+ const NODE = NODE_BAKED.startsWith('__SOLONGATE') || !existsSync(NODE_BAKED) ? 'node' : NODE_BAKED;
50
+
51
+ // The guard carries its own 8s backstop; this is the outer bound for the whole
52
+ // round trip so a wedged process can never hang the editor.
53
+ const GUARD_TIMEOUT_MS = 15000;
54
+
55
+ /**
56
+ * Run a hook with a JSON payload on stdin. Resolves to {code, stdout, stderr},
57
+ * or null when the hook could not be run at all.
58
+ */
59
+ function runHook(script, payload, timeoutMs) {
60
+ return new Promise((resolve) => {
61
+ if (!existsSync(script)) return resolve(null);
62
+ let done = false;
63
+ const finish = (v) => { if (!done) { done = true; resolve(v); } };
64
+ let child;
65
+ try {
66
+ child = spawn(NODE, [script, 'opencode', 'OpenCode'], {
67
+ stdio: ['pipe', 'pipe', 'pipe'],
68
+ windowsHide: true,
69
+ });
70
+ } catch {
71
+ return finish(null);
72
+ }
73
+ let stdout = '', stderr = '';
74
+ child.stdout.on('data', (d) => { stdout += d; });
75
+ child.stderr.on('data', (d) => { stderr += d; });
76
+ child.on('error', () => finish(null));
77
+ child.on('close', (code) => finish({ code, stdout, stderr }));
78
+ const timer = setTimeout(() => { try { child.kill('SIGKILL'); } catch {} finish(null); }, timeoutMs);
79
+ if (typeof timer.unref === 'function') timer.unref();
80
+ try { child.stdin.end(JSON.stringify(payload)); } catch { finish(null); }
81
+ });
82
+ }
83
+
84
+ /** The deny reason, preferring the structured field over the stderr line. */
85
+ function denyReason(res) {
86
+ try {
87
+ const j = JSON.parse(res.stdout);
88
+ const r = j?.hookSpecificOutput?.permissionDecisionReason;
89
+ if (typeof r === 'string' && r.trim()) return r.trim();
90
+ } catch { /* not JSON — fall back to stderr */ }
91
+ const line = String(res.stderr || '').split('\n').map((s) => s.trim()).filter(Boolean).pop();
92
+ return line || 'Blocked by SolonGate.';
93
+ }
94
+
95
+ /** A rewrite instruction, when the guard asked for one (ghost-path listings). */
96
+ function rewritePatch(res) {
97
+ try {
98
+ const j = JSON.parse(res.stdout);
99
+ const h = j?.hookSpecificOutput;
100
+ if (h?.permissionDecision === 'allow' && h.updatedInput && typeof h.updatedInput === 'object') {
101
+ return h.updatedInput;
102
+ }
103
+ } catch { /* no patch */ }
104
+ return null;
105
+ }
106
+
107
+ export const SolonGate = async ({ directory, worktree } = {}) => {
108
+ const cwd = directory || worktree || process.cwd();
109
+
110
+ return {
111
+ 'tool.execute.before': async (input, output) => {
112
+ const res = await runHook(GUARD, {
113
+ tool_name: input?.tool ?? '',
114
+ tool_input: output?.args ?? {},
115
+ session_id: input?.sessionID ?? '',
116
+ tool_use_id: input?.callID ?? '',
117
+ cwd,
118
+ }, GUARD_TIMEOUT_MS);
119
+
120
+ // The guard could not run (not installed, spawn refused, wedged). Fail
121
+ // OPEN, the same way every enforcement layer inside it does: a guard that
122
+ // cannot answer must not become an editor that cannot work.
123
+ if (!res) return;
124
+
125
+ if (res.code === 2) {
126
+ throw new Error(denyReason(res));
127
+ }
128
+
129
+ // Allow-with-rewrite: `output.args` is what actually runs, so replacing it
130
+ // here is how a hidden path gets filtered out of a listing before the
131
+ // model ever sees the result.
132
+ const patch = rewritePatch(res);
133
+ if (patch && output && typeof output === 'object') {
134
+ try { Object.assign(output.args, patch); } catch { /* leave the call as it was */ }
135
+ }
136
+ },
137
+
138
+ 'tool.execute.after': async (input, output) => {
139
+ // The ALLOW-path audit record. Fire and forget: it must never delay the
140
+ // editor, and losing one line is better than a stalled tool.
141
+ void runHook(AUDIT, {
142
+ tool_name: input?.tool ?? '',
143
+ tool_input: input?.args ?? {},
144
+ tool_response: output?.output ?? output?.metadata ?? {},
145
+ session_id: input?.sessionID ?? '',
146
+ tool_use_id: input?.callID ?? '',
147
+ cwd,
148
+ }, GUARD_TIMEOUT_MS);
149
+ },
150
+ };
151
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.83.10",
3
+ "version": "0.83.11",
4
4
  "description": "AI tool security proxy: protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. No code changes required.",
5
5
  "type": "module",
6
6
  "bin": {