@node9/proxy 1.38.0 → 1.40.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 +55 -0
- package/dist/cli.js +1024 -460
- package/dist/cli.mjs +1022 -458
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -185,8 +185,8 @@ function sanitizeConfig(raw) {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
const lines = result.error.issues.map((issue) => {
|
|
188
|
-
const
|
|
189
|
-
return ` \u2022 ${
|
|
188
|
+
const path61 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
189
|
+
return ` \u2022 ${path61}: ${issue.message}`;
|
|
190
190
|
});
|
|
191
191
|
return {
|
|
192
192
|
sanitized,
|
|
@@ -1258,9 +1258,9 @@ function matchesPattern(text, patterns) {
|
|
|
1258
1258
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1259
1259
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1260
1260
|
}
|
|
1261
|
-
function getNestedValue(obj,
|
|
1261
|
+
function getNestedValue(obj, path61) {
|
|
1262
1262
|
if (!obj || typeof obj !== "object") return null;
|
|
1263
|
-
const segments =
|
|
1263
|
+
const segments = path61.split(".");
|
|
1264
1264
|
for (const seg of segments) {
|
|
1265
1265
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
1266
1266
|
}
|
|
@@ -14014,6 +14014,9 @@ function registerScanCommand(program2) {
|
|
|
14014
14014
|
console.log(
|
|
14015
14015
|
" " + chalk5.dim("\u2192 ") + chalk5.cyan("node9 scan --drill-down") + chalk5.dim(" full commands + session IDs")
|
|
14016
14016
|
);
|
|
14017
|
+
console.log(
|
|
14018
|
+
" " + chalk5.dim("\u2192 ") + chalk5.cyan.underline("https://node9.ai/auth/signup?ref=cli_scan") + chalk5.dim(" track your fleet")
|
|
14019
|
+
);
|
|
14017
14020
|
console.log("");
|
|
14018
14021
|
return;
|
|
14019
14022
|
}
|
|
@@ -14199,7 +14202,11 @@ function registerScanCommand(program2) {
|
|
|
14199
14202
|
" Hooks into Claude Code automatically. Every tool call checked before it runs."
|
|
14200
14203
|
)
|
|
14201
14204
|
);
|
|
14202
|
-
console.log("
|
|
14205
|
+
console.log("");
|
|
14206
|
+
console.log(" " + chalk5.bold("See the full report & track your fleet:"));
|
|
14207
|
+
console.log(
|
|
14208
|
+
" " + chalk5.dim("\u2192 ") + chalk5.cyan.underline("https://node9.ai/auth/signup?ref=cli_scan")
|
|
14209
|
+
);
|
|
14203
14210
|
}
|
|
14204
14211
|
console.log("");
|
|
14205
14212
|
}
|
|
@@ -17191,10 +17198,10 @@ __export(tail_exports, {
|
|
|
17191
17198
|
startTail: () => startTail
|
|
17192
17199
|
});
|
|
17193
17200
|
import http3 from "http";
|
|
17194
|
-
import
|
|
17195
|
-
import
|
|
17196
|
-
import
|
|
17197
|
-
import
|
|
17201
|
+
import chalk34 from "chalk";
|
|
17202
|
+
import fs60 from "fs";
|
|
17203
|
+
import os52 from "os";
|
|
17204
|
+
import path58 from "path";
|
|
17198
17205
|
import readline6 from "readline";
|
|
17199
17206
|
import { spawn as spawn8 } from "child_process";
|
|
17200
17207
|
function shortenPathSummary(s) {
|
|
@@ -17218,20 +17225,20 @@ function getModelContextLimit(model) {
|
|
|
17218
17225
|
return 2e5;
|
|
17219
17226
|
}
|
|
17220
17227
|
function readSessionUsage() {
|
|
17221
|
-
const projectsDir =
|
|
17222
|
-
if (!
|
|
17228
|
+
const projectsDir = path58.join(os52.homedir(), ".claude", "projects");
|
|
17229
|
+
if (!fs60.existsSync(projectsDir)) return null;
|
|
17223
17230
|
let latestFile = null;
|
|
17224
17231
|
let latestMtime = 0;
|
|
17225
17232
|
try {
|
|
17226
|
-
for (const dir of
|
|
17227
|
-
const dirPath =
|
|
17233
|
+
for (const dir of fs60.readdirSync(projectsDir)) {
|
|
17234
|
+
const dirPath = path58.join(projectsDir, dir);
|
|
17228
17235
|
try {
|
|
17229
|
-
if (!
|
|
17230
|
-
for (const file of
|
|
17236
|
+
if (!fs60.statSync(dirPath).isDirectory()) continue;
|
|
17237
|
+
for (const file of fs60.readdirSync(dirPath)) {
|
|
17231
17238
|
if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
|
|
17232
|
-
const filePath =
|
|
17239
|
+
const filePath = path58.join(dirPath, file);
|
|
17233
17240
|
try {
|
|
17234
|
-
const mtime =
|
|
17241
|
+
const mtime = fs60.statSync(filePath).mtimeMs;
|
|
17235
17242
|
if (mtime > latestMtime) {
|
|
17236
17243
|
latestMtime = mtime;
|
|
17237
17244
|
latestFile = filePath;
|
|
@@ -17246,7 +17253,7 @@ function readSessionUsage() {
|
|
|
17246
17253
|
}
|
|
17247
17254
|
if (!latestFile) return null;
|
|
17248
17255
|
try {
|
|
17249
|
-
const lines =
|
|
17256
|
+
const lines = fs60.readFileSync(latestFile, "utf-8").split("\n");
|
|
17250
17257
|
let lastModel = "";
|
|
17251
17258
|
let lastInput = 0;
|
|
17252
17259
|
let lastOutput = 0;
|
|
@@ -17271,10 +17278,10 @@ function readSessionUsage() {
|
|
|
17271
17278
|
}
|
|
17272
17279
|
}
|
|
17273
17280
|
function formatContextStat(stat) {
|
|
17274
|
-
const pctColor = stat.fillPct >= 80 ?
|
|
17281
|
+
const pctColor = stat.fillPct >= 80 ? chalk34.red : stat.fillPct >= 50 ? chalk34.yellow : chalk34.cyan;
|
|
17275
17282
|
const k = (n) => `${Math.round(n / 1e3)}k`;
|
|
17276
17283
|
const modelShort = stat.model.replace(/@.*$/, "").replace(/-\d{8}$/, "").replace(/^claude-/, "");
|
|
17277
|
-
return
|
|
17284
|
+
return chalk34.dim("ctx: ") + pctColor(`${stat.fillPct}%`) + chalk34.dim(
|
|
17278
17285
|
` (${k(stat.inputTokens)}/${k(getModelContextLimit(stat.model))} out ${k(stat.outputTokens)} \xB7 ${modelShort})`
|
|
17279
17286
|
);
|
|
17280
17287
|
}
|
|
@@ -17297,32 +17304,32 @@ function agentLabel(agent, mcpServer, sessionId) {
|
|
|
17297
17304
|
const tag = sessionTag(sessionId);
|
|
17298
17305
|
const tagSuffix = tag ? `\xB7${tag}` : "";
|
|
17299
17306
|
if (!agent || agent === "Terminal") {
|
|
17300
|
-
return mcpServer ?
|
|
17307
|
+
return mcpServer ? chalk34.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
17301
17308
|
}
|
|
17302
17309
|
const short = agent === "Claude Code" ? "Claude" : agent === "Gemini CLI" ? "Gemini" : agent === "Unknown Agent" ? "" : agent.split(" ")[0];
|
|
17303
|
-
if (!short) return mcpServer ?
|
|
17304
|
-
return mcpServer ?
|
|
17310
|
+
if (!short) return mcpServer ? chalk34.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
17311
|
+
return mcpServer ? chalk34.dim(`[${short}${tagSuffix} \u2192 ${mcpServer}] `) : chalk34.dim(`[${short}${tagSuffix}] `);
|
|
17305
17312
|
}
|
|
17306
17313
|
function formatBase(activity) {
|
|
17307
17314
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
17308
17315
|
const icon = getIcon(activity.tool);
|
|
17309
17316
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
17310
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(
|
|
17317
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os52.homedir(), "~");
|
|
17311
17318
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
17312
|
-
return `${
|
|
17319
|
+
return `${chalk34.gray(time)} ${icon} ${agentLabel(activity.agent, activity.mcpServer, activity.sessionId)}${chalk34.white.bold(toolName)} ${chalk34.dim(argsPreview)}`;
|
|
17313
17320
|
}
|
|
17314
17321
|
function renderResult(activity, result) {
|
|
17315
17322
|
const base = formatBase(activity);
|
|
17316
17323
|
let status;
|
|
17317
17324
|
if (result.status === "allow") {
|
|
17318
|
-
status =
|
|
17325
|
+
status = chalk34.green("\u2713 ALLOW");
|
|
17319
17326
|
} else if (result.status === "dlp") {
|
|
17320
|
-
status =
|
|
17327
|
+
status = chalk34.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
17321
17328
|
} else {
|
|
17322
|
-
status =
|
|
17329
|
+
status = chalk34.red("\u2717 BLOCK");
|
|
17323
17330
|
}
|
|
17324
17331
|
const cost = result.costEstimate ?? activity.costEstimate;
|
|
17325
|
-
const costSuffix = cost == null ? "" :
|
|
17332
|
+
const costSuffix = cost == null ? "" : chalk34.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
17326
17333
|
if (process.stdout.isTTY) {
|
|
17327
17334
|
if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
|
|
17328
17335
|
readline6.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
|
|
@@ -17339,19 +17346,19 @@ function renderResult(activity, result) {
|
|
|
17339
17346
|
}
|
|
17340
17347
|
function renderPending(activity) {
|
|
17341
17348
|
if (!process.stdout.isTTY) return;
|
|
17342
|
-
const line = `${formatBase(activity)} ${
|
|
17349
|
+
const line = `${formatBase(activity)} ${chalk34.yellow("\u25CF \u2026")}`;
|
|
17343
17350
|
pendingShownForId = activity.id;
|
|
17344
17351
|
pendingWrappedLines = wrappedLineCount(line);
|
|
17345
17352
|
process.stdout.write(`${line}\r`);
|
|
17346
17353
|
}
|
|
17347
17354
|
async function ensureDaemon() {
|
|
17348
17355
|
let pidPort = null;
|
|
17349
|
-
if (
|
|
17356
|
+
if (fs60.existsSync(PID_FILE)) {
|
|
17350
17357
|
try {
|
|
17351
|
-
const { port } = JSON.parse(
|
|
17358
|
+
const { port } = JSON.parse(fs60.readFileSync(PID_FILE, "utf-8"));
|
|
17352
17359
|
pidPort = port;
|
|
17353
17360
|
} catch {
|
|
17354
|
-
console.error(
|
|
17361
|
+
console.error(chalk34.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
17355
17362
|
}
|
|
17356
17363
|
}
|
|
17357
17364
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -17362,7 +17369,7 @@ async function ensureDaemon() {
|
|
|
17362
17369
|
if (res.ok) return checkPort;
|
|
17363
17370
|
} catch {
|
|
17364
17371
|
}
|
|
17365
|
-
console.log(
|
|
17372
|
+
console.log(chalk34.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
17366
17373
|
const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
|
|
17367
17374
|
detached: true,
|
|
17368
17375
|
stdio: "ignore",
|
|
@@ -17379,7 +17386,7 @@ async function ensureDaemon() {
|
|
|
17379
17386
|
} catch {
|
|
17380
17387
|
}
|
|
17381
17388
|
}
|
|
17382
|
-
console.error(
|
|
17389
|
+
console.error(chalk34.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
17383
17390
|
process.exit(1);
|
|
17384
17391
|
}
|
|
17385
17392
|
function postDecisionHttp(id, decision, authToken, port, opts) {
|
|
@@ -17448,7 +17455,7 @@ function buildCardLines(req, localCount = 0) {
|
|
|
17448
17455
|
const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
|
|
17449
17456
|
const rawDesc = req.riskMetadata?.ruleDescription ?? "";
|
|
17450
17457
|
const description = rawDesc ? cleanReason(rawDesc) : "";
|
|
17451
|
-
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${
|
|
17458
|
+
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${chalk34.dim(`(${req.agent})`)}` : "";
|
|
17452
17459
|
const lines = [
|
|
17453
17460
|
``,
|
|
17454
17461
|
`${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
|
|
@@ -17504,9 +17511,9 @@ function buildRecoveryCardLines(req) {
|
|
|
17504
17511
|
];
|
|
17505
17512
|
}
|
|
17506
17513
|
function readApproversFromDisk() {
|
|
17507
|
-
const configPath2 =
|
|
17514
|
+
const configPath2 = path58.join(os52.homedir(), ".node9", "config.json");
|
|
17508
17515
|
try {
|
|
17509
|
-
const raw = JSON.parse(
|
|
17516
|
+
const raw = JSON.parse(fs60.readFileSync(configPath2, "utf-8"));
|
|
17510
17517
|
const settings = raw.settings ?? {};
|
|
17511
17518
|
return settings.approvers ?? {};
|
|
17512
17519
|
} catch {
|
|
@@ -17517,20 +17524,20 @@ function approverStatusLine() {
|
|
|
17517
17524
|
const a = readApproversFromDisk();
|
|
17518
17525
|
const fmt = (label2, key) => {
|
|
17519
17526
|
const on = a[key] !== false;
|
|
17520
|
-
return `[${key[0]}]${label2.slice(1)} ${on ?
|
|
17527
|
+
return `[${key[0]}]${label2.slice(1)} ${on ? chalk34.green("\u2713") : chalk34.dim("\u2717")}`;
|
|
17521
17528
|
};
|
|
17522
17529
|
return `${fmt("native", "native")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
17523
17530
|
}
|
|
17524
17531
|
function toggleApprover(channel) {
|
|
17525
|
-
const configPath2 =
|
|
17532
|
+
const configPath2 = path58.join(os52.homedir(), ".node9", "config.json");
|
|
17526
17533
|
try {
|
|
17527
|
-
const raw = JSON.parse(
|
|
17534
|
+
const raw = JSON.parse(fs60.readFileSync(configPath2, "utf-8"));
|
|
17528
17535
|
const settings = raw.settings ?? {};
|
|
17529
17536
|
const approvers = settings.approvers ?? {};
|
|
17530
17537
|
approvers[channel] = approvers[channel] === false;
|
|
17531
17538
|
settings.approvers = approvers;
|
|
17532
17539
|
raw.settings = settings;
|
|
17533
|
-
|
|
17540
|
+
fs60.writeFileSync(configPath2, JSON.stringify(raw, null, 2) + "\n");
|
|
17534
17541
|
} catch (err2) {
|
|
17535
17542
|
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
17536
17543
|
`);
|
|
@@ -17562,7 +17569,7 @@ async function startTail(options = {}) {
|
|
|
17562
17569
|
req2.end();
|
|
17563
17570
|
});
|
|
17564
17571
|
if (result.ok) {
|
|
17565
|
-
console.log(
|
|
17572
|
+
console.log(chalk34.green("\u2713 Flight Recorder buffer cleared."));
|
|
17566
17573
|
} else if (result.code === "ECONNREFUSED") {
|
|
17567
17574
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
17568
17575
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -17608,7 +17615,7 @@ async function startTail(options = {}) {
|
|
|
17608
17615
|
const channel = name === "n" ? "native" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
17609
17616
|
if (channel) {
|
|
17610
17617
|
toggleApprover(channel);
|
|
17611
|
-
console.log(
|
|
17618
|
+
console.log(chalk34.dim(` Approvers: ${approverStatusLine()}`));
|
|
17612
17619
|
}
|
|
17613
17620
|
};
|
|
17614
17621
|
process.stdin.on("keypress", idleKeypressHandler);
|
|
@@ -17674,7 +17681,7 @@ async function startTail(options = {}) {
|
|
|
17674
17681
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
17675
17682
|
)
|
|
17676
17683
|
);
|
|
17677
|
-
const decisionStamp = action === "always-allow" ?
|
|
17684
|
+
const decisionStamp = action === "always-allow" ? chalk34.yellow("\u2605 ALWAYS ALLOW") : action === "trust" ? chalk34.cyan("\u23F1 TRUST 30m") : action === "allow" ? chalk34.green("\u2713 ALLOWED") : action === "redirect" ? chalk34.yellow("\u21A9 REDIRECT AI") : chalk34.red("\u2717 DENIED");
|
|
17678
17685
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
17679
17686
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
17680
17687
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -17702,8 +17709,8 @@ async function startTail(options = {}) {
|
|
|
17702
17709
|
}
|
|
17703
17710
|
postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).catch((err2) => {
|
|
17704
17711
|
try {
|
|
17705
|
-
|
|
17706
|
-
|
|
17712
|
+
fs60.appendFileSync(
|
|
17713
|
+
path58.join(os52.homedir(), ".node9", "hook-debug.log"),
|
|
17707
17714
|
`[tail] POST /decision failed: ${String(err2)}
|
|
17708
17715
|
`
|
|
17709
17716
|
);
|
|
@@ -17725,7 +17732,7 @@ async function startTail(options = {}) {
|
|
|
17725
17732
|
);
|
|
17726
17733
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
17727
17734
|
if (externalDecision) {
|
|
17728
|
-
const source = externalDecision === "allow" ?
|
|
17735
|
+
const source = externalDecision === "allow" ? chalk34.green("\u2713 ALLOWED") : chalk34.red("\u2717 DENIED");
|
|
17729
17736
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
17730
17737
|
}
|
|
17731
17738
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
@@ -17767,31 +17774,31 @@ async function startTail(options = {}) {
|
|
|
17767
17774
|
};
|
|
17768
17775
|
process.stdin.on("keypress", onKeypress);
|
|
17769
17776
|
}
|
|
17770
|
-
const auditLog =
|
|
17777
|
+
const auditLog = path58.join(os52.homedir(), ".node9", "audit.log");
|
|
17771
17778
|
try {
|
|
17772
|
-
const unackedDlp =
|
|
17779
|
+
const unackedDlp = fs60.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
|
|
17773
17780
|
if (unackedDlp > 0) {
|
|
17774
17781
|
console.log("");
|
|
17775
17782
|
console.log(
|
|
17776
|
-
|
|
17783
|
+
chalk34.bgRed.white.bold(
|
|
17777
17784
|
` \u26A0\uFE0F DLP ALERT: ${unackedDlp} secret${unackedDlp !== 1 ? "s" : ""} found in Claude response text \u2014 run: node9 dlp `
|
|
17778
17785
|
)
|
|
17779
17786
|
);
|
|
17780
17787
|
}
|
|
17781
17788
|
} catch {
|
|
17782
17789
|
}
|
|
17783
|
-
console.log(
|
|
17790
|
+
console.log(chalk34.cyan.bold(`
|
|
17784
17791
|
\u{1F6F0}\uFE0F Node9 tail`));
|
|
17785
17792
|
if (canApprove) {
|
|
17786
|
-
console.log(
|
|
17787
|
-
console.log(
|
|
17793
|
+
console.log(chalk34.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
17794
|
+
console.log(chalk34.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
17788
17795
|
}
|
|
17789
17796
|
const ctxStat = readSessionUsage();
|
|
17790
17797
|
if (ctxStat) console.log(" " + formatContextStat(ctxStat));
|
|
17791
17798
|
if (options.history) {
|
|
17792
|
-
console.log(
|
|
17799
|
+
console.log(chalk34.dim("Showing history + live events.\n"));
|
|
17793
17800
|
} else {
|
|
17794
|
-
console.log(
|
|
17801
|
+
console.log(chalk34.dim("Showing live events only. Use --history to include past.\n"));
|
|
17795
17802
|
}
|
|
17796
17803
|
process.on("SIGINT", () => {
|
|
17797
17804
|
exitIdleMode();
|
|
@@ -17801,7 +17808,7 @@ async function startTail(options = {}) {
|
|
|
17801
17808
|
readline6.clearLine(process.stdout, 0);
|
|
17802
17809
|
readline6.cursorTo(process.stdout, 0);
|
|
17803
17810
|
}
|
|
17804
|
-
console.log(
|
|
17811
|
+
console.log(chalk34.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
17805
17812
|
process.exit(0);
|
|
17806
17813
|
});
|
|
17807
17814
|
const STALL_THRESHOLD_MS = 6e4;
|
|
@@ -17809,11 +17816,11 @@ async function startTail(options = {}) {
|
|
|
17809
17816
|
if (stallWarned) return;
|
|
17810
17817
|
if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
|
|
17811
17818
|
try {
|
|
17812
|
-
const auditMtime =
|
|
17819
|
+
const auditMtime = fs60.statSync(auditLog).mtimeMs;
|
|
17813
17820
|
if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
|
|
17814
17821
|
console.log("");
|
|
17815
17822
|
console.log(
|
|
17816
|
-
|
|
17823
|
+
chalk34.yellow(
|
|
17817
17824
|
"\u26A0\uFE0F Tail appears stalled \u2014 hooks are firing but no events are arriving. Try: node9 daemon restart"
|
|
17818
17825
|
)
|
|
17819
17826
|
);
|
|
@@ -17830,7 +17837,7 @@ async function startTail(options = {}) {
|
|
|
17830
17837
|
},
|
|
17831
17838
|
(res) => {
|
|
17832
17839
|
if (res.statusCode !== 200) {
|
|
17833
|
-
console.error(
|
|
17840
|
+
console.error(chalk34.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
17834
17841
|
process.exit(1);
|
|
17835
17842
|
}
|
|
17836
17843
|
if (canApprove) enterIdleMode();
|
|
@@ -17861,7 +17868,7 @@ async function startTail(options = {}) {
|
|
|
17861
17868
|
readline6.clearLine(process.stdout, 0);
|
|
17862
17869
|
readline6.cursorTo(process.stdout, 0);
|
|
17863
17870
|
}
|
|
17864
|
-
console.log(
|
|
17871
|
+
console.log(chalk34.red("\n\u274C Daemon disconnected."));
|
|
17865
17872
|
process.exit(1);
|
|
17866
17873
|
});
|
|
17867
17874
|
}
|
|
@@ -17874,7 +17881,7 @@ async function startTail(options = {}) {
|
|
|
17874
17881
|
const parsed = JSON.parse(rawData);
|
|
17875
17882
|
const msg = parsed.message ?? "Flight recorder is down \u2014 run: node9 daemon restart";
|
|
17876
17883
|
console.log("");
|
|
17877
|
-
console.log(
|
|
17884
|
+
console.log(chalk34.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
|
|
17878
17885
|
} catch {
|
|
17879
17886
|
}
|
|
17880
17887
|
return;
|
|
@@ -17959,9 +17966,9 @@ async function startTail(options = {}) {
|
|
|
17959
17966
|
const rawSummary = data.argsSummary ?? data.tool;
|
|
17960
17967
|
const summary = shortenPathSummary(rawSummary);
|
|
17961
17968
|
const fileCount = data.fileCount ?? 0;
|
|
17962
|
-
const files = fileCount > 0 ?
|
|
17969
|
+
const files = fileCount > 0 ? chalk34.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
17963
17970
|
process.stdout.write(
|
|
17964
|
-
`${
|
|
17971
|
+
`${chalk34.dim(time)} ${chalk34.cyan("\u{1F4F8} snapshot")} ${chalk34.dim(hash)} ${summary}${files}
|
|
17965
17972
|
`
|
|
17966
17973
|
);
|
|
17967
17974
|
return;
|
|
@@ -17978,18 +17985,18 @@ async function startTail(options = {}) {
|
|
|
17978
17985
|
if (event === "execution-result") {
|
|
17979
17986
|
const exec = data;
|
|
17980
17987
|
const time = new Date(Date.now()).toLocaleTimeString([], { hour12: false });
|
|
17981
|
-
const arrow = exec.isError ?
|
|
17988
|
+
const arrow = exec.isError ? chalk34.red(" \u21B3 \u2717") : chalk34.green(" \u21B3 \u2713");
|
|
17982
17989
|
const label2 = agentLabel(exec.agent, exec.mcpServer);
|
|
17983
17990
|
const tool = (exec.tool ?? "").slice(0, 16);
|
|
17984
|
-
const duration = typeof exec.durationMs === "number" ?
|
|
17991
|
+
const duration = typeof exec.durationMs === "number" ? chalk34.dim(` (${exec.durationMs}ms)`) : "";
|
|
17985
17992
|
console.log(
|
|
17986
|
-
`${
|
|
17993
|
+
`${chalk34.gray(time)} ${arrow} ${label2}${chalk34.dim(tool)}${chalk34.dim(" completed")}${duration}`
|
|
17987
17994
|
);
|
|
17988
17995
|
}
|
|
17989
17996
|
}
|
|
17990
17997
|
req.on("error", (err2) => {
|
|
17991
17998
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
17992
|
-
console.error(
|
|
17999
|
+
console.error(chalk34.red(`
|
|
17993
18000
|
\u274C ${msg}`));
|
|
17994
18001
|
process.exit(1);
|
|
17995
18002
|
});
|
|
@@ -18000,7 +18007,7 @@ var init_tail = __esm({
|
|
|
18000
18007
|
"use strict";
|
|
18001
18008
|
init_daemon2();
|
|
18002
18009
|
init_daemon();
|
|
18003
|
-
PID_FILE =
|
|
18010
|
+
PID_FILE = path58.join(os52.homedir(), ".node9", "daemon.pid");
|
|
18004
18011
|
ICONS = {
|
|
18005
18012
|
bash: "\u{1F4BB}",
|
|
18006
18013
|
shell: "\u{1F4BB}",
|
|
@@ -18048,9 +18055,9 @@ __export(hud_exports, {
|
|
|
18048
18055
|
main: () => main,
|
|
18049
18056
|
renderEnvironmentLine: () => renderEnvironmentLine
|
|
18050
18057
|
});
|
|
18051
|
-
import
|
|
18052
|
-
import
|
|
18053
|
-
import
|
|
18058
|
+
import fs61 from "fs";
|
|
18059
|
+
import path59 from "path";
|
|
18060
|
+
import os53 from "os";
|
|
18054
18061
|
import http4 from "http";
|
|
18055
18062
|
async function readStdin() {
|
|
18056
18063
|
const chunks = [];
|
|
@@ -18126,9 +18133,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
18126
18133
|
return ` (${m}m left)`;
|
|
18127
18134
|
}
|
|
18128
18135
|
function safeReadJson(filePath) {
|
|
18129
|
-
if (!
|
|
18136
|
+
if (!fs61.existsSync(filePath)) return null;
|
|
18130
18137
|
try {
|
|
18131
|
-
return JSON.parse(
|
|
18138
|
+
return JSON.parse(fs61.readFileSync(filePath, "utf-8"));
|
|
18132
18139
|
} catch {
|
|
18133
18140
|
return null;
|
|
18134
18141
|
}
|
|
@@ -18149,12 +18156,12 @@ function countHooksInFile(filePath) {
|
|
|
18149
18156
|
return Object.keys(cfg.hooks).length;
|
|
18150
18157
|
}
|
|
18151
18158
|
function countRulesInDir(rulesDir) {
|
|
18152
|
-
if (!
|
|
18159
|
+
if (!fs61.existsSync(rulesDir)) return 0;
|
|
18153
18160
|
let count = 0;
|
|
18154
18161
|
try {
|
|
18155
|
-
for (const entry of
|
|
18162
|
+
for (const entry of fs61.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
18156
18163
|
if (entry.isDirectory()) {
|
|
18157
|
-
count += countRulesInDir(
|
|
18164
|
+
count += countRulesInDir(path59.join(rulesDir, entry.name));
|
|
18158
18165
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
18159
18166
|
count++;
|
|
18160
18167
|
}
|
|
@@ -18165,46 +18172,46 @@ function countRulesInDir(rulesDir) {
|
|
|
18165
18172
|
}
|
|
18166
18173
|
function isSamePath(a, b) {
|
|
18167
18174
|
try {
|
|
18168
|
-
return
|
|
18175
|
+
return path59.resolve(a) === path59.resolve(b);
|
|
18169
18176
|
} catch {
|
|
18170
18177
|
return false;
|
|
18171
18178
|
}
|
|
18172
18179
|
}
|
|
18173
18180
|
function countConfigs(cwd) {
|
|
18174
|
-
const homeDir2 =
|
|
18175
|
-
const claudeDir =
|
|
18181
|
+
const homeDir2 = os53.homedir();
|
|
18182
|
+
const claudeDir = path59.join(homeDir2, ".claude");
|
|
18176
18183
|
let claudeMdCount = 0;
|
|
18177
18184
|
let rulesCount = 0;
|
|
18178
18185
|
let hooksCount = 0;
|
|
18179
18186
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
18180
18187
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
18181
|
-
if (
|
|
18182
|
-
rulesCount += countRulesInDir(
|
|
18183
|
-
const userSettings =
|
|
18188
|
+
if (fs61.existsSync(path59.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
18189
|
+
rulesCount += countRulesInDir(path59.join(claudeDir, "rules"));
|
|
18190
|
+
const userSettings = path59.join(claudeDir, "settings.json");
|
|
18184
18191
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
18185
18192
|
hooksCount += countHooksInFile(userSettings);
|
|
18186
|
-
const userClaudeJson =
|
|
18193
|
+
const userClaudeJson = path59.join(homeDir2, ".claude.json");
|
|
18187
18194
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
18188
18195
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
18189
18196
|
userMcpServers.delete(name);
|
|
18190
18197
|
}
|
|
18191
18198
|
if (cwd) {
|
|
18192
|
-
if (
|
|
18193
|
-
if (
|
|
18194
|
-
const projectClaudeDir =
|
|
18199
|
+
if (fs61.existsSync(path59.join(cwd, "CLAUDE.md"))) claudeMdCount++;
|
|
18200
|
+
if (fs61.existsSync(path59.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
|
|
18201
|
+
const projectClaudeDir = path59.join(cwd, ".claude");
|
|
18195
18202
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
18196
18203
|
if (!overlapsUserScope) {
|
|
18197
|
-
if (
|
|
18198
|
-
rulesCount += countRulesInDir(
|
|
18199
|
-
const projSettings =
|
|
18204
|
+
if (fs61.existsSync(path59.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
18205
|
+
rulesCount += countRulesInDir(path59.join(projectClaudeDir, "rules"));
|
|
18206
|
+
const projSettings = path59.join(projectClaudeDir, "settings.json");
|
|
18200
18207
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
18201
18208
|
hooksCount += countHooksInFile(projSettings);
|
|
18202
18209
|
}
|
|
18203
|
-
if (
|
|
18204
|
-
const localSettings =
|
|
18210
|
+
if (fs61.existsSync(path59.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
18211
|
+
const localSettings = path59.join(projectClaudeDir, "settings.local.json");
|
|
18205
18212
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
18206
18213
|
hooksCount += countHooksInFile(localSettings);
|
|
18207
|
-
const mcpJsonServers = getMcpServerNames(
|
|
18214
|
+
const mcpJsonServers = getMcpServerNames(path59.join(cwd, ".mcp.json"));
|
|
18208
18215
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
18209
18216
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
18210
18217
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -18237,12 +18244,12 @@ function readActiveShieldsHud() {
|
|
|
18237
18244
|
return shieldsCache.value;
|
|
18238
18245
|
}
|
|
18239
18246
|
try {
|
|
18240
|
-
const shieldsPath =
|
|
18241
|
-
if (!
|
|
18247
|
+
const shieldsPath = path59.join(os53.homedir(), ".node9", "shields.json");
|
|
18248
|
+
if (!fs61.existsSync(shieldsPath)) {
|
|
18242
18249
|
shieldsCache = { value: [], ts: now };
|
|
18243
18250
|
return [];
|
|
18244
18251
|
}
|
|
18245
|
-
const parsed = JSON.parse(
|
|
18252
|
+
const parsed = JSON.parse(fs61.readFileSync(shieldsPath, "utf-8"));
|
|
18246
18253
|
if (!Array.isArray(parsed.active)) {
|
|
18247
18254
|
shieldsCache = { value: [], ts: now };
|
|
18248
18255
|
return [];
|
|
@@ -18344,17 +18351,17 @@ function renderContextLine(stdin) {
|
|
|
18344
18351
|
async function main() {
|
|
18345
18352
|
try {
|
|
18346
18353
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
18347
|
-
if (
|
|
18354
|
+
if (fs61.existsSync(path59.join(os53.homedir(), ".node9", "hud-debug"))) {
|
|
18348
18355
|
try {
|
|
18349
|
-
const logPath =
|
|
18356
|
+
const logPath = path59.join(os53.homedir(), ".node9", "hud-debug.log");
|
|
18350
18357
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
18351
18358
|
let size = 0;
|
|
18352
18359
|
try {
|
|
18353
|
-
size =
|
|
18360
|
+
size = fs61.statSync(logPath).size;
|
|
18354
18361
|
} catch {
|
|
18355
18362
|
}
|
|
18356
18363
|
if (size < MAX_LOG_SIZE) {
|
|
18357
|
-
|
|
18364
|
+
fs61.appendFileSync(
|
|
18358
18365
|
logPath,
|
|
18359
18366
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
18360
18367
|
);
|
|
@@ -18375,11 +18382,11 @@ async function main() {
|
|
|
18375
18382
|
try {
|
|
18376
18383
|
const cwd = stdin.cwd ?? process.cwd();
|
|
18377
18384
|
for (const configPath2 of [
|
|
18378
|
-
|
|
18379
|
-
|
|
18385
|
+
path59.join(cwd, "node9.config.json"),
|
|
18386
|
+
path59.join(os53.homedir(), ".node9", "config.json")
|
|
18380
18387
|
]) {
|
|
18381
|
-
if (!
|
|
18382
|
-
const cfg = JSON.parse(
|
|
18388
|
+
if (!fs61.existsSync(configPath2)) continue;
|
|
18389
|
+
const cfg = JSON.parse(fs61.readFileSync(configPath2, "utf-8"));
|
|
18383
18390
|
const hud = cfg.settings?.hud;
|
|
18384
18391
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
18385
18392
|
}
|
|
@@ -18425,10 +18432,11 @@ init_core();
|
|
|
18425
18432
|
init_setup();
|
|
18426
18433
|
init_daemon2();
|
|
18427
18434
|
import { Command } from "commander";
|
|
18428
|
-
import
|
|
18429
|
-
import
|
|
18430
|
-
import
|
|
18431
|
-
import
|
|
18435
|
+
import chalk35 from "chalk";
|
|
18436
|
+
import fs62 from "fs";
|
|
18437
|
+
import path60 from "path";
|
|
18438
|
+
import os54 from "os";
|
|
18439
|
+
import { spawn as spawn9 } from "child_process";
|
|
18432
18440
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
18433
18441
|
|
|
18434
18442
|
// src/utils/duration.ts
|
|
@@ -24177,6 +24185,132 @@ function checkSecrets(ctx) {
|
|
|
24177
24185
|
|
|
24178
24186
|
// src/posture/egress.ts
|
|
24179
24187
|
init_config();
|
|
24188
|
+
import fs47 from "fs";
|
|
24189
|
+
|
|
24190
|
+
// src/sandbox/templates.ts
|
|
24191
|
+
var AGENT_NPM_PACKAGE = {
|
|
24192
|
+
claude: "@anthropic-ai/claude-code",
|
|
24193
|
+
codex: "@openai/codex"
|
|
24194
|
+
};
|
|
24195
|
+
function pinnedNode9Version(hostVersion) {
|
|
24196
|
+
return hostVersion && /^\d+\.\d+\.\d+$/.test(hostVersion) ? hostVersion : "latest";
|
|
24197
|
+
}
|
|
24198
|
+
var AGENT_BIN = {
|
|
24199
|
+
claude: "claude",
|
|
24200
|
+
codex: "codex"
|
|
24201
|
+
};
|
|
24202
|
+
var RUN_AS_USER = "agent";
|
|
24203
|
+
var ALLOWED_DOMAINS_PATH = "/etc/node9-sandbox/allowed-domains.txt";
|
|
24204
|
+
function renderDockerfile(config, node9Version2) {
|
|
24205
|
+
const agentPkg = AGENT_NPM_PACKAGE[config.agent];
|
|
24206
|
+
return `# Auto-generated by node9 sandbox. Do not edit by hand.
|
|
24207
|
+
FROM node:22-bookworm
|
|
24208
|
+
|
|
24209
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
24210
|
+
|
|
24211
|
+
# Wall + base tooling (iptables/ipset/dig/gosu) \u2014 ported from Isag.
|
|
24212
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
24213
|
+
ca-certificates curl git gosu iproute2 ipset iptables dnsutils jq \\
|
|
24214
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
24215
|
+
|
|
24216
|
+
# The worker (agent CLI).
|
|
24217
|
+
RUN npm install -g ${agentPkg}
|
|
24218
|
+
|
|
24219
|
+
# The guard (node9), pinned to the host version.
|
|
24220
|
+
RUN npm install -g node9-ai@${node9Version2}
|
|
24221
|
+
|
|
24222
|
+
# Non-root runtime user at uid 1000 (matches the typical single-user host so the
|
|
24223
|
+
# mounted ~/.claude / ~/.codex / project are read/writable). The node base image
|
|
24224
|
+
# already claims uid 1000 for the 'node' user \u2014 free it first (cf. Isag/ubuntu).
|
|
24225
|
+
RUN userdel -r node 2>/dev/null || true; \\
|
|
24226
|
+
userdel -r ubuntu 2>/dev/null || true; \\
|
|
24227
|
+
useradd --create-home --uid 1000 --shell /bin/bash ${RUN_AS_USER}
|
|
24228
|
+
|
|
24229
|
+
# Wire the agent's node9 hooks into the runtime user's home (build-time, static).
|
|
24230
|
+
RUN gosu ${RUN_AS_USER} node9 agents add ${config.agent} || true
|
|
24231
|
+
|
|
24232
|
+
RUN mkdir -p /workspace /etc/node9-sandbox \\
|
|
24233
|
+
&& chown ${RUN_AS_USER}:${RUN_AS_USER} /workspace
|
|
24234
|
+
|
|
24235
|
+
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
24236
|
+
RUN chmod +x /usr/local/bin/entrypoint.sh
|
|
24237
|
+
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
|
24238
|
+
`;
|
|
24239
|
+
}
|
|
24240
|
+
function renderEntrypoint(config) {
|
|
24241
|
+
const agentBin = AGENT_BIN[config.agent];
|
|
24242
|
+
return `#!/usr/bin/env bash
|
|
24243
|
+
# Auto-generated by node9 sandbox. Seals the egress wall (root), then drops to the
|
|
24244
|
+
# non-root agent which starts the node9 daemon + execs the agent.
|
|
24245
|
+
set -Eeuo pipefail
|
|
24246
|
+
|
|
24247
|
+
DOMAINS_FILE="${ALLOWED_DOMAINS_PATH}"
|
|
24248
|
+
RUN_AS_USER="${RUN_AS_USER}"
|
|
24249
|
+
|
|
24250
|
+
[[ -s "$DOMAINS_FILE" ]] || { echo "entrypoint: missing/empty $DOMAINS_FILE" >&2; exit 1; }
|
|
24251
|
+
|
|
24252
|
+
# Own the mounted node9 data dir so the agent user can write audit there.
|
|
24253
|
+
mkdir -p "/home/$RUN_AS_USER/.node9"
|
|
24254
|
+
chown -R "$RUN_AS_USER:$RUN_AS_USER" "/home/$RUN_AS_USER/.node9" || true
|
|
24255
|
+
|
|
24256
|
+
# \u2500\u2500 Resolve the allowlist \u2192 ipset (union of every resolver, like Isag) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
24257
|
+
mapfile -t LOCAL_DNS < <(awk '/^nameserver / {print $2}' /etc/resolv.conf)
|
|
24258
|
+
[[ \${#LOCAL_DNS[@]} -gt 0 ]] || { echo "entrypoint: no resolvers in /etc/resolv.conf" >&2; exit 1; }
|
|
24259
|
+
|
|
24260
|
+
ipset create node9_allowed hash:ip family inet -exist
|
|
24261
|
+
ipset flush node9_allowed
|
|
24262
|
+
|
|
24263
|
+
while IFS= read -r domain; do
|
|
24264
|
+
[[ -n "$domain" ]] || continue
|
|
24265
|
+
found=0
|
|
24266
|
+
# union the local resolver + each upstream so CDN/anycast IP rotation is covered
|
|
24267
|
+
for ip in $(getent ahostsv4 "$domain" 2>/dev/null | awk '{print $1}' | sort -u); do
|
|
24268
|
+
ipset add node9_allowed "$ip" -exist; found=1
|
|
24269
|
+
done
|
|
24270
|
+
for r in "\${LOCAL_DNS[@]}"; do
|
|
24271
|
+
for ip in $(dig +short +time=2 +tries=1 @"$r" A "$domain" 2>/dev/null | awk '/^[0-9.]+$/'); do
|
|
24272
|
+
ipset add node9_allowed "$ip" -exist; found=1
|
|
24273
|
+
done
|
|
24274
|
+
done
|
|
24275
|
+
[[ $found -eq 1 ]] || { echo "entrypoint: failed to resolve $domain" >&2; exit 1; }
|
|
24276
|
+
echo "entrypoint: allowed $domain"
|
|
24277
|
+
done < "$DOMAINS_FILE"
|
|
24278
|
+
|
|
24279
|
+
# \u2500\u2500 Seal iptables: deny-by-default except lo, established, DNS, the allowlist \u2500\u2500\u2500\u2500
|
|
24280
|
+
echo "entrypoint: sealing firewall..."
|
|
24281
|
+
iptables -F; iptables -X
|
|
24282
|
+
iptables -P INPUT DROP
|
|
24283
|
+
iptables -P FORWARD DROP
|
|
24284
|
+
iptables -P OUTPUT DROP
|
|
24285
|
+
iptables -A INPUT -i lo -j ACCEPT
|
|
24286
|
+
iptables -A OUTPUT -o lo -j ACCEPT
|
|
24287
|
+
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
|
24288
|
+
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
|
24289
|
+
for r in "\${LOCAL_DNS[@]}"; do
|
|
24290
|
+
iptables -A OUTPUT -p udp -d "$r" --dport 53 -j ACCEPT
|
|
24291
|
+
iptables -A OUTPUT -p tcp -d "$r" --dport 53 -j ACCEPT
|
|
24292
|
+
done
|
|
24293
|
+
iptables -A OUTPUT -m set --match-set node9_allowed dst -j ACCEPT
|
|
24294
|
+
|
|
24295
|
+
# \u2500\u2500 Drop to the agent: start the node9 daemon (as the user), then exec the agent \u2500
|
|
24296
|
+
echo "entrypoint: starting node9 + ${agentBin} as $RUN_AS_USER"
|
|
24297
|
+
exec gosu "$RUN_AS_USER" bash -lc '
|
|
24298
|
+
set -e
|
|
24299
|
+
node9 daemon --background >/dev/null 2>&1 || true
|
|
24300
|
+
cd /workspace
|
|
24301
|
+
exec ${agentBin} "$@"
|
|
24302
|
+
' -- "$@"
|
|
24303
|
+
`;
|
|
24304
|
+
}
|
|
24305
|
+
|
|
24306
|
+
// src/posture/egress.ts
|
|
24307
|
+
function sandboxEgressWallActive() {
|
|
24308
|
+
try {
|
|
24309
|
+
return fs47.existsSync(ALLOWED_DOMAINS_PATH);
|
|
24310
|
+
} catch {
|
|
24311
|
+
return false;
|
|
24312
|
+
}
|
|
24313
|
+
}
|
|
24180
24314
|
function evaluateEgressConfig(egress) {
|
|
24181
24315
|
if (egress.enabled && egress.mode === "block") {
|
|
24182
24316
|
return {
|
|
@@ -24226,6 +24360,21 @@ function evaluateEgressConfig(egress) {
|
|
|
24226
24360
|
};
|
|
24227
24361
|
}
|
|
24228
24362
|
function checkEgress(ctx) {
|
|
24363
|
+
if (sandboxEgressWallActive()) {
|
|
24364
|
+
return [
|
|
24365
|
+
{
|
|
24366
|
+
category: "Egress",
|
|
24367
|
+
severity: "advisory",
|
|
24368
|
+
title: "Egress is hard-blocked by the sandbox kernel wall",
|
|
24369
|
+
what: "Outbound is deny-by-default at the kernel; only the allowlist is reachable.",
|
|
24370
|
+
why: "The sandbox seals egress with an ipset/iptables wall before the agent starts.",
|
|
24371
|
+
who: "Even a compromised agent can only reach the allowlisted hosts.",
|
|
24372
|
+
owner: "node9",
|
|
24373
|
+
detail: [],
|
|
24374
|
+
coverage: { state: "covered", level: "block", via: "sandbox egress wall" }
|
|
24375
|
+
}
|
|
24376
|
+
];
|
|
24377
|
+
}
|
|
24229
24378
|
const config = getConfig(ctx.cwd);
|
|
24230
24379
|
const egress = config.policy.egress;
|
|
24231
24380
|
return [evaluateEgressConfig({ enabled: egress.enabled, mode: egress.mode })];
|
|
@@ -24267,7 +24416,7 @@ async function checkGate(ctx) {
|
|
|
24267
24416
|
|
|
24268
24417
|
// src/posture/supply-chain.ts
|
|
24269
24418
|
init_provenance();
|
|
24270
|
-
import
|
|
24419
|
+
import fs48 from "fs";
|
|
24271
24420
|
import os42 from "os";
|
|
24272
24421
|
import path48 from "path";
|
|
24273
24422
|
import { parse as parseToml3 } from "smol-toml";
|
|
@@ -24283,9 +24432,9 @@ function isNode9Managed(command, args = []) {
|
|
|
24283
24432
|
var MAX_CONFIG_BYTES = 10 * 1024 * 1024;
|
|
24284
24433
|
function readServers(file, format, agent) {
|
|
24285
24434
|
try {
|
|
24286
|
-
const stat =
|
|
24435
|
+
const stat = fs48.statSync(file);
|
|
24287
24436
|
if (!stat.isFile() || stat.size > MAX_CONFIG_BYTES) return [];
|
|
24288
|
-
const text =
|
|
24437
|
+
const text = fs48.readFileSync(file, "utf8");
|
|
24289
24438
|
const map = format === "toml" ? parseToml3(text)?.mcp_servers : JSON.parse(text)?.mcpServers;
|
|
24290
24439
|
if (!map || typeof map !== "object") return [];
|
|
24291
24440
|
return Object.entries(map).map(([name, v]) => ({
|
|
@@ -24381,11 +24530,12 @@ async function checkPrivilege(ctx) {
|
|
|
24381
24530
|
}
|
|
24382
24531
|
|
|
24383
24532
|
// src/posture/containment.ts
|
|
24384
|
-
import
|
|
24533
|
+
import fs49 from "fs";
|
|
24534
|
+
var ISOLATION_WEIGHT = 12;
|
|
24385
24535
|
function inContainer() {
|
|
24386
|
-
if (
|
|
24536
|
+
if (fs49.existsSync("/.dockerenv") || fs49.existsSync("/run/.containerenv")) return true;
|
|
24387
24537
|
try {
|
|
24388
|
-
const cgroup =
|
|
24538
|
+
const cgroup = fs49.readFileSync("/proc/1/cgroup", "utf8");
|
|
24389
24539
|
if (/docker|kubepods|containerd|lxc|libpod/.test(cgroup)) return true;
|
|
24390
24540
|
} catch {
|
|
24391
24541
|
}
|
|
@@ -24404,14 +24554,28 @@ function checkContainment(_ctx) {
|
|
|
24404
24554
|
detail: [],
|
|
24405
24555
|
owner: "os",
|
|
24406
24556
|
node9Reduces: true,
|
|
24407
|
-
|
|
24408
|
-
|
|
24557
|
+
// The single biggest hardening gap, and node9 now fully remedies it
|
|
24558
|
+
// (`node9 sandbox run`). Deducts while open; closing it is the headline
|
|
24559
|
+
// payoff. No coverageProbe → stays OPEN (scored) until adopted; live
|
|
24560
|
+
// partial-credit for the lighter shield path is a fast-follow.
|
|
24561
|
+
scoreWeight: ISOLATION_WEIGHT,
|
|
24562
|
+
gain: "jailed container \xB7 kernel egress wall \xB7 scoped mounts \xB7 governed inside",
|
|
24563
|
+
cost: "the agent works inside /workspace, not your live host",
|
|
24564
|
+
fix: `Two ways to shrink the blast radius \u2014 pick by how much flexibility you need:
|
|
24565
|
+
Strongest \u2014 jail it (closes this gap, +${ISOLATION_WEIGHT}):
|
|
24566
|
+
\u2022 node9 sandbox run <agent>
|
|
24567
|
+
Lighter \u2014 harden in place, keep full host access (about +${Math.round(
|
|
24568
|
+
ISOLATION_WEIGHT / 2
|
|
24569
|
+
)}):
|
|
24570
|
+
\u2022 node9 shield enable project-jail \u2014 block stray credential reads
|
|
24571
|
+
\u2022 node9 egress lock \u2014 block data exfil`
|
|
24409
24572
|
}
|
|
24410
24573
|
];
|
|
24411
24574
|
}
|
|
24412
24575
|
|
|
24413
24576
|
// src/posture/inbound.ts
|
|
24414
|
-
import
|
|
24577
|
+
import fs50 from "fs";
|
|
24578
|
+
var DB_EXPOSURE_WEIGHT = 4;
|
|
24415
24579
|
var KNOWN_SERVICE_PORTS = {
|
|
24416
24580
|
5432: "PostgreSQL",
|
|
24417
24581
|
6379: "Redis",
|
|
@@ -24497,7 +24661,7 @@ function collectListeners() {
|
|
|
24497
24661
|
const byPort = /* @__PURE__ */ new Map();
|
|
24498
24662
|
for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) {
|
|
24499
24663
|
try {
|
|
24500
|
-
for (const l of parseListeners(
|
|
24664
|
+
for (const l of parseListeners(fs50.readFileSync(file, "utf8"))) {
|
|
24501
24665
|
if (!byPort.has(l.port)) byPort.set(l.port, l);
|
|
24502
24666
|
}
|
|
24503
24667
|
} catch {
|
|
@@ -24509,11 +24673,11 @@ function readProc(pid) {
|
|
|
24509
24673
|
let comm = "unknown";
|
|
24510
24674
|
let cmdline = "";
|
|
24511
24675
|
try {
|
|
24512
|
-
comm =
|
|
24676
|
+
comm = fs50.readFileSync(`/proc/${pid}/comm`, "utf8").trim().replace(/-MainThread$/, "") || "unknown";
|
|
24513
24677
|
} catch {
|
|
24514
24678
|
}
|
|
24515
24679
|
try {
|
|
24516
|
-
cmdline =
|
|
24680
|
+
cmdline = fs50.readFileSync(`/proc/${pid}/cmdline`).toString().replace(/\0/g, " ").trim();
|
|
24517
24681
|
} catch {
|
|
24518
24682
|
}
|
|
24519
24683
|
return { comm, cmdline };
|
|
@@ -24523,21 +24687,21 @@ function resolveProcesses(inodes) {
|
|
|
24523
24687
|
if (inodes.size === 0) return map;
|
|
24524
24688
|
let pids;
|
|
24525
24689
|
try {
|
|
24526
|
-
pids =
|
|
24690
|
+
pids = fs50.readdirSync("/proc").filter((d) => /^\d+$/.test(d));
|
|
24527
24691
|
} catch {
|
|
24528
24692
|
return map;
|
|
24529
24693
|
}
|
|
24530
24694
|
for (const pid of pids) {
|
|
24531
24695
|
let fds;
|
|
24532
24696
|
try {
|
|
24533
|
-
fds =
|
|
24697
|
+
fds = fs50.readdirSync(`/proc/${pid}/fd`);
|
|
24534
24698
|
} catch {
|
|
24535
24699
|
continue;
|
|
24536
24700
|
}
|
|
24537
24701
|
for (const fd of fds) {
|
|
24538
24702
|
let link;
|
|
24539
24703
|
try {
|
|
24540
|
-
link =
|
|
24704
|
+
link = fs50.readlinkSync(`/proc/${pid}/fd/${fd}`);
|
|
24541
24705
|
} catch {
|
|
24542
24706
|
continue;
|
|
24543
24707
|
}
|
|
@@ -24592,8 +24756,15 @@ function checkInbound(ctx) {
|
|
|
24592
24756
|
// Only when node9 actually has a shield for an exposed service — otherwise
|
|
24593
24757
|
// (bare dev servers) it stays purely the user's to rebind.
|
|
24594
24758
|
node9Reduces: reduces,
|
|
24595
|
-
|
|
24596
|
-
|
|
24759
|
+
// When a db-shield applies this is real, node9-addressable hardening → it
|
|
24760
|
+
// scores (and stays OPEN, no cantFix probe). Bare dev servers node9 can't
|
|
24761
|
+
// touch stay can't-fix / your-part / unscored.
|
|
24762
|
+
...reduces ? {
|
|
24763
|
+
scoreWeight: DB_EXPOSURE_WEIGHT,
|
|
24764
|
+
gain: "blocks DROP TABLE / TRUNCATE / FLUSHALL on the exposed DB",
|
|
24765
|
+
cost: "you confirm legit destructive migrations"
|
|
24766
|
+
} : { coverageProbe: { kind: "cantFix" } },
|
|
24767
|
+
fix
|
|
24597
24768
|
});
|
|
24598
24769
|
}
|
|
24599
24770
|
return findings;
|
|
@@ -24643,16 +24814,20 @@ function scorePosture(findings, checksRun) {
|
|
|
24643
24814
|
const open = findings.filter(
|
|
24644
24815
|
(f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix"
|
|
24645
24816
|
);
|
|
24646
|
-
const count = (sev) => open.filter((f) => f.severity === sev).length;
|
|
24647
|
-
|
|
24817
|
+
const count = (sev) => open.filter((f) => f.severity === sev && !f.scoreWeight).length;
|
|
24818
|
+
const base = computeSecurityScore({
|
|
24648
24819
|
critical: count("critical"),
|
|
24649
24820
|
high: count("high"),
|
|
24650
24821
|
medium: count("medium"),
|
|
24651
|
-
// Denominator = number of checks evaluated. With computeSecurityScore's
|
|
24652
|
-
// caps this makes any critical → critical tier, any high → at-risk, and a
|
|
24653
|
-
// fully clean run (0 findings, checksRun > 0) → 100/good.
|
|
24654
24822
|
total: Math.max(checksRun, 1)
|
|
24655
24823
|
});
|
|
24824
|
+
const headroom = open.reduce((sum, f) => sum + (f.scoreWeight ?? 0), 0);
|
|
24825
|
+
const score = Math.max(0, base.score - headroom);
|
|
24826
|
+
const tier = score >= 80 ? "good" : score >= 50 ? "at-risk" : "critical";
|
|
24827
|
+
return { score, tier };
|
|
24828
|
+
}
|
|
24829
|
+
function openHeadroom(findings) {
|
|
24830
|
+
return findings.filter((f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix").reduce((sum, f) => sum + (f.scoreWeight ?? 0), 0);
|
|
24656
24831
|
}
|
|
24657
24832
|
|
|
24658
24833
|
// src/posture/headline.ts
|
|
@@ -24862,9 +25037,10 @@ var LABEL_WIDTH = 14;
|
|
|
24862
25037
|
function label(category) {
|
|
24863
25038
|
return chalk24.bold(category.padEnd(Math.max(LABEL_WIDTH, category.length + 1)));
|
|
24864
25039
|
}
|
|
24865
|
-
function renderFinding(f) {
|
|
25040
|
+
function renderFinding(f, showWeight = false) {
|
|
24866
25041
|
const lines = [];
|
|
24867
|
-
|
|
25042
|
+
const wt = showWeight && f.scoreWeight ? chalk24.cyan.bold(`+${f.scoreWeight} `) : "";
|
|
25043
|
+
lines.push(` ${ICON[f.severity]} ${label(f.category)}${wt}${f.title}`);
|
|
24868
25044
|
const indent = " ".repeat(2 + 3 + LABEL_WIDTH);
|
|
24869
25045
|
const width = 80 - indent.length;
|
|
24870
25046
|
for (const s of [f.what, f.why, f.who]) {
|
|
@@ -24880,6 +25056,16 @@ function renderFinding(f) {
|
|
|
24880
25056
|
}
|
|
24881
25057
|
}
|
|
24882
25058
|
}
|
|
25059
|
+
const tradeoff = [
|
|
25060
|
+
[f.gain, "gain: ", chalk24.green],
|
|
25061
|
+
[f.cost, "cost: ", chalk24.yellow]
|
|
25062
|
+
];
|
|
25063
|
+
for (const [text, lbl, color2] of tradeoff) {
|
|
25064
|
+
if (!text) continue;
|
|
25065
|
+
wrap(text, width - 6).forEach((l, i) => {
|
|
25066
|
+
lines.push(indent + (i === 0 ? color2(lbl) : " ") + chalk24.gray(l));
|
|
25067
|
+
});
|
|
25068
|
+
}
|
|
24883
25069
|
return lines;
|
|
24884
25070
|
}
|
|
24885
25071
|
function renderPosture(result) {
|
|
@@ -24889,15 +25075,11 @@ function renderPosture(result) {
|
|
|
24889
25075
|
lines.push(
|
|
24890
25076
|
chalk24.cyan.bold(`\u{1F6E1}\uFE0F Node9 Posture`) + chalk24.gray(` \u2014 ${result.agent}`) + ` ${chalk24.bold(`Score: ${result.score}/100`)} (${tier})`
|
|
24891
25077
|
);
|
|
24892
|
-
const
|
|
24893
|
-
|
|
24894
|
-
).length;
|
|
24895
|
-
if (advisories > 0) {
|
|
24896
|
-
const word = advisories === 1 ? "advisory" : "advisories";
|
|
24897
|
-
const verb = advisories === 1 ? "doesn't" : "don't";
|
|
25078
|
+
const headroom = openHeadroom(result.findings);
|
|
25079
|
+
if (headroom > 0) {
|
|
24898
25080
|
lines.push(
|
|
24899
25081
|
" " + chalk24.gray(
|
|
24900
|
-
`${
|
|
25082
|
+
`${headroom} pts of headroom below \u2014 each layer adds security at some cost to flexibility. You choose which to close.`
|
|
24901
25083
|
)
|
|
24902
25084
|
);
|
|
24903
25085
|
}
|
|
@@ -24913,7 +25095,7 @@ function renderPosture(result) {
|
|
|
24913
25095
|
const covered = result.findings.filter((f) => f.coverage?.state === "covered");
|
|
24914
25096
|
const open = result.findings.filter((f) => f.coverage?.state !== "covered");
|
|
24915
25097
|
if (covered.length > 0) {
|
|
24916
|
-
lines.push(" " + chalk24.green("\u{1F7E2} node9 is
|
|
25098
|
+
lines.push(" " + chalk24.green("\u{1F7E2} ON NOW \u2014 node9 is enforcing these (your floor)"));
|
|
24917
25099
|
for (const f of covered) {
|
|
24918
25100
|
const gated = f.coverage?.level === "review" ? "approval-gating" : "blocking";
|
|
24919
25101
|
const via = f.coverage?.via ?? "node9";
|
|
@@ -24928,18 +25110,16 @@ function renderPosture(result) {
|
|
|
24928
25110
|
const osOpen = open.filter((f) => f.owner !== "node9" && !f.node9Reduces);
|
|
24929
25111
|
if (node9Open.length > 0) {
|
|
24930
25112
|
lines.push(" " + chalk24.cyan.bold("\u{1F527} node9 can fix these \u2014 run the command"));
|
|
24931
|
-
for (const f of node9Open) lines.push(...renderFinding(f));
|
|
25113
|
+
for (const f of node9Open) lines.push(...renderFinding(f, true));
|
|
24932
25114
|
}
|
|
24933
25115
|
if (reduceOpen.length > 0) {
|
|
24934
25116
|
if (node9Open.length > 0) lines.push("");
|
|
24935
|
-
lines.push(
|
|
24936
|
-
|
|
24937
|
-
);
|
|
24938
|
-
for (const f of reduceOpen) lines.push(...renderFinding(f));
|
|
25117
|
+
lines.push(" " + chalk24.yellow.bold("\u{1F512} AVAILABLE \u2014 turn on to harden (each has a tradeoff)"));
|
|
25118
|
+
for (const f of reduceOpen) lines.push(...renderFinding(f, true));
|
|
24939
25119
|
}
|
|
24940
25120
|
if (osOpen.length > 0) {
|
|
24941
25121
|
if (node9Open.length > 0 || reduceOpen.length > 0) lines.push("");
|
|
24942
|
-
lines.push(" " + chalk24.bold("\u{1F9F1}
|
|
25122
|
+
lines.push(" " + chalk24.bold("\u{1F9F1} YOUR PART \u2014 node9 can't fix these (OS-level)"));
|
|
24943
25123
|
for (const f of osOpen) lines.push(...renderFinding(f));
|
|
24944
25124
|
}
|
|
24945
25125
|
for (const cat of result.passedCategories) {
|
|
@@ -24959,7 +25139,12 @@ function renderPosture(result) {
|
|
|
24959
25139
|
if (med) parts.push(chalk24.yellow(`${med} medium`));
|
|
24960
25140
|
if (adv) parts.push(chalk24.gray(`${adv} advisory`));
|
|
24961
25141
|
const summary = parts.length ? parts.join(" \xB7 ") : chalk24.green("no findings");
|
|
24962
|
-
lines.push(` ${summary}
|
|
25142
|
+
lines.push(` ${summary}`);
|
|
25143
|
+
lines.push("");
|
|
25144
|
+
lines.push(" " + chalk24.bold("Track this across your fleet & keep it green:"));
|
|
25145
|
+
lines.push(
|
|
25146
|
+
" " + chalk24.dim("\u2192 ") + chalk24.cyan.underline("https://node9.ai/auth/signup?ref=cli_posture")
|
|
25147
|
+
);
|
|
24963
25148
|
lines.push("");
|
|
24964
25149
|
return lines.join("\n");
|
|
24965
25150
|
}
|
|
@@ -24989,7 +25174,12 @@ function buildShipBody(result) {
|
|
|
24989
25174
|
// The runnable fix / OS action — commands + advice, never a path.
|
|
24990
25175
|
fix: f.fix,
|
|
24991
25176
|
// Whose job it is. Default 'os' so the SaaS never falsely claims node9 can fix it.
|
|
24992
|
-
owner: f.owner ?? "os"
|
|
25177
|
+
owner: f.owner ?? "os",
|
|
25178
|
+
// Hardening weight + the flexibility tradeoff (generic prose / a number —
|
|
25179
|
+
// no values or paths), so the fleet view can show the same headroom story.
|
|
25180
|
+
scoreWeight: f.scoreWeight,
|
|
25181
|
+
gain: f.gain,
|
|
25182
|
+
cost: f.cost
|
|
24993
25183
|
}))
|
|
24994
25184
|
};
|
|
24995
25185
|
}
|
|
@@ -25062,7 +25252,7 @@ function registerPostureCommand(program2) {
|
|
|
25062
25252
|
init_config();
|
|
25063
25253
|
init_dist();
|
|
25064
25254
|
import chalk26 from "chalk";
|
|
25065
|
-
import
|
|
25255
|
+
import fs51 from "fs";
|
|
25066
25256
|
import os45 from "os";
|
|
25067
25257
|
import path49 from "path";
|
|
25068
25258
|
var DEFAULT_EGRESS = {
|
|
@@ -25078,7 +25268,7 @@ function configPath() {
|
|
|
25078
25268
|
function readRawConfig() {
|
|
25079
25269
|
let text;
|
|
25080
25270
|
try {
|
|
25081
|
-
text =
|
|
25271
|
+
text = fs51.readFileSync(configPath(), "utf8");
|
|
25082
25272
|
} catch (err2) {
|
|
25083
25273
|
if (err2.code === "ENOENT") return {};
|
|
25084
25274
|
throw err2;
|
|
@@ -25093,8 +25283,8 @@ function readRawConfig() {
|
|
|
25093
25283
|
}
|
|
25094
25284
|
function writeRawConfig(config) {
|
|
25095
25285
|
const p = configPath();
|
|
25096
|
-
|
|
25097
|
-
|
|
25286
|
+
fs51.mkdirSync(path49.dirname(p), { recursive: true });
|
|
25287
|
+
fs51.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
25098
25288
|
}
|
|
25099
25289
|
function applyEgress(config, change) {
|
|
25100
25290
|
const policy = config.policy = config.policy ?? {};
|
|
@@ -25185,15 +25375,369 @@ function registerEgressCommand(program2) {
|
|
|
25185
25375
|
egress.action(showStatus);
|
|
25186
25376
|
}
|
|
25187
25377
|
|
|
25378
|
+
// src/cli/commands/sandbox.ts
|
|
25379
|
+
init_config();
|
|
25380
|
+
import chalk27 from "chalk";
|
|
25381
|
+
import fs54 from "fs";
|
|
25382
|
+
import path52 from "path";
|
|
25383
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
25384
|
+
|
|
25385
|
+
// src/sandbox/config.ts
|
|
25386
|
+
import fs52 from "fs";
|
|
25387
|
+
import path50 from "path";
|
|
25388
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
25389
|
+
var SANDBOX_CONFIG_FILE = "node9.sandbox.yaml";
|
|
25390
|
+
var FORBIDDEN_ENV = /* @__PURE__ */ new Set(["NODE9_API_KEY", "NODE9_API_URL"]);
|
|
25391
|
+
function defaultSandboxConfig(agent) {
|
|
25392
|
+
return {
|
|
25393
|
+
agent,
|
|
25394
|
+
workspace: { mount: ".", target: "/workspace", mode: "rw" },
|
|
25395
|
+
runtime: { engine: "docker", image: "node9-sandbox:local", rebuild: "auto" },
|
|
25396
|
+
outbound: {
|
|
25397
|
+
mode: "block",
|
|
25398
|
+
allow: agent === "codex" ? ["api.openai.com", "api.github.com", "github.com", "registry.npmjs.org"] : ["api.anthropic.com", "api.github.com", "github.com", "registry.npmjs.org"]
|
|
25399
|
+
},
|
|
25400
|
+
inbound: { expose: [] },
|
|
25401
|
+
// Provider key only — NODE9_API_KEY intentionally absent (fix #1).
|
|
25402
|
+
env: { pass: [agent === "codex" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY"] },
|
|
25403
|
+
// Terminal-only approval in the MVP; cloud/native/browser off (fix #1).
|
|
25404
|
+
node9: {
|
|
25405
|
+
approvals: { terminal: true, native: false, browser: false, cloud: false },
|
|
25406
|
+
// Mount the agent's OAuth/creds dir so it can authenticate in the box.
|
|
25407
|
+
mountAgentCredentials: true
|
|
25408
|
+
}
|
|
25409
|
+
};
|
|
25410
|
+
}
|
|
25411
|
+
function asStringArray(v) {
|
|
25412
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
25413
|
+
}
|
|
25414
|
+
function mergeSandboxConfig(raw, fallbackAgent) {
|
|
25415
|
+
const r = raw && typeof raw === "object" ? raw : {};
|
|
25416
|
+
const agent = typeof r.agent === "string" ? r.agent : fallbackAgent;
|
|
25417
|
+
if (agent !== "claude" && agent !== "codex") {
|
|
25418
|
+
throw new Error(`sandbox: unsupported agent "${String(agent)}" (use claude or codex)`);
|
|
25419
|
+
}
|
|
25420
|
+
const d = defaultSandboxConfig(agent);
|
|
25421
|
+
const ws = r.workspace ?? {};
|
|
25422
|
+
const rt = r.runtime ?? {};
|
|
25423
|
+
const out = r.outbound ?? {};
|
|
25424
|
+
const inb = r.inbound ?? {};
|
|
25425
|
+
const env = r.env ?? {};
|
|
25426
|
+
const n9 = r.node9 ?? {};
|
|
25427
|
+
const appr = n9.approvals ?? {};
|
|
25428
|
+
const pass = asStringArray(env.pass).filter((k) => !FORBIDDEN_ENV.has(k));
|
|
25429
|
+
return {
|
|
25430
|
+
agent,
|
|
25431
|
+
workspace: {
|
|
25432
|
+
mount: typeof ws.mount === "string" ? ws.mount : d.workspace.mount,
|
|
25433
|
+
target: typeof ws.target === "string" ? ws.target : d.workspace.target,
|
|
25434
|
+
mode: ws.mode === "ro" ? "ro" : "rw"
|
|
25435
|
+
},
|
|
25436
|
+
runtime: {
|
|
25437
|
+
engine: rt.engine === "podman" ? "podman" : "docker",
|
|
25438
|
+
image: typeof rt.image === "string" ? rt.image : d.runtime.image,
|
|
25439
|
+
rebuild: rt.rebuild === "never" || rt.rebuild === "always" ? rt.rebuild : d.runtime.rebuild
|
|
25440
|
+
},
|
|
25441
|
+
outbound: { mode: "block", allow: out.allow ? asStringArray(out.allow) : d.outbound.allow },
|
|
25442
|
+
inbound: { expose: inb.expose ? asStringArray(inb.expose) : d.inbound.expose },
|
|
25443
|
+
env: { pass: env.pass ? pass : d.env.pass },
|
|
25444
|
+
node9: {
|
|
25445
|
+
approvals: {
|
|
25446
|
+
terminal: appr.terminal !== false,
|
|
25447
|
+
native: appr.native === true,
|
|
25448
|
+
browser: appr.browser === true,
|
|
25449
|
+
cloud: appr.cloud === true
|
|
25450
|
+
},
|
|
25451
|
+
mountAgentCredentials: n9.mountAgentCredentials !== false
|
|
25452
|
+
}
|
|
25453
|
+
};
|
|
25454
|
+
}
|
|
25455
|
+
function scaffoldSandboxYaml(agent) {
|
|
25456
|
+
const header = "# node9.sandbox.yaml \u2014 sandbox TOPOLOGY (what the agent may touch).\n# Security policy (shields / egress rules / approvers) lives in ~/.node9/config.json\n# and applies to both native and sandbox. NODE9_API_KEY is never passed into the box.\n\n";
|
|
25457
|
+
return header + stringifyYaml(defaultSandboxConfig(agent));
|
|
25458
|
+
}
|
|
25459
|
+
function sandboxConfigPath(cwd = process.cwd()) {
|
|
25460
|
+
return path50.join(cwd, SANDBOX_CONFIG_FILE);
|
|
25461
|
+
}
|
|
25462
|
+
function loadSandboxConfig(cwd = process.cwd(), fallbackAgent = "claude") {
|
|
25463
|
+
const p = sandboxConfigPath(cwd);
|
|
25464
|
+
if (!fs52.existsSync(p)) {
|
|
25465
|
+
throw new Error(`sandbox: ${SANDBOX_CONFIG_FILE} not found \u2014 run \`node9 sandbox new\` first.`);
|
|
25466
|
+
}
|
|
25467
|
+
let raw;
|
|
25468
|
+
try {
|
|
25469
|
+
raw = parseYaml(fs52.readFileSync(p, "utf-8"));
|
|
25470
|
+
} catch (err2) {
|
|
25471
|
+
throw new Error(
|
|
25472
|
+
`sandbox: ${SANDBOX_CONFIG_FILE} is not valid YAML \u2014 ${err2.message}`
|
|
25473
|
+
);
|
|
25474
|
+
}
|
|
25475
|
+
return mergeSandboxConfig(raw, fallbackAgent);
|
|
25476
|
+
}
|
|
25477
|
+
|
|
25478
|
+
// src/sandbox/firewall.ts
|
|
25479
|
+
var AGENT_PROVIDER_HOST = {
|
|
25480
|
+
claude: ["api.anthropic.com"],
|
|
25481
|
+
codex: ["api.openai.com"]
|
|
25482
|
+
};
|
|
25483
|
+
var NODE9_SAAS_HOSTS = ["api.node9.ai", "app.node9.ai", "node9.ai"];
|
|
25484
|
+
function isValidHost2(host) {
|
|
25485
|
+
if (typeof host !== "string") return false;
|
|
25486
|
+
const h = host.trim().toLowerCase();
|
|
25487
|
+
if (!h || h.length > 253) return false;
|
|
25488
|
+
if (/[\s/:@?#\\]/.test(h)) return false;
|
|
25489
|
+
return /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(
|
|
25490
|
+
h
|
|
25491
|
+
);
|
|
25492
|
+
}
|
|
25493
|
+
function compileAllowlist(input) {
|
|
25494
|
+
const norm = (h) => h.trim().toLowerCase();
|
|
25495
|
+
const denySet = /* @__PURE__ */ new Set([...input.configDeny.map(norm), ...NODE9_SAAS_HOSTS.map(norm)]);
|
|
25496
|
+
const candidates = [
|
|
25497
|
+
...AGENT_PROVIDER_HOST[input.agent],
|
|
25498
|
+
...input.sandboxAllow,
|
|
25499
|
+
...input.configAllow
|
|
25500
|
+
].map(norm);
|
|
25501
|
+
const allow = /* @__PURE__ */ new Set();
|
|
25502
|
+
const rejected = [];
|
|
25503
|
+
const denied = [];
|
|
25504
|
+
for (const host of candidates) {
|
|
25505
|
+
if (!host) continue;
|
|
25506
|
+
if (!isValidHost2(host)) {
|
|
25507
|
+
if (!rejected.includes(host)) rejected.push(host);
|
|
25508
|
+
continue;
|
|
25509
|
+
}
|
|
25510
|
+
if (denySet.has(host)) {
|
|
25511
|
+
if (!denied.includes(host)) denied.push(host);
|
|
25512
|
+
continue;
|
|
25513
|
+
}
|
|
25514
|
+
allow.add(host);
|
|
25515
|
+
}
|
|
25516
|
+
return {
|
|
25517
|
+
allow: [...allow].sort(),
|
|
25518
|
+
rejected: rejected.sort(),
|
|
25519
|
+
denied: denied.sort()
|
|
25520
|
+
};
|
|
25521
|
+
}
|
|
25522
|
+
|
|
25523
|
+
// src/sandbox/runtime.ts
|
|
25524
|
+
import fs53 from "fs";
|
|
25525
|
+
import os46 from "os";
|
|
25526
|
+
import path51 from "path";
|
|
25527
|
+
import crypto8 from "crypto";
|
|
25528
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
25529
|
+
function sandboxDataDir(cwd = process.cwd()) {
|
|
25530
|
+
return path51.join(cwd, ".node9", "sandbox", "data");
|
|
25531
|
+
}
|
|
25532
|
+
function detectEngine(engine) {
|
|
25533
|
+
const r = spawnSync5(engine, ["--version"], { encoding: "utf-8" });
|
|
25534
|
+
if (r.status === 0 && typeof r.stdout === "string") {
|
|
25535
|
+
return { available: true, version: r.stdout.trim() };
|
|
25536
|
+
}
|
|
25537
|
+
return { available: false };
|
|
25538
|
+
}
|
|
25539
|
+
function agentCredentialsMount(agent) {
|
|
25540
|
+
const rel = agent === "codex" ? ".codex/auth.json" : ".claude/.credentials.json";
|
|
25541
|
+
return { hostPath: path51.join(os46.homedir(), rel), target: `/home/${RUN_AS_USER}/${rel}` };
|
|
25542
|
+
}
|
|
25543
|
+
function buildRunArgs(opts) {
|
|
25544
|
+
const { config, workspaceHostPath, dataHostPath, allowlistHostPath, agentArgs } = opts;
|
|
25545
|
+
const args = ["run", "--rm", "-it", "--cap-add=NET_ADMIN"];
|
|
25546
|
+
args.push("-v", `${workspaceHostPath}:${config.workspace.target}:${config.workspace.mode}`);
|
|
25547
|
+
args.push("-v", `${dataHostPath}:/home/${RUN_AS_USER}/.node9`);
|
|
25548
|
+
args.push("-v", `${allowlistHostPath}:${ALLOWED_DOMAINS_PATH}:ro`);
|
|
25549
|
+
if (config.node9.mountAgentCredentials) {
|
|
25550
|
+
const creds = agentCredentialsMount(config.agent);
|
|
25551
|
+
if (fs53.existsSync(creds.hostPath)) {
|
|
25552
|
+
args.push("-v", `${creds.hostPath}:${creds.target}`);
|
|
25553
|
+
}
|
|
25554
|
+
}
|
|
25555
|
+
for (const key of config.env.pass) {
|
|
25556
|
+
if (process.env[key] !== void 0) args.push("-e", key);
|
|
25557
|
+
}
|
|
25558
|
+
for (const port of config.inbound.expose) {
|
|
25559
|
+
args.push("-p", port);
|
|
25560
|
+
}
|
|
25561
|
+
args.push(config.runtime.image);
|
|
25562
|
+
if (agentArgs.length) args.push(...agentArgs);
|
|
25563
|
+
return args;
|
|
25564
|
+
}
|
|
25565
|
+
function imageContentHash(dockerfile, entrypoint) {
|
|
25566
|
+
return crypto8.createHash("sha256").update(dockerfile).update("\0").update(entrypoint).digest("hex").slice(0, 16);
|
|
25567
|
+
}
|
|
25568
|
+
function sandboxBuildDir(cwd = process.cwd()) {
|
|
25569
|
+
return path51.join(cwd, ".node9", "sandbox", "build");
|
|
25570
|
+
}
|
|
25571
|
+
function writeBuildContext(cwd, dockerfile, entrypoint) {
|
|
25572
|
+
const dir = sandboxBuildDir(cwd);
|
|
25573
|
+
fs53.mkdirSync(dir, { recursive: true });
|
|
25574
|
+
fs53.writeFileSync(path51.join(dir, "Dockerfile"), dockerfile);
|
|
25575
|
+
fs53.writeFileSync(path51.join(dir, "entrypoint.sh"), entrypoint);
|
|
25576
|
+
return dir;
|
|
25577
|
+
}
|
|
25578
|
+
function writeAllowlist(cwd, hosts) {
|
|
25579
|
+
const dir = path51.join(cwd, ".node9", "sandbox");
|
|
25580
|
+
fs53.mkdirSync(dir, { recursive: true });
|
|
25581
|
+
const p = path51.join(dir, "allowed-domains.txt");
|
|
25582
|
+
fs53.writeFileSync(p, hosts.join("\n") + "\n");
|
|
25583
|
+
return p;
|
|
25584
|
+
}
|
|
25585
|
+
function resolveHomePath(p) {
|
|
25586
|
+
return p.startsWith("~") ? path51.join(os46.homedir(), p.slice(1)) : path51.resolve(p);
|
|
25587
|
+
}
|
|
25588
|
+
|
|
25589
|
+
// src/cli/commands/sandbox.ts
|
|
25590
|
+
function seedDataDirConfig(dataDir, sandbox) {
|
|
25591
|
+
fs54.mkdirSync(dataDir, { recursive: true });
|
|
25592
|
+
const configPath2 = path52.join(dataDir, "config.json");
|
|
25593
|
+
const seed = {
|
|
25594
|
+
settings: {
|
|
25595
|
+
approvers: {
|
|
25596
|
+
terminal: sandbox.node9.approvals.terminal,
|
|
25597
|
+
native: false,
|
|
25598
|
+
browser: false,
|
|
25599
|
+
cloud: false
|
|
25600
|
+
}
|
|
25601
|
+
}
|
|
25602
|
+
};
|
|
25603
|
+
fs54.writeFileSync(configPath2, JSON.stringify(seed, null, 2), { mode: 384 });
|
|
25604
|
+
}
|
|
25605
|
+
function registerSandboxCommand(program2, version2) {
|
|
25606
|
+
const node9Version2 = pinnedNode9Version(version2);
|
|
25607
|
+
const cmd = program2.command("sandbox").description("Run an agent in a disposable, jailed container \u2014 governed + audited inside");
|
|
25608
|
+
cmd.command("new").description(`Scaffold ${SANDBOX_CONFIG_FILE} in this project`).option("--agent <agent>", "claude (default) or codex", "claude").action((opts) => {
|
|
25609
|
+
const agent = opts.agent === "codex" ? "codex" : "claude";
|
|
25610
|
+
const p = sandboxConfigPath();
|
|
25611
|
+
if (fs54.existsSync(p)) {
|
|
25612
|
+
console.log(
|
|
25613
|
+
chalk27.yellow(` ${SANDBOX_CONFIG_FILE} already exists \u2014 leaving it untouched.`)
|
|
25614
|
+
);
|
|
25615
|
+
return;
|
|
25616
|
+
}
|
|
25617
|
+
fs54.writeFileSync(p, scaffoldSandboxYaml(agent));
|
|
25618
|
+
console.log(
|
|
25619
|
+
chalk27.green(` \u2713 wrote ${SANDBOX_CONFIG_FILE}`) + chalk27.dim(` (agent: ${agent})`)
|
|
25620
|
+
);
|
|
25621
|
+
console.log(
|
|
25622
|
+
chalk27.dim(" Edit it (mounts / allow / expose), then: ") + chalk27.cyan("node9 sandbox run")
|
|
25623
|
+
);
|
|
25624
|
+
});
|
|
25625
|
+
cmd.command("run [agent]").description("Build (if needed) + run the agent jailed. Extra args after -- go to the agent.").allowUnknownOption(true).allowExcessArguments(true).action((agentArg, _opts, command) => {
|
|
25626
|
+
const cwd = process.cwd();
|
|
25627
|
+
const sandbox = loadSandboxConfig(cwd, agentArg || "claude");
|
|
25628
|
+
if (agentArg === "claude" || agentArg === "codex") sandbox.agent = agentArg;
|
|
25629
|
+
const engine = detectEngine(sandbox.runtime.engine);
|
|
25630
|
+
if (!engine.available) {
|
|
25631
|
+
console.error(
|
|
25632
|
+
chalk27.red(` ${sandbox.runtime.engine} not found.`) + chalk27.dim(` Install it first \u2014 node9 sandbox needs a container runtime.`)
|
|
25633
|
+
);
|
|
25634
|
+
process.exit(1);
|
|
25635
|
+
}
|
|
25636
|
+
const node9Config = getConfig(cwd);
|
|
25637
|
+
const compiled = compileAllowlist({
|
|
25638
|
+
agent: sandbox.agent,
|
|
25639
|
+
sandboxAllow: sandbox.outbound.allow,
|
|
25640
|
+
configAllow: node9Config.policy.egress.allow,
|
|
25641
|
+
configDeny: node9Config.policy.egress.deny
|
|
25642
|
+
});
|
|
25643
|
+
if (compiled.rejected.length) {
|
|
25644
|
+
console.log(
|
|
25645
|
+
chalk27.yellow(` \u26A0 ignoring invalid allow hosts: ${compiled.rejected.join(", ")}`)
|
|
25646
|
+
);
|
|
25647
|
+
}
|
|
25648
|
+
if (compiled.denied.length) {
|
|
25649
|
+
console.log(chalk27.dim(` (denied: ${compiled.denied.join(", ")})`));
|
|
25650
|
+
}
|
|
25651
|
+
const allowlistPath = writeAllowlist(cwd, compiled.allow);
|
|
25652
|
+
const dockerfile = renderDockerfile(sandbox, node9Version2);
|
|
25653
|
+
const entrypoint = renderEntrypoint(sandbox);
|
|
25654
|
+
const buildDir = writeBuildContext(cwd, dockerfile, entrypoint);
|
|
25655
|
+
const hash = imageContentHash(dockerfile, entrypoint);
|
|
25656
|
+
const image = sandbox.runtime.image;
|
|
25657
|
+
const hashFile = path52.join(sandboxBuildDir(cwd), ".image-hash");
|
|
25658
|
+
const lastHash = fs54.existsSync(hashFile) ? fs54.readFileSync(hashFile, "utf-8").trim() : "";
|
|
25659
|
+
const imageExists = spawnSync6(sandbox.runtime.engine, ["image", "inspect", image], { stdio: "ignore" }).status === 0;
|
|
25660
|
+
const needBuild = sandbox.runtime.rebuild === "always" || !imageExists || sandbox.runtime.rebuild !== "never" && lastHash !== hash;
|
|
25661
|
+
if (needBuild) {
|
|
25662
|
+
console.log(chalk27.dim(` building ${image} \u2026`));
|
|
25663
|
+
const b = spawnSync6(sandbox.runtime.engine, ["build", "-t", image, buildDir], {
|
|
25664
|
+
stdio: "inherit"
|
|
25665
|
+
});
|
|
25666
|
+
if (b.status !== 0) {
|
|
25667
|
+
console.error(chalk27.red(" build failed."));
|
|
25668
|
+
process.exit(b.status ?? 1);
|
|
25669
|
+
}
|
|
25670
|
+
fs54.writeFileSync(hashFile, hash);
|
|
25671
|
+
}
|
|
25672
|
+
const dataDir = sandboxDataDir(cwd);
|
|
25673
|
+
seedDataDirConfig(dataDir, sandbox);
|
|
25674
|
+
const passthru = command.args.slice(agentArg ? 1 : 0);
|
|
25675
|
+
const runArgs = buildRunArgs({
|
|
25676
|
+
config: sandbox,
|
|
25677
|
+
workspaceHostPath: resolveHomePath(sandbox.workspace.mount),
|
|
25678
|
+
dataHostPath: dataDir,
|
|
25679
|
+
allowlistHostPath: allowlistPath,
|
|
25680
|
+
agentArgs: passthru
|
|
25681
|
+
});
|
|
25682
|
+
if (sandbox.node9.mountAgentCredentials) {
|
|
25683
|
+
const creds = agentCredentialsMount(sandbox.agent);
|
|
25684
|
+
if (fs54.existsSync(creds.hostPath)) {
|
|
25685
|
+
console.log(chalk27.dim(` mounting ${creds.hostPath} (agent credentials, rw)`));
|
|
25686
|
+
} else {
|
|
25687
|
+
console.log(
|
|
25688
|
+
chalk27.yellow(` \u26A0 ${creds.hostPath} not found \u2014 `) + chalk27.dim(`the agent must auth via an env key in env.pass.`)
|
|
25689
|
+
);
|
|
25690
|
+
}
|
|
25691
|
+
}
|
|
25692
|
+
console.log(
|
|
25693
|
+
chalk27.green(` \u{1F6E1}\uFE0F ${sandbox.agent} jailed \u2014 ${compiled.allow.length} hosts allowed
|
|
25694
|
+
`)
|
|
25695
|
+
);
|
|
25696
|
+
const r = spawnSync6(sandbox.runtime.engine, runArgs, { stdio: "inherit" });
|
|
25697
|
+
process.exit(r.status ?? 0);
|
|
25698
|
+
});
|
|
25699
|
+
cmd.command("tail").description("Stream the sandbox's audit log (host-side)").action(() => {
|
|
25700
|
+
const auditPath = path52.join(sandboxDataDir(), "audit.log");
|
|
25701
|
+
if (!fs54.existsSync(auditPath)) {
|
|
25702
|
+
console.log(chalk27.dim(" no sandbox audit yet."));
|
|
25703
|
+
return;
|
|
25704
|
+
}
|
|
25705
|
+
spawnSync6("tail", ["-f", auditPath], { stdio: "inherit" });
|
|
25706
|
+
});
|
|
25707
|
+
cmd.command("logs").description("Dump the sandbox's audit log").action(() => {
|
|
25708
|
+
const auditPath = path52.join(sandboxDataDir(), "audit.log");
|
|
25709
|
+
if (!fs54.existsSync(auditPath)) {
|
|
25710
|
+
console.log(chalk27.dim(" no sandbox audit yet."));
|
|
25711
|
+
return;
|
|
25712
|
+
}
|
|
25713
|
+
process.stdout.write(fs54.readFileSync(auditPath, "utf-8"));
|
|
25714
|
+
});
|
|
25715
|
+
cmd.command("clean").description("Remove the sandbox image, build context, and data").action(() => {
|
|
25716
|
+
const cwd = process.cwd();
|
|
25717
|
+
let sandbox = null;
|
|
25718
|
+
try {
|
|
25719
|
+
sandbox = loadSandboxConfig(cwd);
|
|
25720
|
+
} catch {
|
|
25721
|
+
}
|
|
25722
|
+
if (sandbox) {
|
|
25723
|
+
spawnSync6(sandbox.runtime.engine, ["image", "rm", "-f", sandbox.runtime.image], {
|
|
25724
|
+
stdio: "ignore"
|
|
25725
|
+
});
|
|
25726
|
+
}
|
|
25727
|
+
fs54.rmSync(path52.join(cwd, ".node9", "sandbox"), { recursive: true, force: true });
|
|
25728
|
+
console.log(chalk27.green(" \u2713 sandbox image + build + data removed."));
|
|
25729
|
+
});
|
|
25730
|
+
}
|
|
25731
|
+
|
|
25188
25732
|
// src/cli/commands/sessions.ts
|
|
25189
25733
|
init_scan_summary();
|
|
25190
25734
|
init_litellm();
|
|
25191
25735
|
init_cost_gemini();
|
|
25192
25736
|
init_cost_codex();
|
|
25193
|
-
import
|
|
25194
|
-
import
|
|
25195
|
-
import
|
|
25196
|
-
import
|
|
25737
|
+
import chalk28 from "chalk";
|
|
25738
|
+
import fs55 from "fs";
|
|
25739
|
+
import path53 from "path";
|
|
25740
|
+
import os47 from "os";
|
|
25197
25741
|
function modelPrice(model) {
|
|
25198
25742
|
const t = pricingFor(model);
|
|
25199
25743
|
if (!t) return null;
|
|
@@ -25210,10 +25754,10 @@ function encodeProjectPath(projectPath) {
|
|
|
25210
25754
|
}
|
|
25211
25755
|
function sessionJsonlPath(projectPath, sessionId) {
|
|
25212
25756
|
const encoded = encodeProjectPath(projectPath);
|
|
25213
|
-
return
|
|
25757
|
+
return path53.join(os47.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
|
|
25214
25758
|
}
|
|
25215
25759
|
function projectLabel(projectPath) {
|
|
25216
|
-
return projectPath.replace(
|
|
25760
|
+
return projectPath.replace(os47.homedir(), "~");
|
|
25217
25761
|
}
|
|
25218
25762
|
function parseHistoryLines(lines) {
|
|
25219
25763
|
const entries = [];
|
|
@@ -25282,10 +25826,10 @@ function parseSessionLines(lines) {
|
|
|
25282
25826
|
return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
|
|
25283
25827
|
}
|
|
25284
25828
|
function loadAuditEntries(auditPath) {
|
|
25285
|
-
const aPath = auditPath ??
|
|
25829
|
+
const aPath = auditPath ?? path53.join(os47.homedir(), ".node9", "audit.log");
|
|
25286
25830
|
let raw;
|
|
25287
25831
|
try {
|
|
25288
|
-
raw =
|
|
25832
|
+
raw = fs55.readFileSync(aPath, "utf-8");
|
|
25289
25833
|
} catch {
|
|
25290
25834
|
return [];
|
|
25291
25835
|
}
|
|
@@ -25321,8 +25865,8 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
|
|
|
25321
25865
|
return result;
|
|
25322
25866
|
}
|
|
25323
25867
|
function buildGeminiSessions(days, allAuditEntries) {
|
|
25324
|
-
const tmpDir =
|
|
25325
|
-
if (!
|
|
25868
|
+
const tmpDir = path53.join(os47.homedir(), ".gemini", "tmp");
|
|
25869
|
+
if (!fs55.existsSync(tmpDir)) return [];
|
|
25326
25870
|
const cutoff = days !== null ? (() => {
|
|
25327
25871
|
const d = /* @__PURE__ */ new Date();
|
|
25328
25872
|
d.setDate(d.getDate() - days);
|
|
@@ -25331,35 +25875,35 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
25331
25875
|
})() : null;
|
|
25332
25876
|
let slugDirs;
|
|
25333
25877
|
try {
|
|
25334
|
-
slugDirs =
|
|
25878
|
+
slugDirs = fs55.readdirSync(tmpDir);
|
|
25335
25879
|
} catch {
|
|
25336
25880
|
return [];
|
|
25337
25881
|
}
|
|
25338
25882
|
const summaries = [];
|
|
25339
25883
|
for (const slug of slugDirs) {
|
|
25340
|
-
const slugPath =
|
|
25884
|
+
const slugPath = path53.join(tmpDir, slug);
|
|
25341
25885
|
try {
|
|
25342
|
-
if (!
|
|
25886
|
+
if (!fs55.statSync(slugPath).isDirectory()) continue;
|
|
25343
25887
|
} catch {
|
|
25344
25888
|
continue;
|
|
25345
25889
|
}
|
|
25346
|
-
let projectRoot =
|
|
25890
|
+
let projectRoot = path53.join(os47.homedir(), slug);
|
|
25347
25891
|
try {
|
|
25348
|
-
projectRoot =
|
|
25892
|
+
projectRoot = fs55.readFileSync(path53.join(slugPath, ".project_root"), "utf-8").trim();
|
|
25349
25893
|
} catch {
|
|
25350
25894
|
}
|
|
25351
|
-
const chatsDir =
|
|
25352
|
-
if (!
|
|
25895
|
+
const chatsDir = path53.join(slugPath, "chats");
|
|
25896
|
+
if (!fs55.existsSync(chatsDir)) continue;
|
|
25353
25897
|
let chatFiles;
|
|
25354
25898
|
try {
|
|
25355
|
-
chatFiles =
|
|
25899
|
+
chatFiles = fs55.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
|
|
25356
25900
|
} catch {
|
|
25357
25901
|
continue;
|
|
25358
25902
|
}
|
|
25359
25903
|
for (const chatFile of chatFiles) {
|
|
25360
25904
|
let raw;
|
|
25361
25905
|
try {
|
|
25362
|
-
raw =
|
|
25906
|
+
raw = fs55.readFileSync(path53.join(chatsDir, chatFile), "utf-8");
|
|
25363
25907
|
} catch {
|
|
25364
25908
|
continue;
|
|
25365
25909
|
}
|
|
@@ -25439,8 +25983,8 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
25439
25983
|
return summaries;
|
|
25440
25984
|
}
|
|
25441
25985
|
function buildCodexSessions(days, allAuditEntries) {
|
|
25442
|
-
const sessionsBase =
|
|
25443
|
-
if (!
|
|
25986
|
+
const sessionsBase = path53.join(os47.homedir(), ".codex", "sessions");
|
|
25987
|
+
if (!fs55.existsSync(sessionsBase)) return [];
|
|
25444
25988
|
const cutoff = days !== null ? (() => {
|
|
25445
25989
|
const d = /* @__PURE__ */ new Date();
|
|
25446
25990
|
d.setDate(d.getDate() - days);
|
|
@@ -25449,29 +25993,29 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
25449
25993
|
})() : null;
|
|
25450
25994
|
const jsonlFiles = [];
|
|
25451
25995
|
try {
|
|
25452
|
-
for (const year of
|
|
25453
|
-
const yearPath =
|
|
25996
|
+
for (const year of fs55.readdirSync(sessionsBase)) {
|
|
25997
|
+
const yearPath = path53.join(sessionsBase, year);
|
|
25454
25998
|
try {
|
|
25455
|
-
if (!
|
|
25999
|
+
if (!fs55.statSync(yearPath).isDirectory()) continue;
|
|
25456
26000
|
} catch {
|
|
25457
26001
|
continue;
|
|
25458
26002
|
}
|
|
25459
|
-
for (const month of
|
|
25460
|
-
const monthPath =
|
|
26003
|
+
for (const month of fs55.readdirSync(yearPath)) {
|
|
26004
|
+
const monthPath = path53.join(yearPath, month);
|
|
25461
26005
|
try {
|
|
25462
|
-
if (!
|
|
26006
|
+
if (!fs55.statSync(monthPath).isDirectory()) continue;
|
|
25463
26007
|
} catch {
|
|
25464
26008
|
continue;
|
|
25465
26009
|
}
|
|
25466
|
-
for (const day of
|
|
25467
|
-
const dayPath =
|
|
26010
|
+
for (const day of fs55.readdirSync(monthPath)) {
|
|
26011
|
+
const dayPath = path53.join(monthPath, day);
|
|
25468
26012
|
try {
|
|
25469
|
-
if (!
|
|
26013
|
+
if (!fs55.statSync(dayPath).isDirectory()) continue;
|
|
25470
26014
|
} catch {
|
|
25471
26015
|
continue;
|
|
25472
26016
|
}
|
|
25473
|
-
for (const file of
|
|
25474
|
-
if (file.endsWith(".jsonl")) jsonlFiles.push(
|
|
26017
|
+
for (const file of fs55.readdirSync(dayPath)) {
|
|
26018
|
+
if (file.endsWith(".jsonl")) jsonlFiles.push(path53.join(dayPath, file));
|
|
25475
26019
|
}
|
|
25476
26020
|
}
|
|
25477
26021
|
}
|
|
@@ -25483,7 +26027,7 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
25483
26027
|
for (const filePath of jsonlFiles) {
|
|
25484
26028
|
let lines;
|
|
25485
26029
|
try {
|
|
25486
|
-
lines =
|
|
26030
|
+
lines = fs55.readFileSync(filePath, "utf-8").split("\n");
|
|
25487
26031
|
} catch {
|
|
25488
26032
|
continue;
|
|
25489
26033
|
}
|
|
@@ -25569,10 +26113,10 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
25569
26113
|
return summaries;
|
|
25570
26114
|
}
|
|
25571
26115
|
function buildSessions(days, historyPath) {
|
|
25572
|
-
const hPath = historyPath ??
|
|
26116
|
+
const hPath = historyPath ?? path53.join(os47.homedir(), ".claude", "history.jsonl");
|
|
25573
26117
|
let historyRaw = "";
|
|
25574
26118
|
try {
|
|
25575
|
-
historyRaw =
|
|
26119
|
+
historyRaw = fs55.readFileSync(hPath, "utf-8");
|
|
25576
26120
|
} catch {
|
|
25577
26121
|
}
|
|
25578
26122
|
const cutoff = days !== null ? (() => {
|
|
@@ -25596,7 +26140,7 @@ function buildSessions(days, historyPath) {
|
|
|
25596
26140
|
const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
|
|
25597
26141
|
let sessionLines = [];
|
|
25598
26142
|
try {
|
|
25599
|
-
sessionLines =
|
|
26143
|
+
sessionLines = fs55.readFileSync(jsonlFile, "utf-8").split("\n");
|
|
25600
26144
|
} catch {
|
|
25601
26145
|
}
|
|
25602
26146
|
const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
|
|
@@ -25682,11 +26226,11 @@ function toolInputSummary(tool, input) {
|
|
|
25682
26226
|
}
|
|
25683
26227
|
function toolColor(tool) {
|
|
25684
26228
|
const t = tool.toLowerCase();
|
|
25685
|
-
if (t === "bash" || t === "execute_bash") return
|
|
25686
|
-
if (t === "write") return
|
|
25687
|
-
if (t === "edit" || t === "notebookedit") return
|
|
25688
|
-
if (t === "read") return
|
|
25689
|
-
return
|
|
26229
|
+
if (t === "bash" || t === "execute_bash") return chalk28.red;
|
|
26230
|
+
if (t === "write") return chalk28.green;
|
|
26231
|
+
if (t === "edit" || t === "notebookedit") return chalk28.yellow;
|
|
26232
|
+
if (t === "read") return chalk28.cyan;
|
|
26233
|
+
return chalk28.gray;
|
|
25690
26234
|
}
|
|
25691
26235
|
function barStr2(value, max, width) {
|
|
25692
26236
|
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
@@ -25696,7 +26240,7 @@ function barStr2(value, max, width) {
|
|
|
25696
26240
|
function colorBar2(value, max, width) {
|
|
25697
26241
|
const s = barStr2(value, max, width);
|
|
25698
26242
|
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
25699
|
-
return
|
|
26243
|
+
return chalk28.cyan(s.slice(0, filled)) + chalk28.dim(s.slice(filled));
|
|
25700
26244
|
}
|
|
25701
26245
|
function renderSummary(summaries) {
|
|
25702
26246
|
const totalTools = summaries.reduce((n, s) => n + s.toolCalls.length, 0);
|
|
@@ -25726,45 +26270,45 @@ function renderSummary(summaries) {
|
|
|
25726
26270
|
}
|
|
25727
26271
|
const topProjects = [...projCosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
25728
26272
|
const W = 20;
|
|
25729
|
-
console.log(
|
|
26273
|
+
console.log(chalk28.dim(" " + "\u2500".repeat(70)));
|
|
25730
26274
|
console.log(
|
|
25731
|
-
" " +
|
|
26275
|
+
" " + chalk28.bold.white(String(summaries.length).padEnd(4)) + chalk28.dim("sessions ") + chalk28.bold.yellow(fmtCost3(totalCost).padEnd(10)) + chalk28.dim("total ") + chalk28.bold.white(String(totalTools).padEnd(6)) + chalk28.dim("tool calls ") + chalk28.bold.white(String(totalFiles)) + chalk28.dim(" files modified") + (totalBlocked > 0 ? chalk28.dim(" ") + chalk28.red.bold(String(totalBlocked)) + chalk28.dim(" blocked by node9") : "")
|
|
25732
26276
|
);
|
|
25733
26277
|
console.log(
|
|
25734
|
-
" " +
|
|
26278
|
+
" " + chalk28.dim("avg ") + chalk28.white(fmtCost3(avgCost).padEnd(10)) + chalk28.dim("/session ") + chalk28.green(String(snapshots)) + chalk28.dim(` of ${summaries.length} sessions had snapshots`)
|
|
25735
26279
|
);
|
|
25736
26280
|
console.log("");
|
|
25737
|
-
console.log(" " +
|
|
26281
|
+
console.log(" " + chalk28.dim("Tool breakdown:"));
|
|
25738
26282
|
const maxGroup = Math.max(...Object.values(groups));
|
|
25739
26283
|
for (const [label2, count] of Object.entries(groups)) {
|
|
25740
26284
|
if (count === 0) continue;
|
|
25741
26285
|
const pct = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
|
|
25742
26286
|
console.log(
|
|
25743
|
-
" " + label2.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " +
|
|
26287
|
+
" " + label2.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk28.white(String(count).padStart(4)) + chalk28.dim(` (${String(pct)}%)`)
|
|
25744
26288
|
);
|
|
25745
26289
|
}
|
|
25746
26290
|
console.log("");
|
|
25747
26291
|
if (topProjects.length > 1) {
|
|
25748
|
-
console.log(" " +
|
|
26292
|
+
console.log(" " + chalk28.dim("Cost by project:"));
|
|
25749
26293
|
const maxProjCost = topProjects[0][1];
|
|
25750
26294
|
for (const [proj, cost] of topProjects) {
|
|
25751
26295
|
console.log(
|
|
25752
|
-
" " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " +
|
|
26296
|
+
" " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " + chalk28.yellow(fmtCost3(cost))
|
|
25753
26297
|
);
|
|
25754
26298
|
}
|
|
25755
26299
|
console.log("");
|
|
25756
26300
|
}
|
|
25757
|
-
console.log(
|
|
26301
|
+
console.log(chalk28.dim(" " + "\u2500".repeat(70)));
|
|
25758
26302
|
console.log("");
|
|
25759
26303
|
}
|
|
25760
26304
|
function renderList(summaries, totalCost) {
|
|
25761
26305
|
if (summaries.length === 0) {
|
|
25762
|
-
console.log(
|
|
26306
|
+
console.log(chalk28.yellow(" No sessions found in the requested range.\n"));
|
|
25763
26307
|
return;
|
|
25764
26308
|
}
|
|
25765
|
-
const totalLabel = totalCost > 0 ?
|
|
26309
|
+
const totalLabel = totalCost > 0 ? chalk28.dim(" ~" + fmtCost3(totalCost) + " total") : "";
|
|
25766
26310
|
console.log(
|
|
25767
|
-
" " +
|
|
26311
|
+
" " + chalk28.white(String(summaries.length)) + chalk28.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
|
|
25768
26312
|
);
|
|
25769
26313
|
console.log("");
|
|
25770
26314
|
let lastGroup = "";
|
|
@@ -25772,51 +26316,51 @@ function renderList(summaries, totalCost) {
|
|
|
25772
26316
|
const activeDate = fmtDate2(s.lastActiveTime);
|
|
25773
26317
|
const group = activeDate + " " + s.projectLabel;
|
|
25774
26318
|
if (group !== lastGroup) {
|
|
25775
|
-
console.log(
|
|
26319
|
+
console.log(chalk28.dim(" \u2500\u2500\u2500 ") + chalk28.bold(activeDate) + chalk28.dim(" " + s.projectLabel));
|
|
25776
26320
|
lastGroup = group;
|
|
25777
26321
|
}
|
|
25778
26322
|
const startDate = fmtDate2(s.startTime);
|
|
25779
|
-
const dateRange = startDate !== activeDate ?
|
|
25780
|
-
const timeStr =
|
|
25781
|
-
const prompt =
|
|
25782
|
-
const tools = s.toolCalls.length > 0 ?
|
|
25783
|
-
const cost = s.costUSD > 0 ?
|
|
25784
|
-
const blocked = s.blockedCalls.length > 0 ?
|
|
25785
|
-
const snap = s.hasSnapshot ?
|
|
25786
|
-
const agentBadge =
|
|
26323
|
+
const dateRange = startDate !== activeDate ? chalk28.dim(" (" + startDate + " \u2192 " + activeDate + ")") : "";
|
|
26324
|
+
const timeStr = chalk28.dim(fmtTime(s.startTime));
|
|
26325
|
+
const prompt = chalk28.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
|
|
26326
|
+
const tools = s.toolCalls.length > 0 ? chalk28.dim(String(s.toolCalls.length).padStart(3) + " tools") : chalk28.dim(" 0 tools");
|
|
26327
|
+
const cost = s.costUSD > 0 ? chalk28.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
|
|
26328
|
+
const blocked = s.blockedCalls.length > 0 ? chalk28.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
|
|
26329
|
+
const snap = s.hasSnapshot ? chalk28.green(" \u{1F4F8}") : "";
|
|
26330
|
+
const agentBadge = chalk28[agentColorName(s.agent ?? "claude")](
|
|
25787
26331
|
" " + agentBadgeText(s.agent ?? "claude", 0)
|
|
25788
26332
|
);
|
|
25789
|
-
const sid =
|
|
26333
|
+
const sid = chalk28.dim(" " + s.sessionId.slice(0, 8));
|
|
25790
26334
|
console.log(
|
|
25791
26335
|
` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}${dateRange}`
|
|
25792
26336
|
);
|
|
25793
26337
|
}
|
|
25794
26338
|
console.log("");
|
|
25795
26339
|
console.log(
|
|
25796
|
-
|
|
26340
|
+
chalk28.dim(" Run") + " " + chalk28.cyan("node9 sessions --detail <session-id>") + chalk28.dim(" for full tool trace.")
|
|
25797
26341
|
);
|
|
25798
26342
|
console.log("");
|
|
25799
26343
|
}
|
|
25800
26344
|
function renderDetail(s) {
|
|
25801
26345
|
console.log("");
|
|
25802
|
-
console.log(
|
|
26346
|
+
console.log(chalk28.bold(" Session ") + chalk28.dim(s.sessionId));
|
|
25803
26347
|
console.log(
|
|
25804
|
-
|
|
26348
|
+
chalk28.bold(" Prompt ") + chalk28.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
|
|
25805
26349
|
);
|
|
25806
|
-
console.log(
|
|
26350
|
+
console.log(chalk28.bold(" Project ") + chalk28.white(s.projectLabel));
|
|
25807
26351
|
if (s.agent) {
|
|
25808
|
-
const agentLabel2 =
|
|
25809
|
-
console.log(
|
|
26352
|
+
const agentLabel2 = chalk28[agentColorName(s.agent)](agentDisplayName(s.agent));
|
|
26353
|
+
console.log(chalk28.bold(" Agent ") + agentLabel2);
|
|
25810
26354
|
}
|
|
25811
|
-
console.log(
|
|
26355
|
+
console.log(chalk28.bold(" When ") + chalk28.white(fmtDateTime(s.startTime)));
|
|
25812
26356
|
if (s.costUSD > 0)
|
|
25813
|
-
console.log(
|
|
26357
|
+
console.log(chalk28.bold(" Cost ") + chalk28.yellow("~" + fmtCost3(s.costUSD)));
|
|
25814
26358
|
console.log(
|
|
25815
|
-
|
|
26359
|
+
chalk28.bold(" Snapshot ") + (s.hasSnapshot ? chalk28.green("\u2713 taken") : chalk28.dim("none"))
|
|
25816
26360
|
);
|
|
25817
26361
|
console.log("");
|
|
25818
26362
|
if (s.toolCalls.length === 0 && s.blockedCalls.length === 0) {
|
|
25819
|
-
console.log(
|
|
26363
|
+
console.log(chalk28.dim(" No tool calls recorded.\n"));
|
|
25820
26364
|
return;
|
|
25821
26365
|
}
|
|
25822
26366
|
const timeline = [
|
|
@@ -25829,32 +26373,32 @@ function renderDetail(s) {
|
|
|
25829
26373
|
});
|
|
25830
26374
|
const headerParts = [`Tool calls (${s.toolCalls.length})`];
|
|
25831
26375
|
if (s.blockedCalls.length > 0)
|
|
25832
|
-
headerParts.push(
|
|
25833
|
-
console.log(
|
|
26376
|
+
headerParts.push(chalk28.red(`${s.blockedCalls.length} blocked by node9`));
|
|
26377
|
+
console.log(chalk28.bold(" " + headerParts.join(" \xB7 ")));
|
|
25834
26378
|
console.log("");
|
|
25835
26379
|
for (const entry of timeline) {
|
|
25836
26380
|
if (entry.kind === "tool") {
|
|
25837
26381
|
const tc = entry.tc;
|
|
25838
26382
|
const colorFn = toolColor(tc.tool);
|
|
25839
26383
|
const toolPad = colorFn(tc.tool.padEnd(16));
|
|
25840
|
-
const detail =
|
|
25841
|
-
const ts = tc.timestamp ?
|
|
26384
|
+
const detail = chalk28.gray(truncate(toolInputSummary(tc.tool, tc.input), 70));
|
|
26385
|
+
const ts = tc.timestamp ? chalk28.dim(fmtTime(tc.timestamp) + " ") : " ";
|
|
25842
26386
|
console.log(` ${ts}${toolPad} ${detail}`);
|
|
25843
26387
|
} else {
|
|
25844
26388
|
const bc = entry.bc;
|
|
25845
|
-
const ts = bc.timestamp ?
|
|
25846
|
-
const label2 =
|
|
25847
|
-
const toolName =
|
|
25848
|
-
const argsSummary = bc.args ?
|
|
25849
|
-
const reason = bc.checkedBy ?
|
|
26389
|
+
const ts = bc.timestamp ? chalk28.dim(fmtTime(bc.timestamp) + " ") : " ";
|
|
26390
|
+
const label2 = chalk28.red("\u{1F6D1} BLOCKED".padEnd(16));
|
|
26391
|
+
const toolName = chalk28.red(bc.tool.padEnd(10));
|
|
26392
|
+
const argsSummary = bc.args ? chalk28.gray(truncate(toolInputSummary(bc.tool, bc.args), 40)) : chalk28.dim("[args not logged]");
|
|
26393
|
+
const reason = bc.checkedBy ? chalk28.dim(" \u2190 " + bc.checkedBy) : "";
|
|
25850
26394
|
console.log(` ${ts}${label2} ${toolName} ${argsSummary}${reason}`);
|
|
25851
26395
|
}
|
|
25852
26396
|
}
|
|
25853
26397
|
console.log("");
|
|
25854
26398
|
if (s.modifiedFiles.length > 0) {
|
|
25855
|
-
console.log(
|
|
26399
|
+
console.log(chalk28.bold(` Files modified (${s.modifiedFiles.length}):`));
|
|
25856
26400
|
for (const f of s.modifiedFiles) {
|
|
25857
|
-
console.log(" " +
|
|
26401
|
+
console.log(" " + chalk28.yellow(f));
|
|
25858
26402
|
}
|
|
25859
26403
|
console.log("");
|
|
25860
26404
|
}
|
|
@@ -25862,13 +26406,13 @@ function renderDetail(s) {
|
|
|
25862
26406
|
function registerSessionsCommand(program2) {
|
|
25863
26407
|
program2.command("sessions").description("Show what your AI agent did \u2014 sessions, tool calls, cost, and file changes").option("--all", "Show all sessions (default: last 7 days)").option("--days <n>", "Show last N days of sessions", "7").option("--detail <sessionId>", "Show full tool trace for a session").action((options) => {
|
|
25864
26408
|
console.log("");
|
|
25865
|
-
console.log(
|
|
26409
|
+
console.log(chalk28.cyan.bold("\u{1F4CB} node9 sessions") + chalk28.dim(" \u2014 what your AI agent did"));
|
|
25866
26410
|
console.log("");
|
|
25867
26411
|
const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
|
|
25868
26412
|
const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
|
|
25869
|
-
console.log(
|
|
26413
|
+
console.log(chalk28.dim(" " + rangeLabel));
|
|
25870
26414
|
console.log("");
|
|
25871
|
-
process.stdout.write(
|
|
26415
|
+
process.stdout.write(chalk28.dim(" Loading\u2026"));
|
|
25872
26416
|
const summaries = buildSessions(days);
|
|
25873
26417
|
if (process.stdout.isTTY) {
|
|
25874
26418
|
process.stdout.clearLine(0);
|
|
@@ -25881,8 +26425,8 @@ function registerSessionsCommand(program2) {
|
|
|
25881
26425
|
(s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
|
|
25882
26426
|
);
|
|
25883
26427
|
if (!target) {
|
|
25884
|
-
console.log(
|
|
25885
|
-
console.log(
|
|
26428
|
+
console.log(chalk28.red(` Session not found: ${options.detail}`));
|
|
26429
|
+
console.log(chalk28.dim(" Run `node9 sessions` to list recent sessions.\n"));
|
|
25886
26430
|
return;
|
|
25887
26431
|
}
|
|
25888
26432
|
renderDetail(target);
|
|
@@ -25896,7 +26440,7 @@ function registerSessionsCommand(program2) {
|
|
|
25896
26440
|
|
|
25897
26441
|
// src/cli/commands/session-taint.ts
|
|
25898
26442
|
init_daemon();
|
|
25899
|
-
import
|
|
26443
|
+
import chalk29 from "chalk";
|
|
25900
26444
|
function resolveSessionId(records, query) {
|
|
25901
26445
|
const exact = records.find((r) => r.sessionId === query);
|
|
25902
26446
|
if (exact) return { record: exact };
|
|
@@ -25922,22 +26466,22 @@ function registerSessionTaintCommand(program2) {
|
|
|
25922
26466
|
const records = await listSessionTaints();
|
|
25923
26467
|
console.log("");
|
|
25924
26468
|
if (records.length === 0) {
|
|
25925
|
-
console.log(
|
|
25926
|
-
console.log(
|
|
26469
|
+
console.log(chalk29.dim(" No tainted sessions."));
|
|
26470
|
+
console.log(chalk29.dim(" (Taint lives in the daemon \u2014 a stopped daemon has none.)") + "\n");
|
|
25927
26471
|
return;
|
|
25928
26472
|
}
|
|
25929
26473
|
console.log(
|
|
25930
|
-
" " +
|
|
26474
|
+
" " + chalk29.bold(String(records.length)) + chalk29.dim(` tainted session${records.length !== 1 ? "s" : ""}`)
|
|
25931
26475
|
);
|
|
25932
26476
|
console.log("");
|
|
25933
26477
|
for (const r of records) {
|
|
25934
26478
|
console.log(
|
|
25935
|
-
" " +
|
|
26479
|
+
" " + chalk29.yellow(r.sessionId.slice(0, 8).padEnd(10)) + chalk29.red(r.source) + sourceGap(r.source) + chalk29.dim("clears in " + fmtRemaining(r.expiresAt))
|
|
25936
26480
|
);
|
|
25937
26481
|
}
|
|
25938
26482
|
console.log("");
|
|
25939
26483
|
console.log(
|
|
25940
|
-
|
|
26484
|
+
chalk29.dim(" Run ") + chalk29.cyan("node9 session-taint clear <id>") + chalk29.dim(" to release one, or ") + chalk29.cyan("--all") + chalk29.dim(" for every session.") + "\n"
|
|
25941
26485
|
);
|
|
25942
26486
|
});
|
|
25943
26487
|
cmd.command("clear").description("Clear a session's taint so its next network/write action isn't held for review").argument("[sessionId]", "Session id to clear (the 8-char prefix from `list` is accepted)").option("--all", "Clear every session taint").action(async (sessionId, opts) => {
|
|
@@ -25945,32 +26489,32 @@ function registerSessionTaintCommand(program2) {
|
|
|
25945
26489
|
if (opts.all) {
|
|
25946
26490
|
const res2 = await clearSessionTaint({ all: true });
|
|
25947
26491
|
if (res2.daemonUnavailable) {
|
|
25948
|
-
console.log(
|
|
26492
|
+
console.log(chalk29.dim(" node9 daemon not running \u2014 no active taints to clear.") + "\n");
|
|
25949
26493
|
return;
|
|
25950
26494
|
}
|
|
25951
26495
|
console.log(
|
|
25952
|
-
|
|
26496
|
+
chalk29.green(" \u2713 ") + `Cleared ${chalk29.bold(String(res2.cleared))} session taint${res2.cleared !== 1 ? "s" : ""}.
|
|
25953
26497
|
`
|
|
25954
26498
|
);
|
|
25955
26499
|
return;
|
|
25956
26500
|
}
|
|
25957
26501
|
if (!sessionId) {
|
|
25958
|
-
console.log(
|
|
25959
|
-
console.log(
|
|
26502
|
+
console.log(chalk29.red(" Provide a session id or --all."));
|
|
26503
|
+
console.log(chalk29.dim(" Run `node9 session-taint list` to see tainted sessions.") + "\n");
|
|
25960
26504
|
return;
|
|
25961
26505
|
}
|
|
25962
26506
|
const records = await listSessionTaints();
|
|
25963
26507
|
if (records.length === 0) {
|
|
25964
|
-
console.log(
|
|
26508
|
+
console.log(chalk29.dim(" No tainted sessions \u2014 nothing to clear.") + "\n");
|
|
25965
26509
|
return;
|
|
25966
26510
|
}
|
|
25967
26511
|
const resolved = resolveSessionId(records, sessionId);
|
|
25968
26512
|
if ("error" in resolved) {
|
|
25969
26513
|
if (resolved.error === "not-found") {
|
|
25970
|
-
console.log(
|
|
26514
|
+
console.log(chalk29.red(` No tainted session matches "${sessionId}".`));
|
|
25971
26515
|
} else {
|
|
25972
|
-
console.log(
|
|
25973
|
-
for (const m of resolved.matches) console.log(
|
|
26516
|
+
console.log(chalk29.red(` "${sessionId}" is ambiguous \u2014 matches:`));
|
|
26517
|
+
for (const m of resolved.matches) console.log(chalk29.dim(" " + m));
|
|
25974
26518
|
}
|
|
25975
26519
|
console.log("");
|
|
25976
26520
|
return;
|
|
@@ -25978,24 +26522,24 @@ function registerSessionTaintCommand(program2) {
|
|
|
25978
26522
|
const res = await clearSessionTaint({ sessionId: resolved.record.sessionId });
|
|
25979
26523
|
if (res.cleared > 0) {
|
|
25980
26524
|
console.log(
|
|
25981
|
-
|
|
26525
|
+
chalk29.green(" \u2713 ") + `Cleared taint for ${chalk29.yellow(resolved.record.sessionId.slice(0, 8))} ` + chalk29.dim(`(was flagged by ${resolved.record.source}).`) + "\n"
|
|
25982
26526
|
);
|
|
25983
26527
|
} else {
|
|
25984
26528
|
console.log(
|
|
25985
|
-
|
|
26529
|
+
chalk29.dim(` Session ${resolved.record.sessionId.slice(0, 8)} was already clear.`) + "\n"
|
|
25986
26530
|
);
|
|
25987
26531
|
}
|
|
25988
26532
|
});
|
|
25989
26533
|
}
|
|
25990
26534
|
|
|
25991
26535
|
// src/cli/commands/skill-pin.ts
|
|
25992
|
-
import
|
|
25993
|
-
import
|
|
25994
|
-
import
|
|
25995
|
-
import
|
|
26536
|
+
import chalk30 from "chalk";
|
|
26537
|
+
import fs56 from "fs";
|
|
26538
|
+
import os48 from "os";
|
|
26539
|
+
import path54 from "path";
|
|
25996
26540
|
function wipeSkillSessions() {
|
|
25997
26541
|
try {
|
|
25998
|
-
|
|
26542
|
+
fs56.rmSync(path54.join(os48.homedir(), ".node9", "skill-sessions"), {
|
|
25999
26543
|
recursive: true,
|
|
26000
26544
|
force: true
|
|
26001
26545
|
});
|
|
@@ -26009,29 +26553,29 @@ function registerSkillPinCommand(program2) {
|
|
|
26009
26553
|
const result = readSkillPinsSafe();
|
|
26010
26554
|
if (!result.ok) {
|
|
26011
26555
|
if (result.reason === "missing") {
|
|
26012
|
-
console.log(
|
|
26556
|
+
console.log(chalk30.gray("\nNo skill roots are pinned yet."));
|
|
26013
26557
|
console.log(
|
|
26014
|
-
|
|
26558
|
+
chalk30.gray("Pins are created automatically on the first tool call of each session.\n")
|
|
26015
26559
|
);
|
|
26016
26560
|
return;
|
|
26017
26561
|
}
|
|
26018
|
-
console.error(
|
|
26562
|
+
console.error(chalk30.red(`
|
|
26019
26563
|
\u274C Pin file is corrupt: ${result.detail}`));
|
|
26020
|
-
console.error(
|
|
26564
|
+
console.error(chalk30.yellow(" Run: node9 skill pin reset\n"));
|
|
26021
26565
|
process.exit(1);
|
|
26022
26566
|
}
|
|
26023
26567
|
const entries = Object.entries(result.pins.roots);
|
|
26024
26568
|
if (entries.length === 0) {
|
|
26025
|
-
console.log(
|
|
26569
|
+
console.log(chalk30.gray("\nNo skill roots are pinned yet.\n"));
|
|
26026
26570
|
return;
|
|
26027
26571
|
}
|
|
26028
|
-
console.log(
|
|
26572
|
+
console.log(chalk30.bold("\n\u{1F512} Pinned Skill Roots\n"));
|
|
26029
26573
|
for (const [key, entry] of entries) {
|
|
26030
|
-
const missing = entry.exists ? "" :
|
|
26031
|
-
console.log(` ${
|
|
26574
|
+
const missing = entry.exists ? "" : chalk30.yellow(" (not present at pin time)");
|
|
26575
|
+
console.log(` ${chalk30.cyan(key)} ${chalk30.gray(entry.rootPath)}${missing}`);
|
|
26032
26576
|
console.log(` Files (${entry.fileCount})`);
|
|
26033
|
-
console.log(` Hash: ${
|
|
26034
|
-
console.log(` Pinned: ${
|
|
26577
|
+
console.log(` Hash: ${chalk30.gray(entry.contentHash.slice(0, 16))}...`);
|
|
26578
|
+
console.log(` Pinned: ${chalk30.gray(entry.pinnedAt)}
|
|
26035
26579
|
`);
|
|
26036
26580
|
}
|
|
26037
26581
|
});
|
|
@@ -26040,52 +26584,52 @@ function registerSkillPinCommand(program2) {
|
|
|
26040
26584
|
try {
|
|
26041
26585
|
pins = readSkillPins();
|
|
26042
26586
|
} catch {
|
|
26043
|
-
console.error(
|
|
26044
|
-
console.error(
|
|
26587
|
+
console.error(chalk30.red("\n\u274C Pin file is corrupt."));
|
|
26588
|
+
console.error(chalk30.yellow(" Run: node9 skill pin reset\n"));
|
|
26045
26589
|
process.exit(1);
|
|
26046
26590
|
}
|
|
26047
26591
|
if (!pins.roots[rootKey]) {
|
|
26048
|
-
console.error(
|
|
26592
|
+
console.error(chalk30.red(`
|
|
26049
26593
|
\u274C No pin found for root key "${rootKey}"
|
|
26050
26594
|
`));
|
|
26051
|
-
console.error(`Run ${
|
|
26595
|
+
console.error(`Run ${chalk30.cyan("node9 skill pin list")} to see pinned roots.
|
|
26052
26596
|
`);
|
|
26053
26597
|
process.exit(1);
|
|
26054
26598
|
}
|
|
26055
26599
|
const rootPath = pins.roots[rootKey].rootPath;
|
|
26056
26600
|
removePin2(rootKey);
|
|
26057
26601
|
wipeSkillSessions();
|
|
26058
|
-
console.log(
|
|
26059
|
-
\u{1F513} Pin removed for ${
|
|
26060
|
-
console.log(
|
|
26061
|
-
console.log(
|
|
26602
|
+
console.log(chalk30.green(`
|
|
26603
|
+
\u{1F513} Pin removed for ${chalk30.cyan(rootKey)}`));
|
|
26604
|
+
console.log(chalk30.gray(` ${rootPath}`));
|
|
26605
|
+
console.log(chalk30.gray(" Next session will re-pin with current state.\n"));
|
|
26062
26606
|
});
|
|
26063
26607
|
pinSubCmd.command("reset").description("Clear all skill pins and wipe session verification flags").action(() => {
|
|
26064
26608
|
const result = readSkillPinsSafe();
|
|
26065
26609
|
if (!result.ok && result.reason === "missing") {
|
|
26066
26610
|
wipeSkillSessions();
|
|
26067
|
-
console.log(
|
|
26611
|
+
console.log(chalk30.gray("\nNo pins to clear.\n"));
|
|
26068
26612
|
return;
|
|
26069
26613
|
}
|
|
26070
26614
|
const count = result.ok ? Object.keys(result.pins.roots).length : "?";
|
|
26071
26615
|
clearAllPins2();
|
|
26072
26616
|
wipeSkillSessions();
|
|
26073
|
-
console.log(
|
|
26617
|
+
console.log(chalk30.green(`
|
|
26074
26618
|
\u{1F513} Cleared ${count} skill pin(s).`));
|
|
26075
|
-
console.log(
|
|
26619
|
+
console.log(chalk30.gray(" Next session will re-pin with current state.\n"));
|
|
26076
26620
|
});
|
|
26077
26621
|
}
|
|
26078
26622
|
|
|
26079
26623
|
// src/cli/commands/decisions.ts
|
|
26080
|
-
import
|
|
26081
|
-
import
|
|
26082
|
-
import
|
|
26083
|
-
import
|
|
26084
|
-
var DECISIONS_FILE2 =
|
|
26624
|
+
import fs57 from "fs";
|
|
26625
|
+
import os49 from "os";
|
|
26626
|
+
import path55 from "path";
|
|
26627
|
+
import chalk31 from "chalk";
|
|
26628
|
+
var DECISIONS_FILE2 = path55.join(os49.homedir(), ".node9", "decisions.json");
|
|
26085
26629
|
function readDecisions() {
|
|
26086
26630
|
try {
|
|
26087
|
-
if (!
|
|
26088
|
-
const raw =
|
|
26631
|
+
if (!fs57.existsSync(DECISIONS_FILE2)) return {};
|
|
26632
|
+
const raw = fs57.readFileSync(DECISIONS_FILE2, "utf-8");
|
|
26089
26633
|
const parsed = JSON.parse(raw);
|
|
26090
26634
|
const out = {};
|
|
26091
26635
|
for (const [k, v] of Object.entries(parsed)) {
|
|
@@ -26097,11 +26641,11 @@ function readDecisions() {
|
|
|
26097
26641
|
}
|
|
26098
26642
|
}
|
|
26099
26643
|
function writeDecisions(d) {
|
|
26100
|
-
const dir =
|
|
26101
|
-
if (!
|
|
26644
|
+
const dir = path55.dirname(DECISIONS_FILE2);
|
|
26645
|
+
if (!fs57.existsSync(dir)) fs57.mkdirSync(dir, { recursive: true });
|
|
26102
26646
|
const tmp = `${DECISIONS_FILE2}.${process.pid}.tmp`;
|
|
26103
|
-
|
|
26104
|
-
|
|
26647
|
+
fs57.writeFileSync(tmp, JSON.stringify(d, null, 2));
|
|
26648
|
+
fs57.renameSync(tmp, DECISIONS_FILE2);
|
|
26105
26649
|
}
|
|
26106
26650
|
function registerDecisionsCommand(program2) {
|
|
26107
26651
|
const cmd = program2.command("decisions").description('Manage persistent "Always Allow" / "Always Deny" tool decisions');
|
|
@@ -26109,67 +26653,67 @@ function registerDecisionsCommand(program2) {
|
|
|
26109
26653
|
const decisions = readDecisions();
|
|
26110
26654
|
const entries = Object.entries(decisions);
|
|
26111
26655
|
if (entries.length === 0) {
|
|
26112
|
-
console.log(
|
|
26656
|
+
console.log(chalk31.gray(" No persistent decisions stored."));
|
|
26113
26657
|
console.log(
|
|
26114
|
-
|
|
26115
|
-
`) +
|
|
26658
|
+
chalk31.gray(` File: ${DECISIONS_FILE2}
|
|
26659
|
+
`) + chalk31.gray(' Decisions are written when you click "Always Allow" or')
|
|
26116
26660
|
);
|
|
26117
|
-
console.log(
|
|
26661
|
+
console.log(chalk31.gray(' "Always Deny" in node9 tail or the native popup.'));
|
|
26118
26662
|
return;
|
|
26119
26663
|
}
|
|
26120
|
-
console.log(
|
|
26664
|
+
console.log(chalk31.bold(`
|
|
26121
26665
|
Persistent decisions (${entries.length})
|
|
26122
26666
|
`));
|
|
26123
26667
|
const w = Math.max(...entries.map(([k]) => k.length));
|
|
26124
26668
|
for (const [tool, verdict] of entries.sort()) {
|
|
26125
|
-
const colored = verdict === "allow" ?
|
|
26669
|
+
const colored = verdict === "allow" ? chalk31.green(verdict) : chalk31.red(verdict);
|
|
26126
26670
|
console.log(` ${tool.padEnd(w)} ${colored}`);
|
|
26127
26671
|
}
|
|
26128
26672
|
console.log(
|
|
26129
|
-
|
|
26673
|
+
chalk31.gray(`
|
|
26130
26674
|
Stored in ${DECISIONS_FILE2}
|
|
26131
|
-
`) +
|
|
26675
|
+
`) + chalk31.gray(" Run `node9 decisions clear <tool>` to remove an entry.")
|
|
26132
26676
|
);
|
|
26133
26677
|
});
|
|
26134
26678
|
cmd.command("clear <toolName>").description("Remove a persistent decision for one tool").action((toolName) => {
|
|
26135
26679
|
const decisions = readDecisions();
|
|
26136
26680
|
if (!(toolName in decisions)) {
|
|
26137
|
-
console.log(
|
|
26681
|
+
console.log(chalk31.yellow(` No persistent decision for "${toolName}". Nothing to clear.`));
|
|
26138
26682
|
process.exitCode = 1;
|
|
26139
26683
|
return;
|
|
26140
26684
|
}
|
|
26141
26685
|
delete decisions[toolName];
|
|
26142
26686
|
writeDecisions(decisions);
|
|
26143
|
-
console.log(
|
|
26687
|
+
console.log(chalk31.green(` \u2713 Cleared persistent decision for "${toolName}".`));
|
|
26144
26688
|
});
|
|
26145
26689
|
cmd.command("clear-all").description("Remove every persistent decision (irreversible)").action(() => {
|
|
26146
26690
|
const decisions = readDecisions();
|
|
26147
26691
|
const count = Object.keys(decisions).length;
|
|
26148
26692
|
if (count === 0) {
|
|
26149
|
-
console.log(
|
|
26693
|
+
console.log(chalk31.gray(" Nothing to clear \u2014 no persistent decisions stored."));
|
|
26150
26694
|
return;
|
|
26151
26695
|
}
|
|
26152
26696
|
writeDecisions({});
|
|
26153
26697
|
console.log(
|
|
26154
|
-
|
|
26698
|
+
chalk31.green(` \u2713 Cleared ${count} persistent decision${count === 1 ? "" : "s"}.`)
|
|
26155
26699
|
);
|
|
26156
26700
|
});
|
|
26157
26701
|
}
|
|
26158
26702
|
|
|
26159
26703
|
// src/cli/commands/dlp.ts
|
|
26160
|
-
import
|
|
26161
|
-
import
|
|
26162
|
-
import
|
|
26163
|
-
import
|
|
26164
|
-
var AUDIT_LOG =
|
|
26165
|
-
var RESOLVED_FILE =
|
|
26704
|
+
import chalk32 from "chalk";
|
|
26705
|
+
import fs58 from "fs";
|
|
26706
|
+
import path56 from "path";
|
|
26707
|
+
import os50 from "os";
|
|
26708
|
+
var AUDIT_LOG = path56.join(os50.homedir(), ".node9", "audit.log");
|
|
26709
|
+
var RESOLVED_FILE = path56.join(os50.homedir(), ".node9", "dlp-resolved.json");
|
|
26166
26710
|
var ANSI_RE = /\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g;
|
|
26167
26711
|
function stripAnsi(s) {
|
|
26168
26712
|
return s.replace(ANSI_RE, "");
|
|
26169
26713
|
}
|
|
26170
26714
|
function loadResolved() {
|
|
26171
26715
|
try {
|
|
26172
|
-
const raw = JSON.parse(
|
|
26716
|
+
const raw = JSON.parse(fs58.readFileSync(RESOLVED_FILE, "utf-8"));
|
|
26173
26717
|
return new Set(raw);
|
|
26174
26718
|
} catch {
|
|
26175
26719
|
return /* @__PURE__ */ new Set();
|
|
@@ -26177,13 +26721,13 @@ function loadResolved() {
|
|
|
26177
26721
|
}
|
|
26178
26722
|
function saveResolved(resolved) {
|
|
26179
26723
|
try {
|
|
26180
|
-
|
|
26724
|
+
fs58.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
|
|
26181
26725
|
} catch {
|
|
26182
26726
|
}
|
|
26183
26727
|
}
|
|
26184
26728
|
function loadDlpFindings() {
|
|
26185
|
-
if (!
|
|
26186
|
-
return
|
|
26729
|
+
if (!fs58.existsSync(AUDIT_LOG)) return [];
|
|
26730
|
+
return fs58.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
|
|
26187
26731
|
if (!line.trim()) return [];
|
|
26188
26732
|
try {
|
|
26189
26733
|
const e = JSON.parse(line);
|
|
@@ -26212,14 +26756,14 @@ function registerDlpCommand(program2) {
|
|
|
26212
26756
|
cmd.command("resolve").description("Mark all current DLP findings as resolved").action(() => {
|
|
26213
26757
|
const findings = loadDlpFindings();
|
|
26214
26758
|
if (findings.length === 0) {
|
|
26215
|
-
console.log(
|
|
26759
|
+
console.log(chalk32.green("\n \u2705 No response-DLP findings to resolve.\n"));
|
|
26216
26760
|
return;
|
|
26217
26761
|
}
|
|
26218
26762
|
const resolved = loadResolved();
|
|
26219
26763
|
for (const e of findings) resolved.add(entryKey2(e));
|
|
26220
26764
|
saveResolved(resolved);
|
|
26221
26765
|
console.log(
|
|
26222
|
-
|
|
26766
|
+
chalk32.green(
|
|
26223
26767
|
`
|
|
26224
26768
|
\u2705 ${findings.length} finding${findings.length !== 1 ? "s" : ""} marked as resolved.
|
|
26225
26769
|
`
|
|
@@ -26233,47 +26777,47 @@ function registerDlpCommand(program2) {
|
|
|
26233
26777
|
const resolvedCount = findings.length - open.length;
|
|
26234
26778
|
console.log("");
|
|
26235
26779
|
console.log(
|
|
26236
|
-
|
|
26780
|
+
chalk32.bold.cyan("\u{1F510} node9 dlp") + chalk32.dim(" \u2014 secrets found in Claude response text")
|
|
26237
26781
|
);
|
|
26238
26782
|
console.log("");
|
|
26239
26783
|
if (open.length === 0) {
|
|
26240
26784
|
if (resolvedCount > 0) {
|
|
26241
|
-
console.log(
|
|
26785
|
+
console.log(chalk32.green(` \u2705 No open findings \xB7 ${resolvedCount} previously resolved`));
|
|
26242
26786
|
} else {
|
|
26243
26787
|
console.log(
|
|
26244
|
-
|
|
26788
|
+
chalk32.green(" \u2705 No findings \u2014 Claude has not leaked secrets in response text")
|
|
26245
26789
|
);
|
|
26246
26790
|
}
|
|
26247
26791
|
console.log("");
|
|
26248
26792
|
return;
|
|
26249
26793
|
}
|
|
26250
26794
|
console.log(
|
|
26251
|
-
|
|
26795
|
+
chalk32.bgRed.white.bold(` \u26A0\uFE0F ${open.length} open finding${open.length !== 1 ? "s" : ""} `) + chalk32.dim(resolvedCount > 0 ? ` (${resolvedCount} resolved)` : "")
|
|
26252
26796
|
);
|
|
26253
26797
|
console.log("");
|
|
26254
26798
|
console.log(
|
|
26255
|
-
|
|
26799
|
+
chalk32.dim(" These secrets were included in Claude's response text \u2014 NOT blocked.")
|
|
26256
26800
|
);
|
|
26257
|
-
console.log(
|
|
26801
|
+
console.log(chalk32.dim(" Rotate each affected key immediately.\n"));
|
|
26258
26802
|
for (const e of open) {
|
|
26259
26803
|
console.log(
|
|
26260
|
-
" " +
|
|
26804
|
+
" " + chalk32.red("\u25CF") + " " + chalk32.white(e.dlpPattern ?? "Secret") + chalk32.dim(" " + fmtDate3(e.ts))
|
|
26261
26805
|
);
|
|
26262
26806
|
if (e.dlpSample) {
|
|
26263
|
-
console.log(" " +
|
|
26807
|
+
console.log(" " + chalk32.dim("Sample: ") + chalk32.yellow(stripAnsi(e.dlpSample)));
|
|
26264
26808
|
}
|
|
26265
26809
|
if (e.project) {
|
|
26266
|
-
console.log(" " +
|
|
26810
|
+
console.log(" " + chalk32.dim("Project: ") + chalk32.dim(stripAnsi(e.project)));
|
|
26267
26811
|
}
|
|
26268
26812
|
console.log("");
|
|
26269
26813
|
}
|
|
26270
|
-
console.log(" " +
|
|
26271
|
-
console.log(" " +
|
|
26814
|
+
console.log(" " + chalk32.bold("Next steps:"));
|
|
26815
|
+
console.log(" " + chalk32.cyan("1.") + " Rotate any exposed keys shown above");
|
|
26272
26816
|
console.log(
|
|
26273
|
-
" " +
|
|
26817
|
+
" " + chalk32.cyan("2.") + " Run " + chalk32.white("node9 dlp resolve") + " to acknowledge"
|
|
26274
26818
|
);
|
|
26275
26819
|
console.log(
|
|
26276
|
-
" " +
|
|
26820
|
+
" " + chalk32.cyan("3.") + " Run " + chalk32.white("node9 report") + " for full audit history"
|
|
26277
26821
|
);
|
|
26278
26822
|
console.log("");
|
|
26279
26823
|
});
|
|
@@ -26281,15 +26825,15 @@ function registerDlpCommand(program2) {
|
|
|
26281
26825
|
|
|
26282
26826
|
// src/cli/commands/mask.ts
|
|
26283
26827
|
init_dlp();
|
|
26284
|
-
import
|
|
26285
|
-
import
|
|
26286
|
-
import
|
|
26287
|
-
import
|
|
26828
|
+
import chalk33 from "chalk";
|
|
26829
|
+
import fs59 from "fs";
|
|
26830
|
+
import path57 from "path";
|
|
26831
|
+
import os51 from "os";
|
|
26288
26832
|
function findJsonlFiles(dir) {
|
|
26289
26833
|
const results = [];
|
|
26290
|
-
if (!
|
|
26291
|
-
for (const entry of
|
|
26292
|
-
const full =
|
|
26834
|
+
if (!fs59.existsSync(dir)) return results;
|
|
26835
|
+
for (const entry of fs59.readdirSync(dir, { withFileTypes: true })) {
|
|
26836
|
+
const full = path57.join(dir, entry.name);
|
|
26293
26837
|
if (entry.isDirectory()) results.push(...findJsonlFiles(full));
|
|
26294
26838
|
else if (entry.isFile() && entry.name.endsWith(".jsonl")) results.push(full);
|
|
26295
26839
|
}
|
|
@@ -26332,7 +26876,7 @@ function redactJson(obj) {
|
|
|
26332
26876
|
function processFile(filePath, dryRun) {
|
|
26333
26877
|
let raw;
|
|
26334
26878
|
try {
|
|
26335
|
-
raw =
|
|
26879
|
+
raw = fs59.readFileSync(filePath, "utf-8");
|
|
26336
26880
|
} catch {
|
|
26337
26881
|
return { redactedLines: 0, patterns: [] };
|
|
26338
26882
|
}
|
|
@@ -26364,14 +26908,14 @@ function processFile(filePath, dryRun) {
|
|
|
26364
26908
|
}
|
|
26365
26909
|
}
|
|
26366
26910
|
if (!dryRun && redactedLines > 0) {
|
|
26367
|
-
|
|
26911
|
+
fs59.writeFileSync(filePath, newLines.join("\n"), "utf-8");
|
|
26368
26912
|
}
|
|
26369
26913
|
return { redactedLines, patterns };
|
|
26370
26914
|
}
|
|
26371
26915
|
function processJsonFile(filePath, dryRun) {
|
|
26372
26916
|
let raw;
|
|
26373
26917
|
try {
|
|
26374
|
-
raw =
|
|
26918
|
+
raw = fs59.readFileSync(filePath, "utf-8");
|
|
26375
26919
|
} catch {
|
|
26376
26920
|
return { redactedLines: 0, patterns: [] };
|
|
26377
26921
|
}
|
|
@@ -26384,15 +26928,15 @@ function processJsonFile(filePath, dryRun) {
|
|
|
26384
26928
|
const { value, modified, found } = redactJson(parsed);
|
|
26385
26929
|
if (!modified) return { redactedLines: 0, patterns: [] };
|
|
26386
26930
|
if (!dryRun) {
|
|
26387
|
-
|
|
26931
|
+
fs59.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
|
|
26388
26932
|
}
|
|
26389
26933
|
return { redactedLines: 1, patterns: found };
|
|
26390
26934
|
}
|
|
26391
26935
|
function findJsonFiles(dir) {
|
|
26392
26936
|
const results = [];
|
|
26393
|
-
if (!
|
|
26394
|
-
for (const entry of
|
|
26395
|
-
const full =
|
|
26937
|
+
if (!fs59.existsSync(dir)) return results;
|
|
26938
|
+
for (const entry of fs59.readdirSync(dir, { withFileTypes: true })) {
|
|
26939
|
+
const full = path57.join(dir, entry.name);
|
|
26396
26940
|
if (entry.isDirectory()) results.push(...findJsonFiles(full));
|
|
26397
26941
|
else if (entry.isFile() && entry.name.endsWith(".json")) results.push(full);
|
|
26398
26942
|
}
|
|
@@ -26401,9 +26945,9 @@ function findJsonFiles(dir) {
|
|
|
26401
26945
|
function registerMaskCommand(program2) {
|
|
26402
26946
|
program2.command("mask").description("Redact plaintext secrets from local AI session history files").option("--dry-run", "show what would be redacted without making changes").option("--all", "scan all history (default: last 30 days)").action(async (options) => {
|
|
26403
26947
|
const dryRun = !!options.dryRun;
|
|
26404
|
-
const home =
|
|
26405
|
-
const claudeDir =
|
|
26406
|
-
const geminiDir =
|
|
26948
|
+
const home = os51.homedir();
|
|
26949
|
+
const claudeDir = path57.join(home, ".claude", "projects");
|
|
26950
|
+
const geminiDir = path57.join(home, ".gemini", "tmp");
|
|
26407
26951
|
const allFiles = [
|
|
26408
26952
|
...findJsonlFiles(claudeDir).map((p) => ({ path: p, type: "jsonl" })),
|
|
26409
26953
|
...findJsonFiles(geminiDir).map((p) => ({ path: p, type: "json" }))
|
|
@@ -26411,18 +26955,18 @@ function registerMaskCommand(program2) {
|
|
|
26411
26955
|
const cutoff = options.all ? null : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
|
|
26412
26956
|
const filtered = cutoff ? allFiles.filter((f) => {
|
|
26413
26957
|
try {
|
|
26414
|
-
return
|
|
26958
|
+
return fs59.statSync(f.path).mtime >= cutoff;
|
|
26415
26959
|
} catch {
|
|
26416
26960
|
return false;
|
|
26417
26961
|
}
|
|
26418
26962
|
}) : allFiles;
|
|
26419
26963
|
if (filtered.length === 0) {
|
|
26420
|
-
console.log(
|
|
26964
|
+
console.log(chalk33.yellow(" No session files found."));
|
|
26421
26965
|
return;
|
|
26422
26966
|
}
|
|
26423
26967
|
console.log("");
|
|
26424
26968
|
if (dryRun) {
|
|
26425
|
-
console.log(
|
|
26969
|
+
console.log(chalk33.dim(" Dry run \u2014 no files will be modified.\n"));
|
|
26426
26970
|
}
|
|
26427
26971
|
let totalFiles = 0;
|
|
26428
26972
|
let totalLines = 0;
|
|
@@ -26438,23 +26982,23 @@ function registerMaskCommand(program2) {
|
|
|
26438
26982
|
});
|
|
26439
26983
|
const verb = dryRun ? "Would redact" : "Redacted";
|
|
26440
26984
|
console.log(
|
|
26441
|
-
" " +
|
|
26985
|
+
" " + chalk33.dim(shortPath.slice(0, 60).padEnd(62)) + chalk33.red(`${verb}: `) + chalk33.yellow(patterns.join(", ")) + chalk33.dim(` (${redactedLines} line${redactedLines !== 1 ? "s" : ""})`)
|
|
26442
26986
|
);
|
|
26443
26987
|
}
|
|
26444
26988
|
}
|
|
26445
26989
|
console.log("");
|
|
26446
26990
|
if (totalFiles === 0) {
|
|
26447
|
-
console.log(
|
|
26991
|
+
console.log(chalk33.green(" No secrets found in session history."));
|
|
26448
26992
|
} else {
|
|
26449
26993
|
const verb = dryRun ? "would be modified" : "modified";
|
|
26450
26994
|
console.log(
|
|
26451
|
-
|
|
26995
|
+
chalk33.bold(` ${totalFiles} file${totalFiles !== 1 ? "s" : ""} ${verb}`) + chalk33.dim(`, ${totalLines} line${totalLines !== 1 ? "s" : ""} redacted`)
|
|
26452
26996
|
);
|
|
26453
|
-
console.log(" Patterns: " +
|
|
26997
|
+
console.log(" Patterns: " + chalk33.yellow(totalPatterns.join(", ")));
|
|
26454
26998
|
if (!dryRun) {
|
|
26455
26999
|
console.log("");
|
|
26456
27000
|
console.log(
|
|
26457
|
-
|
|
27001
|
+
chalk33.dim(
|
|
26458
27002
|
" Note: secrets were already sent to the AI provider during the active session.\n This cleans your local disk only. Rotate any exposed keys."
|
|
26459
27003
|
)
|
|
26460
27004
|
);
|
|
@@ -26467,20 +27011,20 @@ function registerMaskCommand(program2) {
|
|
|
26467
27011
|
// src/cli.ts
|
|
26468
27012
|
init_blast();
|
|
26469
27013
|
var { version } = JSON.parse(
|
|
26470
|
-
|
|
27014
|
+
fs62.readFileSync(path60.join(__dirname, "../package.json"), "utf-8")
|
|
26471
27015
|
);
|
|
26472
27016
|
var program = new Command();
|
|
26473
27017
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
26474
27018
|
program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
|
|
26475
27019
|
const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
|
|
26476
|
-
const credPath =
|
|
26477
|
-
if (!
|
|
26478
|
-
|
|
27020
|
+
const credPath = path60.join(os54.homedir(), ".node9", "credentials.json");
|
|
27021
|
+
if (!fs62.existsSync(path60.dirname(credPath)))
|
|
27022
|
+
fs62.mkdirSync(path60.dirname(credPath), { recursive: true });
|
|
26479
27023
|
const profileName = options.profile || "default";
|
|
26480
27024
|
let existingCreds = {};
|
|
26481
27025
|
try {
|
|
26482
|
-
if (
|
|
26483
|
-
const raw = JSON.parse(
|
|
27026
|
+
if (fs62.existsSync(credPath)) {
|
|
27027
|
+
const raw = JSON.parse(fs62.readFileSync(credPath, "utf-8"));
|
|
26484
27028
|
if (raw.apiKey) {
|
|
26485
27029
|
existingCreds = {
|
|
26486
27030
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
|
|
@@ -26492,14 +27036,14 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
26492
27036
|
} catch {
|
|
26493
27037
|
}
|
|
26494
27038
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
|
|
26495
|
-
|
|
27039
|
+
fs62.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
26496
27040
|
let effectiveCloud = null;
|
|
26497
27041
|
if (profileName === "default") {
|
|
26498
|
-
const configPath2 =
|
|
27042
|
+
const configPath2 = path60.join(os54.homedir(), ".node9", "config.json");
|
|
26499
27043
|
let config = {};
|
|
26500
27044
|
try {
|
|
26501
|
-
if (
|
|
26502
|
-
config = JSON.parse(
|
|
27045
|
+
if (fs62.existsSync(configPath2))
|
|
27046
|
+
config = JSON.parse(fs62.readFileSync(configPath2, "utf-8"));
|
|
26503
27047
|
} catch {
|
|
26504
27048
|
}
|
|
26505
27049
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -26514,29 +27058,48 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
26514
27058
|
approvers.cloud = false;
|
|
26515
27059
|
}
|
|
26516
27060
|
s.approvers = approvers;
|
|
26517
|
-
if (!
|
|
26518
|
-
|
|
26519
|
-
|
|
27061
|
+
if (!fs62.existsSync(path60.dirname(configPath2)))
|
|
27062
|
+
fs62.mkdirSync(path60.dirname(configPath2), { recursive: true });
|
|
27063
|
+
fs62.writeFileSync(configPath2, JSON.stringify(config, null, 2), { mode: 384 });
|
|
26520
27064
|
effectiveCloud = approvers.cloud === true;
|
|
26521
27065
|
}
|
|
26522
27066
|
if (options.profile && profileName !== "default") {
|
|
26523
|
-
console.log(
|
|
26524
|
-
console.log(
|
|
27067
|
+
console.log(chalk35.green(`\u2705 Profile "${profileName}" saved`));
|
|
27068
|
+
console.log(chalk35.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
26525
27069
|
} else if (options.local || effectiveCloud === false) {
|
|
26526
|
-
console.log(
|
|
26527
|
-
console.log(
|
|
27070
|
+
console.log(chalk35.green(`\u2705 Key saved \u2014 Privacy mode \u{1F6E1}\uFE0F`));
|
|
27071
|
+
console.log(chalk35.gray(` All decisions stay on this machine. Nothing syncs to the cloud.`));
|
|
26528
27072
|
if (!options.local) {
|
|
26529
27073
|
console.log(
|
|
26530
|
-
|
|
27074
|
+
chalk35.yellow(` Your config has cloud approvals OFF (settings.approvers.cloud).`)
|
|
26531
27075
|
);
|
|
26532
27076
|
console.log(
|
|
26533
|
-
|
|
27077
|
+
chalk35.gray(` To enable team policy + dashboard sync: set it to true, or re-init.`)
|
|
26534
27078
|
);
|
|
26535
27079
|
}
|
|
26536
27080
|
} else {
|
|
26537
|
-
console.log(
|
|
26538
|
-
console.log(
|
|
27081
|
+
console.log(chalk35.green(`\u2705 Logged in \u2014 agent mode`));
|
|
27082
|
+
console.log(chalk35.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
27083
|
+
}
|
|
27084
|
+
});
|
|
27085
|
+
program.command("signup").description("Create your node9 account / open the dashboard in your browser").option("--login", "Open the login page instead of signup").action((options) => {
|
|
27086
|
+
const route = options.login ? "auth/login" : "auth/signup";
|
|
27087
|
+
const url = `https://node9.ai/${route}?ref=cli_cmd`;
|
|
27088
|
+
console.log("");
|
|
27089
|
+
console.log(" " + chalk35.dim("Opening ") + chalk35.cyan.underline(url));
|
|
27090
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
27091
|
+
try {
|
|
27092
|
+
const child = spawn9(opener, [url], {
|
|
27093
|
+
stdio: "ignore",
|
|
27094
|
+
detached: true,
|
|
27095
|
+
shell: process.platform === "win32"
|
|
27096
|
+
});
|
|
27097
|
+
child.on("error", () => {
|
|
27098
|
+
});
|
|
27099
|
+
child.unref();
|
|
27100
|
+
} catch {
|
|
26539
27101
|
}
|
|
27102
|
+
console.log("");
|
|
26540
27103
|
});
|
|
26541
27104
|
program.command("addto", { hidden: true }).description("Integrate Node9 with an AI agent").addHelpText(
|
|
26542
27105
|
"after",
|
|
@@ -26556,7 +27119,7 @@ program.command("addto", { hidden: true }).description("Integrate Node9 with an
|
|
|
26556
27119
|
if (target === "hermes") return setupHermes();
|
|
26557
27120
|
if (target === "hud") return setupHud();
|
|
26558
27121
|
console.error(
|
|
26559
|
-
|
|
27122
|
+
chalk35.red(
|
|
26560
27123
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
26561
27124
|
)
|
|
26562
27125
|
);
|
|
@@ -26570,20 +27133,20 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
|
|
|
26570
27133
|
"The agent to protect: claude | antigravity | copilot | gemini | cursor | codex | windsurf | vscode | hud"
|
|
26571
27134
|
).action(async (target) => {
|
|
26572
27135
|
if (!target) {
|
|
26573
|
-
console.log(
|
|
26574
|
-
console.log(" Usage: " +
|
|
27136
|
+
console.log(chalk35.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
27137
|
+
console.log(" Usage: " + chalk35.white("node9 setup <target>") + "\n");
|
|
26575
27138
|
console.log(" Targets:");
|
|
26576
|
-
console.log(" " +
|
|
26577
|
-
console.log(" " +
|
|
26578
|
-
console.log(" " +
|
|
26579
|
-
console.log(" " +
|
|
26580
|
-
console.log(" " +
|
|
26581
|
-
console.log(" " +
|
|
26582
|
-
console.log(" " +
|
|
26583
|
-
console.log(" " +
|
|
26584
|
-
console.log(" " +
|
|
27139
|
+
console.log(" " + chalk35.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
27140
|
+
console.log(" " + chalk35.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
27141
|
+
console.log(" " + chalk35.green("antigravity") + " \u2014 Antigravity / agy (hook mode)");
|
|
27142
|
+
console.log(" " + chalk35.green("copilot") + " \u2014 GitHub Copilot CLI (hook mode)");
|
|
27143
|
+
console.log(" " + chalk35.green("cursor") + " \u2014 Cursor (MCP proxy)");
|
|
27144
|
+
console.log(" " + chalk35.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
|
|
27145
|
+
console.log(" " + chalk35.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
|
|
27146
|
+
console.log(" " + chalk35.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
|
|
27147
|
+
console.log(" " + chalk35.green("hermes") + " \u2014 Hermes Agent (hook mode)");
|
|
26585
27148
|
process.stdout.write(
|
|
26586
|
-
" " +
|
|
27149
|
+
" " + chalk35.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
26587
27150
|
);
|
|
26588
27151
|
console.log("");
|
|
26589
27152
|
return;
|
|
@@ -26600,7 +27163,7 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
|
|
|
26600
27163
|
if (t === "hermes") return setupHermes();
|
|
26601
27164
|
if (t === "hud") return setupHud();
|
|
26602
27165
|
console.error(
|
|
26603
|
-
|
|
27166
|
+
chalk35.red(
|
|
26604
27167
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
26605
27168
|
)
|
|
26606
27169
|
);
|
|
@@ -26626,33 +27189,33 @@ program.command("removefrom", { hidden: true }).description("Remove Node9 hooks
|
|
|
26626
27189
|
else if (target === "hud") fn = teardownHud;
|
|
26627
27190
|
else {
|
|
26628
27191
|
console.error(
|
|
26629
|
-
|
|
27192
|
+
chalk35.red(
|
|
26630
27193
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
26631
27194
|
)
|
|
26632
27195
|
);
|
|
26633
27196
|
process.exit(1);
|
|
26634
27197
|
}
|
|
26635
|
-
console.log(
|
|
27198
|
+
console.log(chalk35.cyan(`
|
|
26636
27199
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
26637
27200
|
`));
|
|
26638
27201
|
try {
|
|
26639
27202
|
fn();
|
|
26640
27203
|
} catch (err2) {
|
|
26641
|
-
console.error(
|
|
27204
|
+
console.error(chalk35.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
26642
27205
|
process.exit(1);
|
|
26643
27206
|
}
|
|
26644
|
-
console.log(
|
|
27207
|
+
console.log(chalk35.gray("\n Restart the agent for changes to take effect."));
|
|
26645
27208
|
});
|
|
26646
27209
|
program.command("uninstall").description("Remove all Node9 hooks and optionally delete config files").option("--purge", "Also delete ~/.node9/ directory (config, audit log, credentials)").action(async (options) => {
|
|
26647
|
-
console.log(
|
|
26648
|
-
console.log(
|
|
27210
|
+
console.log(chalk35.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
27211
|
+
console.log(chalk35.bold("Stopping daemon..."));
|
|
26649
27212
|
try {
|
|
26650
27213
|
stopDaemon();
|
|
26651
|
-
console.log(
|
|
27214
|
+
console.log(chalk35.green(" \u2705 Daemon stopped"));
|
|
26652
27215
|
} catch {
|
|
26653
|
-
console.log(
|
|
27216
|
+
console.log(chalk35.blue(" \u2139\uFE0F Daemon was not running"));
|
|
26654
27217
|
}
|
|
26655
|
-
console.log(
|
|
27218
|
+
console.log(chalk35.bold("\nRemoving hooks..."));
|
|
26656
27219
|
let teardownFailed = false;
|
|
26657
27220
|
for (const [label2, fn] of [
|
|
26658
27221
|
["Claude", teardownClaude],
|
|
@@ -26668,45 +27231,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
26668
27231
|
} catch (err2) {
|
|
26669
27232
|
teardownFailed = true;
|
|
26670
27233
|
console.error(
|
|
26671
|
-
|
|
27234
|
+
chalk35.red(
|
|
26672
27235
|
` \u26A0\uFE0F Failed to remove ${label2} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
26673
27236
|
)
|
|
26674
27237
|
);
|
|
26675
27238
|
}
|
|
26676
27239
|
}
|
|
26677
27240
|
if (options.purge) {
|
|
26678
|
-
const node9Dir =
|
|
26679
|
-
if (
|
|
27241
|
+
const node9Dir = path60.join(os54.homedir(), ".node9");
|
|
27242
|
+
if (fs62.existsSync(node9Dir)) {
|
|
26680
27243
|
const confirmed = await confirm2({
|
|
26681
27244
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
26682
27245
|
default: false
|
|
26683
27246
|
});
|
|
26684
27247
|
if (confirmed) {
|
|
26685
|
-
|
|
26686
|
-
if (
|
|
27248
|
+
fs62.rmSync(node9Dir, { recursive: true });
|
|
27249
|
+
if (fs62.existsSync(node9Dir)) {
|
|
26687
27250
|
console.error(
|
|
26688
|
-
|
|
27251
|
+
chalk35.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
26689
27252
|
);
|
|
26690
27253
|
} else {
|
|
26691
|
-
console.log(
|
|
27254
|
+
console.log(chalk35.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
26692
27255
|
}
|
|
26693
27256
|
} else {
|
|
26694
|
-
console.log(
|
|
27257
|
+
console.log(chalk35.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
26695
27258
|
}
|
|
26696
27259
|
} else {
|
|
26697
|
-
console.log(
|
|
27260
|
+
console.log(chalk35.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
26698
27261
|
}
|
|
26699
27262
|
} else {
|
|
26700
27263
|
console.log(
|
|
26701
|
-
|
|
27264
|
+
chalk35.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
26702
27265
|
);
|
|
26703
27266
|
}
|
|
26704
27267
|
if (teardownFailed) {
|
|
26705
|
-
console.error(
|
|
27268
|
+
console.error(chalk35.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
26706
27269
|
process.exit(1);
|
|
26707
27270
|
}
|
|
26708
|
-
console.log(
|
|
26709
|
-
console.log(
|
|
27271
|
+
console.log(chalk35.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g node9-ai"));
|
|
27272
|
+
console.log(chalk35.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
26710
27273
|
});
|
|
26711
27274
|
registerDoctorCommand(program, version);
|
|
26712
27275
|
program.command("explain").description(
|
|
@@ -26719,7 +27282,7 @@ program.command("explain").description(
|
|
|
26719
27282
|
try {
|
|
26720
27283
|
args = JSON.parse(trimmed);
|
|
26721
27284
|
} catch {
|
|
26722
|
-
console.error(
|
|
27285
|
+
console.error(chalk35.red(`
|
|
26723
27286
|
\u274C Invalid JSON: ${trimmed}
|
|
26724
27287
|
`));
|
|
26725
27288
|
process.exit(1);
|
|
@@ -26730,54 +27293,54 @@ program.command("explain").description(
|
|
|
26730
27293
|
}
|
|
26731
27294
|
const result = await explainPolicy(tool, args);
|
|
26732
27295
|
console.log("");
|
|
26733
|
-
console.log(
|
|
27296
|
+
console.log(chalk35.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
26734
27297
|
console.log("");
|
|
26735
|
-
console.log(` ${
|
|
27298
|
+
console.log(` ${chalk35.bold("Tool:")} ${chalk35.white(result.tool)}`);
|
|
26736
27299
|
if (argsRaw) {
|
|
26737
27300
|
const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
26738
|
-
console.log(` ${
|
|
27301
|
+
console.log(` ${chalk35.bold("Input:")} ${chalk35.gray(preview2)}`);
|
|
26739
27302
|
}
|
|
26740
27303
|
console.log("");
|
|
26741
|
-
console.log(
|
|
27304
|
+
console.log(chalk35.bold("Config Sources (Waterfall):"));
|
|
26742
27305
|
for (const tier of result.waterfall) {
|
|
26743
|
-
const num3 =
|
|
27306
|
+
const num3 = chalk35.gray(` ${tier.tier}.`);
|
|
26744
27307
|
const label2 = tier.label.padEnd(16);
|
|
26745
27308
|
let statusStr;
|
|
26746
27309
|
if (tier.tier === 1) {
|
|
26747
|
-
statusStr =
|
|
27310
|
+
statusStr = chalk35.gray(tier.note ?? "");
|
|
26748
27311
|
} else if (tier.status === "active") {
|
|
26749
|
-
const loc = tier.path ?
|
|
26750
|
-
const note = tier.note ?
|
|
26751
|
-
statusStr =
|
|
27312
|
+
const loc = tier.path ? chalk35.gray(tier.path) : "";
|
|
27313
|
+
const note = tier.note ? chalk35.gray(`(${tier.note})`) : "";
|
|
27314
|
+
statusStr = chalk35.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
26752
27315
|
} else {
|
|
26753
|
-
statusStr =
|
|
27316
|
+
statusStr = chalk35.gray("\u25CB " + (tier.note ?? "not found"));
|
|
26754
27317
|
}
|
|
26755
|
-
console.log(`${num3} ${
|
|
27318
|
+
console.log(`${num3} ${chalk35.white(label2)} ${statusStr}`);
|
|
26756
27319
|
}
|
|
26757
27320
|
console.log("");
|
|
26758
|
-
console.log(
|
|
27321
|
+
console.log(chalk35.bold("Policy Evaluation:"));
|
|
26759
27322
|
for (const step of result.steps) {
|
|
26760
27323
|
const isFinal = step.isFinal;
|
|
26761
27324
|
let icon;
|
|
26762
|
-
if (step.outcome === "allow") icon =
|
|
26763
|
-
else if (step.outcome === "review") icon =
|
|
26764
|
-
else if (step.outcome === "skip") icon =
|
|
26765
|
-
else icon =
|
|
27325
|
+
if (step.outcome === "allow") icon = chalk35.green(" \u2705");
|
|
27326
|
+
else if (step.outcome === "review") icon = chalk35.red(" \u{1F534}");
|
|
27327
|
+
else if (step.outcome === "skip") icon = chalk35.gray(" \u2500 ");
|
|
27328
|
+
else icon = chalk35.gray(" \u25CB ");
|
|
26766
27329
|
const name = step.name.padEnd(18);
|
|
26767
|
-
const nameStr = isFinal ?
|
|
26768
|
-
const detail = isFinal ?
|
|
26769
|
-
const arrow = isFinal ?
|
|
27330
|
+
const nameStr = isFinal ? chalk35.white.bold(name) : chalk35.white(name);
|
|
27331
|
+
const detail = isFinal ? chalk35.white(step.detail) : chalk35.gray(step.detail);
|
|
27332
|
+
const arrow = isFinal ? chalk35.yellow(" \u2190 STOP") : "";
|
|
26770
27333
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
26771
27334
|
}
|
|
26772
27335
|
console.log("");
|
|
26773
27336
|
if (result.decision === "allow") {
|
|
26774
|
-
console.log(
|
|
27337
|
+
console.log(chalk35.green.bold(" Decision: \u2705 ALLOW") + chalk35.gray(" \u2014 no approval needed"));
|
|
26775
27338
|
} else {
|
|
26776
27339
|
console.log(
|
|
26777
|
-
|
|
27340
|
+
chalk35.red.bold(" Decision: \u{1F534} REVIEW") + chalk35.gray(" \u2014 human approval required")
|
|
26778
27341
|
);
|
|
26779
27342
|
if (result.blockedByLabel) {
|
|
26780
|
-
console.log(
|
|
27343
|
+
console.log(chalk35.gray(` Reason: ${result.blockedByLabel}`));
|
|
26781
27344
|
}
|
|
26782
27345
|
}
|
|
26783
27346
|
console.log("");
|
|
@@ -26792,18 +27355,18 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
26792
27355
|
try {
|
|
26793
27356
|
await startTail2(options);
|
|
26794
27357
|
} catch (err2) {
|
|
26795
|
-
console.error(
|
|
27358
|
+
console.error(chalk35.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
26796
27359
|
process.exit(1);
|
|
26797
27360
|
}
|
|
26798
27361
|
});
|
|
26799
27362
|
program.command("monitor").description("Live interactive dashboard \u2014 activity feed, approvals, security signals").action(async () => {
|
|
26800
27363
|
try {
|
|
26801
|
-
const dashboardPath =
|
|
27364
|
+
const dashboardPath = path60.join(__dirname, "dashboard.mjs");
|
|
26802
27365
|
const dynamicImport = new Function("id", "return import(id)");
|
|
26803
27366
|
const mod = await dynamicImport(`file://${dashboardPath}`);
|
|
26804
27367
|
await mod.startMonitor();
|
|
26805
27368
|
} catch (err2) {
|
|
26806
|
-
console.error(
|
|
27369
|
+
console.error(chalk35.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
26807
27370
|
process.exit(1);
|
|
26808
27371
|
}
|
|
26809
27372
|
});
|
|
@@ -26836,14 +27399,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
|
26836
27399
|
Run "node9 addto claude" to register it as the statusLine.`
|
|
26837
27400
|
).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
|
|
26838
27401
|
if (subcommand === "debug") {
|
|
26839
|
-
const flagFile =
|
|
27402
|
+
const flagFile = path60.join(os54.homedir(), ".node9", "hud-debug");
|
|
26840
27403
|
if (state === "on") {
|
|
26841
|
-
|
|
26842
|
-
|
|
27404
|
+
fs62.mkdirSync(path60.dirname(flagFile), { recursive: true });
|
|
27405
|
+
fs62.writeFileSync(flagFile, "");
|
|
26843
27406
|
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
26844
27407
|
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
26845
27408
|
} else if (state === "off") {
|
|
26846
|
-
if (
|
|
27409
|
+
if (fs62.existsSync(flagFile)) fs62.unlinkSync(flagFile);
|
|
26847
27410
|
console.log("HUD debug logging disabled.");
|
|
26848
27411
|
} else {
|
|
26849
27412
|
console.error("Usage: node9 hud debug on|off");
|
|
@@ -26858,7 +27421,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
26858
27421
|
const ms = parseDuration(options.duration);
|
|
26859
27422
|
if (ms === null) {
|
|
26860
27423
|
console.error(
|
|
26861
|
-
|
|
27424
|
+
chalk35.red(`
|
|
26862
27425
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
26863
27426
|
`)
|
|
26864
27427
|
);
|
|
@@ -26866,20 +27429,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
26866
27429
|
}
|
|
26867
27430
|
pauseNode9(ms, options.duration);
|
|
26868
27431
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
26869
|
-
console.log(
|
|
27432
|
+
console.log(chalk35.yellow(`
|
|
26870
27433
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
26871
|
-
console.log(
|
|
26872
|
-
console.log(
|
|
27434
|
+
console.log(chalk35.gray(` All tool calls will be allowed without review.`));
|
|
27435
|
+
console.log(chalk35.gray(` Run "node9 resume" to re-enable early.
|
|
26873
27436
|
`));
|
|
26874
27437
|
});
|
|
26875
27438
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
26876
27439
|
const { paused } = checkPause();
|
|
26877
27440
|
if (!paused) {
|
|
26878
|
-
console.log(
|
|
27441
|
+
console.log(chalk35.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
26879
27442
|
return;
|
|
26880
27443
|
}
|
|
26881
27444
|
resumeNode9();
|
|
26882
|
-
console.log(
|
|
27445
|
+
console.log(chalk35.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
26883
27446
|
});
|
|
26884
27447
|
var HOOK_BASED_AGENTS = {
|
|
26885
27448
|
claude: "claude",
|
|
@@ -26895,15 +27458,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
26895
27458
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
26896
27459
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
26897
27460
|
console.error(
|
|
26898
|
-
|
|
27461
|
+
chalk35.yellow(`
|
|
26899
27462
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
26900
27463
|
);
|
|
26901
|
-
console.error(
|
|
27464
|
+
console.error(chalk35.white(`
|
|
26902
27465
|
"${target}" uses its own hook system. Use:`));
|
|
26903
27466
|
console.error(
|
|
26904
|
-
|
|
27467
|
+
chalk35.green(` node9 addto ${target} `) + chalk35.gray("# one-time setup")
|
|
26905
27468
|
);
|
|
26906
|
-
console.error(
|
|
27469
|
+
console.error(chalk35.green(` ${target} `) + chalk35.gray("# run normally"));
|
|
26907
27470
|
process.exit(1);
|
|
26908
27471
|
}
|
|
26909
27472
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -26920,7 +27483,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
26920
27483
|
}
|
|
26921
27484
|
);
|
|
26922
27485
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
26923
|
-
console.error(
|
|
27486
|
+
console.error(chalk35.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
26924
27487
|
const daemonReady = await autoStartDaemonAndWait();
|
|
26925
27488
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
26926
27489
|
}
|
|
@@ -26933,12 +27496,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
26933
27496
|
}
|
|
26934
27497
|
if (!result.approved) {
|
|
26935
27498
|
console.error(
|
|
26936
|
-
|
|
27499
|
+
chalk35.red(`
|
|
26937
27500
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
26938
27501
|
);
|
|
26939
27502
|
process.exit(1);
|
|
26940
27503
|
}
|
|
26941
|
-
console.error(
|
|
27504
|
+
console.error(chalk35.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
26942
27505
|
await runProxy(fullCommand);
|
|
26943
27506
|
} else {
|
|
26944
27507
|
program.help();
|
|
@@ -26953,6 +27516,7 @@ registerAgentsCommand(program);
|
|
|
26953
27516
|
registerScanCommand(program);
|
|
26954
27517
|
registerPostureCommand(program);
|
|
26955
27518
|
registerEgressCommand(program);
|
|
27519
|
+
registerSandboxCommand(program, version);
|
|
26956
27520
|
registerSessionsCommand(program);
|
|
26957
27521
|
registerSessionTaintCommand(program);
|
|
26958
27522
|
registerDlpCommand(program);
|
|
@@ -26963,9 +27527,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
26963
27527
|
const isCheckHook = process.argv[2] === "check";
|
|
26964
27528
|
if (isCheckHook) {
|
|
26965
27529
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
26966
|
-
const logPath =
|
|
27530
|
+
const logPath = path60.join(os54.homedir(), ".node9", "hook-debug.log");
|
|
26967
27531
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
26968
|
-
|
|
27532
|
+
fs62.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
26969
27533
|
`);
|
|
26970
27534
|
}
|
|
26971
27535
|
process.exit(0);
|