@gethmy/mcp 3.2.0 → 3.4.0
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/README.md +41 -2
- package/dist/cli.js +1188 -157
- package/dist/index.js +805 -42
- package/dist/lib/api-client.js +3 -1
- package/dist/run-hook-cli.js +742 -0
- package/package.json +4 -3
- package/src/api-client.ts +57 -1
- package/src/auto-session.ts +33 -0
- package/src/cli.ts +104 -0
- package/src/comment-session.ts +149 -0
- package/src/hook-install.ts +388 -0
- package/src/plan-task-link.ts +130 -0
- package/src/run-event-forwarder.ts +363 -0
- package/src/run-hook-cli.ts +55 -0
- package/src/run-hook-main.ts +159 -0
- package/src/run-hook.ts +203 -0
- package/src/run-redaction.ts +461 -0
- package/src/run-state.ts +679 -0
- package/src/server.ts +342 -34
- package/src/tui/setup.ts +3 -0
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __returnValue = (v) => v;
|
|
5
|
+
function __exportSetter(name, newValue) {
|
|
6
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
+
}
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
set: __exportSetter.bind(all, name)
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
// src/run-state.ts
|
|
21
|
+
var exports_run_state = {};
|
|
22
|
+
__export(exports_run_state, {
|
|
23
|
+
writeSpoolBatch: () => writeSpoolBatch,
|
|
24
|
+
writeRouteMemo: () => writeRouteMemo,
|
|
25
|
+
trimSpool: () => trimSpool,
|
|
26
|
+
spoolDir: () => spoolDir,
|
|
27
|
+
sessionsDir: () => sessionsDir,
|
|
28
|
+
runStateExists: () => runStateExists,
|
|
29
|
+
runStateDir: () => runStateDir,
|
|
30
|
+
removeSpoolBatches: () => removeSpoolBatches,
|
|
31
|
+
readSpoolBatches: () => readSpoolBatches,
|
|
32
|
+
readRouteMemo: () => readRouteMemo,
|
|
33
|
+
readPublishedSessions: () => readPublishedSessions,
|
|
34
|
+
publishRunSession: () => publishRunSession,
|
|
35
|
+
pidIsAlive: () => pidIsAlive,
|
|
36
|
+
clearRunSession: () => clearRunSession,
|
|
37
|
+
chooseRunSessionForHook: () => chooseRunSessionForHook,
|
|
38
|
+
ancestorPids: () => ancestorPids,
|
|
39
|
+
RUN_STATE_DIR_ENV: () => RUN_STATE_DIR_ENV,
|
|
40
|
+
MAX_POINTER_AGE_MS: () => MAX_POINTER_AGE_MS
|
|
41
|
+
});
|
|
42
|
+
import { execFileSync } from "node:child_process";
|
|
43
|
+
import {
|
|
44
|
+
existsSync,
|
|
45
|
+
mkdirSync,
|
|
46
|
+
readdirSync,
|
|
47
|
+
readFileSync,
|
|
48
|
+
renameSync,
|
|
49
|
+
rmSync,
|
|
50
|
+
statSync,
|
|
51
|
+
unlinkSync,
|
|
52
|
+
writeFileSync
|
|
53
|
+
} from "node:fs";
|
|
54
|
+
import { homedir } from "node:os";
|
|
55
|
+
import { join } from "node:path";
|
|
56
|
+
function runStateDir(env = process.env) {
|
|
57
|
+
const override = env[RUN_STATE_DIR_ENV]?.trim();
|
|
58
|
+
if (override)
|
|
59
|
+
return override;
|
|
60
|
+
return join(homedir(), ".harmony", "runs");
|
|
61
|
+
}
|
|
62
|
+
function sessionsDir(stateDir) {
|
|
63
|
+
return join(stateDir, "sessions");
|
|
64
|
+
}
|
|
65
|
+
function spoolDir(stateDir, agentSessionId) {
|
|
66
|
+
return join(stateDir, "spool", sanitizeIdForPath(agentSessionId));
|
|
67
|
+
}
|
|
68
|
+
function sanitizeIdForPath(id) {
|
|
69
|
+
return id.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 100);
|
|
70
|
+
}
|
|
71
|
+
function pointerFileName(publisherPid, cardId) {
|
|
72
|
+
return `${publisherPid}.${sanitizeIdForPath(cardId)}.json`;
|
|
73
|
+
}
|
|
74
|
+
function pidIsAlive(pid) {
|
|
75
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
76
|
+
return false;
|
|
77
|
+
try {
|
|
78
|
+
process.kill(pid, 0);
|
|
79
|
+
return true;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return err?.code === "EPERM";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function readProcParent(pid) {
|
|
85
|
+
try {
|
|
86
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
87
|
+
const tail = stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/);
|
|
88
|
+
const ppid = Number.parseInt(tail[1] ?? "", 10);
|
|
89
|
+
return Number.isInteger(ppid) && ppid > 0 ? ppid : null;
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function psParentTable() {
|
|
95
|
+
if (psTableCache)
|
|
96
|
+
return psTableCache;
|
|
97
|
+
const table = new Map;
|
|
98
|
+
try {
|
|
99
|
+
const out = execFileSync("ps", ["-Ao", "pid=,ppid="], {
|
|
100
|
+
encoding: "utf-8",
|
|
101
|
+
timeout: 2000,
|
|
102
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
103
|
+
});
|
|
104
|
+
for (const line of out.split(`
|
|
105
|
+
`)) {
|
|
106
|
+
const match = line.trim().match(/^(\d+)\s+(\d+)$/);
|
|
107
|
+
if (!match)
|
|
108
|
+
continue;
|
|
109
|
+
table.set(Number(match[1]), Number(match[2]));
|
|
110
|
+
}
|
|
111
|
+
} catch {}
|
|
112
|
+
psTableCache = table;
|
|
113
|
+
return table;
|
|
114
|
+
}
|
|
115
|
+
function ancestorPids(pid, readParent) {
|
|
116
|
+
const parentOf = readParent ?? ((child) => {
|
|
117
|
+
const viaProc = readProcParent(child);
|
|
118
|
+
if (viaProc !== null)
|
|
119
|
+
return viaProc;
|
|
120
|
+
return psParentTable().get(child) ?? null;
|
|
121
|
+
});
|
|
122
|
+
const chain = [];
|
|
123
|
+
const seen = new Set([pid]);
|
|
124
|
+
let current = pid;
|
|
125
|
+
if (!readParent && pid === process.pid) {
|
|
126
|
+
const ppid = process.ppid;
|
|
127
|
+
if (Number.isInteger(ppid) && ppid > 1) {
|
|
128
|
+
chain.push(ppid);
|
|
129
|
+
seen.add(ppid);
|
|
130
|
+
current = ppid;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (let depth = chain.length;depth < MAX_ANCESTOR_DEPTH; depth++) {
|
|
134
|
+
const parent = parentOf(current);
|
|
135
|
+
if (parent === null || parent <= 1 || seen.has(parent))
|
|
136
|
+
break;
|
|
137
|
+
chain.push(parent);
|
|
138
|
+
seen.add(parent);
|
|
139
|
+
current = parent;
|
|
140
|
+
}
|
|
141
|
+
return chain;
|
|
142
|
+
}
|
|
143
|
+
function publishRunSession(session, options) {
|
|
144
|
+
const stateDir = options?.stateDir ?? runStateDir();
|
|
145
|
+
const pid = options?.pid ?? process.pid;
|
|
146
|
+
const record = {
|
|
147
|
+
cardId: session.cardId,
|
|
148
|
+
agentSessionId: session.agentSessionId,
|
|
149
|
+
publisherPid: pid,
|
|
150
|
+
ancestorPids: options?.ancestors ?? ancestorPids(pid),
|
|
151
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
152
|
+
updatedAt: new Date().toISOString()
|
|
153
|
+
};
|
|
154
|
+
try {
|
|
155
|
+
const dir = sessionsDir(stateDir);
|
|
156
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
157
|
+
const target = join(dir, pointerFileName(pid, session.cardId));
|
|
158
|
+
const temp = `${target}.${process.pid}.tmp`;
|
|
159
|
+
writeFileSync(temp, JSON.stringify(record), { mode: 384 });
|
|
160
|
+
renameSync(temp, target);
|
|
161
|
+
return record;
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function clearRunSession(cardId, options) {
|
|
167
|
+
const stateDir = options?.stateDir ?? runStateDir();
|
|
168
|
+
const pid = options?.pid ?? process.pid;
|
|
169
|
+
if (!options?.keepPointer) {
|
|
170
|
+
try {
|
|
171
|
+
unlinkSync(join(sessionsDir(stateDir), pointerFileName(pid, cardId)));
|
|
172
|
+
} catch {}
|
|
173
|
+
}
|
|
174
|
+
if (options?.agentSessionId) {
|
|
175
|
+
try {
|
|
176
|
+
rmSync(spoolDir(stateDir, options.agentSessionId), {
|
|
177
|
+
recursive: true,
|
|
178
|
+
force: true
|
|
179
|
+
});
|
|
180
|
+
} catch {}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function readPublishedSessions(options) {
|
|
184
|
+
const stateDir = options?.stateDir ?? runStateDir();
|
|
185
|
+
const now = options?.now ?? Date.now();
|
|
186
|
+
const dir = sessionsDir(stateDir);
|
|
187
|
+
let names;
|
|
188
|
+
try {
|
|
189
|
+
names = readdirSync(dir);
|
|
190
|
+
} catch {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
const live = [];
|
|
194
|
+
for (const name of names) {
|
|
195
|
+
if (!name.endsWith(".json"))
|
|
196
|
+
continue;
|
|
197
|
+
const path = join(dir, name);
|
|
198
|
+
let record;
|
|
199
|
+
try {
|
|
200
|
+
record = JSON.parse(readFileSync(path, "utf-8"));
|
|
201
|
+
} catch {
|
|
202
|
+
safeUnlink(path);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (typeof record?.cardId !== "string" || typeof record?.agentSessionId !== "string" || !record.cardId || !record.agentSessionId) {
|
|
206
|
+
safeUnlink(path);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const age = now - Date.parse(record.updatedAt ?? "");
|
|
210
|
+
if (!Number.isFinite(age) || age > MAX_POINTER_AGE_MS) {
|
|
211
|
+
safeUnlink(path);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (!pidIsAlive(record.publisherPid)) {
|
|
215
|
+
safeUnlink(path);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
live.push({ ...record, ancestorPids: record.ancestorPids ?? [] });
|
|
219
|
+
}
|
|
220
|
+
return live;
|
|
221
|
+
}
|
|
222
|
+
function safeUnlink(path) {
|
|
223
|
+
try {
|
|
224
|
+
unlinkSync(path);
|
|
225
|
+
} catch {}
|
|
226
|
+
}
|
|
227
|
+
function chooseRunSessionForHook(args) {
|
|
228
|
+
const { candidates, hookAncestorPids } = args;
|
|
229
|
+
if (candidates.length === 0)
|
|
230
|
+
return null;
|
|
231
|
+
const hasCwd = typeof args.cwd === "string" && args.cwd.length > 0;
|
|
232
|
+
const pool = hasCwd ? candidates.filter((candidate) => candidate.cwd === args.cwd) : candidates;
|
|
233
|
+
if (pool.length === 0)
|
|
234
|
+
return null;
|
|
235
|
+
let best = null;
|
|
236
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
237
|
+
for (const candidate of pool) {
|
|
238
|
+
const claimed = new Set([
|
|
239
|
+
candidate.publisherPid,
|
|
240
|
+
...candidate.ancestorPids ?? []
|
|
241
|
+
]);
|
|
242
|
+
let score = Number.POSITIVE_INFINITY;
|
|
243
|
+
for (let i = 0;i < hookAncestorPids.length; i++) {
|
|
244
|
+
if (claimed.has(hookAncestorPids[i])) {
|
|
245
|
+
score = i;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (score === Number.POSITIVE_INFINITY)
|
|
250
|
+
continue;
|
|
251
|
+
if (score < bestScore || score === bestScore && best !== null && Date.parse(candidate.updatedAt) > Date.parse(best.updatedAt)) {
|
|
252
|
+
best = candidate;
|
|
253
|
+
bestScore = score;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (best)
|
|
257
|
+
return best;
|
|
258
|
+
if (hasCwd && pool.length === 1)
|
|
259
|
+
return pool[0];
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
function writeSpoolBatch(dir, events, options) {
|
|
263
|
+
if (events.length === 0)
|
|
264
|
+
return null;
|
|
265
|
+
const now = options?.now ?? Date.now();
|
|
266
|
+
const pid = options?.pid ?? process.pid;
|
|
267
|
+
const nonce = options?.nonce ?? Math.random().toString(36).slice(2, 8).padEnd(6, "0");
|
|
268
|
+
try {
|
|
269
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
270
|
+
const name = `${String(now).padStart(14, "0")}-${pid}-${nonce}.json`;
|
|
271
|
+
const target = join(dir, name);
|
|
272
|
+
const temp = `${target}.tmp`;
|
|
273
|
+
writeFileSync(temp, JSON.stringify(events), { mode: 384 });
|
|
274
|
+
renameSync(temp, target);
|
|
275
|
+
return target;
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function readSpoolBatches(dir, limit = 200) {
|
|
281
|
+
let names;
|
|
282
|
+
try {
|
|
283
|
+
names = readdirSync(dir);
|
|
284
|
+
} catch {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
const batches = [];
|
|
288
|
+
for (const name of names.filter((n) => n.endsWith(".json")).sort()) {
|
|
289
|
+
if (batches.length >= limit)
|
|
290
|
+
break;
|
|
291
|
+
const path = join(dir, name);
|
|
292
|
+
try {
|
|
293
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
294
|
+
if (!Array.isArray(parsed)) {
|
|
295
|
+
safeUnlink(path);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
batches.push({ path, events: parsed });
|
|
299
|
+
} catch {
|
|
300
|
+
safeUnlink(path);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return batches;
|
|
304
|
+
}
|
|
305
|
+
function removeSpoolBatches(paths) {
|
|
306
|
+
for (const path of paths)
|
|
307
|
+
safeUnlink(path);
|
|
308
|
+
}
|
|
309
|
+
function trimSpool(dir, max) {
|
|
310
|
+
let names;
|
|
311
|
+
try {
|
|
312
|
+
names = readdirSync(dir).filter((n) => n.endsWith(".json")).sort();
|
|
313
|
+
} catch {
|
|
314
|
+
return 0;
|
|
315
|
+
}
|
|
316
|
+
if (names.length <= max)
|
|
317
|
+
return 0;
|
|
318
|
+
const excess = names.slice(0, names.length - max);
|
|
319
|
+
for (const name of excess)
|
|
320
|
+
safeUnlink(join(dir, name));
|
|
321
|
+
return excess.length;
|
|
322
|
+
}
|
|
323
|
+
function routeMemoPath(stateDir, harnessSessionId) {
|
|
324
|
+
return join(stateDir, "routes", `${sanitizeIdForPath(harnessSessionId)}.json`);
|
|
325
|
+
}
|
|
326
|
+
function readRouteMemo(stateDir, harnessSessionId) {
|
|
327
|
+
try {
|
|
328
|
+
const raw = JSON.parse(readFileSync(routeMemoPath(stateDir, harnessSessionId), "utf-8"));
|
|
329
|
+
const pid = raw?.publisherPid;
|
|
330
|
+
const cardId = raw?.cardId;
|
|
331
|
+
const agentSessionId = raw?.agentSessionId;
|
|
332
|
+
if (typeof pid !== "number" || !Number.isInteger(pid))
|
|
333
|
+
return null;
|
|
334
|
+
if (typeof cardId !== "string" || cardId.length === 0)
|
|
335
|
+
return null;
|
|
336
|
+
if (typeof agentSessionId !== "string" || agentSessionId.length === 0) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
return { publisherPid: pid, cardId, agentSessionId };
|
|
340
|
+
} catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function writeRouteMemo(stateDir, harnessSessionId, memo) {
|
|
345
|
+
try {
|
|
346
|
+
const path = routeMemoPath(stateDir, harnessSessionId);
|
|
347
|
+
mkdirSync(join(stateDir, "routes"), { recursive: true, mode: 448 });
|
|
348
|
+
writeFileSync(path, JSON.stringify({
|
|
349
|
+
publisherPid: memo.publisherPid,
|
|
350
|
+
cardId: memo.cardId,
|
|
351
|
+
agentSessionId: memo.agentSessionId
|
|
352
|
+
}), { mode: 384 });
|
|
353
|
+
} catch {}
|
|
354
|
+
}
|
|
355
|
+
function runStateExists(stateDir = runStateDir()) {
|
|
356
|
+
try {
|
|
357
|
+
return existsSync(sessionsDir(stateDir)) && statSync(sessionsDir(stateDir)).isDirectory();
|
|
358
|
+
} catch {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
var RUN_STATE_DIR_ENV = "HARMONY_RUN_STATE_DIR", MAX_POINTER_AGE_MS, MAX_ANCESTOR_DEPTH = 12, psTableCache = null;
|
|
363
|
+
var init_run_state = __esm(() => {
|
|
364
|
+
MAX_POINTER_AGE_MS = 10 * 60000;
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// src/run-redaction.ts
|
|
368
|
+
var MAX_INPUT_CHARS = 2000;
|
|
369
|
+
var MAX_OUTPUT_CHARS = 4000;
|
|
370
|
+
var MAX_INPUT_STRING_CHARS = 600;
|
|
371
|
+
var REDACTION_MARK = "«redacted»";
|
|
372
|
+
var SENSITIVE_SEGMENTS = [
|
|
373
|
+
".ssh",
|
|
374
|
+
".gnupg",
|
|
375
|
+
".aws",
|
|
376
|
+
".codex",
|
|
377
|
+
".gemini",
|
|
378
|
+
".docker",
|
|
379
|
+
".kube",
|
|
380
|
+
".harmony-mcp",
|
|
381
|
+
".password-store",
|
|
382
|
+
".claude",
|
|
383
|
+
"gh",
|
|
384
|
+
"gcloud",
|
|
385
|
+
"op",
|
|
386
|
+
"anthropic"
|
|
387
|
+
];
|
|
388
|
+
var CONFIG_SCOPED_SEGMENTS = new Set([
|
|
389
|
+
"gh",
|
|
390
|
+
"gcloud",
|
|
391
|
+
"op",
|
|
392
|
+
"anthropic"
|
|
393
|
+
]);
|
|
394
|
+
var SENSITIVE_BASENAMES = new Set([
|
|
395
|
+
".netrc",
|
|
396
|
+
"_netrc",
|
|
397
|
+
".npmrc",
|
|
398
|
+
".pgpass",
|
|
399
|
+
".git-credentials",
|
|
400
|
+
".htpasswd",
|
|
401
|
+
".claude.json",
|
|
402
|
+
"credentials",
|
|
403
|
+
".credentials",
|
|
404
|
+
"credentials.json",
|
|
405
|
+
".credentials.json",
|
|
406
|
+
"credentials.yml",
|
|
407
|
+
"credentials.yaml",
|
|
408
|
+
"auth.json",
|
|
409
|
+
".auth.json",
|
|
410
|
+
"secrets",
|
|
411
|
+
"secrets.json",
|
|
412
|
+
"secrets.yaml",
|
|
413
|
+
"secrets.yml",
|
|
414
|
+
"id_rsa",
|
|
415
|
+
"id_dsa",
|
|
416
|
+
"id_ecdsa",
|
|
417
|
+
"id_ed25519",
|
|
418
|
+
"known_hosts"
|
|
419
|
+
]);
|
|
420
|
+
var SENSITIVE_EXTENSIONS = [
|
|
421
|
+
".pem",
|
|
422
|
+
".key",
|
|
423
|
+
".p12",
|
|
424
|
+
".pfx",
|
|
425
|
+
".keystore",
|
|
426
|
+
".jks",
|
|
427
|
+
".asc",
|
|
428
|
+
".gpg"
|
|
429
|
+
];
|
|
430
|
+
function isSensitivePath(rawPath) {
|
|
431
|
+
if (typeof rawPath !== "string" || rawPath.length === 0)
|
|
432
|
+
return false;
|
|
433
|
+
const path = rawPath.trim().toLowerCase();
|
|
434
|
+
const segments = path.split(/[\\/]+/).filter((s) => s.length > 0);
|
|
435
|
+
if (segments.length === 0)
|
|
436
|
+
return false;
|
|
437
|
+
for (let i = 0;i < segments.length; i++) {
|
|
438
|
+
const segment = segments[i];
|
|
439
|
+
if (!SENSITIVE_SEGMENTS.includes(segment))
|
|
440
|
+
continue;
|
|
441
|
+
if (CONFIG_SCOPED_SEGMENTS.has(segment)) {
|
|
442
|
+
if (i > 0 && segments[i - 1] === ".config")
|
|
443
|
+
return true;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
const basename = segments[segments.length - 1];
|
|
449
|
+
if (SENSITIVE_BASENAMES.has(basename))
|
|
450
|
+
return true;
|
|
451
|
+
if (basename === ".env" || basename.startsWith(".env."))
|
|
452
|
+
return true;
|
|
453
|
+
if (basename.endsWith(".env"))
|
|
454
|
+
return true;
|
|
455
|
+
if (SENSITIVE_EXTENSIONS.some((ext) => basename.endsWith(ext)))
|
|
456
|
+
return true;
|
|
457
|
+
if (/service[-_]?account.*\.json$/.test(basename))
|
|
458
|
+
return true;
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
function sensitivePathsIn(input, depth = 0) {
|
|
462
|
+
if (depth > 6)
|
|
463
|
+
return [];
|
|
464
|
+
if (typeof input === "string") {
|
|
465
|
+
return isSensitivePath(input) ? [input] : [];
|
|
466
|
+
}
|
|
467
|
+
if (Array.isArray(input)) {
|
|
468
|
+
return input.flatMap((item) => sensitivePathsIn(item, depth + 1));
|
|
469
|
+
}
|
|
470
|
+
if (input !== null && typeof input === "object") {
|
|
471
|
+
return Object.values(input).flatMap((value) => sensitivePathsIn(value, depth + 1));
|
|
472
|
+
}
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
var SECRET_PATTERNS = [
|
|
476
|
+
{
|
|
477
|
+
pattern: /-----BEGIN[^-]*PRIVATE KEY-----[\s\S]*?-----END[^-]*-----/g,
|
|
478
|
+
replace: REDACTION_MARK
|
|
479
|
+
},
|
|
480
|
+
{ pattern: /\bhmy_at_[A-Za-z0-9_-]{8,}/g, replace: REDACTION_MARK },
|
|
481
|
+
{ pattern: /\bhmy_[A-Za-z0-9_-]{8,}/g, replace: REDACTION_MARK },
|
|
482
|
+
{ pattern: /\bsk-(?:ant-)?[A-Za-z0-9_-]{16,}/g, replace: REDACTION_MARK },
|
|
483
|
+
{ pattern: /\bgh[pousr]_[A-Za-z0-9]{16,}/g, replace: REDACTION_MARK },
|
|
484
|
+
{ pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}/g, replace: REDACTION_MARK },
|
|
485
|
+
{ pattern: /\bxox[abprs]-[A-Za-z0-9-]{10,}/g, replace: REDACTION_MARK },
|
|
486
|
+
{ pattern: /\bAKIA[0-9A-Z]{16}\b/g, replace: REDACTION_MARK },
|
|
487
|
+
{ pattern: /\bAIza[0-9A-Za-z_-]{20,}/g, replace: REDACTION_MARK },
|
|
488
|
+
{
|
|
489
|
+
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
|
|
490
|
+
replace: REDACTION_MARK
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
pattern: /\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{12,}/gi,
|
|
494
|
+
replace: `$1 ${REDACTION_MARK}`
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
pattern: /(\w+:\/\/)[^/\s:@]+:[^/\s@]+@/g,
|
|
498
|
+
replace: `$1${REDACTION_MARK}@`
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
pattern: /\b([A-Za-z0-9_]{0,40}(?:TOKEN|SECRET|PASSWORD|PASSWD|APIKEY|API_KEY|ACCESS_KEY|PRIVATE_KEY|CREDENTIAL|AUTH)[A-Za-z0-9_]{0,40})\s*[=:]\s*(?:"[^"]*"|'[^']*'|`[^`]*`|[^\s,;)}\]]+)/gi,
|
|
502
|
+
replace: `$1=${REDACTION_MARK}`
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
pattern: /(--?(?:password|passwd|token|api-?key|secret|auth)(?:=|\s+))(?:"[^"]*"|'[^']*'|[^\s]+)/gi,
|
|
506
|
+
replace: `$1${REDACTION_MARK}`
|
|
507
|
+
}
|
|
508
|
+
];
|
|
509
|
+
function redactSecrets(text) {
|
|
510
|
+
if (typeof text !== "string" || text.length === 0)
|
|
511
|
+
return text;
|
|
512
|
+
let out = text;
|
|
513
|
+
for (const { pattern, replace } of SECRET_PATTERNS) {
|
|
514
|
+
pattern.lastIndex = 0;
|
|
515
|
+
out = out.replace(pattern, replace);
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
function truncate(text, max, originalLength) {
|
|
520
|
+
const total = originalLength ?? text.length;
|
|
521
|
+
if (total <= max)
|
|
522
|
+
return text;
|
|
523
|
+
return `${text.slice(0, max)}… [+${total - max} chars]`;
|
|
524
|
+
}
|
|
525
|
+
function redactThenTruncate(text, max) {
|
|
526
|
+
const preCap = max * 4 + 64;
|
|
527
|
+
const scanned = text.length > preCap ? text.slice(0, preCap) : text;
|
|
528
|
+
return truncate(redactSecrets(scanned), max, text.length);
|
|
529
|
+
}
|
|
530
|
+
function redactStructure(value, depth = 0) {
|
|
531
|
+
if (depth > 6)
|
|
532
|
+
return REDACTION_MARK;
|
|
533
|
+
if (typeof value === "string") {
|
|
534
|
+
return redactThenTruncate(value, MAX_INPUT_STRING_CHARS);
|
|
535
|
+
}
|
|
536
|
+
if (Array.isArray(value)) {
|
|
537
|
+
return value.slice(0, 20).map((item) => redactStructure(item, depth + 1));
|
|
538
|
+
}
|
|
539
|
+
if (value !== null && typeof value === "object") {
|
|
540
|
+
const out = {};
|
|
541
|
+
for (const [key, item] of Object.entries(value)) {
|
|
542
|
+
out[key] = redactStructure(item, depth + 1);
|
|
543
|
+
}
|
|
544
|
+
return out;
|
|
545
|
+
}
|
|
546
|
+
return value;
|
|
547
|
+
}
|
|
548
|
+
function redactToolCall(args) {
|
|
549
|
+
const sensitive = sensitivePathsIn(args.input);
|
|
550
|
+
if (sensitive.length > 0) {
|
|
551
|
+
return { withheld: "sensitive-path" };
|
|
552
|
+
}
|
|
553
|
+
const result = {};
|
|
554
|
+
if (args.input !== undefined) {
|
|
555
|
+
let input = redactStructure(args.input);
|
|
556
|
+
let serialized;
|
|
557
|
+
try {
|
|
558
|
+
serialized = JSON.stringify(input) ?? "";
|
|
559
|
+
} catch {
|
|
560
|
+
serialized = "";
|
|
561
|
+
input = REDACTION_MARK;
|
|
562
|
+
}
|
|
563
|
+
if (serialized.length > MAX_INPUT_CHARS) {
|
|
564
|
+
input = truncate(serialized, MAX_INPUT_CHARS);
|
|
565
|
+
}
|
|
566
|
+
result.input = input;
|
|
567
|
+
}
|
|
568
|
+
if (typeof args.output === "string" && args.output.length > 0) {
|
|
569
|
+
result.output = redactThenTruncate(args.output, MAX_OUTPUT_CHARS);
|
|
570
|
+
}
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/run-hook.ts
|
|
575
|
+
function extractOutputText(response, depth = 0) {
|
|
576
|
+
if (depth > 4)
|
|
577
|
+
return "";
|
|
578
|
+
if (response === null || response === undefined)
|
|
579
|
+
return "";
|
|
580
|
+
if (typeof response === "string")
|
|
581
|
+
return response;
|
|
582
|
+
if (typeof response === "number" || typeof response === "boolean") {
|
|
583
|
+
return String(response);
|
|
584
|
+
}
|
|
585
|
+
if (Array.isArray(response)) {
|
|
586
|
+
return response.map((item) => extractOutputText(item, depth + 1)).filter((part) => part.length > 0).join(`
|
|
587
|
+
`);
|
|
588
|
+
}
|
|
589
|
+
if (typeof response === "object") {
|
|
590
|
+
const record = response;
|
|
591
|
+
if (typeof record.text === "string")
|
|
592
|
+
return record.text;
|
|
593
|
+
const parts = [];
|
|
594
|
+
for (const key of ["stdout", "stderr", "output", "content", "result"]) {
|
|
595
|
+
const value = record[key];
|
|
596
|
+
if (value === undefined || value === null)
|
|
597
|
+
continue;
|
|
598
|
+
const text = extractOutputText(value, depth + 1);
|
|
599
|
+
if (text.length > 0)
|
|
600
|
+
parts.push(text);
|
|
601
|
+
}
|
|
602
|
+
if (parts.length > 0)
|
|
603
|
+
return parts.join(`
|
|
604
|
+
`);
|
|
605
|
+
try {
|
|
606
|
+
const serialized = JSON.stringify(record) ?? "";
|
|
607
|
+
return serialized === "{}" || serialized === "[]" ? "" : serialized;
|
|
608
|
+
} catch {
|
|
609
|
+
return "";
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return "";
|
|
613
|
+
}
|
|
614
|
+
function extractIsError(response) {
|
|
615
|
+
if (response === null || typeof response !== "object")
|
|
616
|
+
return false;
|
|
617
|
+
const record = response;
|
|
618
|
+
if (record.is_error === true || record.isError === true)
|
|
619
|
+
return true;
|
|
620
|
+
if (record.interrupted === true)
|
|
621
|
+
return true;
|
|
622
|
+
if (typeof record.error === "string" && record.error.length > 0)
|
|
623
|
+
return true;
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
function correlationId(payload, nonce) {
|
|
627
|
+
const supplied = payload.tool_use_id;
|
|
628
|
+
if (typeof supplied === "string" && supplied.length > 0)
|
|
629
|
+
return supplied;
|
|
630
|
+
return `hook-${nonce()}`;
|
|
631
|
+
}
|
|
632
|
+
function buildHookEvents(payload, options) {
|
|
633
|
+
const toolName = typeof payload?.tool_name === "string" ? payload.tool_name.trim() : "";
|
|
634
|
+
if (!toolName)
|
|
635
|
+
return [];
|
|
636
|
+
if (typeof payload.hook_event_name === "string" && payload.hook_event_name !== "PostToolUse") {
|
|
637
|
+
return [];
|
|
638
|
+
}
|
|
639
|
+
const now = options?.now ?? Date.now();
|
|
640
|
+
const nonce = options?.nonce ?? (() => Math.random().toString(36).slice(2, 12));
|
|
641
|
+
const toolUseId = correlationId(payload, nonce);
|
|
642
|
+
const rawOutput = extractOutputText(payload.tool_response);
|
|
643
|
+
const isError = extractIsError(payload.tool_response);
|
|
644
|
+
const redacted = redactToolCall({
|
|
645
|
+
input: payload.tool_input,
|
|
646
|
+
output: rawOutput
|
|
647
|
+
});
|
|
648
|
+
const startPayload = { toolName, toolUseId };
|
|
649
|
+
const endPayload = { toolName, toolUseId };
|
|
650
|
+
if (redacted.withheld) {
|
|
651
|
+
startPayload.withheld = redacted.withheld;
|
|
652
|
+
endPayload.withheld = redacted.withheld;
|
|
653
|
+
endPayload.output = `[withheld: ${redacted.withheld}]`;
|
|
654
|
+
} else {
|
|
655
|
+
if (redacted.input !== undefined)
|
|
656
|
+
startPayload.input = redacted.input;
|
|
657
|
+
if (redacted.output !== undefined)
|
|
658
|
+
endPayload.output = redacted.output;
|
|
659
|
+
}
|
|
660
|
+
if (isError)
|
|
661
|
+
endPayload.isError = true;
|
|
662
|
+
return [
|
|
663
|
+
{
|
|
664
|
+
kind: "tool_started",
|
|
665
|
+
source: "agent",
|
|
666
|
+
payload: startPayload,
|
|
667
|
+
createdAt: new Date(now).toISOString()
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
kind: "tool_ended",
|
|
671
|
+
source: "agent",
|
|
672
|
+
payload: endPayload,
|
|
673
|
+
createdAt: new Date(now + 1).toISOString()
|
|
674
|
+
}
|
|
675
|
+
];
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// src/run-hook-main.ts
|
|
679
|
+
init_run_state();
|
|
680
|
+
var MAX_SPOOL_BATCHES = 1000;
|
|
681
|
+
async function readStdin() {
|
|
682
|
+
const chunks = [];
|
|
683
|
+
for await (const chunk of process.stdin) {
|
|
684
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
685
|
+
if (chunks.reduce((n, c) => n + c.length, 0) > 4000000)
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
689
|
+
}
|
|
690
|
+
async function runPostToolUseHook(runtime = {}) {
|
|
691
|
+
const stateDir = runtime.stateDir ?? runStateDir();
|
|
692
|
+
if (!runStateExists(stateDir))
|
|
693
|
+
return;
|
|
694
|
+
const raw = await (runtime.readInput ?? readStdin)();
|
|
695
|
+
if (!raw.trim())
|
|
696
|
+
return;
|
|
697
|
+
let payload;
|
|
698
|
+
try {
|
|
699
|
+
payload = JSON.parse(raw);
|
|
700
|
+
} catch {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const candidates = readPublishedSessions({ stateDir });
|
|
704
|
+
if (candidates.length === 0)
|
|
705
|
+
return;
|
|
706
|
+
const harnessSessionId = typeof payload.session_id === "string" ? payload.session_id : "";
|
|
707
|
+
const payloadCwd = typeof payload.cwd === "string" ? payload.cwd : undefined;
|
|
708
|
+
let chosen = null;
|
|
709
|
+
if (harnessSessionId) {
|
|
710
|
+
const memo = readRouteMemo(stateDir, harnessSessionId);
|
|
711
|
+
if (memo !== null) {
|
|
712
|
+
chosen = candidates.find((c) => c.publisherPid === memo.publisherPid && c.cardId === memo.cardId && c.agentSessionId === memo.agentSessionId && (payloadCwd === undefined || c.cwd === payloadCwd)) ?? null;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (!chosen) {
|
|
716
|
+
chosen = chooseRunSessionForHook({
|
|
717
|
+
candidates,
|
|
718
|
+
hookAncestorPids: (runtime.hookAncestorPids ?? (() => ancestorPids(process.pid)))(),
|
|
719
|
+
cwd: payloadCwd
|
|
720
|
+
});
|
|
721
|
+
if (chosen && harnessSessionId) {
|
|
722
|
+
writeRouteMemo(stateDir, harnessSessionId, {
|
|
723
|
+
publisherPid: chosen.publisherPid,
|
|
724
|
+
cardId: chosen.cardId,
|
|
725
|
+
agentSessionId: chosen.agentSessionId
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (!chosen)
|
|
730
|
+
return;
|
|
731
|
+
const events = buildHookEvents(payload);
|
|
732
|
+
if (events.length === 0)
|
|
733
|
+
return;
|
|
734
|
+
const dir = spoolDir(stateDir, chosen.agentSessionId);
|
|
735
|
+
trimSpool(dir, MAX_SPOOL_BATCHES);
|
|
736
|
+
writeSpoolBatch(dir, events);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/run-hook-cli.ts
|
|
740
|
+
runPostToolUseHook().catch(() => {}).finally(() => {
|
|
741
|
+
process.exit(0);
|
|
742
|
+
});
|