@solongate/proxy 0.83.10 → 0.83.12
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 +62 -7
- package/dist/global-install.d.ts +7 -0
- package/dist/global-install.js +87 -9
- package/dist/index.js +123 -23
- package/dist/tui/index.js +109 -22
- 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) {
|
|
@@ -122,6 +131,20 @@ function codexHooksStatus() {
|
|
|
122
131
|
}
|
|
123
132
|
return { registered, trusted, disabled };
|
|
124
133
|
}
|
|
134
|
+
function isOpencodeGuardInstalled() {
|
|
135
|
+
try {
|
|
136
|
+
return readFileSync(globalPaths().opencodePluginPath, "utf-8").includes("tool.execute.before");
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function opencodeDetected() {
|
|
142
|
+
try {
|
|
143
|
+
return existsSync(globalPaths().opencodeDir);
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
125
148
|
function installedGuardVersion() {
|
|
126
149
|
try {
|
|
127
150
|
const p = globalPaths();
|
|
@@ -132,6 +155,16 @@ function installedGuardVersion() {
|
|
|
132
155
|
return null;
|
|
133
156
|
}
|
|
134
157
|
}
|
|
158
|
+
function isGuardInstalled() {
|
|
159
|
+
try {
|
|
160
|
+
const p = globalPaths();
|
|
161
|
+
if (!existsSync(p.settingsPath)) return false;
|
|
162
|
+
const s = JSON.parse(readFileSync(p.settingsPath, "utf-8"));
|
|
163
|
+
return !!s.hooks && JSON.stringify(s.hooks).includes(".solongate");
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
135
168
|
|
|
136
169
|
// src/api-client/client.ts
|
|
137
170
|
var DEFAULT_API_URL = "https://api.solongate.com";
|
|
@@ -267,7 +300,7 @@ async function request(method, path, opts = {}) {
|
|
|
267
300
|
}
|
|
268
301
|
|
|
269
302
|
// src/api-client/device-login.ts
|
|
270
|
-
import { spawn } from "child_process";
|
|
303
|
+
import { spawn as spawn2 } from "child_process";
|
|
271
304
|
|
|
272
305
|
// src/api-client/auth.ts
|
|
273
306
|
var auth_exports = {};
|
|
@@ -1236,18 +1269,40 @@ async function collectChecks() {
|
|
|
1236
1269
|
} catch {
|
|
1237
1270
|
}
|
|
1238
1271
|
}
|
|
1272
|
+
const claudeReg = isGuardInstalled();
|
|
1273
|
+
checks.push({
|
|
1274
|
+
name: "Claude hooks",
|
|
1275
|
+
ok: claudeReg,
|
|
1276
|
+
detail: claudeReg ? "guard registered" : "guard NOT registered - run `solongate repair`"
|
|
1277
|
+
});
|
|
1278
|
+
if (existsSync4(globalPaths().antigravityDir)) {
|
|
1279
|
+
const reg = existsSync4(globalPaths().antigravityHooksPath);
|
|
1280
|
+
checks.push({
|
|
1281
|
+
name: "agy hooks",
|
|
1282
|
+
ok: reg,
|
|
1283
|
+
detail: reg ? "guard registered" : "guard NOT registered - run `solongate repair`"
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1239
1286
|
if (codexDetected()) {
|
|
1240
1287
|
const cx = codexHooksStatus();
|
|
1241
1288
|
if (!cx.registered) {
|
|
1242
|
-
checks.push({ name: "
|
|
1289
|
+
checks.push({ name: "Codex hooks", ok: false, detail: "guard NOT registered - run `solongate repair`" });
|
|
1243
1290
|
} else if (cx.disabled) {
|
|
1244
|
-
checks.push({ name: "
|
|
1291
|
+
checks.push({ name: "Codex hooks", ok: false, detail: "hooks disabled in ~/.codex/config.toml ([features] hooks = false)" });
|
|
1245
1292
|
} else if (!cx.trusted) {
|
|
1246
|
-
checks.push({ name: "
|
|
1293
|
+
checks.push({ name: "Codex hooks", ok: "warn", detail: "registered - run `/hooks` in Codex once and trust them (Codex skips untrusted hooks)" });
|
|
1247
1294
|
} else {
|
|
1248
|
-
checks.push({ name: "
|
|
1295
|
+
checks.push({ name: "Codex hooks", ok: true, detail: "registered + trusted" });
|
|
1249
1296
|
}
|
|
1250
1297
|
}
|
|
1298
|
+
if (opencodeDetected()) {
|
|
1299
|
+
const reg = isOpencodeGuardInstalled();
|
|
1300
|
+
checks.push({
|
|
1301
|
+
name: "OpenCode hooks",
|
|
1302
|
+
ok: reg,
|
|
1303
|
+
detail: reg ? "guard registered (not active under `opencode --pure`)" : "guard NOT registered - run `solongate repair`"
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1251
1306
|
try {
|
|
1252
1307
|
const raw = readFileSync5(join4(homedir4(), ".solongate", ".key-rejected.json"), "utf-8");
|
|
1253
1308
|
const m = JSON.parse(raw);
|
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.`);
|
|
@@ -381,17 +439,19 @@ function repairQuiet() {
|
|
|
381
439
|
const before = [
|
|
382
440
|
line("guard hook file", has(guardFile), "present", "MISSING"),
|
|
383
441
|
line("cloud credential", has(p.configPath), "present", "MISSING"),
|
|
384
|
-
line("Claude
|
|
385
|
-
line("
|
|
386
|
-
line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered")
|
|
442
|
+
line("Claude hooks", isGuardInstalled(), "guard registered", "guard NOT registered"),
|
|
443
|
+
line("agy hooks", has(p.antigravityHooksPath), "guard registered", "guard NOT registered"),
|
|
444
|
+
line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered"),
|
|
445
|
+
line("OpenCode hooks", isOpencodeGuardInstalled(), "guard registered", "guard NOT registered")
|
|
387
446
|
];
|
|
388
447
|
const r = installGlobalQuiet();
|
|
389
448
|
if (!r.ok) return { ok: false, message: r.message, before, after: [], notes: [] };
|
|
390
449
|
const after = [
|
|
391
450
|
{ label: "guard hook file", ok: true, detail: `present (v${installedGuardVersion() ?? "?"})` },
|
|
392
|
-
line("Claude
|
|
393
|
-
line("
|
|
394
|
-
line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered")
|
|
451
|
+
line("Claude hooks", isGuardInstalled(), "guard registered", "NOT registered"),
|
|
452
|
+
line("agy hooks", has(p.antigravityHooksPath), "guard registered", "NOT registered"),
|
|
453
|
+
line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered"),
|
|
454
|
+
line("OpenCode hooks", isOpencodeGuardInstalled(), "guard registered", "NOT registered")
|
|
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.`);
|
|
@@ -6588,17 +6648,19 @@ function repairQuiet() {
|
|
|
6588
6648
|
const before = [
|
|
6589
6649
|
line("guard hook file", has(guardFile), "present", "MISSING"),
|
|
6590
6650
|
line("cloud credential", has(p.configPath), "present", "MISSING"),
|
|
6591
|
-
line("Claude
|
|
6592
|
-
line("
|
|
6593
|
-
line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered")
|
|
6651
|
+
line("Claude hooks", isGuardInstalled(), "guard registered", "guard NOT registered"),
|
|
6652
|
+
line("agy hooks", has(p.antigravityHooksPath), "guard registered", "guard NOT registered"),
|
|
6653
|
+
line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered"),
|
|
6654
|
+
line("OpenCode hooks", isOpencodeGuardInstalled(), "guard registered", "guard NOT registered")
|
|
6594
6655
|
];
|
|
6595
6656
|
const r = installGlobalQuiet();
|
|
6596
6657
|
if (!r.ok) return { ok: false, message: r.message, before, after: [], notes: [] };
|
|
6597
6658
|
const after = [
|
|
6598
6659
|
{ label: "guard hook file", ok: true, detail: `present (v${installedGuardVersion() ?? "?"})` },
|
|
6599
|
-
line("Claude
|
|
6600
|
-
line("
|
|
6601
|
-
line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered")
|
|
6660
|
+
line("Claude hooks", isGuardInstalled(), "guard registered", "NOT registered"),
|
|
6661
|
+
line("agy hooks", has(p.antigravityHooksPath), "guard registered", "NOT registered"),
|
|
6662
|
+
line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered"),
|
|
6663
|
+
line("OpenCode hooks", isOpencodeGuardInstalled(), "guard registered", "NOT registered")
|
|
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();
|
|
@@ -11529,18 +11607,40 @@ async function collectChecks() {
|
|
|
11529
11607
|
} catch {
|
|
11530
11608
|
}
|
|
11531
11609
|
}
|
|
11610
|
+
const claudeReg = isGuardInstalled();
|
|
11611
|
+
checks.push({
|
|
11612
|
+
name: "Claude hooks",
|
|
11613
|
+
ok: claudeReg,
|
|
11614
|
+
detail: claudeReg ? "guard registered" : "guard NOT registered - run `solongate repair`"
|
|
11615
|
+
});
|
|
11616
|
+
if (existsSync6(globalPaths().antigravityDir)) {
|
|
11617
|
+
const reg = existsSync6(globalPaths().antigravityHooksPath);
|
|
11618
|
+
checks.push({
|
|
11619
|
+
name: "agy hooks",
|
|
11620
|
+
ok: reg,
|
|
11621
|
+
detail: reg ? "guard registered" : "guard NOT registered - run `solongate repair`"
|
|
11622
|
+
});
|
|
11623
|
+
}
|
|
11532
11624
|
if (codexDetected()) {
|
|
11533
11625
|
const cx = codexHooksStatus();
|
|
11534
11626
|
if (!cx.registered) {
|
|
11535
|
-
checks.push({ name: "
|
|
11627
|
+
checks.push({ name: "Codex hooks", ok: false, detail: "guard NOT registered - run `solongate repair`" });
|
|
11536
11628
|
} else if (cx.disabled) {
|
|
11537
|
-
checks.push({ name: "
|
|
11629
|
+
checks.push({ name: "Codex hooks", ok: false, detail: "hooks disabled in ~/.codex/config.toml ([features] hooks = false)" });
|
|
11538
11630
|
} else if (!cx.trusted) {
|
|
11539
|
-
checks.push({ name: "
|
|
11631
|
+
checks.push({ name: "Codex hooks", ok: "warn", detail: "registered - run `/hooks` in Codex once and trust them (Codex skips untrusted hooks)" });
|
|
11540
11632
|
} else {
|
|
11541
|
-
checks.push({ name: "
|
|
11633
|
+
checks.push({ name: "Codex hooks", ok: true, detail: "registered + trusted" });
|
|
11542
11634
|
}
|
|
11543
11635
|
}
|
|
11636
|
+
if (opencodeDetected()) {
|
|
11637
|
+
const reg = isOpencodeGuardInstalled();
|
|
11638
|
+
checks.push({
|
|
11639
|
+
name: "OpenCode hooks",
|
|
11640
|
+
ok: reg,
|
|
11641
|
+
detail: reg ? "guard registered (not active under `opencode --pure`)" : "guard NOT registered - run `solongate repair`"
|
|
11642
|
+
});
|
|
11643
|
+
}
|
|
11544
11644
|
try {
|
|
11545
11645
|
const raw = readFileSync10(join12(homedir10(), ".solongate", ".key-rejected.json"), "utf-8");
|
|
11546
11646
|
const m = JSON.parse(raw);
|
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,47 @@ 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
|
+
}
|
|
388
|
+
function opencodeDetected() {
|
|
389
|
+
try {
|
|
390
|
+
return existsSync2(globalPaths().opencodeDir);
|
|
391
|
+
} catch {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
341
395
|
function repairQuiet() {
|
|
342
396
|
const p = globalPaths();
|
|
343
397
|
const has = (f) => existsSync2(f);
|
|
@@ -346,17 +400,19 @@ function repairQuiet() {
|
|
|
346
400
|
const before = [
|
|
347
401
|
line("guard hook file", has(guardFile), "present", "MISSING"),
|
|
348
402
|
line("cloud credential", has(p.configPath), "present", "MISSING"),
|
|
349
|
-
line("Claude
|
|
350
|
-
line("
|
|
351
|
-
line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered")
|
|
403
|
+
line("Claude hooks", isGuardInstalled(), "guard registered", "guard NOT registered"),
|
|
404
|
+
line("agy hooks", has(p.antigravityHooksPath), "guard registered", "guard NOT registered"),
|
|
405
|
+
line("Codex hooks", isCodexGuardInstalled(), "guard registered", "guard NOT registered"),
|
|
406
|
+
line("OpenCode hooks", isOpencodeGuardInstalled(), "guard registered", "guard NOT registered")
|
|
352
407
|
];
|
|
353
408
|
const r = installGlobalQuiet();
|
|
354
409
|
if (!r.ok) return { ok: false, message: r.message, before, after: [], notes: [] };
|
|
355
410
|
const after = [
|
|
356
411
|
{ label: "guard hook file", ok: true, detail: `present (v${installedGuardVersion() ?? "?"})` },
|
|
357
|
-
line("Claude
|
|
358
|
-
line("
|
|
359
|
-
line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered")
|
|
412
|
+
line("Claude hooks", isGuardInstalled(), "guard registered", "NOT registered"),
|
|
413
|
+
line("agy hooks", has(p.antigravityHooksPath), "guard registered", "NOT registered"),
|
|
414
|
+
line("Codex hooks", isCodexGuardInstalled(), "guard registered", "NOT registered"),
|
|
415
|
+
line("OpenCode hooks", isOpencodeGuardInstalled(), "guard registered", "NOT registered")
|
|
360
416
|
];
|
|
361
417
|
const notes = [];
|
|
362
418
|
const cx = codexHooksStatus();
|
|
@@ -425,6 +481,11 @@ function installGlobalQuiet() {
|
|
|
425
481
|
installCodexGuard(p, p.hooksDir);
|
|
426
482
|
} catch {
|
|
427
483
|
}
|
|
484
|
+
try {
|
|
485
|
+
installOpencodeGuard(p);
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
warmPolicyCache(p.hooksDir, ["claude-code", "codex", "antigravity", "opencode"]);
|
|
428
489
|
if (process.env["SOLONGATE_NO_OS_LOCK"] !== "1") lockProtected();
|
|
429
490
|
return { ok: true, message: "guard installed (open a new session)" };
|
|
430
491
|
} catch (e) {
|
|
@@ -472,6 +533,10 @@ function uninstallGlobalQuiet() {
|
|
|
472
533
|
removeCodexGuard(p);
|
|
473
534
|
} catch {
|
|
474
535
|
}
|
|
536
|
+
try {
|
|
537
|
+
removeOpencodeGuard(p);
|
|
538
|
+
} catch {
|
|
539
|
+
}
|
|
475
540
|
if (!existsSync2(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
|
|
476
541
|
const s = JSON.parse(readFileSync3(p.settingsPath, "utf-8"));
|
|
477
542
|
delete s.hooks;
|
|
@@ -1146,12 +1211,12 @@ async function request(method, path, opts = {}) {
|
|
|
1146
1211
|
}
|
|
1147
1212
|
|
|
1148
1213
|
// src/api-client/device-login.ts
|
|
1149
|
-
import { spawn } from "child_process";
|
|
1214
|
+
import { spawn as spawn2 } from "child_process";
|
|
1150
1215
|
function openBrowser(url) {
|
|
1151
1216
|
try {
|
|
1152
1217
|
const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
1153
1218
|
const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
|
|
1154
|
-
const child =
|
|
1219
|
+
const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
|
|
1155
1220
|
child.on("error", () => {
|
|
1156
1221
|
});
|
|
1157
1222
|
child.unref();
|
|
@@ -1433,24 +1498,24 @@ function list4() {
|
|
|
1433
1498
|
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
1499
|
|
|
1435
1500
|
// src/tui/notify.ts
|
|
1436
|
-
import { spawn as
|
|
1501
|
+
import { spawn as spawn3 } from "child_process";
|
|
1437
1502
|
function desktopNotify(title, msg) {
|
|
1438
1503
|
try {
|
|
1439
1504
|
if (process.platform === "linux") {
|
|
1440
|
-
const p =
|
|
1505
|
+
const p = spawn3("notify-send", ["-a", "SolonGate", title, msg], { stdio: "ignore", detached: true });
|
|
1441
1506
|
p.on("error", () => {
|
|
1442
1507
|
});
|
|
1443
1508
|
p.unref();
|
|
1444
1509
|
} else if (process.platform === "win32") {
|
|
1445
1510
|
const q = (s) => s.replace(/'/g, "''");
|
|
1446
1511
|
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 =
|
|
1512
|
+
const p = spawn3("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], { stdio: "ignore", detached: true, windowsHide: true });
|
|
1448
1513
|
p.on("error", () => {
|
|
1449
1514
|
});
|
|
1450
1515
|
p.unref();
|
|
1451
1516
|
} else if (process.platform === "darwin") {
|
|
1452
1517
|
const esc = (s) => s.replace(/"/g, '\\"');
|
|
1453
|
-
const p =
|
|
1518
|
+
const p = spawn3("osascript", ["-e", `display notification "${esc(msg)}" with title "${esc(title)}"`], { stdio: "ignore", detached: true });
|
|
1454
1519
|
p.on("error", () => {
|
|
1455
1520
|
});
|
|
1456
1521
|
p.unref();
|
|
@@ -4411,18 +4476,40 @@ async function collectChecks() {
|
|
|
4411
4476
|
} catch {
|
|
4412
4477
|
}
|
|
4413
4478
|
}
|
|
4479
|
+
const claudeReg = isGuardInstalled();
|
|
4480
|
+
checks.push({
|
|
4481
|
+
name: "Claude hooks",
|
|
4482
|
+
ok: claudeReg,
|
|
4483
|
+
detail: claudeReg ? "guard registered" : "guard NOT registered - run `solongate repair`"
|
|
4484
|
+
});
|
|
4485
|
+
if (existsSync4(globalPaths().antigravityDir)) {
|
|
4486
|
+
const reg = existsSync4(globalPaths().antigravityHooksPath);
|
|
4487
|
+
checks.push({
|
|
4488
|
+
name: "agy hooks",
|
|
4489
|
+
ok: reg,
|
|
4490
|
+
detail: reg ? "guard registered" : "guard NOT registered - run `solongate repair`"
|
|
4491
|
+
});
|
|
4492
|
+
}
|
|
4414
4493
|
if (codexDetected()) {
|
|
4415
4494
|
const cx = codexHooksStatus();
|
|
4416
4495
|
if (!cx.registered) {
|
|
4417
|
-
checks.push({ name: "
|
|
4496
|
+
checks.push({ name: "Codex hooks", ok: false, detail: "guard NOT registered - run `solongate repair`" });
|
|
4418
4497
|
} else if (cx.disabled) {
|
|
4419
|
-
checks.push({ name: "
|
|
4498
|
+
checks.push({ name: "Codex hooks", ok: false, detail: "hooks disabled in ~/.codex/config.toml ([features] hooks = false)" });
|
|
4420
4499
|
} else if (!cx.trusted) {
|
|
4421
|
-
checks.push({ name: "
|
|
4500
|
+
checks.push({ name: "Codex hooks", ok: "warn", detail: "registered - run `/hooks` in Codex once and trust them (Codex skips untrusted hooks)" });
|
|
4422
4501
|
} else {
|
|
4423
|
-
checks.push({ name: "
|
|
4502
|
+
checks.push({ name: "Codex hooks", ok: true, detail: "registered + trusted" });
|
|
4424
4503
|
}
|
|
4425
4504
|
}
|
|
4505
|
+
if (opencodeDetected()) {
|
|
4506
|
+
const reg = isOpencodeGuardInstalled();
|
|
4507
|
+
checks.push({
|
|
4508
|
+
name: "OpenCode hooks",
|
|
4509
|
+
ok: reg,
|
|
4510
|
+
detail: reg ? "guard registered (not active under `opencode --pure`)" : "guard NOT registered - run `solongate repair`"
|
|
4511
|
+
});
|
|
4512
|
+
}
|
|
4426
4513
|
try {
|
|
4427
4514
|
const raw = readFileSync5(join7(homedir7(), ".solongate", ".key-rejected.json"), "utf-8");
|
|
4428
4515
|
const m = JSON.parse(raw);
|
|
@@ -4446,7 +4533,7 @@ async function collectChecks() {
|
|
|
4446
4533
|
}
|
|
4447
4534
|
|
|
4448
4535
|
// src/logs-server-daemon.ts
|
|
4449
|
-
import { spawn as
|
|
4536
|
+
import { spawn as spawn4 } from "child_process";
|
|
4450
4537
|
import { mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
4451
4538
|
import { homedir as homedir8 } from "os";
|
|
4452
4539
|
import { dirname as dirname2, join as join8 } from "path";
|
|
@@ -4494,7 +4581,7 @@ function startLogsServerDaemon() {
|
|
|
4494
4581
|
mkdirSync5(DIR, { recursive: true });
|
|
4495
4582
|
const log = openSync2(LOG_FILE, "a");
|
|
4496
4583
|
const cli = join8(dirname2(fileURLToPath2(import.meta.url)), "index.js");
|
|
4497
|
-
const p =
|
|
4584
|
+
const p = spawn4(process.execPath, [cli, "logs-server"], {
|
|
4498
4585
|
detached: true,
|
|
4499
4586
|
stdio: ["ignore", log, log],
|
|
4500
4587
|
windowsHide: true,
|
|
@@ -4524,7 +4611,7 @@ function stopLogsServerDaemon() {
|
|
|
4524
4611
|
}
|
|
4525
4612
|
|
|
4526
4613
|
// src/self-update.ts
|
|
4527
|
-
import { execFile, execFileSync as execFileSync2, spawn as
|
|
4614
|
+
import { execFile, execFileSync as execFileSync2, spawn as spawn5 } from "child_process";
|
|
4528
4615
|
import { access, mkdirSync as mkdirSync6, openSync as openSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync7, constants as FS } from "fs";
|
|
4529
4616
|
import { homedir as homedir9 } from "os";
|
|
4530
4617
|
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.12",
|
|
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": {
|