@solongate/proxy 0.83.19 → 0.83.21

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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli-launch.ts
4
+ import { accessSync, constants, existsSync } from "fs";
5
+ import { spawnSync } from "child_process";
6
+ import { createRequire } from "module";
7
+ import { homedir } from "os";
8
+ import { dirname, join, resolve } from "path";
9
+ var GO_SUBCOMMANDS = /* @__PURE__ */ new Set([
10
+ "update",
11
+ "repair",
12
+ "logs-server",
13
+ "local-logs",
14
+ "policy",
15
+ "ratelimit",
16
+ "dlp",
17
+ "stats",
18
+ "audit",
19
+ "sessions",
20
+ "session",
21
+ "doctor",
22
+ "watch",
23
+ "alerts",
24
+ "webhooks",
25
+ "dataroom"
26
+ ]);
27
+ var GO_INFO_ARGS = /* @__PURE__ */ new Set(["login", "help", "--help", "-h", "--version", "-v", "version"]);
28
+ var argv = process.argv.slice(2);
29
+ var first = argv[0] ?? "";
30
+ var delegable = argv.length === 0 || GO_SUBCOMMANDS.has(first) || GO_INFO_ARGS.has(first);
31
+ function candidates() {
32
+ const out = [];
33
+ if (process.env.SOLONGATE_CLI_BIN) out.push(process.env.SOLONGATE_CLI_BIN);
34
+ const exe = process.platform === "win32" ? "solongate.exe" : "solongate";
35
+ const os_ = process.platform === "win32" ? "win32" : process.platform;
36
+ const cpu = process.arch === "x64" ? "x64" : process.arch;
37
+ try {
38
+ const req = createRequire(import.meta.url);
39
+ out.push(join(dirname(req.resolve(`@solongate/guard-${os_}-${cpu}/package.json`)), exe));
40
+ } catch {
41
+ }
42
+ out.push(resolve(homedir(), ".solongate", "bin", exe));
43
+ return out;
44
+ }
45
+ function runGo() {
46
+ for (const bin of candidates()) {
47
+ try {
48
+ if (!existsSync(bin)) continue;
49
+ if (process.platform !== "win32") accessSync(bin, constants.X_OK);
50
+ } catch {
51
+ continue;
52
+ }
53
+ const r = spawnSync(bin, argv, { stdio: "inherit" });
54
+ if (r.error || r.signal) return null;
55
+ if (r.status === 69) return null;
56
+ return r.status ?? 0;
57
+ }
58
+ return null;
59
+ }
60
+ if (delegable && process.env.SOLONGATE_NO_GO_CLI !== "1") {
61
+ let code = null;
62
+ try {
63
+ code = runGo();
64
+ } catch {
65
+ code = null;
66
+ }
67
+ if (code !== null) process.exit(code);
68
+ }
69
+ await import(new URL("./index.js", import.meta.url).href);
@@ -10,9 +10,10 @@ import { resolve as resolve2, join as join2 } from "path";
10
10
  import { homedir as homedir2 } from "os";
11
11
 
12
12
  // src/global-install.ts
13
- import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, rmdirSync, readdirSync, statSync, chmodSync } from "fs";
13
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, rmdirSync, readdirSync, statSync, chmodSync, copyFileSync, renameSync } from "fs";
14
14
  import { resolve, join, dirname } from "path";
15
15
  import { homedir } from "os";
16
+ import { createRequire } from "module";
16
17
  import { fileURLToPath } from "url";
17
18
  import { createInterface } from "readline";
18
19
  import { execFileSync, spawn } from "child_process";
