@agentapprove/opencode 0.1.3 → 0.1.4
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/index.js +114 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { homedir as homedir3 } from "os";
|
|
|
9
9
|
import { execSync } from "child_process";
|
|
10
10
|
|
|
11
11
|
// src/e2e-crypto.ts
|
|
12
|
-
import { createHash, createHmac, createCipheriv, randomBytes } from "crypto";
|
|
12
|
+
import { createHash, createHmac, createCipheriv, createDecipheriv, randomBytes, timingSafeEqual } from "crypto";
|
|
13
13
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, copyFileSync } from "fs";
|
|
14
14
|
import { join as join2 } from "path";
|
|
15
15
|
import { homedir as homedir2 } from "os";
|
|
@@ -135,6 +135,33 @@ function e2eEncrypt(keyHex, plaintext) {
|
|
|
135
135
|
const hmac = createHmac("sha256", macKey).update(Buffer.concat([iv, ciphertext])).digest("hex");
|
|
136
136
|
return `E2E:v1:${kid}:${ivHex}:${ciphertextBase64}:${hmac}`;
|
|
137
137
|
}
|
|
138
|
+
function isE2EEncrypted(value) {
|
|
139
|
+
return value.startsWith("E2E:v1:");
|
|
140
|
+
}
|
|
141
|
+
function e2eDecrypt(keyHex, encrypted) {
|
|
142
|
+
try {
|
|
143
|
+
if (!isE2EEncrypted(encrypted)) return null;
|
|
144
|
+
const parts = encrypted.split(":");
|
|
145
|
+
if (parts.length !== 6) return null;
|
|
146
|
+
const ivHex = parts[3];
|
|
147
|
+
const ciphertextBase64 = parts[4];
|
|
148
|
+
const hmacHex = parts[5];
|
|
149
|
+
const encKey = deriveEncKey(keyHex);
|
|
150
|
+
const macKey = deriveMacKey(keyHex);
|
|
151
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
152
|
+
const ciphertext = Buffer.from(ciphertextBase64, "base64");
|
|
153
|
+
const expectedHmac = createHmac("sha256", macKey).update(Buffer.concat([iv, ciphertext])).digest();
|
|
154
|
+
const actualHmac = Buffer.from(hmacHex, "hex");
|
|
155
|
+
if (expectedHmac.length !== actualHmac.length || !timingSafeEqual(expectedHmac, actualHmac)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const decipher = createDecipheriv("aes-256-ctr", encKey, iv);
|
|
159
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
160
|
+
return plaintext.toString("utf-8");
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
138
165
|
function applyApprovalE2E(payload, userKey, serverKey) {
|
|
139
166
|
const sensitiveFields = {};
|
|
140
167
|
for (const field of ["command", "toolInput", "cwd"]) {
|
|
@@ -731,6 +758,45 @@ async function sendApprovalRequest(request, config, pluginPath) {
|
|
|
731
758
|
processConfigSync(parsed, config);
|
|
732
759
|
return parsed;
|
|
733
760
|
}
|
|
761
|
+
async function sendStopRequest(request, config, pluginPath) {
|
|
762
|
+
if (!config.token) {
|
|
763
|
+
throw new Error("No Agent Approve token configured");
|
|
764
|
+
}
|
|
765
|
+
const payload = applyEventPrivacyFilter(
|
|
766
|
+
{ ...request },
|
|
767
|
+
config.privacyTier
|
|
768
|
+
);
|
|
769
|
+
const bodyStr = JSON.stringify(payload);
|
|
770
|
+
const pluginHash = getPluginHash(pluginPath, config.debug);
|
|
771
|
+
const hmacHeaders = buildHMACHeaders(bodyStr, config.token, config.hookVersion, pluginHash);
|
|
772
|
+
const headers = {
|
|
773
|
+
"Authorization": `Bearer ${config.token}`,
|
|
774
|
+
...hmacHeaders
|
|
775
|
+
};
|
|
776
|
+
const url = `${config.apiUrl}/${config.apiVersion}/stop`;
|
|
777
|
+
const stopTimeoutMs = 3e5;
|
|
778
|
+
if (config.debug) {
|
|
779
|
+
debugLog(`Sending stop request to ${url}`);
|
|
780
|
+
debugLog(`=== SENT TO ${url} ===`);
|
|
781
|
+
debugLogRawInline(bodyStr);
|
|
782
|
+
debugLog("=== END SENT ===");
|
|
783
|
+
}
|
|
784
|
+
const response = await httpPost(url, bodyStr, headers, stopTimeoutMs);
|
|
785
|
+
if (config.debug) {
|
|
786
|
+
debugLog(`Stop response: ${response.body || "<empty>"}`);
|
|
787
|
+
}
|
|
788
|
+
if (response.status !== 200) {
|
|
789
|
+
throw new Error(`Stop API returned status ${response.status}: ${response.body.slice(0, 200)}`);
|
|
790
|
+
}
|
|
791
|
+
let parsed;
|
|
792
|
+
try {
|
|
793
|
+
parsed = JSON.parse(response.body);
|
|
794
|
+
} catch {
|
|
795
|
+
parsed = {};
|
|
796
|
+
}
|
|
797
|
+
processConfigSync(parsed, config);
|
|
798
|
+
return parsed;
|
|
799
|
+
}
|
|
734
800
|
async function sendEvent(event, config, pluginPath) {
|
|
735
801
|
if (!config.token) return;
|
|
736
802
|
const eventType = event.eventType;
|
|
@@ -795,9 +861,11 @@ var fallbackSessionId = randomBytes2(12).toString("hex");
|
|
|
795
861
|
var COMPACTION_DEDUP_WINDOW_MS = 2e3;
|
|
796
862
|
var openCodeClient = void 0;
|
|
797
863
|
var lastCompactionEventAt = 0;
|
|
864
|
+
var processingIdleSessions = /* @__PURE__ */ new Set();
|
|
798
865
|
var seenUnmappedEventTypes = /* @__PURE__ */ new Set();
|
|
799
866
|
var sentFinalMessageIds = /* @__PURE__ */ new Set();
|
|
800
867
|
var messageParts = /* @__PURE__ */ new Map();
|
|
868
|
+
var NOISY_EVENT_TYPES = /* @__PURE__ */ new Set(["message.part.delta"]);
|
|
801
869
|
function classifyTool(toolName) {
|
|
802
870
|
const lower = toolName.toLowerCase();
|
|
803
871
|
if (lower === "bash") {
|
|
@@ -1109,10 +1177,11 @@ async function plugin(input) {
|
|
|
1109
1177
|
"event": async (input2) => {
|
|
1110
1178
|
const eventName = input2.event?.type;
|
|
1111
1179
|
if (!eventName) return;
|
|
1180
|
+
if (NOISY_EVENT_TYPES.has(eventName)) return;
|
|
1112
1181
|
const eventProperties = input2.event.properties || {};
|
|
1113
1182
|
const eventSessionId = getEventSessionId(eventName, eventProperties);
|
|
1114
1183
|
const ids = resolveIds(eventSessionId);
|
|
1115
|
-
if (config.debug
|
|
1184
|
+
if (config.debug) {
|
|
1116
1185
|
debugLog(`Event received: ${eventName}`, HOOK_EVENT);
|
|
1117
1186
|
}
|
|
1118
1187
|
if (eventName === "message.part.updated") {
|
|
@@ -1190,40 +1259,71 @@ async function plugin(input) {
|
|
|
1190
1259
|
}
|
|
1191
1260
|
if (eventName === "session.idle") return;
|
|
1192
1261
|
const status = eventProperties.status;
|
|
1262
|
+
if (eventName === "session.status" && status?.type !== "idle") {
|
|
1263
|
+
processingIdleSessions.delete(ids.sessionId);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1193
1266
|
if (eventName === "session.status" && status?.type === "idle") {
|
|
1267
|
+
if (processingIdleSessions.has(ids.sessionId)) {
|
|
1268
|
+
if (config.debug) {
|
|
1269
|
+
debugLog("Skipping duplicate session.status idle (already processing)", HOOK_EVENT);
|
|
1270
|
+
}
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
processingIdleSessions.add(ids.sessionId);
|
|
1194
1274
|
if (config.debug) {
|
|
1195
|
-
debugLog(
|
|
1275
|
+
debugLog("Session idle detected via session.status, sending stop request", HOOK_EVENT);
|
|
1196
1276
|
}
|
|
1197
1277
|
const stopRequest = {
|
|
1198
|
-
toolName: "session_idle",
|
|
1199
|
-
toolType: "session",
|
|
1200
1278
|
agent: config.agentType,
|
|
1201
1279
|
agentName: config.agentName,
|
|
1202
|
-
|
|
1280
|
+
eventType: "stop",
|
|
1281
|
+
status: "completed",
|
|
1282
|
+
loopCount: 0,
|
|
1203
1283
|
sessionId: ids.sessionId,
|
|
1204
1284
|
conversationId: ids.conversationId,
|
|
1205
|
-
|
|
1206
|
-
|
|
1285
|
+
timestamp: Date.now(),
|
|
1286
|
+
...config.e2eEnabled && config.e2eUserKey ? { e2eKeyId: keyId(config.e2eUserKey) } : {}
|
|
1207
1287
|
};
|
|
1208
1288
|
try {
|
|
1209
|
-
const response = await
|
|
1210
|
-
if (
|
|
1289
|
+
const response = await sendStopRequest(stopRequest, config, pluginFilePath);
|
|
1290
|
+
if (config.debug) {
|
|
1291
|
+
debugLog(`Stop response: followup=${response.followup_message ? "yes" : "no"}`, HOOK_EVENT);
|
|
1292
|
+
}
|
|
1293
|
+
if (response.followup_message && openCodeClient) {
|
|
1294
|
+
let followupText = response.followup_message;
|
|
1295
|
+
if (config.e2eEnabled && config.e2eUserKey && isE2EEncrypted(followupText)) {
|
|
1296
|
+
const decrypted = e2eDecrypt(config.e2eUserKey, followupText);
|
|
1297
|
+
if (decrypted) {
|
|
1298
|
+
if (config.debug) {
|
|
1299
|
+
debugLog(`Decrypted follow-up: ${decrypted.slice(0, 50)}...`, HOOK_EVENT);
|
|
1300
|
+
}
|
|
1301
|
+
followupText = decrypted;
|
|
1302
|
+
} else {
|
|
1303
|
+
if (config.debug) {
|
|
1304
|
+
debugLog("E2E decryption failed for follow-up message, skipping injection", HOOK_EVENT);
|
|
1305
|
+
}
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1211
1309
|
if (config.debug) {
|
|
1212
|
-
debugLog(`
|
|
1310
|
+
debugLog(`Injecting user follow-up: ${followupText.slice(0, 100)}`, HOOK_EVENT);
|
|
1213
1311
|
}
|
|
1214
1312
|
try {
|
|
1215
|
-
await openCodeClient.tui.appendPrompt({ body: { text:
|
|
1313
|
+
await openCodeClient.tui.appendPrompt({ body: { text: followupText } });
|
|
1216
1314
|
await openCodeClient.tui.submitPrompt();
|
|
1217
1315
|
} catch (injectionError) {
|
|
1218
1316
|
if (config.debug) {
|
|
1219
|
-
debugLog(`Failed to inject follow-up
|
|
1317
|
+
debugLog(`Failed to inject follow-up: ${injectionError instanceof Error ? injectionError.message : String(injectionError)}`, HOOK_EVENT);
|
|
1220
1318
|
}
|
|
1221
1319
|
}
|
|
1222
1320
|
}
|
|
1223
1321
|
} catch (error) {
|
|
1224
1322
|
if (config.debug) {
|
|
1225
|
-
debugLog(`Stop
|
|
1323
|
+
debugLog(`Stop request error: ${error instanceof Error ? error.message : String(error)}`, HOOK_EVENT);
|
|
1226
1324
|
}
|
|
1325
|
+
} finally {
|
|
1326
|
+
processingIdleSessions.delete(ids.sessionId);
|
|
1227
1327
|
}
|
|
1228
1328
|
return;
|
|
1229
1329
|
}
|
package/package.json
CHANGED