@solongate/proxy 0.83.57 → 0.83.59
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/global-install.js +5 -1
- package/dist/index.js +5 -1
- package/dist/tui/index.js +5 -1
- package/hooks/conversation.mjs +103 -42
- package/hooks/opencode-plugin.mjs +73 -0
- package/package.json +7 -7
package/dist/global-install.js
CHANGED
|
@@ -412,6 +412,9 @@ var ANTIGRAVITY_GROUP = "solongate-guard";
|
|
|
412
412
|
function antigravityHookCommand(guardAbs) {
|
|
413
413
|
return hookCommandFor(dirname(guardAbs), basename(guardAbs), "antigravity", "Antigravity");
|
|
414
414
|
}
|
|
415
|
+
function antigravityConversationCommand(guardAbs) {
|
|
416
|
+
return hookCommandFor(dirname(guardAbs), "conversation.mjs", "antigravity", "Antigravity");
|
|
417
|
+
}
|
|
415
418
|
function installAntigravityGuard(p, guardAbs) {
|
|
416
419
|
mkdirSync(p.antigravityDir, { recursive: true });
|
|
417
420
|
let existing = {};
|
|
@@ -427,7 +430,8 @@ function installAntigravityGuard(p, guardAbs) {
|
|
|
427
430
|
const merged = {
|
|
428
431
|
...existing,
|
|
429
432
|
[ANTIGRAVITY_GROUP]: {
|
|
430
|
-
PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: antigravityHookCommand(guardAbs) }] }]
|
|
433
|
+
PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: antigravityHookCommand(guardAbs) }] }],
|
|
434
|
+
Stop: [{ matcher: "", hooks: [{ type: "command", command: antigravityConversationCommand(guardAbs) }] }]
|
|
431
435
|
}
|
|
432
436
|
};
|
|
433
437
|
writeFileSync(p.antigravityHooksPath, JSON.stringify(merged, null, 2) + "\n");
|
package/dist/index.js
CHANGED
|
@@ -6644,6 +6644,9 @@ function installGoBinaries(binDir) {
|
|
|
6644
6644
|
function antigravityHookCommand(guardAbs) {
|
|
6645
6645
|
return hookCommandFor(dirname(guardAbs), basename(guardAbs), "antigravity", "Antigravity");
|
|
6646
6646
|
}
|
|
6647
|
+
function antigravityConversationCommand(guardAbs) {
|
|
6648
|
+
return hookCommandFor(dirname(guardAbs), "conversation.mjs", "antigravity", "Antigravity");
|
|
6649
|
+
}
|
|
6647
6650
|
function installAntigravityGuard(p, guardAbs) {
|
|
6648
6651
|
mkdirSync3(p.antigravityDir, { recursive: true });
|
|
6649
6652
|
let existing = {};
|
|
@@ -6659,7 +6662,8 @@ function installAntigravityGuard(p, guardAbs) {
|
|
|
6659
6662
|
const merged = {
|
|
6660
6663
|
...existing,
|
|
6661
6664
|
[ANTIGRAVITY_GROUP]: {
|
|
6662
|
-
PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: antigravityHookCommand(guardAbs) }] }]
|
|
6665
|
+
PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: antigravityHookCommand(guardAbs) }] }],
|
|
6666
|
+
Stop: [{ matcher: "", hooks: [{ type: "command", command: antigravityConversationCommand(guardAbs) }] }]
|
|
6663
6667
|
}
|
|
6664
6668
|
};
|
|
6665
6669
|
writeFileSync3(p.antigravityHooksPath, JSON.stringify(merged, null, 2) + "\n");
|
package/dist/tui/index.js
CHANGED
|
@@ -422,6 +422,9 @@ function installGoBinaries(binDir) {
|
|
|
422
422
|
function antigravityHookCommand(guardAbs) {
|
|
423
423
|
return hookCommandFor(dirname(guardAbs), basename(guardAbs), "antigravity", "Antigravity");
|
|
424
424
|
}
|
|
425
|
+
function antigravityConversationCommand(guardAbs) {
|
|
426
|
+
return hookCommandFor(dirname(guardAbs), "conversation.mjs", "antigravity", "Antigravity");
|
|
427
|
+
}
|
|
425
428
|
function installAntigravityGuard(p, guardAbs) {
|
|
426
429
|
mkdirSync(p.antigravityDir, { recursive: true });
|
|
427
430
|
let existing = {};
|
|
@@ -437,7 +440,8 @@ function installAntigravityGuard(p, guardAbs) {
|
|
|
437
440
|
const merged = {
|
|
438
441
|
...existing,
|
|
439
442
|
[ANTIGRAVITY_GROUP]: {
|
|
440
|
-
PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: antigravityHookCommand(guardAbs) }] }]
|
|
443
|
+
PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: antigravityHookCommand(guardAbs) }] }],
|
|
444
|
+
Stop: [{ matcher: "", hooks: [{ type: "command", command: antigravityConversationCommand(guardAbs) }] }]
|
|
441
445
|
}
|
|
442
446
|
};
|
|
443
447
|
writeFileSync2(p.antigravityHooksPath, JSON.stringify(merged, null, 2) + "\n");
|
package/hooks/conversation.mjs
CHANGED
|
@@ -16,12 +16,10 @@
|
|
|
16
16
|
* UserPromptSubmit → user_input what the person typed
|
|
17
17
|
* Stop → last_assistant_message what the agent said back
|
|
18
18
|
*
|
|
19
|
-
* WHAT IS SENT, AND WHEN NOTHING IS.
|
|
19
|
+
* WHAT IS SENT, AND WHEN NOTHING IS. Two gates, cheapest first:
|
|
20
20
|
*
|
|
21
21
|
* 1. No credential → nothing. The same rule every hook here follows.
|
|
22
|
-
* 2.
|
|
23
|
-
* on their machine has asked about this more than about anything else.
|
|
24
|
-
* 3. Not in a fleet → the SERVER drops it. This hook cannot know whether a
|
|
22
|
+
* 2. Not in a fleet → the SERVER drops it. This hook cannot know whether a
|
|
25
23
|
* grant exists, and asking would be a round trip before every send; the
|
|
26
24
|
* API answers 204 and the row is never written. That check is on the
|
|
27
25
|
* server deliberately: a check that lived here would live on the machine
|
|
@@ -43,7 +41,7 @@ import { resolve } from 'node:path';
|
|
|
43
41
|
import { homedir } from 'node:os';
|
|
44
42
|
|
|
45
43
|
// Bump on every change to this file, alongside the other hooks.
|
|
46
|
-
const HOOK_VERSION =
|
|
44
|
+
const HOOK_VERSION = 3;
|
|
47
45
|
|
|
48
46
|
const AGENT_ID = (process.env.SOLONGATE_AGENT_ID || process.argv[2] || 'default').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
49
47
|
|
|
@@ -108,19 +106,80 @@ function redact(text, cache) {
|
|
|
108
106
|
return { text: out, masked };
|
|
109
107
|
}
|
|
110
108
|
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
// There is no local-logs gate here any more, and its absence is deliberate.
|
|
110
|
+
//
|
|
111
|
+
// It read the local-log setting and sent nothing when it looked on — which it
|
|
112
|
+
// did for anybody who had ever typed a folder into that field, because the
|
|
113
|
+
// predicate counted the PATH as well as the switch and the path survives
|
|
114
|
+
// turning the feature off. So the record was silently empty on accounts whose
|
|
115
|
+
// settings said local logging was off, with nothing anywhere to say so.
|
|
116
|
+
//
|
|
117
|
+
// Fixing the predicate was the small answer. The gate itself is the wrong idea:
|
|
118
|
+
// this record exists ONLY for a machine in a fleet, where a host is answerable
|
|
119
|
+
// for what runs there, and a setting on the guest's own machine that switches
|
|
120
|
+
// off the host's view of it is a self-disarm — the exact shape this product
|
|
121
|
+
// exists to prevent everywhere else. Local logging decides where a machine
|
|
122
|
+
// keeps its OWN copy; it was never a claim on somebody else's record.
|
|
123
|
+
//
|
|
124
|
+
// What still applies is unchanged and is what actually protects people: no
|
|
125
|
+
// credential, nothing sent; no accepted grant, the server drops it; and secrets
|
|
126
|
+
// are removed before the text leaves the machine.
|
|
119
127
|
|
|
120
128
|
function readStdin() {
|
|
121
129
|
try { return readFileSync(0, 'utf-8'); } catch { return ''; }
|
|
122
130
|
}
|
|
123
131
|
|
|
132
|
+
// send is one turn, redacted, to the API. Both payload shapes end here.
|
|
133
|
+
async function send(sessionId, role, body, agentName) {
|
|
134
|
+
if (!sessionId || !String(body).trim()) return;
|
|
135
|
+
|
|
136
|
+
const cfg = loadGlobalCloudConfig();
|
|
137
|
+
const apiKey = process.env.SOLONGATE_API_KEY || cfg.apiKey || '';
|
|
138
|
+
const apiUrl = process.env.SOLONGATE_API_URL || cfg.apiUrl || 'https://api.solongate.com';
|
|
139
|
+
if (!apiKey) return;
|
|
140
|
+
|
|
141
|
+
const cache = loadPolicyCache();
|
|
142
|
+
const cleaned = redact(body, cache);
|
|
143
|
+
try {
|
|
144
|
+
await fetch(`${apiUrl}/api/v1/conversations`, {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: {
|
|
147
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
148
|
+
'Content-Type': 'application/json',
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
session_id: sessionId,
|
|
152
|
+
role,
|
|
153
|
+
body: cleaned.text,
|
|
154
|
+
redacted: cleaned.masked,
|
|
155
|
+
agent_id: AGENT_ID,
|
|
156
|
+
agent_name: agentName || '',
|
|
157
|
+
source: `${AGENT_ID}-hook`,
|
|
158
|
+
hook_version: HOOK_VERSION,
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
} catch { /* silent: a record that could not be sent must not fail a turn */ }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// sendAntigravity writes both halves of the exchange from one Stop payload.
|
|
165
|
+
//
|
|
166
|
+
// The prompt goes first and the reply second, and they are sent in that order
|
|
167
|
+
// rather than concurrently: the rows are ordered by their timestamps, and two
|
|
168
|
+
// requests in flight at once can land in the other order on a fast link — which
|
|
169
|
+
// would draw the answer above the question.
|
|
170
|
+
async function sendAntigravity(common, stop) {
|
|
171
|
+
const sessionId = common.conversationId || common.conversation_id ||
|
|
172
|
+
common.sessionId || common.session_id || '';
|
|
173
|
+
if (!sessionId) return;
|
|
174
|
+
const name = common.agentName || common.agent_name || 'Antigravity';
|
|
175
|
+
|
|
176
|
+
const prompt = common.lastUserInput || common.last_user_input || '';
|
|
177
|
+
if (String(prompt).trim()) await send(sessionId, 'prompt', prompt, name);
|
|
178
|
+
|
|
179
|
+
const reply = stop ? (stop.finalModelOutput || stop.final_model_output || '') : '';
|
|
180
|
+
if (String(reply).trim()) await send(sessionId, 'reply', reply, name);
|
|
181
|
+
}
|
|
182
|
+
|
|
124
183
|
(async () => {
|
|
125
184
|
// Whatever happens below, this hook allows the turn. It has no opinion about
|
|
126
185
|
// whether the work should proceed; it is a record of it.
|
|
@@ -130,6 +189,36 @@ function readStdin() {
|
|
|
130
189
|
try { data = JSON.parse(readStdin() || '{}'); } catch { return; }
|
|
131
190
|
|
|
132
191
|
const event = data.hook_event_name || data.hookEventName || '';
|
|
192
|
+
|
|
193
|
+
// ANTIGRAVITY sends a different shape, and one hook carries both halves.
|
|
194
|
+
//
|
|
195
|
+
// Its payload is `{common: {lastUserInput, conversationId, ...}, args: {...}}`
|
|
196
|
+
// — the person's last message rides on EVERY hook rather than on an event of
|
|
197
|
+
// its own, and the answer arrives as `stopHookArgs.finalModelOutput`. So one
|
|
198
|
+
// Stop registration records the whole exchange, where Claude Code and Codex
|
|
199
|
+
// need two events to do the same thing.
|
|
200
|
+
//
|
|
201
|
+
// It is detected by shape rather than by a flag, because the client name is
|
|
202
|
+
// an argv argument this hook is also given for the other three, and a payload
|
|
203
|
+
// is a fact while an argument is a claim.
|
|
204
|
+
// OPENCODE calls this hook directly from its plugin, which already knows
|
|
205
|
+
// which half it holds — its two halves arrive by different routes (a
|
|
206
|
+
// chat.message hook for the prompt, streamed text parts for the reply), so
|
|
207
|
+
// the plugin does the deciding and this file does the sending.
|
|
208
|
+
if (data.opencode === true) {
|
|
209
|
+
await send(data.session_id || '', data.role === 'reply' ? 'reply' : 'prompt',
|
|
210
|
+
data.body || '', 'OpenCode');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const agCommon = data.common || (data.hookArgs && data.hookArgs.common) || null;
|
|
215
|
+
const agStop = data.stopHookArgs || (data.args && data.args.stopHookArgs) ||
|
|
216
|
+
(data.hookArgs && data.hookArgs.stopHookArgs) || null;
|
|
217
|
+
if (agCommon && (agStop || agCommon.lastUserInput)) {
|
|
218
|
+
await sendAntigravity(agCommon, agStop);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
133
222
|
// The two halves. Nothing else is listened for: PreToolUse and PostToolUse
|
|
134
223
|
// already have hooks and are about tool calls rather than about words.
|
|
135
224
|
let role = '';
|
|
@@ -151,33 +240,5 @@ function readStdin() {
|
|
|
151
240
|
const sessionId = data.session_id || data.sessionId || data.conversation_id || '';
|
|
152
241
|
if (!sessionId) return;
|
|
153
242
|
|
|
154
|
-
|
|
155
|
-
const apiKey = process.env.SOLONGATE_API_KEY || cfg.apiKey || '';
|
|
156
|
-
const apiUrl = process.env.SOLONGATE_API_URL || cfg.apiUrl || 'https://api.solongate.com';
|
|
157
|
-
if (!apiKey) return;
|
|
158
|
-
|
|
159
|
-
const cache = loadPolicyCache();
|
|
160
|
-
if (localLogsOn(cache)) return;
|
|
161
|
-
|
|
162
|
-
const cleaned = redact(body, cache);
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
await fetch(`${apiUrl}/api/v1/conversations`, {
|
|
166
|
-
method: 'POST',
|
|
167
|
-
headers: {
|
|
168
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
169
|
-
'Content-Type': 'application/json',
|
|
170
|
-
},
|
|
171
|
-
body: JSON.stringify({
|
|
172
|
-
session_id: sessionId,
|
|
173
|
-
role,
|
|
174
|
-
body: cleaned.text,
|
|
175
|
-
redacted: cleaned.masked,
|
|
176
|
-
agent_id: AGENT_ID,
|
|
177
|
-
agent_name: data.agent_name || '',
|
|
178
|
-
source: `${AGENT_ID}-hook`,
|
|
179
|
-
hook_version: HOOK_VERSION,
|
|
180
|
-
}),
|
|
181
|
-
});
|
|
182
|
-
} catch { /* silent: a record that could not be sent must not fail a turn */ }
|
|
243
|
+
await send(sessionId, role, body, data.agent_name || '');
|
|
183
244
|
})();
|
|
@@ -36,6 +36,7 @@ import { join } from 'node:path';
|
|
|
36
36
|
const HOOKS_DIR = join(homedir(), '.solongate', 'hooks');
|
|
37
37
|
const GUARD = join(HOOKS_DIR, 'guard.mjs');
|
|
38
38
|
const AUDIT = join(HOOKS_DIR, 'audit.mjs');
|
|
39
|
+
const CONVERSATION = join(HOOKS_DIR, 'conversation.mjs');
|
|
39
40
|
|
|
40
41
|
// The node binary, written in by the installer.
|
|
41
42
|
//
|
|
@@ -124,6 +125,26 @@ function rewritePatch(res) {
|
|
|
124
125
|
return null;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
// The assistant text held between the part events and the message completing.
|
|
129
|
+
//
|
|
130
|
+
// Keyed by message id, cleared on flush, and dropped whole past a bound: a
|
|
131
|
+
// plugin that grows a buffer for the life of an editor session is a leak in
|
|
132
|
+
// somebody's editor, which is a worse bug than a missing transcript.
|
|
133
|
+
const pending = new Map();
|
|
134
|
+
const MAX_PENDING = 64;
|
|
135
|
+
|
|
136
|
+
// textOf joins the text parts of a message and ignores the rest — a file
|
|
137
|
+
// attachment or a tool call is not something a person said. A synthetic part
|
|
138
|
+
// is one OpenCode wrote itself, which is not either.
|
|
139
|
+
function textOf(parts) {
|
|
140
|
+
if (!Array.isArray(parts)) return '';
|
|
141
|
+
return parts
|
|
142
|
+
.filter((x) => x && x.type === 'text' && typeof x.text === 'string' && !x.synthetic)
|
|
143
|
+
.map((x) => x.text)
|
|
144
|
+
.join('\n')
|
|
145
|
+
.trim();
|
|
146
|
+
}
|
|
147
|
+
|
|
127
148
|
export const SolonGate = async ({ directory, worktree } = {}) => {
|
|
128
149
|
const cwd = directory || worktree || process.cwd();
|
|
129
150
|
|
|
@@ -155,6 +176,58 @@ export const SolonGate = async ({ directory, worktree } = {}) => {
|
|
|
155
176
|
}
|
|
156
177
|
},
|
|
157
178
|
|
|
179
|
+
// ── the conversation record ──────────────────────────────────────────
|
|
180
|
+
//
|
|
181
|
+
// OpenCode is the third client to record this and the only one that needs
|
|
182
|
+
// two different hooks to do it, because its halves arrive by different
|
|
183
|
+
// routes: `chat.message` carries what the person wrote, and the answer is
|
|
184
|
+
// streamed as text PARTS which are only final when the assistant message
|
|
185
|
+
// is.
|
|
186
|
+
//
|
|
187
|
+
// So the parts are held per message id and flushed when that message
|
|
188
|
+
// completes.
|
|
189
|
+
'chat.message': async (input, output) => {
|
|
190
|
+
const text = textOf(output?.parts);
|
|
191
|
+
if (!text) return;
|
|
192
|
+
void runHook(CONVERSATION, {
|
|
193
|
+
opencode: true,
|
|
194
|
+
role: 'prompt',
|
|
195
|
+
body: text,
|
|
196
|
+
session_id: input?.sessionID ?? output?.message?.sessionID ?? '',
|
|
197
|
+
}, GUARD_TIMEOUT_MS);
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
event: async ({ event } = {}) => {
|
|
201
|
+
if (!event || typeof event !== 'object') return;
|
|
202
|
+
|
|
203
|
+
// A text part of a message. `text` is the WHOLE text so far rather than a
|
|
204
|
+
// delta, so the last one wins and nothing is concatenated.
|
|
205
|
+
if (event.type === 'message.part.updated') {
|
|
206
|
+
const part = event.properties?.part;
|
|
207
|
+
if (part?.type === 'text' && part.messageID && typeof part.text === 'string') {
|
|
208
|
+
if (pending.size > MAX_PENDING) pending.clear();
|
|
209
|
+
pending.set(part.messageID, { text: part.text, sessionID: part.sessionID ?? '' });
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// The message finished. Anything else — a user message, one still being
|
|
215
|
+
// written — is not a completed answer and is left alone.
|
|
216
|
+
if (event.type === 'message.updated') {
|
|
217
|
+
const info = event.properties?.info;
|
|
218
|
+
if (!info || info.role !== 'assistant' || !info.time?.completed) return;
|
|
219
|
+
const held = pending.get(info.id);
|
|
220
|
+
pending.delete(info.id);
|
|
221
|
+
if (!held || !held.text.trim()) return;
|
|
222
|
+
void runHook(CONVERSATION, {
|
|
223
|
+
opencode: true,
|
|
224
|
+
role: 'reply',
|
|
225
|
+
body: held.text,
|
|
226
|
+
session_id: info.sessionID || held.sessionID || '',
|
|
227
|
+
}, GUARD_TIMEOUT_MS);
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
158
231
|
'tool.execute.after': async (input, output) => {
|
|
159
232
|
// The ALLOW-path audit record. Fire and forget: it must never delay the
|
|
160
233
|
// editor, and losing one line is better than a stalled tool.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.83.
|
|
3
|
+
"version": "0.83.59",
|
|
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": {
|
|
@@ -61,12 +61,12 @@
|
|
|
61
61
|
"node": ">=20.0.0"
|
|
62
62
|
},
|
|
63
63
|
"optionalDependencies": {
|
|
64
|
-
"@solongate/guard-linux-x64": "0.83.
|
|
65
|
-
"@solongate/guard-linux-arm64": "0.83.
|
|
66
|
-
"@solongate/guard-darwin-x64": "0.83.
|
|
67
|
-
"@solongate/guard-darwin-arm64": "0.83.
|
|
68
|
-
"@solongate/guard-win32-x64": "0.83.
|
|
69
|
-
"@solongate/guard-win32-arm64": "0.83.
|
|
64
|
+
"@solongate/guard-linux-x64": "0.83.59",
|
|
65
|
+
"@solongate/guard-linux-arm64": "0.83.59",
|
|
66
|
+
"@solongate/guard-darwin-x64": "0.83.59",
|
|
67
|
+
"@solongate/guard-darwin-arm64": "0.83.59",
|
|
68
|
+
"@solongate/guard-win32-x64": "0.83.59",
|
|
69
|
+
"@solongate/guard-win32-arm64": "0.83.59"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@modelcontextprotocol/sdk": "^1.26.0",
|