@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.
- package/dist/commands/index.js +12 -3
- package/dist/global-install.d.ts +7 -0
- package/dist/global-install.js +83 -5
- package/dist/index.js +93 -15
- package/dist/tui/index.js +72 -14
- package/hooks/guard.bundled.mjs +15 -1
- package/hooks/guard.mjs +16 -1
- package/hooks/opencode-plugin.mjs +151 -0
- package/package.json +1 -1
package/dist/commands/index.js
CHANGED
|
@@ -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 = {};
|
package/dist/global-install.d.ts
CHANGED
|
@@ -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.
|
package/dist/global-install.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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";
|
package/hooks/guard.bundled.mjs
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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.
|
|
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": {
|