@@ -26,10 +27,12 @@ function globalPaths() {
26
27
  const antigravityDir = join(home, ".gemini", "config");
27
28
  const codexDir = process.env["CODEX_HOME"] ? resolve(process.env["CODEX_HOME"]) : join(home, ".codex");
28
29
  const opencodeDir = join(process.env["XDG_CONFIG_HOME"] ? resolve(process.env["XDG_CONFIG_HOME"]) : join(home, ".config"), "opencode");
30
+ const binDir = join(sgDir, "bin");
29
31
  return {
30
32
  home,
31
33
  sgDir,
32
34
  hooksDir,
35
+ binDir,
33
36
  claudeDir,
34
37
  antigravityDir,
35
38
  codexDir,
@@ -305,12 +305,12 @@ export declare const PolicySetSchema: z.ZodObject<{
305
305
  createdAt: z.ZodString;
306
306
  updatedAt: z.ZodString;
307
307
  }, "strip", z.ZodTypeAny, {
308
+ version: number;
308
309
  name: string;
309
310
  id: string;
310
311
  description: string;
311
312
  createdAt: string;
312
313
  updatedAt: string;
313
- version: number;
314
314
  rules: {
315
315
  id: string;
316
316
  description: string;
@@ -343,12 +343,12 @@ export declare const PolicySetSchema: z.ZodObject<{
343
343
  } | undefined;
344
344
  }[];
345
345
  }, {
346
+ version: number;
346
347
  name: string;
347
348
  id: string;
348
349
  description: string;
349
350
  createdAt: string;
350
351
  updatedAt: string;
351
- version: number;
352
352
  rules: {
353
353
  id: string;
354
354
  description: string;
@@ -19,6 +19,7 @@ export declare function globalPaths(): {
19
19
  home: string;
20
20
  sgDir: string;
21
21
  hooksDir: string;
22
+ binDir: string;
22
23
  claudeDir: string;
23
24
  antigravityDir: string;
24
25
  codexDir: string;
@@ -1,7 +1,8 @@
1
1
  // src/global-install.ts
2
- import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, rmdirSync, readdirSync, statSync, chmodSync } from "fs";
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, rmdirSync, readdirSync, statSync, chmodSync, copyFileSync, renameSync } from "fs";
3
3
  import { resolve, join, dirname } from "path";
4
4
  import { homedir } from "os";
5
+ import { createRequire } from "module";
5
6
  import { fileURLToPath } from "url";
6
7
  import { createInterface } from "readline";
7
8
  import { execFileSync, spawn } from "child_process";
@@ -126,10 +127,12 @@ function globalPaths() {
126
127
  const antigravityDir = join(home, ".gemini", "config");
127
128
  const codexDir = process.env["CODEX_HOME"] ? resolve(process.env["CODEX_HOME"]) : join(home, ".codex");
128
129
  const opencodeDir = join(process.env["XDG_CONFIG_HOME"] ? resolve(process.env["XDG_CONFIG_HOME"]) : join(home, ".config"), "opencode");
130
+ const binDir = join(sgDir, "bin");
129
131
  return {
130
132
  home,
131
133
  sgDir,
132
134
  hooksDir,
135
+ binDir,
133
136
  claudeDir,
134
137
  antigravityDir,
135
138
  codexDir,
@@ -183,6 +186,38 @@ function readGuard() {
183
186
  const bundled = join(HOOKS_DIR, "guard.bundled.mjs");
184
187
  return existsSync(bundled) ? readFileSync(bundled, "utf-8") : readHook("guard.mjs");
185
188
  }
189
+ function installGoBinaries(binDir) {
190
+ const os_ = process.platform === "win32" ? "win32" : process.platform;
191
+ const cpu = process.arch === "x64" ? "x64" : process.arch;
192
+ const suffix = process.platform === "win32" ? ".exe" : "";
193
+ const placed = [];
194
+ let pkgDir;
195
+ try {
196
+ const require_ = createRequire(import.meta.url);
197
+ pkgDir = dirname(require_.resolve(`@solongate/guard-${os_}-${cpu}/package.json`));
198
+ } catch {
199
+ return placed;
200
+ }
201
+ try {
202
+ mkdirSync(binDir, { recursive: true });
203
+ } catch {
204
+ return placed;
205
+ }
206
+ for (const name of ["solongate-guard", "solongate"]) {
207
+ const from = join(pkgDir, name + suffix);
208
+ const to = join(binDir, name + suffix);
209
+ try {
210
+ if (!existsSync(from)) continue;
211
+ const tmp = to + ".new";
212
+ copyFileSync(from, tmp);
213
+ chmodSync(tmp, 493);
214
+ renameSync(tmp, to);
215
+ placed.push(name + suffix);
216
+ } catch {
217
+ }
218
+ }
219
+ return placed;
220
+ }
186
221
  var ANTIGRAVITY_GROUP = "solongate-guard";
187
222
  function antigravityHookCommand(guardAbs) {
188
223
  const nodeBin = process.execPath.replace(/\\/g, "/");
@@ -576,6 +611,7 @@ function installGlobalQuiet() {
576
611
  mkdirSync(p.claudeDir, { recursive: true });
577
612
  unlockProtected();
578
613
  writeFileSync(join(p.hooksDir, "guard.mjs"), readGuard());
614
+ installGoBinaries(p.binDir);
579
615
  writeFileSync(join(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
580
616
  writeFileSync(join(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
581
617
  writeFileSync(join(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
package/dist/index.js CHANGED
@@ -6214,9 +6214,10 @@ __export(global_install_exports, {
6214
6214
  unlockProtected: () => unlockProtected,
6215
6215
  writeProtectedFile: () => writeProtectedFile
6216
6216
  });
6217
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync3, rmSync as rmSync2, rmdirSync, readdirSync, statSync, chmodSync } from "fs";
6217
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync3, rmSync as rmSync2, rmdirSync, readdirSync, statSync, chmodSync, copyFileSync, renameSync } from "fs";
6218
6218
  import { resolve as resolve3, join as join4, dirname } from "path";
6219
6219
  import { homedir as homedir2 } from "os";
6220
+ import { createRequire } from "module";
6220
6221
  import { fileURLToPath } from "url";
6221
6222
  import { createInterface } from "readline";
6222
6223
  import { execFileSync as execFileSync2, spawn } from "child_process";
@@ -6339,10 +6340,12 @@ function globalPaths() {
6339
6340
  const antigravityDir = join4(home, ".gemini", "config");
6340
6341
  const codexDir = process.env["CODEX_HOME"] ? resolve3(process.env["CODEX_HOME"]) : join4(home, ".codex");
6341
6342
  const opencodeDir = join4(process.env["XDG_CONFIG_HOME"] ? resolve3(process.env["XDG_CONFIG_HOME"]) : join4(home, ".config"), "opencode");
6343
+ const binDir = join4(sgDir, "bin");
6342
6344
  return {
6343
6345
  home,
6344
6346
  sgDir,
6345
6347
  hooksDir,
6348
+ binDir,
6346
6349
  claudeDir,
6347
6350
  antigravityDir,
6348
6351
  codexDir,
@@ -6396,6 +6399,38 @@ function readGuard() {
6396
6399
  const bundled = join4(HOOKS_DIR, "guard.bundled.mjs");
6397
6400
  return existsSync3(bundled) ? readFileSync4(bundled, "utf-8") : readHook("guard.mjs");
6398
6401
  }
6402
+ function installGoBinaries(binDir) {
6403
+ const os_ = process.platform === "win32" ? "win32" : process.platform;
6404
+ const cpu = process.arch === "x64" ? "x64" : process.arch;
6405
+ const suffix = process.platform === "win32" ? ".exe" : "";
6406
+ const placed = [];
6407
+ let pkgDir;
6408
+ try {
6409
+ const require_ = createRequire(import.meta.url);
6410
+ pkgDir = dirname(require_.resolve(`@solongate/guard-${os_}-${cpu}/package.json`));
6411
+ } catch {
6412
+ return placed;
6413
+ }
6414
+ try {
6415
+ mkdirSync3(binDir, { recursive: true });
6416
+ } catch {
6417
+ return placed;
6418
+ }
6419
+ for (const name of ["solongate-guard", "solongate"]) {
6420
+ const from = join4(pkgDir, name + suffix);
6421
+ const to = join4(binDir, name + suffix);
6422
+ try {
6423
+ if (!existsSync3(from)) continue;
6424
+ const tmp = to + ".new";
6425
+ copyFileSync(from, tmp);
6426
+ chmodSync(tmp, 493);
6427
+ renameSync(tmp, to);
6428
+ placed.push(name + suffix);
6429
+ } catch {
6430
+ }
6431
+ }
6432
+ return placed;
6433
+ }
6399
6434
  function antigravityHookCommand(guardAbs) {
6400
6435
  const nodeBin = process.execPath.replace(/\\/g, "/");
6401
6436
  const call = process.platform === "win32" ? "& " : "";
@@ -6785,6 +6820,7 @@ function installGlobalQuiet() {
6785
6820
  mkdirSync3(p.claudeDir, { recursive: true });
6786
6821
  unlockProtected();
6787
6822
  writeFileSync3(join4(p.hooksDir, "guard.mjs"), readGuard());
6823
+ installGoBinaries(p.binDir);
6788
6824
  writeFileSync3(join4(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
6789
6825
  writeFileSync3(join4(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
6790
6826
  writeFileSync3(join4(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
package/dist/tui/index.js CHANGED
@@ -9,9 +9,10 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/global-install.ts
12
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync, rmSync, rmdirSync, readdirSync as readdirSync2, statSync as statSync2, chmodSync } from "fs";
12
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync, rmSync, rmdirSync, readdirSync as readdirSync2, statSync as statSync2, chmodSync, copyFileSync, renameSync } from "fs";
13
13
  import { resolve, join as join3, dirname } from "path";
14
14
  import { homedir as homedir3 } from "os";
15
+ import { createRequire } from "module";
15
16
  import { fileURLToPath } from "url";
16
17
  import { createInterface } from "readline";
17
18
  import { execFileSync, spawn } from "child_process";
@@ -134,10 +135,12 @@ function globalPaths() {
134
135
  const antigravityDir = join3(home, ".gemini", "config");
135
136
  const codexDir = process.env["CODEX_HOME"] ? resolve(process.env["CODEX_HOME"]) : join3(home, ".codex");
136
137
  const opencodeDir = join3(process.env["XDG_CONFIG_HOME"] ? resolve(process.env["XDG_CONFIG_HOME"]) : join3(home, ".config"), "opencode");
138
+ const binDir = join3(sgDir, "bin");
137
139
  return {
138
140
  home,
139
141
  sgDir,
140
142
  hooksDir,
143
+ binDir,
141
144
  claudeDir,
142
145
  antigravityDir,
143
146
  codexDir,
@@ -183,6 +186,38 @@ function readGuard() {
183
186
  const bundled = join3(HOOKS_DIR, "guard.bundled.mjs");
184
187
  return existsSync2(bundled) ? readFileSync3(bundled, "utf-8") : readHook("guard.mjs");
185
188
  }
189
+ function installGoBinaries(binDir) {
190
+ const os_ = process.platform === "win32" ? "win32" : process.platform;
191
+ const cpu = process.arch === "x64" ? "x64" : process.arch;
192
+ const suffix = process.platform === "win32" ? ".exe" : "";
193
+ const placed = [];
194
+ let pkgDir;
195
+ try {
196
+ const require_ = createRequire(import.meta.url);
197
+ pkgDir = dirname(require_.resolve(`@solongate/guard-${os_}-${cpu}/package.json`));
198
+ } catch {
199
+ return placed;
200
+ }
201
+ try {
202
+ mkdirSync(binDir, { recursive: true });
203
+ } catch {
204
+ return placed;
205
+ }
206
+ for (const name of ["solongate-guard", "solongate"]) {
207
+ const from = join3(pkgDir, name + suffix);
208
+ const to = join3(binDir, name + suffix);
209
+ try {
210
+ if (!existsSync2(from)) continue;
211
+ const tmp = to + ".new";
212
+ copyFileSync(from, tmp);
213
+ chmodSync(tmp, 493);
214
+ renameSync(tmp, to);
215
+ placed.push(name + suffix);
216
+ } catch {
217
+ }
218
+ }
219
+ return placed;
220
+ }
186
221
  function antigravityHookCommand(guardAbs) {
187
222
  const nodeBin = process.execPath.replace(/\\/g, "/");
188
223
  const call = process.platform === "win32" ? "& " : "";
@@ -502,6 +537,7 @@ function installGlobalQuiet() {
502
537
  mkdirSync(p.claudeDir, { recursive: true });
503
538
  unlockProtected();
504
539
  writeFileSync2(join3(p.hooksDir, "guard.mjs"), readGuard());
540
+ installGoBinaries(p.binDir);
505
541
  writeFileSync2(join3(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
506
542
  writeFileSync2(join3(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
507
543
  writeFileSync2(join3(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
@@ -6530,10 +6530,11 @@ var init_src = __esm({
6530
6530
  });
6531
6531
 
6532
6532
  // hooks/guard.mjs
6533
- import { readFileSync, existsSync, statSync, readdirSync, writeFileSync, mkdirSync, chmodSync, renameSync, appendFileSync, rmSync, rmdirSync, openSync, readSync, closeSync } from "node:fs";
6534
- import { spawn } from "node:child_process";
6533
+ import { readFileSync, existsSync, statSync, readdirSync, writeFileSync, mkdirSync, chmodSync, renameSync, appendFileSync, rmSync, rmdirSync, openSync, readSync, closeSync, accessSync, constants } from "node:fs";
6534
+ import { spawn, spawnSync } from "node:child_process";
6535
6535
  import { resolve, join, dirname, isAbsolute } from "node:path";
6536
6536
  import { homedir } from "node:os";
6537
+ import { createRequire } from "node:module";
6537
6538
  import { gunzipSync } from "node:zlib";
6538
6539
  import { createHash } from "node:crypto";
6539
6540
  function projectKey(dir) {
@@ -6578,7 +6579,88 @@ function sweepLegacyFlagDir() {
6578
6579
  } catch {
6579
6580
  }
6580
6581
  }
6581
- var HOOK_VERSION = 79;
6582
+ var HOOK_VERSION = 80;
6583
+ var SG_REFRESH_ARG = process.argv.includes("--sg-refresh-policy");
6584
+ var SG_STDIN = SG_REFRESH_ARG ? "" : (() => {
6585
+ try {
6586
+ return readFileSync(0, "utf-8");
6587
+ } catch {
6588
+ return "";
6589
+ }
6590
+ })();
6591
+ function sgGuardCandidates() {
6592
+ const out = [];
6593
+ if (process.env.SOLONGATE_GUARD_BIN)
6594
+ out.push(process.env.SOLONGATE_GUARD_BIN);
6595
+ const exe = process.platform === "win32" ? "solongate-guard.exe" : "solongate-guard";
6596
+ const os_ = process.platform === "win32" ? "win32" : process.platform;
6597
+ const cpu = process.arch === "x64" ? "x64" : process.arch;
6598
+ try {
6599
+ const req = createRequire(import.meta.url);
6600
+ out.push(join(dirname(req.resolve(`@solongate/guard-${os_}-${cpu}/package.json`)), exe));
6601
+ } catch {
6602
+ }
6603
+ out.push(resolve(homedir(), ".solongate", "bin", exe));
6604
+ return out;
6605
+ }
6606
+ function sgTryGoGuard() {
6607
+ for (const bin of sgGuardCandidates()) {
6608
+ try {
6609
+ if (!existsSync(bin))
6610
+ continue;
6611
+ if (process.platform !== "win32")
6612
+ accessSync(bin, constants.X_OK);
6613
+ } catch {
6614
+ continue;
6615
+ }
6616
+ let version;
6617
+ try {
6618
+ const v = spawnSync(bin, ["--sg-version"], { encoding: "utf-8", timeout: 2e3 });
6619
+ if (v.error || v.status !== 0)
6620
+ continue;
6621
+ version = String(v.stdout || "").trim();
6622
+ } catch {
6623
+ continue;
6624
+ }
6625
+ if (version !== String(HOOK_VERSION))
6626
+ continue;
6627
+ let r;
6628
+ try {
6629
+ r = spawnSync(bin, process.argv.slice(2), {
6630
+ input: SG_STDIN,
6631
+ encoding: "utf-8",
6632
+ // Well past every network call the guard makes. A binary still running
6633
+ // at this point is not going to produce an answer worth waiting for.
6634
+ timeout: 1e4
6635
+ });
6636
+ } catch {
6637
+ return;
6638
+ }
6639
+ if (!r || r.error || r.signal)
6640
+ return;
6641
+ const status = r.status;
6642
+ const stdout = r.stdout || "";
6643
+ if (status !== 0 && status !== 2)
6644
+ return;
6645
+ if (status === 2 && stdout.trim() === "")
6646
+ return;
6647
+ try {
6648
+ if (stdout)
6649
+ process.stdout.write(stdout);
6650
+ if (r.stderr)
6651
+ process.stderr.write(r.stderr);
6652
+ } catch {
6653
+ return;
6654
+ }
6655
+ process.exit(status);
6656
+ }
6657
+ }
6658
+ if (!SG_REFRESH_ARG && process.env.SOLONGATE_NO_GO_GUARD !== "1") {
6659
+ try {
6660
+ sgTryGoGuard();
6661
+ } catch {
6662
+ }
6663
+ }
6582
6664
  function localLogsOnly(security) {
6583
6665
  if (security !== void 0) {
6584
6666
  const l = security && security.localLogs;
@@ -6606,6 +6688,17 @@ function accountMark() {
6606
6688
  return "";
6607
6689
  }
6608
6690
  }
6691
+ function postAuditDetached(entry) {
6692
+ try {
6693
+ const payload = Buffer.from(JSON.stringify({
6694
+ url: API_URL + "/api/v1/audit-logs",
6695
+ headers: AUTH_HEADERS,
6696
+ body: entry
6697
+ }), "utf-8").toString("base64");
6698
+ spawn(process.execPath, [process.argv[1], "--sg-audit-post", payload], { detached: true, stdio: "ignore" }).unref();
6699
+ } catch {
6700
+ }
6701
+ }
6609
6702
  function writeLocalLog(security, entry) {
6610
6703
  try {
6611
6704
  const mark = accountMark();
@@ -6818,6 +6911,28 @@ var AGENT_TYPE = process.argv[2] || "claude-code";
6818
6911
  var POLICY_SELECTOR = process.env.SOLONGATE_AGENT_ID || "";
6819
6912
  var AGENT_ID = POLICY_SELECTOR || AGENT_TYPE;
6820
6913
  var AGENT_NAME = process.env.SOLONGATE_AGENT_NAME || process.argv[3] || AGENT_TYPE;
6914
+ {
6915
+ const _ai = process.argv.indexOf("--sg-audit-post");
6916
+ if (_ai !== -1) {
6917
+ try {
6918
+ const _raw = Buffer.from(process.argv[_ai + 1] || "", "base64").toString("utf-8");
6919
+ const _p = JSON.parse(_raw);
6920
+ if (_p && _p.url && _p.body) {
6921
+ fetch(_p.url, {
6922
+ method: "POST",
6923
+ headers: { "Content-Type": "application/json", ..._p.headers || {} },
6924
+ body: JSON.stringify(_p.body),
6925
+ signal: AbortSignal.timeout(1e4)
6926
+ }).catch(() => {
6927
+ }).finally(() => process.exit(0));
6928
+ setTimeout(() => process.exit(0), 11e3).unref();
6929
+ } else
6930
+ process.exit(0);
6931
+ } catch {
6932
+ process.exit(0);
6933
+ }
6934
+ }
6935
+ }
6821
6936
  {
6822
6937
  const _wi = process.argv.indexOf("--sg-log-write");
6823
6938
  if (_wi !== -1) {
@@ -8418,10 +8533,7 @@ function readReferencedFiles(args, cwd) {
8418
8533
  return out;
8419
8534
  }
8420
8535
  if (!REFRESH_MODE) {
8421
- try {
8422
- input += readFileSync(0, "utf-8");
8423
- } catch {
8424
- }
8536
+ input += SG_STDIN;
8425
8537
  }
8426
8538
  (async () => {
8427
8539
  try {
@@ -8511,12 +8623,7 @@ if (!REFRESH_MODE) {
8511
8623
  }
8512
8624
  try {
8513
8625
  if (!localLogsOnly(_sec))
8514
- await fetch(API_URL + "/api/v1/audit-logs", {
8515
- method: "POST",
8516
- headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
8517
- body: JSON.stringify(_logEntry),
8518
- signal: AbortSignal.timeout(3e3)
8519
- });
8626
+ postAuditDetached(_logEntry);
8520
8627
  } catch {
8521
8628
  }
8522
8629
  if (AGENT_TYPE !== "codex")
@@ -8629,22 +8736,17 @@ if (!REFRESH_MODE) {
8629
8736
  writeLocalLog(securityCfg, { ts: (/* @__PURE__ */ new Date()).toISOString(), tool: toolName, arguments: args, decision: "DENY", reason: "ghost path (hidden from agent)", permission: guessPermission(toolName), source: `${AGENT_TYPE}-guard`, agent_id: AGENT_TYPE, agent_name: AGENT_NAME, session_id: call.sessionId, evaluation_time_ms: Date.now() - _evalStart });
8630
8737
  try {
8631
8738
  if (!localLogsOnly(securityCfg))
8632
- await fetch(API_URL + "/api/v1/audit-logs", {
8633
- method: "POST",
8634
- headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
8635
- body: JSON.stringify({
8636
- tool: toolName,
8637
- arguments: args,
8638
- decision: "DENY",
8639
- reason: "ghost path (hidden from agent)",
8640
- permission: guessPermission(toolName),
8641
- source: `${AGENT_TYPE}-guard`,
8642
- agent_id: AGENT_TYPE,
8643
- agent_name: AGENT_NAME,
8644
- session_id: call.sessionId,
8645
- evaluation_time_ms: Date.now() - _evalStart
8646
- }),
8647
- signal: AbortSignal.timeout(3e3)
8739
+ postAuditDetached({
8740
+ tool: toolName,
8741
+ arguments: args,
8742
+ decision: "DENY",
8743
+ reason: "ghost path (hidden from agent)",
8744
+ permission: guessPermission(toolName),
8745
+ source: `${AGENT_TYPE}-guard`,
8746
+ agent_id: AGENT_TYPE,
8747
+ agent_name: AGENT_NAME,
8748
+ session_id: call.sessionId,
8749
+ evaluation_time_ms: Date.now() - _evalStart
8648
8750
  });
8649
8751
  } catch {
8650
8752
  }
@@ -8728,12 +8830,7 @@ if (!REFRESH_MODE) {
8728
8830
  };
8729
8831
  writeLocalLog(securityCfg, { ts: (/* @__PURE__ */ new Date()).toISOString(), ...logEntry });
8730
8832
  if (!localLogsOnly(securityCfg))
8731
- await fetch(API_URL + "/api/v1/audit-logs", {
8732
- method: "POST",
8733
- headers: { "Content-Type": "application/json", ...AUTH_HEADERS },
8734
- body: JSON.stringify(logEntry),
8735
- signal: AbortSignal.timeout(3e3)
8736
- });
8833
+ postAuditDetached(logEntry);
8737
8834
  } catch {
8738
8835
  }
8739
8836
  }
package/hooks/guard.mjs CHANGED
@@ -21,10 +21,11 @@
21
21
  * Logs DENY decisions to SolonGate Cloud. ALLOWs are logged by audit.mjs.
22
22
  * Auto-installed by: npx @solongate/proxy init --global
23
23
  */
24
- import { readFileSync, existsSync, statSync, readdirSync, writeFileSync, mkdirSync, chmodSync, renameSync, appendFileSync, rmSync, rmdirSync, openSync, readSync, closeSync } from 'node:fs';
25
- import { spawn } from 'node:child_process';
24
+ import { readFileSync, existsSync, statSync, readdirSync, writeFileSync, mkdirSync, chmodSync, renameSync, appendFileSync, rmSync, rmdirSync, openSync, readSync, closeSync, accessSync, constants } from 'node:fs';
25
+ import { spawn, spawnSync } from 'node:child_process';
26
26
  import { resolve, join, dirname, isAbsolute } from 'node:path';
27
27
  import { homedir } from 'node:os';
28
+ import { createRequire } from 'node:module';
28
29
  import { gunzipSync } from 'node:zlib';
29
30
 
30
31
  // ── Per-project scratch, kept OUT of the project ─────────────────────────────
@@ -81,7 +82,136 @@ import { createHash } from 'node:crypto';
81
82
  // the installed hook self-updates when the cloud version is higher (see
82
83
  // maybeSelfUpdate). This is what makes guard fixes propagate without a manual
83
84
  // reinstall — the same trust model as the OPA WASM this hook already runs.
84
- const HOOK_VERSION = 79;
85
+ const HOOK_VERSION = 80;
86
+
87
+ // ── The Go guard, when there is one and it is the right one ──────────────────
88
+ //
89
+ // Everything below this block is the Node decision engine. It stays, it stays
90
+ // correct, and it stays the thing that runs whenever the fast path cannot be
91
+ // trusted. THE RULE: a missing, stale, unreadable or crashing binary must mean
92
+ // SLOW BUT GUARDED, never FAST BUT UNGUARDED. Every failure here falls through.
93
+ //
94
+ // Three things this gets right that a naive `exec the binary` would not:
95
+ //
96
+ // 1. STDIN IS READ HERE, ONCE, BEFORE THE BINARY RUNS. Handing the child fd 0
97
+ // directly would be faster to write and impossible to recover from: a binary
98
+ // that dies after consuming the payload leaves Node with nothing to fall back
99
+ // WITH, so it would evaluate an empty call and allow it. The payload is a few
100
+ // KB and the read is synchronous; the safety is worth more than the copy.
101
+ //
102
+ // 2. EXIT 2 IS AMBIGUOUS. It is this hook's block signal AND the code the Go
103
+ // runtime uses for an unhandled panic. A block always writes a decision to
104
+ // stdout first; a panic writes a stack trace to stderr and nothing to stdout.
105
+ // So exit 2 is honoured only with output to show for it, and a panic falls
106
+ // through to Node — which will reach its own verdict on the same payload.
107
+ //
108
+ // 3. THE VERSION MUST MATCH EXACTLY, not merely exist. This hook self-updates
109
+ // from the cloud on its own schedule while the binary only moves when npm
110
+ // moves it, so a machine can hold a hook from this week and a binary from
111
+ // last month. HOOK_VERSION bumps on every change to this file, and the binary
112
+ // prints the HOOK_VERSION it implements. Different numbers mean different
113
+ // decisions, and a binary that is merely OLD is not a smaller problem than
114
+ // one that is missing.
115
+ //
116
+ // SOLONGATE_NO_GO_GUARD=1 pins execution to Node. It exists so a failure can be
117
+ // bisected without uninstalling anything.
118
+
119
+ // The refresh invocation has no tool call on stdin — it is spawned detached and
120
+ // its stdin may be a pipe nobody ever closes, so reading it would hang the hook
121
+ // forever. It also has nothing for the binary to decide. Both paths skip it.
122
+ const SG_REFRESH_ARG = process.argv.includes('--sg-refresh-policy');
123
+
124
+ // The one read of fd 0 in this process. Everything downstream uses this string.
125
+ const SG_STDIN = SG_REFRESH_ARG ? '' : (() => {
126
+ try { return readFileSync(0, 'utf-8'); } catch { return ''; }
127
+ })();
128
+
129
+ // Where a binary may legitimately live, in the order they are trusted.
130
+ //
131
+ // The env override is first so a build can be pointed at without reinstalling.
132
+ // The platform package is the normal path for a project-local install. The copy
133
+ // under ~/.solongate/bin is what a GLOBAL install has: the hook is written to
134
+ // ~/.solongate/hooks as a lone file and launched from arbitrary working
135
+ // directories, where resolving through node_modules finds nothing.
136
+ function sgGuardCandidates() {
137
+ const out = [];
138
+ if (process.env.SOLONGATE_GUARD_BIN) out.push(process.env.SOLONGATE_GUARD_BIN);
139
+ const exe = process.platform === 'win32' ? 'solongate-guard.exe' : 'solongate-guard';
140
+ const os_ = process.platform === 'win32' ? 'win32' : process.platform;
141
+ const cpu = process.arch === 'x64' ? 'x64' : process.arch;
142
+ try {
143
+ // The manifest, not a main entry: these packages contain a binary and
144
+ // nothing else, so there is no JavaScript in them to resolve.
145
+ //
146
+ // createRequire rather than import.meta.resolve: the latter is not a sync
147
+ // function on every Node 18 this package supports, and a hook is not the
148
+ // place to find that out. node:module is a builtin, so importing it cannot
149
+ // fail the way a real dependency could.
150
+ const req = createRequire(import.meta.url);
151
+ out.push(join(dirname(req.resolve(`@solongate/guard-${os_}-${cpu}/package.json`)), exe));
152
+ } catch {
153
+ // Not installed for this platform, which is what optionalDependencies does
154
+ // on a host with no published binary, and is not an error.
155
+ }
156
+ out.push(resolve(homedir(), '.solongate', 'bin', exe));
157
+ return out;
158
+ }
159
+
160
+ // Hand the whole call to the binary. Returns only when Node must decide instead.
161
+ function sgTryGoGuard() {
162
+ for (const bin of sgGuardCandidates()) {
163
+ try {
164
+ if (!existsSync(bin)) continue;
165
+ if (process.platform !== 'win32') accessSync(bin, constants.X_OK);
166
+ } catch { continue; }
167
+
168
+ // Ask what it is. Bounded, because this runs before anything has decided the
169
+ // binary can be trusted and it must not be able to hang what it speeds up.
170
+ let version;
171
+ try {
172
+ const v = spawnSync(bin, ['--sg-version'], { encoding: 'utf-8', timeout: 2000 });
173
+ if (v.error || v.status !== 0) continue;
174
+ version = String(v.stdout || '').trim();
175
+ } catch { continue; }
176
+ if (version !== String(HOOK_VERSION)) continue;
177
+
178
+ let r;
179
+ try {
180
+ r = spawnSync(bin, process.argv.slice(2), {
181
+ input: SG_STDIN,
182
+ encoding: 'utf-8',
183
+ // Well past every network call the guard makes. A binary still running
184
+ // at this point is not going to produce an answer worth waiting for.
185
+ timeout: 10000,
186
+ });
187
+ } catch { return; }
188
+
189
+ // Killed, timed out, or never started: Node decides.
190
+ if (!r || r.error || r.signal) return;
191
+ const status = r.status;
192
+ const stdout = r.stdout || '';
193
+ if (status !== 0 && status !== 2) return;
194
+ // See (2) above: a block without a decision to show is a crash wearing a
195
+ // block's exit code.
196
+ if (status === 2 && stdout.trim() === '') return;
197
+
198
+ try {
199
+ if (stdout) process.stdout.write(stdout);
200
+ if (r.stderr) process.stderr.write(r.stderr);
201
+ } catch {
202
+ // Writing the answer is the whole job. If it could not be delivered, Node
203
+ // has not written anything either, so it can still do the work.
204
+ return;
205
+ }
206
+ process.exit(status);
207
+ }
208
+ }
209
+
210
+ if (!SG_REFRESH_ARG && process.env.SOLONGATE_NO_GO_GUARD !== '1') {
211
+ // A throw anywhere in here is a bug in the fast path, and a bug in the fast
212
+ // path must not become an unguarded tool call.
213
+ try { sgTryGoGuard(); } catch {}
214
+ }
85
215
 
86
216
  // True when local log storage is ON. In that mode logs are kept LOCAL ONLY and
87
217
  // nothing is sent to the cloud audit log — local and cloud are one or the
@@ -170,6 +300,25 @@ function accountMark() {
170
300
  }
171
301
  }
172
302
 
303
+ /**
304
+ * Record a denial in the cloud WITHOUT making the agent wait for it.
305
+ *
306
+ * The verdict is the product; the audit line is bookkeeping. Awaiting the POST
307
+ * put a full network round trip between "blocked" and the agent hearing it —
308
+ * measured at 1643ms per denial, against 78ms with the API unreachable. Handing
309
+ * it to a detached child keeps the record and gives the time back.
310
+ */
311
+ function postAuditDetached(entry) {
312
+ try {
313
+ const payload = Buffer.from(JSON.stringify({
314
+ url: API_URL + '/api/v1/audit-logs',
315
+ headers: AUTH_HEADERS,
316
+ body: entry,
317
+ }), 'utf-8').toString('base64');
318
+ spawn(process.execPath, [process.argv[1], '--sg-audit-post', payload], { detached: true, stdio: 'ignore' }).unref();
319
+ } catch { /* the denial still stands; only the record is at risk */ }
320
+ }
321
+
173
322
  function writeLocalLog(security, entry) {
174
323
  try {
175
324
  const mark = accountMark();
@@ -460,6 +609,31 @@ const AGENT_NAME = process.env.SOLONGATE_AGENT_NAME || process.argv[3] || AGENT_
460
609
  // lands even if Claude Code kills the parent hook mid-write during a concurrent
461
610
  // denial burst. Handled FIRST and exits immediately — the child never loads the
462
611
  // heavy guard logic below.
612
+ // Survivable CLOUD writer, same idea. A denial used to `await` its audit POST
613
+ // before telling the agent it was blocked, so the agent sat waiting on a network
614
+ // round trip to be told "no": measured at 1643ms per denial against 78ms with
615
+ // the API unreachable. The record still has to land, so it is handed to a
616
+ // detached child rather than dropped — the verdict goes out at once and the POST
617
+ // finishes on its own.
618
+ {
619
+ const _ai = process.argv.indexOf('--sg-audit-post');
620
+ if (_ai !== -1) {
621
+ try {
622
+ const _raw = Buffer.from(process.argv[_ai + 1] || '', 'base64').toString('utf-8');
623
+ const _p = JSON.parse(_raw);
624
+ if (_p && _p.url && _p.body) {
625
+ fetch(_p.url, {
626
+ method: 'POST',
627
+ headers: { 'Content-Type': 'application/json', ...(_p.headers || {}) },
628
+ body: JSON.stringify(_p.body),
629
+ signal: AbortSignal.timeout(10000),
630
+ }).catch(() => {}).finally(() => process.exit(0));
631
+ setTimeout(() => process.exit(0), 11000).unref();
632
+ } else process.exit(0);
633
+ } catch { process.exit(0); }
634
+ }
635
+ }
636
+
463
637
  {
464
638
  const _wi = process.argv.indexOf('--sg-log-write');
465
639
  if (_wi !== -1) {
@@ -2411,15 +2585,21 @@ function readReferencedFiles(args, cwd) {
2411
2585
  return out;
2412
2586
  }
2413
2587
 
2414
- // Read stdin SYNCHRONOUSLY (fd 0) instead of via the process.stdin stream. On
2415
- // Windows + Node 24, calling process.exit() from inside the stdin stream's 'end'
2416
- // callback aborts with `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING),
2417
- // file src\win\async.c` libuv double-closes the stdin pipe handle mid-teardown,
2418
- // the hook crashes before its exit code lands, and Claude Code treats the DENY as
2419
- // a non-blocking hook failure (so the block never applies). Reading fd 0 to EOF
2420
- // synchronously never creates that pipe handle, and the exit below no longer runs
2421
- // inside a stream callback, so the loop tears down cleanly.
2422
- if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
2588
+ // The payload was read at the top of this file, synchronously from fd 0, before
2589
+ // the Go guard was offered the call — see SG_STDIN. It is reused rather than
2590
+ // re-read for two reasons: fd 0 is already at EOF so a second read would return
2591
+ // nothing, and the fallback only means anything if Node still holds what the
2592
+ // binary was given.
2593
+ //
2594
+ // Reading fd 0 synchronously rather than through the process.stdin stream is
2595
+ // itself deliberate. On Windows + Node 24, calling process.exit() from inside the
2596
+ // stdin stream's 'end' callback aborts with `Assertion failed: !(handle->flags &
2597
+ // UV_HANDLE_CLOSING), file src\win\async.c` — libuv double-closes the stdin pipe
2598
+ // handle mid-teardown, the hook crashes before its exit code lands, and Claude
2599
+ // Code treats the DENY as a non-blocking hook failure (so the block never
2600
+ // applies). Reading fd 0 to EOF never creates that pipe handle, and the exit
2601
+ // below no longer runs inside a stream callback, so the loop tears down cleanly.
2602
+ if (!REFRESH_MODE) { input += SG_STDIN; }
2423
2603
  ;(async () => {
2424
2604
  // Safety backstop ONLY: the normal path exits by natural drain (~1ms after work
2425
2605
  // finishes). If the loop somehow fails to drain, force-exit — 8s is well past every
@@ -2523,12 +2703,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
2523
2703
  };
2524
2704
  try { writeLocalLog(_sec, { ts: new Date().toISOString(), ..._logEntry }); } catch {}
2525
2705
  try {
2526
- if (!localLogsOnly(_sec)) await fetch(API_URL + '/api/v1/audit-logs', {
2527
- method: 'POST',
2528
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
2529
- body: JSON.stringify(_logEntry),
2530
- signal: AbortSignal.timeout(3000),
2531
- });
2706
+ if (!localLogsOnly(_sec)) postAuditDetached(_logEntry);
2532
2707
  } catch {}
2533
2708
  // Not on Codex: its block reason IS the hook's stderr (see the ROUTE
2534
2709
  // write at the end of the slow path for the full rationale).
@@ -2676,18 +2851,13 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
2676
2851
  try { writeDenyFlag(toolName); } catch {}
2677
2852
  writeLocalLog(securityCfg, { ts: new Date().toISOString(), tool: toolName, arguments: args, decision: 'DENY', reason: 'ghost path (hidden from agent)', permission: guessPermission(toolName), source: `${AGENT_TYPE}-guard`, agent_id: AGENT_TYPE, agent_name: AGENT_NAME, session_id: call.sessionId, evaluation_time_ms: Date.now() - _evalStart });
2678
2853
  try {
2679
- if (!localLogsOnly(securityCfg)) await fetch(API_URL + '/api/v1/audit-logs', {
2680
- method: 'POST',
2681
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
2682
- body: JSON.stringify({
2683
- tool: toolName, arguments: args, decision: 'DENY',
2684
- reason: 'ghost path (hidden from agent)',
2685
- permission: guessPermission(toolName),
2686
- source: `${AGENT_TYPE}-guard`, agent_id: AGENT_TYPE, agent_name: AGENT_NAME,
2687
- session_id: call.sessionId,
2688
- evaluation_time_ms: Date.now() - _evalStart,
2689
- }),
2690
- signal: AbortSignal.timeout(3000),
2854
+ if (!localLogsOnly(securityCfg)) postAuditDetached({
2855
+ tool: toolName, arguments: args, decision: 'DENY',
2856
+ reason: 'ghost path (hidden from agent)',
2857
+ permission: guessPermission(toolName),
2858
+ source: `${AGENT_TYPE}-guard`, agent_id: AGENT_TYPE, agent_name: AGENT_NAME,
2859
+ session_id: call.sessionId,
2860
+ evaluation_time_ms: Date.now() - _evalStart,
2691
2861
  });
2692
2862
  } catch {}
2693
2863
  // NOT maybeSelfUpdate() first: see the deny at the end of this flow.
@@ -2819,12 +2989,7 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
2819
2989
  writeLocalLog(securityCfg, { ts: new Date().toISOString(), ...logEntry });
2820
2990
  // PI hook layer removed — piResult fields no longer attached.
2821
2991
  // Local-only mode: keep the log on the user's machine, skip the cloud.
2822
- if (!localLogsOnly(securityCfg)) await fetch(API_URL + '/api/v1/audit-logs', {
2823
- method: 'POST',
2824
- headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
2825
- body: JSON.stringify(logEntry),
2826
- signal: AbortSignal.timeout(3000),
2827
- });
2992
+ if (!localLogsOnly(securityCfg)) postAuditDetached(logEntry);
2828
2993
  } catch {}
2829
2994
  }
2830
2995
  writeDenyFlag(toolName);
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.83.19",
3
+ "version": "0.83.21",
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": {
7
- "solongate": "./dist/index.js",
8
- "solongate-proxy": "./dist/index.js",
9
- "solongate-audit": "./dist/audit/index.js",
10
- "proxy": "./dist/index.js"
7
+ "solongate": "./dist/cli-launch.js",
8
+ "solongate-proxy": "./dist/cli-launch.js",
9
+ "proxy": "./dist/cli-launch.js",
10
+ "solongate-audit": "./dist/audit/index.js"
11
11
  },
12
12
  "main": "./dist/lib.js",
13
13
  "types": "./dist/lib.d.ts",
@@ -32,7 +32,9 @@
32
32
  "build:hooks": "node scripts/bundle-hooks.mjs",
33
33
  "dev": "tsx src/index.ts",
34
34
  "typecheck": "tsc --noEmit",
35
- "clean": "rm -rf dist .turbo"
35
+ "clean": "rm -rf dist .turbo platforms",
36
+ "test": "node test/run-all.mjs",
37
+ "build:go": "node scripts/build-go-binaries.mjs"
36
38
  },
37
39
  "keywords": [
38
40
  "ai-tool-security",
@@ -58,6 +60,14 @@
58
60
  "engines": {
59
61
  "node": ">=20.0.0"
60
62
  },
63
+ "optionalDependencies": {
64
+ "@solongate/guard-linux-x64": "0.83.21",
65
+ "@solongate/guard-linux-arm64": "0.83.21",
66
+ "@solongate/guard-darwin-x64": "0.83.21",
67
+ "@solongate/guard-darwin-arm64": "0.83.21",
68
+ "@solongate/guard-win32-x64": "0.83.21",
69
+ "@solongate/guard-win32-arm64": "0.83.21"
70
+ },
61
71
  "dependencies": {
62
72
  "@modelcontextprotocol/sdk": "^1.26.0",
63
73
  "ink": "^5.0.1",