@node9/proxy 1.39.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 +993 -461
- package/dist/cli.mjs +989 -457
- 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
|
}
|
|
@@ -17198,10 +17198,10 @@ __export(tail_exports, {
|
|
|
17198
17198
|
startTail: () => startTail
|
|
17199
17199
|
});
|
|
17200
17200
|
import http3 from "http";
|
|
17201
|
-
import
|
|
17202
|
-
import
|
|
17203
|
-
import
|
|
17204
|
-
import
|
|
17201
|
+
import chalk34 from "chalk";
|
|
17202
|
+
import fs60 from "fs";
|
|
17203
|
+
import os52 from "os";
|
|
17204
|
+
import path58 from "path";
|
|
17205
17205
|
import readline6 from "readline";
|
|
17206
17206
|
import { spawn as spawn8 } from "child_process";
|
|
17207
17207
|
function shortenPathSummary(s) {
|
|
@@ -17225,20 +17225,20 @@ function getModelContextLimit(model) {
|
|
|
17225
17225
|
return 2e5;
|
|
17226
17226
|
}
|
|
17227
17227
|
function readSessionUsage() {
|
|
17228
|
-
const projectsDir =
|
|
17229
|
-
if (!
|
|
17228
|
+
const projectsDir = path58.join(os52.homedir(), ".claude", "projects");
|
|
17229
|
+
if (!fs60.existsSync(projectsDir)) return null;
|
|
17230
17230
|
let latestFile = null;
|
|
17231
17231
|
let latestMtime = 0;
|
|
17232
17232
|
try {
|
|
17233
|
-
for (const dir of
|
|
17234
|
-
const dirPath =
|
|
17233
|
+
for (const dir of fs60.readdirSync(projectsDir)) {
|
|
17234
|
+
const dirPath = path58.join(projectsDir, dir);
|
|
17235
17235
|
try {
|
|
17236
|
-
if (!
|
|
17237
|
-
for (const file of
|
|
17236
|
+
if (!fs60.statSync(dirPath).isDirectory()) continue;
|
|
17237
|
+
for (const file of fs60.readdirSync(dirPath)) {
|
|
17238
17238
|
if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
|
|
17239
|
-
const filePath =
|
|
17239
|
+
const filePath = path58.join(dirPath, file);
|
|
17240
17240
|
try {
|
|
17241
|
-
const mtime =
|
|
17241
|
+
const mtime = fs60.statSync(filePath).mtimeMs;
|
|
17242
17242
|
if (mtime > latestMtime) {
|
|
17243
17243
|
latestMtime = mtime;
|
|
17244
17244
|
latestFile = filePath;
|
|
@@ -17253,7 +17253,7 @@ function readSessionUsage() {
|
|
|
17253
17253
|
}
|
|
17254
17254
|
if (!latestFile) return null;
|
|
17255
17255
|
try {
|
|
17256
|
-
const lines =
|
|
17256
|
+
const lines = fs60.readFileSync(latestFile, "utf-8").split("\n");
|
|
17257
17257
|
let lastModel = "";
|
|
17258
17258
|
let lastInput = 0;
|
|
17259
17259
|
let lastOutput = 0;
|
|
@@ -17278,10 +17278,10 @@ function readSessionUsage() {
|
|
|
17278
17278
|
}
|
|
17279
17279
|
}
|
|
17280
17280
|
function formatContextStat(stat) {
|
|
17281
|
-
const pctColor = stat.fillPct >= 80 ?
|
|
17281
|
+
const pctColor = stat.fillPct >= 80 ? chalk34.red : stat.fillPct >= 50 ? chalk34.yellow : chalk34.cyan;
|
|
17282
17282
|
const k = (n) => `${Math.round(n / 1e3)}k`;
|
|
17283
17283
|
const modelShort = stat.model.replace(/@.*$/, "").replace(/-\d{8}$/, "").replace(/^claude-/, "");
|
|
17284
|
-
return
|
|
17284
|
+
return chalk34.dim("ctx: ") + pctColor(`${stat.fillPct}%`) + chalk34.dim(
|
|
17285
17285
|
` (${k(stat.inputTokens)}/${k(getModelContextLimit(stat.model))} out ${k(stat.outputTokens)} \xB7 ${modelShort})`
|
|
17286
17286
|
);
|
|
17287
17287
|
}
|
|
@@ -17304,32 +17304,32 @@ function agentLabel(agent, mcpServer, sessionId) {
|
|
|
17304
17304
|
const tag = sessionTag(sessionId);
|
|
17305
17305
|
const tagSuffix = tag ? `\xB7${tag}` : "";
|
|
17306
17306
|
if (!agent || agent === "Terminal") {
|
|
17307
|
-
return mcpServer ?
|
|
17307
|
+
return mcpServer ? chalk34.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
17308
17308
|
}
|
|
17309
17309
|
const short = agent === "Claude Code" ? "Claude" : agent === "Gemini CLI" ? "Gemini" : agent === "Unknown Agent" ? "" : agent.split(" ")[0];
|
|
17310
|
-
if (!short) return mcpServer ?
|
|
17311
|
-
return mcpServer ?
|
|
17310
|
+
if (!short) return mcpServer ? chalk34.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
17311
|
+
return mcpServer ? chalk34.dim(`[${short}${tagSuffix} \u2192 ${mcpServer}] `) : chalk34.dim(`[${short}${tagSuffix}] `);
|
|
17312
17312
|
}
|
|
17313
17313
|
function formatBase(activity) {
|
|
17314
17314
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
17315
17315
|
const icon = getIcon(activity.tool);
|
|
17316
17316
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
17317
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(
|
|
17317
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os52.homedir(), "~");
|
|
17318
17318
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
17319
|
-
return `${
|
|
17319
|
+
return `${chalk34.gray(time)} ${icon} ${agentLabel(activity.agent, activity.mcpServer, activity.sessionId)}${chalk34.white.bold(toolName)} ${chalk34.dim(argsPreview)}`;
|
|
17320
17320
|
}
|
|
17321
17321
|
function renderResult(activity, result) {
|
|
17322
17322
|
const base = formatBase(activity);
|
|
17323
17323
|
let status;
|
|
17324
17324
|
if (result.status === "allow") {
|
|
17325
|
-
status =
|
|
17325
|
+
status = chalk34.green("\u2713 ALLOW");
|
|
17326
17326
|
} else if (result.status === "dlp") {
|
|
17327
|
-
status =
|
|
17327
|
+
status = chalk34.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
17328
17328
|
} else {
|
|
17329
|
-
status =
|
|
17329
|
+
status = chalk34.red("\u2717 BLOCK");
|
|
17330
17330
|
}
|
|
17331
17331
|
const cost = result.costEstimate ?? activity.costEstimate;
|
|
17332
|
-
const costSuffix = cost == null ? "" :
|
|
17332
|
+
const costSuffix = cost == null ? "" : chalk34.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
17333
17333
|
if (process.stdout.isTTY) {
|
|
17334
17334
|
if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
|
|
17335
17335
|
readline6.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
|
|
@@ -17346,19 +17346,19 @@ function renderResult(activity, result) {
|
|
|
17346
17346
|
}
|
|
17347
17347
|
function renderPending(activity) {
|
|
17348
17348
|
if (!process.stdout.isTTY) return;
|
|
17349
|
-
const line = `${formatBase(activity)} ${
|
|
17349
|
+
const line = `${formatBase(activity)} ${chalk34.yellow("\u25CF \u2026")}`;
|
|
17350
17350
|
pendingShownForId = activity.id;
|
|
17351
17351
|
pendingWrappedLines = wrappedLineCount(line);
|
|
17352
17352
|
process.stdout.write(`${line}\r`);
|
|
17353
17353
|
}
|
|
17354
17354
|
async function ensureDaemon() {
|
|
17355
17355
|
let pidPort = null;
|
|
17356
|
-
if (
|
|
17356
|
+
if (fs60.existsSync(PID_FILE)) {
|
|
17357
17357
|
try {
|
|
17358
|
-
const { port } = JSON.parse(
|
|
17358
|
+
const { port } = JSON.parse(fs60.readFileSync(PID_FILE, "utf-8"));
|
|
17359
17359
|
pidPort = port;
|
|
17360
17360
|
} catch {
|
|
17361
|
-
console.error(
|
|
17361
|
+
console.error(chalk34.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
17362
17362
|
}
|
|
17363
17363
|
}
|
|
17364
17364
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -17369,7 +17369,7 @@ async function ensureDaemon() {
|
|
|
17369
17369
|
if (res.ok) return checkPort;
|
|
17370
17370
|
} catch {
|
|
17371
17371
|
}
|
|
17372
|
-
console.log(
|
|
17372
|
+
console.log(chalk34.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
17373
17373
|
const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
|
|
17374
17374
|
detached: true,
|
|
17375
17375
|
stdio: "ignore",
|
|
@@ -17386,7 +17386,7 @@ async function ensureDaemon() {
|
|
|
17386
17386
|
} catch {
|
|
17387
17387
|
}
|
|
17388
17388
|
}
|
|
17389
|
-
console.error(
|
|
17389
|
+
console.error(chalk34.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
17390
17390
|
process.exit(1);
|
|
17391
17391
|
}
|
|
17392
17392
|
function postDecisionHttp(id, decision, authToken, port, opts) {
|
|
@@ -17455,7 +17455,7 @@ function buildCardLines(req, localCount = 0) {
|
|
|
17455
17455
|
const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
|
|
17456
17456
|
const rawDesc = req.riskMetadata?.ruleDescription ?? "";
|
|
17457
17457
|
const description = rawDesc ? cleanReason(rawDesc) : "";
|
|
17458
|
-
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${
|
|
17458
|
+
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${chalk34.dim(`(${req.agent})`)}` : "";
|
|
17459
17459
|
const lines = [
|
|
17460
17460
|
``,
|
|
17461
17461
|
`${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
|
|
@@ -17511,9 +17511,9 @@ function buildRecoveryCardLines(req) {
|
|
|
17511
17511
|
];
|
|
17512
17512
|
}
|
|
17513
17513
|
function readApproversFromDisk() {
|
|
17514
|
-
const configPath2 =
|
|
17514
|
+
const configPath2 = path58.join(os52.homedir(), ".node9", "config.json");
|
|
17515
17515
|
try {
|
|
17516
|
-
const raw = JSON.parse(
|
|
17516
|
+
const raw = JSON.parse(fs60.readFileSync(configPath2, "utf-8"));
|
|
17517
17517
|
const settings = raw.settings ?? {};
|
|
17518
17518
|
return settings.approvers ?? {};
|
|
17519
17519
|
} catch {
|
|
@@ -17524,20 +17524,20 @@ function approverStatusLine() {
|
|
|
17524
17524
|
const a = readApproversFromDisk();
|
|
17525
17525
|
const fmt = (label2, key) => {
|
|
17526
17526
|
const on = a[key] !== false;
|
|
17527
|
-
return `[${key[0]}]${label2.slice(1)} ${on ?
|
|
17527
|
+
return `[${key[0]}]${label2.slice(1)} ${on ? chalk34.green("\u2713") : chalk34.dim("\u2717")}`;
|
|
17528
17528
|
};
|
|
17529
17529
|
return `${fmt("native", "native")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
17530
17530
|
}
|
|
17531
17531
|
function toggleApprover(channel) {
|
|
17532
|
-
const configPath2 =
|
|
17532
|
+
const configPath2 = path58.join(os52.homedir(), ".node9", "config.json");
|
|
17533
17533
|
try {
|
|
17534
|
-
const raw = JSON.parse(
|
|
17534
|
+
const raw = JSON.parse(fs60.readFileSync(configPath2, "utf-8"));
|
|
17535
17535
|
const settings = raw.settings ?? {};
|
|
17536
17536
|
const approvers = settings.approvers ?? {};
|
|
17537
17537
|
approvers[channel] = approvers[channel] === false;
|
|
17538
17538
|
settings.approvers = approvers;
|
|
17539
17539
|
raw.settings = settings;
|
|
17540
|
-
|
|
17540
|
+
fs60.writeFileSync(configPath2, JSON.stringify(raw, null, 2) + "\n");
|
|
17541
17541
|
} catch (err2) {
|
|
17542
17542
|
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
17543
17543
|
`);
|
|
@@ -17569,7 +17569,7 @@ async function startTail(options = {}) {
|
|
|
17569
17569
|
req2.end();
|
|
17570
17570
|
});
|
|
17571
17571
|
if (result.ok) {
|
|
17572
|
-
console.log(
|
|
17572
|
+
console.log(chalk34.green("\u2713 Flight Recorder buffer cleared."));
|
|
17573
17573
|
} else if (result.code === "ECONNREFUSED") {
|
|
17574
17574
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
17575
17575
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -17615,7 +17615,7 @@ async function startTail(options = {}) {
|
|
|
17615
17615
|
const channel = name === "n" ? "native" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
17616
17616
|
if (channel) {
|
|
17617
17617
|
toggleApprover(channel);
|
|
17618
|
-
console.log(
|
|
17618
|
+
console.log(chalk34.dim(` Approvers: ${approverStatusLine()}`));
|
|
17619
17619
|
}
|
|
17620
17620
|
};
|
|
17621
17621
|
process.stdin.on("keypress", idleKeypressHandler);
|
|
@@ -17681,7 +17681,7 @@ async function startTail(options = {}) {
|
|
|
17681
17681
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
17682
17682
|
)
|
|
17683
17683
|
);
|
|
17684
|
-
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");
|
|
17685
17685
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
17686
17686
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
17687
17687
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -17709,8 +17709,8 @@ async function startTail(options = {}) {
|
|
|
17709
17709
|
}
|
|
17710
17710
|
postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).catch((err2) => {
|
|
17711
17711
|
try {
|
|
17712
|
-
|
|
17713
|
-
|
|
17712
|
+
fs60.appendFileSync(
|
|
17713
|
+
path58.join(os52.homedir(), ".node9", "hook-debug.log"),
|
|
17714
17714
|
`[tail] POST /decision failed: ${String(err2)}
|
|
17715
17715
|
`
|
|
17716
17716
|
);
|
|
@@ -17732,7 +17732,7 @@ async function startTail(options = {}) {
|
|
|
17732
17732
|
);
|
|
17733
17733
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
17734
17734
|
if (externalDecision) {
|
|
17735
|
-
const source = externalDecision === "allow" ?
|
|
17735
|
+
const source = externalDecision === "allow" ? chalk34.green("\u2713 ALLOWED") : chalk34.red("\u2717 DENIED");
|
|
17736
17736
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
17737
17737
|
}
|
|
17738
17738
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
@@ -17774,31 +17774,31 @@ async function startTail(options = {}) {
|
|
|
17774
17774
|
};
|
|
17775
17775
|
process.stdin.on("keypress", onKeypress);
|
|
17776
17776
|
}
|
|
17777
|
-
const auditLog =
|
|
17777
|
+
const auditLog = path58.join(os52.homedir(), ".node9", "audit.log");
|
|
17778
17778
|
try {
|
|
17779
|
-
const unackedDlp =
|
|
17779
|
+
const unackedDlp = fs60.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
|
|
17780
17780
|
if (unackedDlp > 0) {
|
|
17781
17781
|
console.log("");
|
|
17782
17782
|
console.log(
|
|
17783
|
-
|
|
17783
|
+
chalk34.bgRed.white.bold(
|
|
17784
17784
|
` \u26A0\uFE0F DLP ALERT: ${unackedDlp} secret${unackedDlp !== 1 ? "s" : ""} found in Claude response text \u2014 run: node9 dlp `
|
|
17785
17785
|
)
|
|
17786
17786
|
);
|
|
17787
17787
|
}
|
|
17788
17788
|
} catch {
|
|
17789
17789
|
}
|
|
17790
|
-
console.log(
|
|
17790
|
+
console.log(chalk34.cyan.bold(`
|
|
17791
17791
|
\u{1F6F0}\uFE0F Node9 tail`));
|
|
17792
17792
|
if (canApprove) {
|
|
17793
|
-
console.log(
|
|
17794
|
-
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`));
|
|
17795
17795
|
}
|
|
17796
17796
|
const ctxStat = readSessionUsage();
|
|
17797
17797
|
if (ctxStat) console.log(" " + formatContextStat(ctxStat));
|
|
17798
17798
|
if (options.history) {
|
|
17799
|
-
console.log(
|
|
17799
|
+
console.log(chalk34.dim("Showing history + live events.\n"));
|
|
17800
17800
|
} else {
|
|
17801
|
-
console.log(
|
|
17801
|
+
console.log(chalk34.dim("Showing live events only. Use --history to include past.\n"));
|
|
17802
17802
|
}
|
|
17803
17803
|
process.on("SIGINT", () => {
|
|
17804
17804
|
exitIdleMode();
|
|
@@ -17808,7 +17808,7 @@ async function startTail(options = {}) {
|
|
|
17808
17808
|
readline6.clearLine(process.stdout, 0);
|
|
17809
17809
|
readline6.cursorTo(process.stdout, 0);
|
|
17810
17810
|
}
|
|
17811
|
-
console.log(
|
|
17811
|
+
console.log(chalk34.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
17812
17812
|
process.exit(0);
|
|
17813
17813
|
});
|
|
17814
17814
|
const STALL_THRESHOLD_MS = 6e4;
|
|
@@ -17816,11 +17816,11 @@ async function startTail(options = {}) {
|
|
|
17816
17816
|
if (stallWarned) return;
|
|
17817
17817
|
if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
|
|
17818
17818
|
try {
|
|
17819
|
-
const auditMtime =
|
|
17819
|
+
const auditMtime = fs60.statSync(auditLog).mtimeMs;
|
|
17820
17820
|
if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
|
|
17821
17821
|
console.log("");
|
|
17822
17822
|
console.log(
|
|
17823
|
-
|
|
17823
|
+
chalk34.yellow(
|
|
17824
17824
|
"\u26A0\uFE0F Tail appears stalled \u2014 hooks are firing but no events are arriving. Try: node9 daemon restart"
|
|
17825
17825
|
)
|
|
17826
17826
|
);
|
|
@@ -17837,7 +17837,7 @@ async function startTail(options = {}) {
|
|
|
17837
17837
|
},
|
|
17838
17838
|
(res) => {
|
|
17839
17839
|
if (res.statusCode !== 200) {
|
|
17840
|
-
console.error(
|
|
17840
|
+
console.error(chalk34.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
17841
17841
|
process.exit(1);
|
|
17842
17842
|
}
|
|
17843
17843
|
if (canApprove) enterIdleMode();
|
|
@@ -17868,7 +17868,7 @@ async function startTail(options = {}) {
|
|
|
17868
17868
|
readline6.clearLine(process.stdout, 0);
|
|
17869
17869
|
readline6.cursorTo(process.stdout, 0);
|
|
17870
17870
|
}
|
|
17871
|
-
console.log(
|
|
17871
|
+
console.log(chalk34.red("\n\u274C Daemon disconnected."));
|
|
17872
17872
|
process.exit(1);
|
|
17873
17873
|
});
|
|
17874
17874
|
}
|
|
@@ -17881,7 +17881,7 @@ async function startTail(options = {}) {
|
|
|
17881
17881
|
const parsed = JSON.parse(rawData);
|
|
17882
17882
|
const msg = parsed.message ?? "Flight recorder is down \u2014 run: node9 daemon restart";
|
|
17883
17883
|
console.log("");
|
|
17884
|
-
console.log(
|
|
17884
|
+
console.log(chalk34.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
|
|
17885
17885
|
} catch {
|
|
17886
17886
|
}
|
|
17887
17887
|
return;
|
|
@@ -17966,9 +17966,9 @@ async function startTail(options = {}) {
|
|
|
17966
17966
|
const rawSummary = data.argsSummary ?? data.tool;
|
|
17967
17967
|
const summary = shortenPathSummary(rawSummary);
|
|
17968
17968
|
const fileCount = data.fileCount ?? 0;
|
|
17969
|
-
const files = fileCount > 0 ?
|
|
17969
|
+
const files = fileCount > 0 ? chalk34.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
17970
17970
|
process.stdout.write(
|
|
17971
|
-
`${
|
|
17971
|
+
`${chalk34.dim(time)} ${chalk34.cyan("\u{1F4F8} snapshot")} ${chalk34.dim(hash)} ${summary}${files}
|
|
17972
17972
|
`
|
|
17973
17973
|
);
|
|
17974
17974
|
return;
|
|
@@ -17985,18 +17985,18 @@ async function startTail(options = {}) {
|
|
|
17985
17985
|
if (event === "execution-result") {
|
|
17986
17986
|
const exec = data;
|
|
17987
17987
|
const time = new Date(Date.now()).toLocaleTimeString([], { hour12: false });
|
|
17988
|
-
const arrow = exec.isError ?
|
|
17988
|
+
const arrow = exec.isError ? chalk34.red(" \u21B3 \u2717") : chalk34.green(" \u21B3 \u2713");
|
|
17989
17989
|
const label2 = agentLabel(exec.agent, exec.mcpServer);
|
|
17990
17990
|
const tool = (exec.tool ?? "").slice(0, 16);
|
|
17991
|
-
const duration = typeof exec.durationMs === "number" ?
|
|
17991
|
+
const duration = typeof exec.durationMs === "number" ? chalk34.dim(` (${exec.durationMs}ms)`) : "";
|
|
17992
17992
|
console.log(
|
|
17993
|
-
`${
|
|
17993
|
+
`${chalk34.gray(time)} ${arrow} ${label2}${chalk34.dim(tool)}${chalk34.dim(" completed")}${duration}`
|
|
17994
17994
|
);
|
|
17995
17995
|
}
|
|
17996
17996
|
}
|
|
17997
17997
|
req.on("error", (err2) => {
|
|
17998
17998
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
17999
|
-
console.error(
|
|
17999
|
+
console.error(chalk34.red(`
|
|
18000
18000
|
\u274C ${msg}`));
|
|
18001
18001
|
process.exit(1);
|
|
18002
18002
|
});
|
|
@@ -18007,7 +18007,7 @@ var init_tail = __esm({
|
|
|
18007
18007
|
"use strict";
|
|
18008
18008
|
init_daemon2();
|
|
18009
18009
|
init_daemon();
|
|
18010
|
-
PID_FILE =
|
|
18010
|
+
PID_FILE = path58.join(os52.homedir(), ".node9", "daemon.pid");
|
|
18011
18011
|
ICONS = {
|
|
18012
18012
|
bash: "\u{1F4BB}",
|
|
18013
18013
|
shell: "\u{1F4BB}",
|
|
@@ -18055,9 +18055,9 @@ __export(hud_exports, {
|
|
|
18055
18055
|
main: () => main,
|
|
18056
18056
|
renderEnvironmentLine: () => renderEnvironmentLine
|
|
18057
18057
|
});
|
|
18058
|
-
import
|
|
18059
|
-
import
|
|
18060
|
-
import
|
|
18058
|
+
import fs61 from "fs";
|
|
18059
|
+
import path59 from "path";
|
|
18060
|
+
import os53 from "os";
|
|
18061
18061
|
import http4 from "http";
|
|
18062
18062
|
async function readStdin() {
|
|
18063
18063
|
const chunks = [];
|
|
@@ -18133,9 +18133,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
18133
18133
|
return ` (${m}m left)`;
|
|
18134
18134
|
}
|
|
18135
18135
|
function safeReadJson(filePath) {
|
|
18136
|
-
if (!
|
|
18136
|
+
if (!fs61.existsSync(filePath)) return null;
|
|
18137
18137
|
try {
|
|
18138
|
-
return JSON.parse(
|
|
18138
|
+
return JSON.parse(fs61.readFileSync(filePath, "utf-8"));
|
|
18139
18139
|
} catch {
|
|
18140
18140
|
return null;
|
|
18141
18141
|
}
|
|
@@ -18156,12 +18156,12 @@ function countHooksInFile(filePath) {
|
|
|
18156
18156
|
return Object.keys(cfg.hooks).length;
|
|
18157
18157
|
}
|
|
18158
18158
|
function countRulesInDir(rulesDir) {
|
|
18159
|
-
if (!
|
|
18159
|
+
if (!fs61.existsSync(rulesDir)) return 0;
|
|
18160
18160
|
let count = 0;
|
|
18161
18161
|
try {
|
|
18162
|
-
for (const entry of
|
|
18162
|
+
for (const entry of fs61.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
18163
18163
|
if (entry.isDirectory()) {
|
|
18164
|
-
count += countRulesInDir(
|
|
18164
|
+
count += countRulesInDir(path59.join(rulesDir, entry.name));
|
|
18165
18165
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
18166
18166
|
count++;
|
|
18167
18167
|
}
|
|
@@ -18172,46 +18172,46 @@ function countRulesInDir(rulesDir) {
|
|
|
18172
18172
|
}
|
|
18173
18173
|
function isSamePath(a, b) {
|
|
18174
18174
|
try {
|
|
18175
|
-
return
|
|
18175
|
+
return path59.resolve(a) === path59.resolve(b);
|
|
18176
18176
|
} catch {
|
|
18177
18177
|
return false;
|
|
18178
18178
|
}
|
|
18179
18179
|
}
|
|
18180
18180
|
function countConfigs(cwd) {
|
|
18181
|
-
const homeDir2 =
|
|
18182
|
-
const claudeDir =
|
|
18181
|
+
const homeDir2 = os53.homedir();
|
|
18182
|
+
const claudeDir = path59.join(homeDir2, ".claude");
|
|
18183
18183
|
let claudeMdCount = 0;
|
|
18184
18184
|
let rulesCount = 0;
|
|
18185
18185
|
let hooksCount = 0;
|
|
18186
18186
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
18187
18187
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
18188
|
-
if (
|
|
18189
|
-
rulesCount += countRulesInDir(
|
|
18190
|
-
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");
|
|
18191
18191
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
18192
18192
|
hooksCount += countHooksInFile(userSettings);
|
|
18193
|
-
const userClaudeJson =
|
|
18193
|
+
const userClaudeJson = path59.join(homeDir2, ".claude.json");
|
|
18194
18194
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
18195
18195
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
18196
18196
|
userMcpServers.delete(name);
|
|
18197
18197
|
}
|
|
18198
18198
|
if (cwd) {
|
|
18199
|
-
if (
|
|
18200
|
-
if (
|
|
18201
|
-
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");
|
|
18202
18202
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
18203
18203
|
if (!overlapsUserScope) {
|
|
18204
|
-
if (
|
|
18205
|
-
rulesCount += countRulesInDir(
|
|
18206
|
-
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");
|
|
18207
18207
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
18208
18208
|
hooksCount += countHooksInFile(projSettings);
|
|
18209
18209
|
}
|
|
18210
|
-
if (
|
|
18211
|
-
const localSettings =
|
|
18210
|
+
if (fs61.existsSync(path59.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
18211
|
+
const localSettings = path59.join(projectClaudeDir, "settings.local.json");
|
|
18212
18212
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
18213
18213
|
hooksCount += countHooksInFile(localSettings);
|
|
18214
|
-
const mcpJsonServers = getMcpServerNames(
|
|
18214
|
+
const mcpJsonServers = getMcpServerNames(path59.join(cwd, ".mcp.json"));
|
|
18215
18215
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
18216
18216
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
18217
18217
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -18244,12 +18244,12 @@ function readActiveShieldsHud() {
|
|
|
18244
18244
|
return shieldsCache.value;
|
|
18245
18245
|
}
|
|
18246
18246
|
try {
|
|
18247
|
-
const shieldsPath =
|
|
18248
|
-
if (!
|
|
18247
|
+
const shieldsPath = path59.join(os53.homedir(), ".node9", "shields.json");
|
|
18248
|
+
if (!fs61.existsSync(shieldsPath)) {
|
|
18249
18249
|
shieldsCache = { value: [], ts: now };
|
|
18250
18250
|
return [];
|
|
18251
18251
|
}
|
|
18252
|
-
const parsed = JSON.parse(
|
|
18252
|
+
const parsed = JSON.parse(fs61.readFileSync(shieldsPath, "utf-8"));
|
|
18253
18253
|
if (!Array.isArray(parsed.active)) {
|
|
18254
18254
|
shieldsCache = { value: [], ts: now };
|
|
18255
18255
|
return [];
|
|
@@ -18351,17 +18351,17 @@ function renderContextLine(stdin) {
|
|
|
18351
18351
|
async function main() {
|
|
18352
18352
|
try {
|
|
18353
18353
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
18354
|
-
if (
|
|
18354
|
+
if (fs61.existsSync(path59.join(os53.homedir(), ".node9", "hud-debug"))) {
|
|
18355
18355
|
try {
|
|
18356
|
-
const logPath =
|
|
18356
|
+
const logPath = path59.join(os53.homedir(), ".node9", "hud-debug.log");
|
|
18357
18357
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
18358
18358
|
let size = 0;
|
|
18359
18359
|
try {
|
|
18360
|
-
size =
|
|
18360
|
+
size = fs61.statSync(logPath).size;
|
|
18361
18361
|
} catch {
|
|
18362
18362
|
}
|
|
18363
18363
|
if (size < MAX_LOG_SIZE) {
|
|
18364
|
-
|
|
18364
|
+
fs61.appendFileSync(
|
|
18365
18365
|
logPath,
|
|
18366
18366
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
18367
18367
|
);
|
|
@@ -18382,11 +18382,11 @@ async function main() {
|
|
|
18382
18382
|
try {
|
|
18383
18383
|
const cwd = stdin.cwd ?? process.cwd();
|
|
18384
18384
|
for (const configPath2 of [
|
|
18385
|
-
|
|
18386
|
-
|
|
18385
|
+
path59.join(cwd, "node9.config.json"),
|
|
18386
|
+
path59.join(os53.homedir(), ".node9", "config.json")
|
|
18387
18387
|
]) {
|
|
18388
|
-
if (!
|
|
18389
|
-
const cfg = JSON.parse(
|
|
18388
|
+
if (!fs61.existsSync(configPath2)) continue;
|
|
18389
|
+
const cfg = JSON.parse(fs61.readFileSync(configPath2, "utf-8"));
|
|
18390
18390
|
const hud = cfg.settings?.hud;
|
|
18391
18391
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
18392
18392
|
}
|
|
@@ -18432,10 +18432,10 @@ init_core();
|
|
|
18432
18432
|
init_setup();
|
|
18433
18433
|
init_daemon2();
|
|
18434
18434
|
import { Command } from "commander";
|
|
18435
|
-
import
|
|
18436
|
-
import
|
|
18437
|
-
import
|
|
18438
|
-
import
|
|
18435
|
+
import chalk35 from "chalk";
|
|
18436
|
+
import fs62 from "fs";
|
|
18437
|
+
import path60 from "path";
|
|
18438
|
+
import os54 from "os";
|
|
18439
18439
|
import { spawn as spawn9 } from "child_process";
|
|
18440
18440
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
18441
18441
|
|
|
@@ -24185,6 +24185,132 @@ function checkSecrets(ctx) {
|
|
|
24185
24185
|
|
|
24186
24186
|
// src/posture/egress.ts
|
|
24187
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
|
+
}
|
|
24188
24314
|
function evaluateEgressConfig(egress) {
|
|
24189
24315
|
if (egress.enabled && egress.mode === "block") {
|
|
24190
24316
|
return {
|
|
@@ -24234,6 +24360,21 @@ function evaluateEgressConfig(egress) {
|
|
|
24234
24360
|
};
|
|
24235
24361
|
}
|
|
24236
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
|
+
}
|
|
24237
24378
|
const config = getConfig(ctx.cwd);
|
|
24238
24379
|
const egress = config.policy.egress;
|
|
24239
24380
|
return [evaluateEgressConfig({ enabled: egress.enabled, mode: egress.mode })];
|
|
@@ -24275,7 +24416,7 @@ async function checkGate(ctx) {
|
|
|
24275
24416
|
|
|
24276
24417
|
// src/posture/supply-chain.ts
|
|
24277
24418
|
init_provenance();
|
|
24278
|
-
import
|
|
24419
|
+
import fs48 from "fs";
|
|
24279
24420
|
import os42 from "os";
|
|
24280
24421
|
import path48 from "path";
|
|
24281
24422
|
import { parse as parseToml3 } from "smol-toml";
|
|
@@ -24291,9 +24432,9 @@ function isNode9Managed(command, args = []) {
|
|
|
24291
24432
|
var MAX_CONFIG_BYTES = 10 * 1024 * 1024;
|
|
24292
24433
|
function readServers(file, format, agent) {
|
|
24293
24434
|
try {
|
|
24294
|
-
const stat =
|
|
24435
|
+
const stat = fs48.statSync(file);
|
|
24295
24436
|
if (!stat.isFile() || stat.size > MAX_CONFIG_BYTES) return [];
|
|
24296
|
-
const text =
|
|
24437
|
+
const text = fs48.readFileSync(file, "utf8");
|
|
24297
24438
|
const map = format === "toml" ? parseToml3(text)?.mcp_servers : JSON.parse(text)?.mcpServers;
|
|
24298
24439
|
if (!map || typeof map !== "object") return [];
|
|
24299
24440
|
return Object.entries(map).map(([name, v]) => ({
|
|
@@ -24389,11 +24530,12 @@ async function checkPrivilege(ctx) {
|
|
|
24389
24530
|
}
|
|
24390
24531
|
|
|
24391
24532
|
// src/posture/containment.ts
|
|
24392
|
-
import
|
|
24533
|
+
import fs49 from "fs";
|
|
24534
|
+
var ISOLATION_WEIGHT = 12;
|
|
24393
24535
|
function inContainer() {
|
|
24394
|
-
if (
|
|
24536
|
+
if (fs49.existsSync("/.dockerenv") || fs49.existsSync("/run/.containerenv")) return true;
|
|
24395
24537
|
try {
|
|
24396
|
-
const cgroup =
|
|
24538
|
+
const cgroup = fs49.readFileSync("/proc/1/cgroup", "utf8");
|
|
24397
24539
|
if (/docker|kubepods|containerd|lxc|libpod/.test(cgroup)) return true;
|
|
24398
24540
|
} catch {
|
|
24399
24541
|
}
|
|
@@ -24412,14 +24554,28 @@ function checkContainment(_ctx) {
|
|
|
24412
24554
|
detail: [],
|
|
24413
24555
|
owner: "os",
|
|
24414
24556
|
node9Reduces: true,
|
|
24415
|
-
|
|
24416
|
-
|
|
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`
|
|
24417
24572
|
}
|
|
24418
24573
|
];
|
|
24419
24574
|
}
|
|
24420
24575
|
|
|
24421
24576
|
// src/posture/inbound.ts
|
|
24422
|
-
import
|
|
24577
|
+
import fs50 from "fs";
|
|
24578
|
+
var DB_EXPOSURE_WEIGHT = 4;
|
|
24423
24579
|
var KNOWN_SERVICE_PORTS = {
|
|
24424
24580
|
5432: "PostgreSQL",
|
|
24425
24581
|
6379: "Redis",
|
|
@@ -24505,7 +24661,7 @@ function collectListeners() {
|
|
|
24505
24661
|
const byPort = /* @__PURE__ */ new Map();
|
|
24506
24662
|
for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) {
|
|
24507
24663
|
try {
|
|
24508
|
-
for (const l of parseListeners(
|
|
24664
|
+
for (const l of parseListeners(fs50.readFileSync(file, "utf8"))) {
|
|
24509
24665
|
if (!byPort.has(l.port)) byPort.set(l.port, l);
|
|
24510
24666
|
}
|
|
24511
24667
|
} catch {
|
|
@@ -24517,11 +24673,11 @@ function readProc(pid) {
|
|
|
24517
24673
|
let comm = "unknown";
|
|
24518
24674
|
let cmdline = "";
|
|
24519
24675
|
try {
|
|
24520
|
-
comm =
|
|
24676
|
+
comm = fs50.readFileSync(`/proc/${pid}/comm`, "utf8").trim().replace(/-MainThread$/, "") || "unknown";
|
|
24521
24677
|
} catch {
|
|
24522
24678
|
}
|
|
24523
24679
|
try {
|
|
24524
|
-
cmdline =
|
|
24680
|
+
cmdline = fs50.readFileSync(`/proc/${pid}/cmdline`).toString().replace(/\0/g, " ").trim();
|
|
24525
24681
|
} catch {
|
|
24526
24682
|
}
|
|
24527
24683
|
return { comm, cmdline };
|
|
@@ -24531,21 +24687,21 @@ function resolveProcesses(inodes) {
|
|
|
24531
24687
|
if (inodes.size === 0) return map;
|
|
24532
24688
|
let pids;
|
|
24533
24689
|
try {
|
|
24534
|
-
pids =
|
|
24690
|
+
pids = fs50.readdirSync("/proc").filter((d) => /^\d+$/.test(d));
|
|
24535
24691
|
} catch {
|
|
24536
24692
|
return map;
|
|
24537
24693
|
}
|
|
24538
24694
|
for (const pid of pids) {
|
|
24539
24695
|
let fds;
|
|
24540
24696
|
try {
|
|
24541
|
-
fds =
|
|
24697
|
+
fds = fs50.readdirSync(`/proc/${pid}/fd`);
|
|
24542
24698
|
} catch {
|
|
24543
24699
|
continue;
|
|
24544
24700
|
}
|
|
24545
24701
|
for (const fd of fds) {
|
|
24546
24702
|
let link;
|
|
24547
24703
|
try {
|
|
24548
|
-
link =
|
|
24704
|
+
link = fs50.readlinkSync(`/proc/${pid}/fd/${fd}`);
|
|
24549
24705
|
} catch {
|
|
24550
24706
|
continue;
|
|
24551
24707
|
}
|
|
@@ -24600,8 +24756,15 @@ function checkInbound(ctx) {
|
|
|
24600
24756
|
// Only when node9 actually has a shield for an exposed service — otherwise
|
|
24601
24757
|
// (bare dev servers) it stays purely the user's to rebind.
|
|
24602
24758
|
node9Reduces: reduces,
|
|
24603
|
-
|
|
24604
|
-
|
|
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
|
|
24605
24768
|
});
|
|
24606
24769
|
}
|
|
24607
24770
|
return findings;
|
|
@@ -24651,16 +24814,20 @@ function scorePosture(findings, checksRun) {
|
|
|
24651
24814
|
const open = findings.filter(
|
|
24652
24815
|
(f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix"
|
|
24653
24816
|
);
|
|
24654
|
-
const count = (sev) => open.filter((f) => f.severity === sev).length;
|
|
24655
|
-
|
|
24817
|
+
const count = (sev) => open.filter((f) => f.severity === sev && !f.scoreWeight).length;
|
|
24818
|
+
const base = computeSecurityScore({
|
|
24656
24819
|
critical: count("critical"),
|
|
24657
24820
|
high: count("high"),
|
|
24658
24821
|
medium: count("medium"),
|
|
24659
|
-
// Denominator = number of checks evaluated. With computeSecurityScore's
|
|
24660
|
-
// caps this makes any critical → critical tier, any high → at-risk, and a
|
|
24661
|
-
// fully clean run (0 findings, checksRun > 0) → 100/good.
|
|
24662
24822
|
total: Math.max(checksRun, 1)
|
|
24663
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);
|
|
24664
24831
|
}
|
|
24665
24832
|
|
|
24666
24833
|
// src/posture/headline.ts
|
|
@@ -24870,9 +25037,10 @@ var LABEL_WIDTH = 14;
|
|
|
24870
25037
|
function label(category) {
|
|
24871
25038
|
return chalk24.bold(category.padEnd(Math.max(LABEL_WIDTH, category.length + 1)));
|
|
24872
25039
|
}
|
|
24873
|
-
function renderFinding(f) {
|
|
25040
|
+
function renderFinding(f, showWeight = false) {
|
|
24874
25041
|
const lines = [];
|
|
24875
|
-
|
|
25042
|
+
const wt = showWeight && f.scoreWeight ? chalk24.cyan.bold(`+${f.scoreWeight} `) : "";
|
|
25043
|
+
lines.push(` ${ICON[f.severity]} ${label(f.category)}${wt}${f.title}`);
|
|
24876
25044
|
const indent = " ".repeat(2 + 3 + LABEL_WIDTH);
|
|
24877
25045
|
const width = 80 - indent.length;
|
|
24878
25046
|
for (const s of [f.what, f.why, f.who]) {
|
|
@@ -24888,6 +25056,16 @@ function renderFinding(f) {
|
|
|
24888
25056
|
}
|
|
24889
25057
|
}
|
|
24890
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
|
+
}
|
|
24891
25069
|
return lines;
|
|
24892
25070
|
}
|
|
24893
25071
|
function renderPosture(result) {
|
|
@@ -24897,15 +25075,11 @@ function renderPosture(result) {
|
|
|
24897
25075
|
lines.push(
|
|
24898
25076
|
chalk24.cyan.bold(`\u{1F6E1}\uFE0F Node9 Posture`) + chalk24.gray(` \u2014 ${result.agent}`) + ` ${chalk24.bold(`Score: ${result.score}/100`)} (${tier})`
|
|
24899
25077
|
);
|
|
24900
|
-
const
|
|
24901
|
-
|
|
24902
|
-
).length;
|
|
24903
|
-
if (advisories > 0) {
|
|
24904
|
-
const word = advisories === 1 ? "advisory" : "advisories";
|
|
24905
|
-
const verb = advisories === 1 ? "doesn't" : "don't";
|
|
25078
|
+
const headroom = openHeadroom(result.findings);
|
|
25079
|
+
if (headroom > 0) {
|
|
24906
25080
|
lines.push(
|
|
24907
25081
|
" " + chalk24.gray(
|
|
24908
|
-
`${
|
|
25082
|
+
`${headroom} pts of headroom below \u2014 each layer adds security at some cost to flexibility. You choose which to close.`
|
|
24909
25083
|
)
|
|
24910
25084
|
);
|
|
24911
25085
|
}
|
|
@@ -24921,7 +25095,7 @@ function renderPosture(result) {
|
|
|
24921
25095
|
const covered = result.findings.filter((f) => f.coverage?.state === "covered");
|
|
24922
25096
|
const open = result.findings.filter((f) => f.coverage?.state !== "covered");
|
|
24923
25097
|
if (covered.length > 0) {
|
|
24924
|
-
lines.push(" " + chalk24.green("\u{1F7E2} node9 is
|
|
25098
|
+
lines.push(" " + chalk24.green("\u{1F7E2} ON NOW \u2014 node9 is enforcing these (your floor)"));
|
|
24925
25099
|
for (const f of covered) {
|
|
24926
25100
|
const gated = f.coverage?.level === "review" ? "approval-gating" : "blocking";
|
|
24927
25101
|
const via = f.coverage?.via ?? "node9";
|
|
@@ -24936,18 +25110,16 @@ function renderPosture(result) {
|
|
|
24936
25110
|
const osOpen = open.filter((f) => f.owner !== "node9" && !f.node9Reduces);
|
|
24937
25111
|
if (node9Open.length > 0) {
|
|
24938
25112
|
lines.push(" " + chalk24.cyan.bold("\u{1F527} node9 can fix these \u2014 run the command"));
|
|
24939
|
-
for (const f of node9Open) lines.push(...renderFinding(f));
|
|
25113
|
+
for (const f of node9Open) lines.push(...renderFinding(f, true));
|
|
24940
25114
|
}
|
|
24941
25115
|
if (reduceOpen.length > 0) {
|
|
24942
25116
|
if (node9Open.length > 0) lines.push("");
|
|
24943
|
-
lines.push(
|
|
24944
|
-
|
|
24945
|
-
);
|
|
24946
|
-
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));
|
|
24947
25119
|
}
|
|
24948
25120
|
if (osOpen.length > 0) {
|
|
24949
25121
|
if (node9Open.length > 0 || reduceOpen.length > 0) lines.push("");
|
|
24950
|
-
lines.push(" " + chalk24.bold("\u{1F9F1}
|
|
25122
|
+
lines.push(" " + chalk24.bold("\u{1F9F1} YOUR PART \u2014 node9 can't fix these (OS-level)"));
|
|
24951
25123
|
for (const f of osOpen) lines.push(...renderFinding(f));
|
|
24952
25124
|
}
|
|
24953
25125
|
for (const cat of result.passedCategories) {
|
|
@@ -25002,7 +25174,12 @@ function buildShipBody(result) {
|
|
|
25002
25174
|
// The runnable fix / OS action — commands + advice, never a path.
|
|
25003
25175
|
fix: f.fix,
|
|
25004
25176
|
// Whose job it is. Default 'os' so the SaaS never falsely claims node9 can fix it.
|
|
25005
|
-
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
|
|
25006
25183
|
}))
|
|
25007
25184
|
};
|
|
25008
25185
|
}
|
|
@@ -25075,7 +25252,7 @@ function registerPostureCommand(program2) {
|
|
|
25075
25252
|
init_config();
|
|
25076
25253
|
init_dist();
|
|
25077
25254
|
import chalk26 from "chalk";
|
|
25078
|
-
import
|
|
25255
|
+
import fs51 from "fs";
|
|
25079
25256
|
import os45 from "os";
|
|
25080
25257
|
import path49 from "path";
|
|
25081
25258
|
var DEFAULT_EGRESS = {
|
|
@@ -25091,7 +25268,7 @@ function configPath() {
|
|
|
25091
25268
|
function readRawConfig() {
|
|
25092
25269
|
let text;
|
|
25093
25270
|
try {
|
|
25094
|
-
text =
|
|
25271
|
+
text = fs51.readFileSync(configPath(), "utf8");
|
|
25095
25272
|
} catch (err2) {
|
|
25096
25273
|
if (err2.code === "ENOENT") return {};
|
|
25097
25274
|
throw err2;
|
|
@@ -25106,8 +25283,8 @@ function readRawConfig() {
|
|
|
25106
25283
|
}
|
|
25107
25284
|
function writeRawConfig(config) {
|
|
25108
25285
|
const p = configPath();
|
|
25109
|
-
|
|
25110
|
-
|
|
25286
|
+
fs51.mkdirSync(path49.dirname(p), { recursive: true });
|
|
25287
|
+
fs51.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
25111
25288
|
}
|
|
25112
25289
|
function applyEgress(config, change) {
|
|
25113
25290
|
const policy = config.policy = config.policy ?? {};
|
|
@@ -25198,15 +25375,369 @@ function registerEgressCommand(program2) {
|
|
|
25198
25375
|
egress.action(showStatus);
|
|
25199
25376
|
}
|
|
25200
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
|
+
|
|
25201
25732
|
// src/cli/commands/sessions.ts
|
|
25202
25733
|
init_scan_summary();
|
|
25203
25734
|
init_litellm();
|
|
25204
25735
|
init_cost_gemini();
|
|
25205
25736
|
init_cost_codex();
|
|
25206
|
-
import
|
|
25207
|
-
import
|
|
25208
|
-
import
|
|
25209
|
-
import
|
|
25737
|
+
import chalk28 from "chalk";
|
|
25738
|
+
import fs55 from "fs";
|
|
25739
|
+
import path53 from "path";
|
|
25740
|
+
import os47 from "os";
|
|
25210
25741
|
function modelPrice(model) {
|
|
25211
25742
|
const t = pricingFor(model);
|
|
25212
25743
|
if (!t) return null;
|
|
@@ -25223,10 +25754,10 @@ function encodeProjectPath(projectPath) {
|
|
|
25223
25754
|
}
|
|
25224
25755
|
function sessionJsonlPath(projectPath, sessionId) {
|
|
25225
25756
|
const encoded = encodeProjectPath(projectPath);
|
|
25226
|
-
return
|
|
25757
|
+
return path53.join(os47.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
|
|
25227
25758
|
}
|
|
25228
25759
|
function projectLabel(projectPath) {
|
|
25229
|
-
return projectPath.replace(
|
|
25760
|
+
return projectPath.replace(os47.homedir(), "~");
|
|
25230
25761
|
}
|
|
25231
25762
|
function parseHistoryLines(lines) {
|
|
25232
25763
|
const entries = [];
|
|
@@ -25295,10 +25826,10 @@ function parseSessionLines(lines) {
|
|
|
25295
25826
|
return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
|
|
25296
25827
|
}
|
|
25297
25828
|
function loadAuditEntries(auditPath) {
|
|
25298
|
-
const aPath = auditPath ??
|
|
25829
|
+
const aPath = auditPath ?? path53.join(os47.homedir(), ".node9", "audit.log");
|
|
25299
25830
|
let raw;
|
|
25300
25831
|
try {
|
|
25301
|
-
raw =
|
|
25832
|
+
raw = fs55.readFileSync(aPath, "utf-8");
|
|
25302
25833
|
} catch {
|
|
25303
25834
|
return [];
|
|
25304
25835
|
}
|
|
@@ -25334,8 +25865,8 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
|
|
|
25334
25865
|
return result;
|
|
25335
25866
|
}
|
|
25336
25867
|
function buildGeminiSessions(days, allAuditEntries) {
|
|
25337
|
-
const tmpDir =
|
|
25338
|
-
if (!
|
|
25868
|
+
const tmpDir = path53.join(os47.homedir(), ".gemini", "tmp");
|
|
25869
|
+
if (!fs55.existsSync(tmpDir)) return [];
|
|
25339
25870
|
const cutoff = days !== null ? (() => {
|
|
25340
25871
|
const d = /* @__PURE__ */ new Date();
|
|
25341
25872
|
d.setDate(d.getDate() - days);
|
|
@@ -25344,35 +25875,35 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
25344
25875
|
})() : null;
|
|
25345
25876
|
let slugDirs;
|
|
25346
25877
|
try {
|
|
25347
|
-
slugDirs =
|
|
25878
|
+
slugDirs = fs55.readdirSync(tmpDir);
|
|
25348
25879
|
} catch {
|
|
25349
25880
|
return [];
|
|
25350
25881
|
}
|
|
25351
25882
|
const summaries = [];
|
|
25352
25883
|
for (const slug of slugDirs) {
|
|
25353
|
-
const slugPath =
|
|
25884
|
+
const slugPath = path53.join(tmpDir, slug);
|
|
25354
25885
|
try {
|
|
25355
|
-
if (!
|
|
25886
|
+
if (!fs55.statSync(slugPath).isDirectory()) continue;
|
|
25356
25887
|
} catch {
|
|
25357
25888
|
continue;
|
|
25358
25889
|
}
|
|
25359
|
-
let projectRoot =
|
|
25890
|
+
let projectRoot = path53.join(os47.homedir(), slug);
|
|
25360
25891
|
try {
|
|
25361
|
-
projectRoot =
|
|
25892
|
+
projectRoot = fs55.readFileSync(path53.join(slugPath, ".project_root"), "utf-8").trim();
|
|
25362
25893
|
} catch {
|
|
25363
25894
|
}
|
|
25364
|
-
const chatsDir =
|
|
25365
|
-
if (!
|
|
25895
|
+
const chatsDir = path53.join(slugPath, "chats");
|
|
25896
|
+
if (!fs55.existsSync(chatsDir)) continue;
|
|
25366
25897
|
let chatFiles;
|
|
25367
25898
|
try {
|
|
25368
|
-
chatFiles =
|
|
25899
|
+
chatFiles = fs55.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
|
|
25369
25900
|
} catch {
|
|
25370
25901
|
continue;
|
|
25371
25902
|
}
|
|
25372
25903
|
for (const chatFile of chatFiles) {
|
|
25373
25904
|
let raw;
|
|
25374
25905
|
try {
|
|
25375
|
-
raw =
|
|
25906
|
+
raw = fs55.readFileSync(path53.join(chatsDir, chatFile), "utf-8");
|
|
25376
25907
|
} catch {
|
|
25377
25908
|
continue;
|
|
25378
25909
|
}
|
|
@@ -25452,8 +25983,8 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
25452
25983
|
return summaries;
|
|
25453
25984
|
}
|
|
25454
25985
|
function buildCodexSessions(days, allAuditEntries) {
|
|
25455
|
-
const sessionsBase =
|
|
25456
|
-
if (!
|
|
25986
|
+
const sessionsBase = path53.join(os47.homedir(), ".codex", "sessions");
|
|
25987
|
+
if (!fs55.existsSync(sessionsBase)) return [];
|
|
25457
25988
|
const cutoff = days !== null ? (() => {
|
|
25458
25989
|
const d = /* @__PURE__ */ new Date();
|
|
25459
25990
|
d.setDate(d.getDate() - days);
|
|
@@ -25462,29 +25993,29 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
25462
25993
|
})() : null;
|
|
25463
25994
|
const jsonlFiles = [];
|
|
25464
25995
|
try {
|
|
25465
|
-
for (const year of
|
|
25466
|
-
const yearPath =
|
|
25996
|
+
for (const year of fs55.readdirSync(sessionsBase)) {
|
|
25997
|
+
const yearPath = path53.join(sessionsBase, year);
|
|
25467
25998
|
try {
|
|
25468
|
-
if (!
|
|
25999
|
+
if (!fs55.statSync(yearPath).isDirectory()) continue;
|
|
25469
26000
|
} catch {
|
|
25470
26001
|
continue;
|
|
25471
26002
|
}
|
|
25472
|
-
for (const month of
|
|
25473
|
-
const monthPath =
|
|
26003
|
+
for (const month of fs55.readdirSync(yearPath)) {
|
|
26004
|
+
const monthPath = path53.join(yearPath, month);
|
|
25474
26005
|
try {
|
|
25475
|
-
if (!
|
|
26006
|
+
if (!fs55.statSync(monthPath).isDirectory()) continue;
|
|
25476
26007
|
} catch {
|
|
25477
26008
|
continue;
|
|
25478
26009
|
}
|
|
25479
|
-
for (const day of
|
|
25480
|
-
const dayPath =
|
|
26010
|
+
for (const day of fs55.readdirSync(monthPath)) {
|
|
26011
|
+
const dayPath = path53.join(monthPath, day);
|
|
25481
26012
|
try {
|
|
25482
|
-
if (!
|
|
26013
|
+
if (!fs55.statSync(dayPath).isDirectory()) continue;
|
|
25483
26014
|
} catch {
|
|
25484
26015
|
continue;
|
|
25485
26016
|
}
|
|
25486
|
-
for (const file of
|
|
25487
|
-
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));
|
|
25488
26019
|
}
|
|
25489
26020
|
}
|
|
25490
26021
|
}
|
|
@@ -25496,7 +26027,7 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
25496
26027
|
for (const filePath of jsonlFiles) {
|
|
25497
26028
|
let lines;
|
|
25498
26029
|
try {
|
|
25499
|
-
lines =
|
|
26030
|
+
lines = fs55.readFileSync(filePath, "utf-8").split("\n");
|
|
25500
26031
|
} catch {
|
|
25501
26032
|
continue;
|
|
25502
26033
|
}
|
|
@@ -25582,10 +26113,10 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
25582
26113
|
return summaries;
|
|
25583
26114
|
}
|
|
25584
26115
|
function buildSessions(days, historyPath) {
|
|
25585
|
-
const hPath = historyPath ??
|
|
26116
|
+
const hPath = historyPath ?? path53.join(os47.homedir(), ".claude", "history.jsonl");
|
|
25586
26117
|
let historyRaw = "";
|
|
25587
26118
|
try {
|
|
25588
|
-
historyRaw =
|
|
26119
|
+
historyRaw = fs55.readFileSync(hPath, "utf-8");
|
|
25589
26120
|
} catch {
|
|
25590
26121
|
}
|
|
25591
26122
|
const cutoff = days !== null ? (() => {
|
|
@@ -25609,7 +26140,7 @@ function buildSessions(days, historyPath) {
|
|
|
25609
26140
|
const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
|
|
25610
26141
|
let sessionLines = [];
|
|
25611
26142
|
try {
|
|
25612
|
-
sessionLines =
|
|
26143
|
+
sessionLines = fs55.readFileSync(jsonlFile, "utf-8").split("\n");
|
|
25613
26144
|
} catch {
|
|
25614
26145
|
}
|
|
25615
26146
|
const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
|
|
@@ -25695,11 +26226,11 @@ function toolInputSummary(tool, input) {
|
|
|
25695
26226
|
}
|
|
25696
26227
|
function toolColor(tool) {
|
|
25697
26228
|
const t = tool.toLowerCase();
|
|
25698
|
-
if (t === "bash" || t === "execute_bash") return
|
|
25699
|
-
if (t === "write") return
|
|
25700
|
-
if (t === "edit" || t === "notebookedit") return
|
|
25701
|
-
if (t === "read") return
|
|
25702
|
-
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;
|
|
25703
26234
|
}
|
|
25704
26235
|
function barStr2(value, max, width) {
|
|
25705
26236
|
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
@@ -25709,7 +26240,7 @@ function barStr2(value, max, width) {
|
|
|
25709
26240
|
function colorBar2(value, max, width) {
|
|
25710
26241
|
const s = barStr2(value, max, width);
|
|
25711
26242
|
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
25712
|
-
return
|
|
26243
|
+
return chalk28.cyan(s.slice(0, filled)) + chalk28.dim(s.slice(filled));
|
|
25713
26244
|
}
|
|
25714
26245
|
function renderSummary(summaries) {
|
|
25715
26246
|
const totalTools = summaries.reduce((n, s) => n + s.toolCalls.length, 0);
|
|
@@ -25739,45 +26270,45 @@ function renderSummary(summaries) {
|
|
|
25739
26270
|
}
|
|
25740
26271
|
const topProjects = [...projCosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
25741
26272
|
const W = 20;
|
|
25742
|
-
console.log(
|
|
26273
|
+
console.log(chalk28.dim(" " + "\u2500".repeat(70)));
|
|
25743
26274
|
console.log(
|
|
25744
|
-
" " +
|
|
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") : "")
|
|
25745
26276
|
);
|
|
25746
26277
|
console.log(
|
|
25747
|
-
" " +
|
|
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`)
|
|
25748
26279
|
);
|
|
25749
26280
|
console.log("");
|
|
25750
|
-
console.log(" " +
|
|
26281
|
+
console.log(" " + chalk28.dim("Tool breakdown:"));
|
|
25751
26282
|
const maxGroup = Math.max(...Object.values(groups));
|
|
25752
26283
|
for (const [label2, count] of Object.entries(groups)) {
|
|
25753
26284
|
if (count === 0) continue;
|
|
25754
26285
|
const pct = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
|
|
25755
26286
|
console.log(
|
|
25756
|
-
" " + 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)}%)`)
|
|
25757
26288
|
);
|
|
25758
26289
|
}
|
|
25759
26290
|
console.log("");
|
|
25760
26291
|
if (topProjects.length > 1) {
|
|
25761
|
-
console.log(" " +
|
|
26292
|
+
console.log(" " + chalk28.dim("Cost by project:"));
|
|
25762
26293
|
const maxProjCost = topProjects[0][1];
|
|
25763
26294
|
for (const [proj, cost] of topProjects) {
|
|
25764
26295
|
console.log(
|
|
25765
|
-
" " + 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))
|
|
25766
26297
|
);
|
|
25767
26298
|
}
|
|
25768
26299
|
console.log("");
|
|
25769
26300
|
}
|
|
25770
|
-
console.log(
|
|
26301
|
+
console.log(chalk28.dim(" " + "\u2500".repeat(70)));
|
|
25771
26302
|
console.log("");
|
|
25772
26303
|
}
|
|
25773
26304
|
function renderList(summaries, totalCost) {
|
|
25774
26305
|
if (summaries.length === 0) {
|
|
25775
|
-
console.log(
|
|
26306
|
+
console.log(chalk28.yellow(" No sessions found in the requested range.\n"));
|
|
25776
26307
|
return;
|
|
25777
26308
|
}
|
|
25778
|
-
const totalLabel = totalCost > 0 ?
|
|
26309
|
+
const totalLabel = totalCost > 0 ? chalk28.dim(" ~" + fmtCost3(totalCost) + " total") : "";
|
|
25779
26310
|
console.log(
|
|
25780
|
-
" " +
|
|
26311
|
+
" " + chalk28.white(String(summaries.length)) + chalk28.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
|
|
25781
26312
|
);
|
|
25782
26313
|
console.log("");
|
|
25783
26314
|
let lastGroup = "";
|
|
@@ -25785,51 +26316,51 @@ function renderList(summaries, totalCost) {
|
|
|
25785
26316
|
const activeDate = fmtDate2(s.lastActiveTime);
|
|
25786
26317
|
const group = activeDate + " " + s.projectLabel;
|
|
25787
26318
|
if (group !== lastGroup) {
|
|
25788
|
-
console.log(
|
|
26319
|
+
console.log(chalk28.dim(" \u2500\u2500\u2500 ") + chalk28.bold(activeDate) + chalk28.dim(" " + s.projectLabel));
|
|
25789
26320
|
lastGroup = group;
|
|
25790
26321
|
}
|
|
25791
26322
|
const startDate = fmtDate2(s.startTime);
|
|
25792
|
-
const dateRange = startDate !== activeDate ?
|
|
25793
|
-
const timeStr =
|
|
25794
|
-
const prompt =
|
|
25795
|
-
const tools = s.toolCalls.length > 0 ?
|
|
25796
|
-
const cost = s.costUSD > 0 ?
|
|
25797
|
-
const blocked = s.blockedCalls.length > 0 ?
|
|
25798
|
-
const snap = s.hasSnapshot ?
|
|
25799
|
-
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")](
|
|
25800
26331
|
" " + agentBadgeText(s.agent ?? "claude", 0)
|
|
25801
26332
|
);
|
|
25802
|
-
const sid =
|
|
26333
|
+
const sid = chalk28.dim(" " + s.sessionId.slice(0, 8));
|
|
25803
26334
|
console.log(
|
|
25804
26335
|
` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}${dateRange}`
|
|
25805
26336
|
);
|
|
25806
26337
|
}
|
|
25807
26338
|
console.log("");
|
|
25808
26339
|
console.log(
|
|
25809
|
-
|
|
26340
|
+
chalk28.dim(" Run") + " " + chalk28.cyan("node9 sessions --detail <session-id>") + chalk28.dim(" for full tool trace.")
|
|
25810
26341
|
);
|
|
25811
26342
|
console.log("");
|
|
25812
26343
|
}
|
|
25813
26344
|
function renderDetail(s) {
|
|
25814
26345
|
console.log("");
|
|
25815
|
-
console.log(
|
|
26346
|
+
console.log(chalk28.bold(" Session ") + chalk28.dim(s.sessionId));
|
|
25816
26347
|
console.log(
|
|
25817
|
-
|
|
26348
|
+
chalk28.bold(" Prompt ") + chalk28.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
|
|
25818
26349
|
);
|
|
25819
|
-
console.log(
|
|
26350
|
+
console.log(chalk28.bold(" Project ") + chalk28.white(s.projectLabel));
|
|
25820
26351
|
if (s.agent) {
|
|
25821
|
-
const agentLabel2 =
|
|
25822
|
-
console.log(
|
|
26352
|
+
const agentLabel2 = chalk28[agentColorName(s.agent)](agentDisplayName(s.agent));
|
|
26353
|
+
console.log(chalk28.bold(" Agent ") + agentLabel2);
|
|
25823
26354
|
}
|
|
25824
|
-
console.log(
|
|
26355
|
+
console.log(chalk28.bold(" When ") + chalk28.white(fmtDateTime(s.startTime)));
|
|
25825
26356
|
if (s.costUSD > 0)
|
|
25826
|
-
console.log(
|
|
26357
|
+
console.log(chalk28.bold(" Cost ") + chalk28.yellow("~" + fmtCost3(s.costUSD)));
|
|
25827
26358
|
console.log(
|
|
25828
|
-
|
|
26359
|
+
chalk28.bold(" Snapshot ") + (s.hasSnapshot ? chalk28.green("\u2713 taken") : chalk28.dim("none"))
|
|
25829
26360
|
);
|
|
25830
26361
|
console.log("");
|
|
25831
26362
|
if (s.toolCalls.length === 0 && s.blockedCalls.length === 0) {
|
|
25832
|
-
console.log(
|
|
26363
|
+
console.log(chalk28.dim(" No tool calls recorded.\n"));
|
|
25833
26364
|
return;
|
|
25834
26365
|
}
|
|
25835
26366
|
const timeline = [
|
|
@@ -25842,32 +26373,32 @@ function renderDetail(s) {
|
|
|
25842
26373
|
});
|
|
25843
26374
|
const headerParts = [`Tool calls (${s.toolCalls.length})`];
|
|
25844
26375
|
if (s.blockedCalls.length > 0)
|
|
25845
|
-
headerParts.push(
|
|
25846
|
-
console.log(
|
|
26376
|
+
headerParts.push(chalk28.red(`${s.blockedCalls.length} blocked by node9`));
|
|
26377
|
+
console.log(chalk28.bold(" " + headerParts.join(" \xB7 ")));
|
|
25847
26378
|
console.log("");
|
|
25848
26379
|
for (const entry of timeline) {
|
|
25849
26380
|
if (entry.kind === "tool") {
|
|
25850
26381
|
const tc = entry.tc;
|
|
25851
26382
|
const colorFn = toolColor(tc.tool);
|
|
25852
26383
|
const toolPad = colorFn(tc.tool.padEnd(16));
|
|
25853
|
-
const detail =
|
|
25854
|
-
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) + " ") : " ";
|
|
25855
26386
|
console.log(` ${ts}${toolPad} ${detail}`);
|
|
25856
26387
|
} else {
|
|
25857
26388
|
const bc = entry.bc;
|
|
25858
|
-
const ts = bc.timestamp ?
|
|
25859
|
-
const label2 =
|
|
25860
|
-
const toolName =
|
|
25861
|
-
const argsSummary = bc.args ?
|
|
25862
|
-
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) : "";
|
|
25863
26394
|
console.log(` ${ts}${label2} ${toolName} ${argsSummary}${reason}`);
|
|
25864
26395
|
}
|
|
25865
26396
|
}
|
|
25866
26397
|
console.log("");
|
|
25867
26398
|
if (s.modifiedFiles.length > 0) {
|
|
25868
|
-
console.log(
|
|
26399
|
+
console.log(chalk28.bold(` Files modified (${s.modifiedFiles.length}):`));
|
|
25869
26400
|
for (const f of s.modifiedFiles) {
|
|
25870
|
-
console.log(" " +
|
|
26401
|
+
console.log(" " + chalk28.yellow(f));
|
|
25871
26402
|
}
|
|
25872
26403
|
console.log("");
|
|
25873
26404
|
}
|
|
@@ -25875,13 +26406,13 @@ function renderDetail(s) {
|
|
|
25875
26406
|
function registerSessionsCommand(program2) {
|
|
25876
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) => {
|
|
25877
26408
|
console.log("");
|
|
25878
|
-
console.log(
|
|
26409
|
+
console.log(chalk28.cyan.bold("\u{1F4CB} node9 sessions") + chalk28.dim(" \u2014 what your AI agent did"));
|
|
25879
26410
|
console.log("");
|
|
25880
26411
|
const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
|
|
25881
26412
|
const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
|
|
25882
|
-
console.log(
|
|
26413
|
+
console.log(chalk28.dim(" " + rangeLabel));
|
|
25883
26414
|
console.log("");
|
|
25884
|
-
process.stdout.write(
|
|
26415
|
+
process.stdout.write(chalk28.dim(" Loading\u2026"));
|
|
25885
26416
|
const summaries = buildSessions(days);
|
|
25886
26417
|
if (process.stdout.isTTY) {
|
|
25887
26418
|
process.stdout.clearLine(0);
|
|
@@ -25894,8 +26425,8 @@ function registerSessionsCommand(program2) {
|
|
|
25894
26425
|
(s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
|
|
25895
26426
|
);
|
|
25896
26427
|
if (!target) {
|
|
25897
|
-
console.log(
|
|
25898
|
-
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"));
|
|
25899
26430
|
return;
|
|
25900
26431
|
}
|
|
25901
26432
|
renderDetail(target);
|
|
@@ -25909,7 +26440,7 @@ function registerSessionsCommand(program2) {
|
|
|
25909
26440
|
|
|
25910
26441
|
// src/cli/commands/session-taint.ts
|
|
25911
26442
|
init_daemon();
|
|
25912
|
-
import
|
|
26443
|
+
import chalk29 from "chalk";
|
|
25913
26444
|
function resolveSessionId(records, query) {
|
|
25914
26445
|
const exact = records.find((r) => r.sessionId === query);
|
|
25915
26446
|
if (exact) return { record: exact };
|
|
@@ -25935,22 +26466,22 @@ function registerSessionTaintCommand(program2) {
|
|
|
25935
26466
|
const records = await listSessionTaints();
|
|
25936
26467
|
console.log("");
|
|
25937
26468
|
if (records.length === 0) {
|
|
25938
|
-
console.log(
|
|
25939
|
-
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");
|
|
25940
26471
|
return;
|
|
25941
26472
|
}
|
|
25942
26473
|
console.log(
|
|
25943
|
-
" " +
|
|
26474
|
+
" " + chalk29.bold(String(records.length)) + chalk29.dim(` tainted session${records.length !== 1 ? "s" : ""}`)
|
|
25944
26475
|
);
|
|
25945
26476
|
console.log("");
|
|
25946
26477
|
for (const r of records) {
|
|
25947
26478
|
console.log(
|
|
25948
|
-
" " +
|
|
26479
|
+
" " + chalk29.yellow(r.sessionId.slice(0, 8).padEnd(10)) + chalk29.red(r.source) + sourceGap(r.source) + chalk29.dim("clears in " + fmtRemaining(r.expiresAt))
|
|
25949
26480
|
);
|
|
25950
26481
|
}
|
|
25951
26482
|
console.log("");
|
|
25952
26483
|
console.log(
|
|
25953
|
-
|
|
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"
|
|
25954
26485
|
);
|
|
25955
26486
|
});
|
|
25956
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) => {
|
|
@@ -25958,32 +26489,32 @@ function registerSessionTaintCommand(program2) {
|
|
|
25958
26489
|
if (opts.all) {
|
|
25959
26490
|
const res2 = await clearSessionTaint({ all: true });
|
|
25960
26491
|
if (res2.daemonUnavailable) {
|
|
25961
|
-
console.log(
|
|
26492
|
+
console.log(chalk29.dim(" node9 daemon not running \u2014 no active taints to clear.") + "\n");
|
|
25962
26493
|
return;
|
|
25963
26494
|
}
|
|
25964
26495
|
console.log(
|
|
25965
|
-
|
|
26496
|
+
chalk29.green(" \u2713 ") + `Cleared ${chalk29.bold(String(res2.cleared))} session taint${res2.cleared !== 1 ? "s" : ""}.
|
|
25966
26497
|
`
|
|
25967
26498
|
);
|
|
25968
26499
|
return;
|
|
25969
26500
|
}
|
|
25970
26501
|
if (!sessionId) {
|
|
25971
|
-
console.log(
|
|
25972
|
-
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");
|
|
25973
26504
|
return;
|
|
25974
26505
|
}
|
|
25975
26506
|
const records = await listSessionTaints();
|
|
25976
26507
|
if (records.length === 0) {
|
|
25977
|
-
console.log(
|
|
26508
|
+
console.log(chalk29.dim(" No tainted sessions \u2014 nothing to clear.") + "\n");
|
|
25978
26509
|
return;
|
|
25979
26510
|
}
|
|
25980
26511
|
const resolved = resolveSessionId(records, sessionId);
|
|
25981
26512
|
if ("error" in resolved) {
|
|
25982
26513
|
if (resolved.error === "not-found") {
|
|
25983
|
-
console.log(
|
|
26514
|
+
console.log(chalk29.red(` No tainted session matches "${sessionId}".`));
|
|
25984
26515
|
} else {
|
|
25985
|
-
console.log(
|
|
25986
|
-
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));
|
|
25987
26518
|
}
|
|
25988
26519
|
console.log("");
|
|
25989
26520
|
return;
|
|
@@ -25991,24 +26522,24 @@ function registerSessionTaintCommand(program2) {
|
|
|
25991
26522
|
const res = await clearSessionTaint({ sessionId: resolved.record.sessionId });
|
|
25992
26523
|
if (res.cleared > 0) {
|
|
25993
26524
|
console.log(
|
|
25994
|
-
|
|
26525
|
+
chalk29.green(" \u2713 ") + `Cleared taint for ${chalk29.yellow(resolved.record.sessionId.slice(0, 8))} ` + chalk29.dim(`(was flagged by ${resolved.record.source}).`) + "\n"
|
|
25995
26526
|
);
|
|
25996
26527
|
} else {
|
|
25997
26528
|
console.log(
|
|
25998
|
-
|
|
26529
|
+
chalk29.dim(` Session ${resolved.record.sessionId.slice(0, 8)} was already clear.`) + "\n"
|
|
25999
26530
|
);
|
|
26000
26531
|
}
|
|
26001
26532
|
});
|
|
26002
26533
|
}
|
|
26003
26534
|
|
|
26004
26535
|
// src/cli/commands/skill-pin.ts
|
|
26005
|
-
import
|
|
26006
|
-
import
|
|
26007
|
-
import
|
|
26008
|
-
import
|
|
26536
|
+
import chalk30 from "chalk";
|
|
26537
|
+
import fs56 from "fs";
|
|
26538
|
+
import os48 from "os";
|
|
26539
|
+
import path54 from "path";
|
|
26009
26540
|
function wipeSkillSessions() {
|
|
26010
26541
|
try {
|
|
26011
|
-
|
|
26542
|
+
fs56.rmSync(path54.join(os48.homedir(), ".node9", "skill-sessions"), {
|
|
26012
26543
|
recursive: true,
|
|
26013
26544
|
force: true
|
|
26014
26545
|
});
|
|
@@ -26022,29 +26553,29 @@ function registerSkillPinCommand(program2) {
|
|
|
26022
26553
|
const result = readSkillPinsSafe();
|
|
26023
26554
|
if (!result.ok) {
|
|
26024
26555
|
if (result.reason === "missing") {
|
|
26025
|
-
console.log(
|
|
26556
|
+
console.log(chalk30.gray("\nNo skill roots are pinned yet."));
|
|
26026
26557
|
console.log(
|
|
26027
|
-
|
|
26558
|
+
chalk30.gray("Pins are created automatically on the first tool call of each session.\n")
|
|
26028
26559
|
);
|
|
26029
26560
|
return;
|
|
26030
26561
|
}
|
|
26031
|
-
console.error(
|
|
26562
|
+
console.error(chalk30.red(`
|
|
26032
26563
|
\u274C Pin file is corrupt: ${result.detail}`));
|
|
26033
|
-
console.error(
|
|
26564
|
+
console.error(chalk30.yellow(" Run: node9 skill pin reset\n"));
|
|
26034
26565
|
process.exit(1);
|
|
26035
26566
|
}
|
|
26036
26567
|
const entries = Object.entries(result.pins.roots);
|
|
26037
26568
|
if (entries.length === 0) {
|
|
26038
|
-
console.log(
|
|
26569
|
+
console.log(chalk30.gray("\nNo skill roots are pinned yet.\n"));
|
|
26039
26570
|
return;
|
|
26040
26571
|
}
|
|
26041
|
-
console.log(
|
|
26572
|
+
console.log(chalk30.bold("\n\u{1F512} Pinned Skill Roots\n"));
|
|
26042
26573
|
for (const [key, entry] of entries) {
|
|
26043
|
-
const missing = entry.exists ? "" :
|
|
26044
|
-
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}`);
|
|
26045
26576
|
console.log(` Files (${entry.fileCount})`);
|
|
26046
|
-
console.log(` Hash: ${
|
|
26047
|
-
console.log(` Pinned: ${
|
|
26577
|
+
console.log(` Hash: ${chalk30.gray(entry.contentHash.slice(0, 16))}...`);
|
|
26578
|
+
console.log(` Pinned: ${chalk30.gray(entry.pinnedAt)}
|
|
26048
26579
|
`);
|
|
26049
26580
|
}
|
|
26050
26581
|
});
|
|
@@ -26053,52 +26584,52 @@ function registerSkillPinCommand(program2) {
|
|
|
26053
26584
|
try {
|
|
26054
26585
|
pins = readSkillPins();
|
|
26055
26586
|
} catch {
|
|
26056
|
-
console.error(
|
|
26057
|
-
console.error(
|
|
26587
|
+
console.error(chalk30.red("\n\u274C Pin file is corrupt."));
|
|
26588
|
+
console.error(chalk30.yellow(" Run: node9 skill pin reset\n"));
|
|
26058
26589
|
process.exit(1);
|
|
26059
26590
|
}
|
|
26060
26591
|
if (!pins.roots[rootKey]) {
|
|
26061
|
-
console.error(
|
|
26592
|
+
console.error(chalk30.red(`
|
|
26062
26593
|
\u274C No pin found for root key "${rootKey}"
|
|
26063
26594
|
`));
|
|
26064
|
-
console.error(`Run ${
|
|
26595
|
+
console.error(`Run ${chalk30.cyan("node9 skill pin list")} to see pinned roots.
|
|
26065
26596
|
`);
|
|
26066
26597
|
process.exit(1);
|
|
26067
26598
|
}
|
|
26068
26599
|
const rootPath = pins.roots[rootKey].rootPath;
|
|
26069
26600
|
removePin2(rootKey);
|
|
26070
26601
|
wipeSkillSessions();
|
|
26071
|
-
console.log(
|
|
26072
|
-
\u{1F513} Pin removed for ${
|
|
26073
|
-
console.log(
|
|
26074
|
-
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"));
|
|
26075
26606
|
});
|
|
26076
26607
|
pinSubCmd.command("reset").description("Clear all skill pins and wipe session verification flags").action(() => {
|
|
26077
26608
|
const result = readSkillPinsSafe();
|
|
26078
26609
|
if (!result.ok && result.reason === "missing") {
|
|
26079
26610
|
wipeSkillSessions();
|
|
26080
|
-
console.log(
|
|
26611
|
+
console.log(chalk30.gray("\nNo pins to clear.\n"));
|
|
26081
26612
|
return;
|
|
26082
26613
|
}
|
|
26083
26614
|
const count = result.ok ? Object.keys(result.pins.roots).length : "?";
|
|
26084
26615
|
clearAllPins2();
|
|
26085
26616
|
wipeSkillSessions();
|
|
26086
|
-
console.log(
|
|
26617
|
+
console.log(chalk30.green(`
|
|
26087
26618
|
\u{1F513} Cleared ${count} skill pin(s).`));
|
|
26088
|
-
console.log(
|
|
26619
|
+
console.log(chalk30.gray(" Next session will re-pin with current state.\n"));
|
|
26089
26620
|
});
|
|
26090
26621
|
}
|
|
26091
26622
|
|
|
26092
26623
|
// src/cli/commands/decisions.ts
|
|
26093
|
-
import
|
|
26094
|
-
import
|
|
26095
|
-
import
|
|
26096
|
-
import
|
|
26097
|
-
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");
|
|
26098
26629
|
function readDecisions() {
|
|
26099
26630
|
try {
|
|
26100
|
-
if (!
|
|
26101
|
-
const raw =
|
|
26631
|
+
if (!fs57.existsSync(DECISIONS_FILE2)) return {};
|
|
26632
|
+
const raw = fs57.readFileSync(DECISIONS_FILE2, "utf-8");
|
|
26102
26633
|
const parsed = JSON.parse(raw);
|
|
26103
26634
|
const out = {};
|
|
26104
26635
|
for (const [k, v] of Object.entries(parsed)) {
|
|
@@ -26110,11 +26641,11 @@ function readDecisions() {
|
|
|
26110
26641
|
}
|
|
26111
26642
|
}
|
|
26112
26643
|
function writeDecisions(d) {
|
|
26113
|
-
const dir =
|
|
26114
|
-
if (!
|
|
26644
|
+
const dir = path55.dirname(DECISIONS_FILE2);
|
|
26645
|
+
if (!fs57.existsSync(dir)) fs57.mkdirSync(dir, { recursive: true });
|
|
26115
26646
|
const tmp = `${DECISIONS_FILE2}.${process.pid}.tmp`;
|
|
26116
|
-
|
|
26117
|
-
|
|
26647
|
+
fs57.writeFileSync(tmp, JSON.stringify(d, null, 2));
|
|
26648
|
+
fs57.renameSync(tmp, DECISIONS_FILE2);
|
|
26118
26649
|
}
|
|
26119
26650
|
function registerDecisionsCommand(program2) {
|
|
26120
26651
|
const cmd = program2.command("decisions").description('Manage persistent "Always Allow" / "Always Deny" tool decisions');
|
|
@@ -26122,67 +26653,67 @@ function registerDecisionsCommand(program2) {
|
|
|
26122
26653
|
const decisions = readDecisions();
|
|
26123
26654
|
const entries = Object.entries(decisions);
|
|
26124
26655
|
if (entries.length === 0) {
|
|
26125
|
-
console.log(
|
|
26656
|
+
console.log(chalk31.gray(" No persistent decisions stored."));
|
|
26126
26657
|
console.log(
|
|
26127
|
-
|
|
26128
|
-
`) +
|
|
26658
|
+
chalk31.gray(` File: ${DECISIONS_FILE2}
|
|
26659
|
+
`) + chalk31.gray(' Decisions are written when you click "Always Allow" or')
|
|
26129
26660
|
);
|
|
26130
|
-
console.log(
|
|
26661
|
+
console.log(chalk31.gray(' "Always Deny" in node9 tail or the native popup.'));
|
|
26131
26662
|
return;
|
|
26132
26663
|
}
|
|
26133
|
-
console.log(
|
|
26664
|
+
console.log(chalk31.bold(`
|
|
26134
26665
|
Persistent decisions (${entries.length})
|
|
26135
26666
|
`));
|
|
26136
26667
|
const w = Math.max(...entries.map(([k]) => k.length));
|
|
26137
26668
|
for (const [tool, verdict] of entries.sort()) {
|
|
26138
|
-
const colored = verdict === "allow" ?
|
|
26669
|
+
const colored = verdict === "allow" ? chalk31.green(verdict) : chalk31.red(verdict);
|
|
26139
26670
|
console.log(` ${tool.padEnd(w)} ${colored}`);
|
|
26140
26671
|
}
|
|
26141
26672
|
console.log(
|
|
26142
|
-
|
|
26673
|
+
chalk31.gray(`
|
|
26143
26674
|
Stored in ${DECISIONS_FILE2}
|
|
26144
|
-
`) +
|
|
26675
|
+
`) + chalk31.gray(" Run `node9 decisions clear <tool>` to remove an entry.")
|
|
26145
26676
|
);
|
|
26146
26677
|
});
|
|
26147
26678
|
cmd.command("clear <toolName>").description("Remove a persistent decision for one tool").action((toolName) => {
|
|
26148
26679
|
const decisions = readDecisions();
|
|
26149
26680
|
if (!(toolName in decisions)) {
|
|
26150
|
-
console.log(
|
|
26681
|
+
console.log(chalk31.yellow(` No persistent decision for "${toolName}". Nothing to clear.`));
|
|
26151
26682
|
process.exitCode = 1;
|
|
26152
26683
|
return;
|
|
26153
26684
|
}
|
|
26154
26685
|
delete decisions[toolName];
|
|
26155
26686
|
writeDecisions(decisions);
|
|
26156
|
-
console.log(
|
|
26687
|
+
console.log(chalk31.green(` \u2713 Cleared persistent decision for "${toolName}".`));
|
|
26157
26688
|
});
|
|
26158
26689
|
cmd.command("clear-all").description("Remove every persistent decision (irreversible)").action(() => {
|
|
26159
26690
|
const decisions = readDecisions();
|
|
26160
26691
|
const count = Object.keys(decisions).length;
|
|
26161
26692
|
if (count === 0) {
|
|
26162
|
-
console.log(
|
|
26693
|
+
console.log(chalk31.gray(" Nothing to clear \u2014 no persistent decisions stored."));
|
|
26163
26694
|
return;
|
|
26164
26695
|
}
|
|
26165
26696
|
writeDecisions({});
|
|
26166
26697
|
console.log(
|
|
26167
|
-
|
|
26698
|
+
chalk31.green(` \u2713 Cleared ${count} persistent decision${count === 1 ? "" : "s"}.`)
|
|
26168
26699
|
);
|
|
26169
26700
|
});
|
|
26170
26701
|
}
|
|
26171
26702
|
|
|
26172
26703
|
// src/cli/commands/dlp.ts
|
|
26173
|
-
import
|
|
26174
|
-
import
|
|
26175
|
-
import
|
|
26176
|
-
import
|
|
26177
|
-
var AUDIT_LOG =
|
|
26178
|
-
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");
|
|
26179
26710
|
var ANSI_RE = /\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g;
|
|
26180
26711
|
function stripAnsi(s) {
|
|
26181
26712
|
return s.replace(ANSI_RE, "");
|
|
26182
26713
|
}
|
|
26183
26714
|
function loadResolved() {
|
|
26184
26715
|
try {
|
|
26185
|
-
const raw = JSON.parse(
|
|
26716
|
+
const raw = JSON.parse(fs58.readFileSync(RESOLVED_FILE, "utf-8"));
|
|
26186
26717
|
return new Set(raw);
|
|
26187
26718
|
} catch {
|
|
26188
26719
|
return /* @__PURE__ */ new Set();
|
|
@@ -26190,13 +26721,13 @@ function loadResolved() {
|
|
|
26190
26721
|
}
|
|
26191
26722
|
function saveResolved(resolved) {
|
|
26192
26723
|
try {
|
|
26193
|
-
|
|
26724
|
+
fs58.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
|
|
26194
26725
|
} catch {
|
|
26195
26726
|
}
|
|
26196
26727
|
}
|
|
26197
26728
|
function loadDlpFindings() {
|
|
26198
|
-
if (!
|
|
26199
|
-
return
|
|
26729
|
+
if (!fs58.existsSync(AUDIT_LOG)) return [];
|
|
26730
|
+
return fs58.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
|
|
26200
26731
|
if (!line.trim()) return [];
|
|
26201
26732
|
try {
|
|
26202
26733
|
const e = JSON.parse(line);
|
|
@@ -26225,14 +26756,14 @@ function registerDlpCommand(program2) {
|
|
|
26225
26756
|
cmd.command("resolve").description("Mark all current DLP findings as resolved").action(() => {
|
|
26226
26757
|
const findings = loadDlpFindings();
|
|
26227
26758
|
if (findings.length === 0) {
|
|
26228
|
-
console.log(
|
|
26759
|
+
console.log(chalk32.green("\n \u2705 No response-DLP findings to resolve.\n"));
|
|
26229
26760
|
return;
|
|
26230
26761
|
}
|
|
26231
26762
|
const resolved = loadResolved();
|
|
26232
26763
|
for (const e of findings) resolved.add(entryKey2(e));
|
|
26233
26764
|
saveResolved(resolved);
|
|
26234
26765
|
console.log(
|
|
26235
|
-
|
|
26766
|
+
chalk32.green(
|
|
26236
26767
|
`
|
|
26237
26768
|
\u2705 ${findings.length} finding${findings.length !== 1 ? "s" : ""} marked as resolved.
|
|
26238
26769
|
`
|
|
@@ -26246,47 +26777,47 @@ function registerDlpCommand(program2) {
|
|
|
26246
26777
|
const resolvedCount = findings.length - open.length;
|
|
26247
26778
|
console.log("");
|
|
26248
26779
|
console.log(
|
|
26249
|
-
|
|
26780
|
+
chalk32.bold.cyan("\u{1F510} node9 dlp") + chalk32.dim(" \u2014 secrets found in Claude response text")
|
|
26250
26781
|
);
|
|
26251
26782
|
console.log("");
|
|
26252
26783
|
if (open.length === 0) {
|
|
26253
26784
|
if (resolvedCount > 0) {
|
|
26254
|
-
console.log(
|
|
26785
|
+
console.log(chalk32.green(` \u2705 No open findings \xB7 ${resolvedCount} previously resolved`));
|
|
26255
26786
|
} else {
|
|
26256
26787
|
console.log(
|
|
26257
|
-
|
|
26788
|
+
chalk32.green(" \u2705 No findings \u2014 Claude has not leaked secrets in response text")
|
|
26258
26789
|
);
|
|
26259
26790
|
}
|
|
26260
26791
|
console.log("");
|
|
26261
26792
|
return;
|
|
26262
26793
|
}
|
|
26263
26794
|
console.log(
|
|
26264
|
-
|
|
26795
|
+
chalk32.bgRed.white.bold(` \u26A0\uFE0F ${open.length} open finding${open.length !== 1 ? "s" : ""} `) + chalk32.dim(resolvedCount > 0 ? ` (${resolvedCount} resolved)` : "")
|
|
26265
26796
|
);
|
|
26266
26797
|
console.log("");
|
|
26267
26798
|
console.log(
|
|
26268
|
-
|
|
26799
|
+
chalk32.dim(" These secrets were included in Claude's response text \u2014 NOT blocked.")
|
|
26269
26800
|
);
|
|
26270
|
-
console.log(
|
|
26801
|
+
console.log(chalk32.dim(" Rotate each affected key immediately.\n"));
|
|
26271
26802
|
for (const e of open) {
|
|
26272
26803
|
console.log(
|
|
26273
|
-
" " +
|
|
26804
|
+
" " + chalk32.red("\u25CF") + " " + chalk32.white(e.dlpPattern ?? "Secret") + chalk32.dim(" " + fmtDate3(e.ts))
|
|
26274
26805
|
);
|
|
26275
26806
|
if (e.dlpSample) {
|
|
26276
|
-
console.log(" " +
|
|
26807
|
+
console.log(" " + chalk32.dim("Sample: ") + chalk32.yellow(stripAnsi(e.dlpSample)));
|
|
26277
26808
|
}
|
|
26278
26809
|
if (e.project) {
|
|
26279
|
-
console.log(" " +
|
|
26810
|
+
console.log(" " + chalk32.dim("Project: ") + chalk32.dim(stripAnsi(e.project)));
|
|
26280
26811
|
}
|
|
26281
26812
|
console.log("");
|
|
26282
26813
|
}
|
|
26283
|
-
console.log(" " +
|
|
26284
|
-
console.log(" " +
|
|
26814
|
+
console.log(" " + chalk32.bold("Next steps:"));
|
|
26815
|
+
console.log(" " + chalk32.cyan("1.") + " Rotate any exposed keys shown above");
|
|
26285
26816
|
console.log(
|
|
26286
|
-
" " +
|
|
26817
|
+
" " + chalk32.cyan("2.") + " Run " + chalk32.white("node9 dlp resolve") + " to acknowledge"
|
|
26287
26818
|
);
|
|
26288
26819
|
console.log(
|
|
26289
|
-
" " +
|
|
26820
|
+
" " + chalk32.cyan("3.") + " Run " + chalk32.white("node9 report") + " for full audit history"
|
|
26290
26821
|
);
|
|
26291
26822
|
console.log("");
|
|
26292
26823
|
});
|
|
@@ -26294,15 +26825,15 @@ function registerDlpCommand(program2) {
|
|
|
26294
26825
|
|
|
26295
26826
|
// src/cli/commands/mask.ts
|
|
26296
26827
|
init_dlp();
|
|
26297
|
-
import
|
|
26298
|
-
import
|
|
26299
|
-
import
|
|
26300
|
-
import
|
|
26828
|
+
import chalk33 from "chalk";
|
|
26829
|
+
import fs59 from "fs";
|
|
26830
|
+
import path57 from "path";
|
|
26831
|
+
import os51 from "os";
|
|
26301
26832
|
function findJsonlFiles(dir) {
|
|
26302
26833
|
const results = [];
|
|
26303
|
-
if (!
|
|
26304
|
-
for (const entry of
|
|
26305
|
-
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);
|
|
26306
26837
|
if (entry.isDirectory()) results.push(...findJsonlFiles(full));
|
|
26307
26838
|
else if (entry.isFile() && entry.name.endsWith(".jsonl")) results.push(full);
|
|
26308
26839
|
}
|
|
@@ -26345,7 +26876,7 @@ function redactJson(obj) {
|
|
|
26345
26876
|
function processFile(filePath, dryRun) {
|
|
26346
26877
|
let raw;
|
|
26347
26878
|
try {
|
|
26348
|
-
raw =
|
|
26879
|
+
raw = fs59.readFileSync(filePath, "utf-8");
|
|
26349
26880
|
} catch {
|
|
26350
26881
|
return { redactedLines: 0, patterns: [] };
|
|
26351
26882
|
}
|
|
@@ -26377,14 +26908,14 @@ function processFile(filePath, dryRun) {
|
|
|
26377
26908
|
}
|
|
26378
26909
|
}
|
|
26379
26910
|
if (!dryRun && redactedLines > 0) {
|
|
26380
|
-
|
|
26911
|
+
fs59.writeFileSync(filePath, newLines.join("\n"), "utf-8");
|
|
26381
26912
|
}
|
|
26382
26913
|
return { redactedLines, patterns };
|
|
26383
26914
|
}
|
|
26384
26915
|
function processJsonFile(filePath, dryRun) {
|
|
26385
26916
|
let raw;
|
|
26386
26917
|
try {
|
|
26387
|
-
raw =
|
|
26918
|
+
raw = fs59.readFileSync(filePath, "utf-8");
|
|
26388
26919
|
} catch {
|
|
26389
26920
|
return { redactedLines: 0, patterns: [] };
|
|
26390
26921
|
}
|
|
@@ -26397,15 +26928,15 @@ function processJsonFile(filePath, dryRun) {
|
|
|
26397
26928
|
const { value, modified, found } = redactJson(parsed);
|
|
26398
26929
|
if (!modified) return { redactedLines: 0, patterns: [] };
|
|
26399
26930
|
if (!dryRun) {
|
|
26400
|
-
|
|
26931
|
+
fs59.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
|
|
26401
26932
|
}
|
|
26402
26933
|
return { redactedLines: 1, patterns: found };
|
|
26403
26934
|
}
|
|
26404
26935
|
function findJsonFiles(dir) {
|
|
26405
26936
|
const results = [];
|
|
26406
|
-
if (!
|
|
26407
|
-
for (const entry of
|
|
26408
|
-
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);
|
|
26409
26940
|
if (entry.isDirectory()) results.push(...findJsonFiles(full));
|
|
26410
26941
|
else if (entry.isFile() && entry.name.endsWith(".json")) results.push(full);
|
|
26411
26942
|
}
|
|
@@ -26414,9 +26945,9 @@ function findJsonFiles(dir) {
|
|
|
26414
26945
|
function registerMaskCommand(program2) {
|
|
26415
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) => {
|
|
26416
26947
|
const dryRun = !!options.dryRun;
|
|
26417
|
-
const home =
|
|
26418
|
-
const claudeDir =
|
|
26419
|
-
const geminiDir =
|
|
26948
|
+
const home = os51.homedir();
|
|
26949
|
+
const claudeDir = path57.join(home, ".claude", "projects");
|
|
26950
|
+
const geminiDir = path57.join(home, ".gemini", "tmp");
|
|
26420
26951
|
const allFiles = [
|
|
26421
26952
|
...findJsonlFiles(claudeDir).map((p) => ({ path: p, type: "jsonl" })),
|
|
26422
26953
|
...findJsonFiles(geminiDir).map((p) => ({ path: p, type: "json" }))
|
|
@@ -26424,18 +26955,18 @@ function registerMaskCommand(program2) {
|
|
|
26424
26955
|
const cutoff = options.all ? null : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
|
|
26425
26956
|
const filtered = cutoff ? allFiles.filter((f) => {
|
|
26426
26957
|
try {
|
|
26427
|
-
return
|
|
26958
|
+
return fs59.statSync(f.path).mtime >= cutoff;
|
|
26428
26959
|
} catch {
|
|
26429
26960
|
return false;
|
|
26430
26961
|
}
|
|
26431
26962
|
}) : allFiles;
|
|
26432
26963
|
if (filtered.length === 0) {
|
|
26433
|
-
console.log(
|
|
26964
|
+
console.log(chalk33.yellow(" No session files found."));
|
|
26434
26965
|
return;
|
|
26435
26966
|
}
|
|
26436
26967
|
console.log("");
|
|
26437
26968
|
if (dryRun) {
|
|
26438
|
-
console.log(
|
|
26969
|
+
console.log(chalk33.dim(" Dry run \u2014 no files will be modified.\n"));
|
|
26439
26970
|
}
|
|
26440
26971
|
let totalFiles = 0;
|
|
26441
26972
|
let totalLines = 0;
|
|
@@ -26451,23 +26982,23 @@ function registerMaskCommand(program2) {
|
|
|
26451
26982
|
});
|
|
26452
26983
|
const verb = dryRun ? "Would redact" : "Redacted";
|
|
26453
26984
|
console.log(
|
|
26454
|
-
" " +
|
|
26985
|
+
" " + chalk33.dim(shortPath.slice(0, 60).padEnd(62)) + chalk33.red(`${verb}: `) + chalk33.yellow(patterns.join(", ")) + chalk33.dim(` (${redactedLines} line${redactedLines !== 1 ? "s" : ""})`)
|
|
26455
26986
|
);
|
|
26456
26987
|
}
|
|
26457
26988
|
}
|
|
26458
26989
|
console.log("");
|
|
26459
26990
|
if (totalFiles === 0) {
|
|
26460
|
-
console.log(
|
|
26991
|
+
console.log(chalk33.green(" No secrets found in session history."));
|
|
26461
26992
|
} else {
|
|
26462
26993
|
const verb = dryRun ? "would be modified" : "modified";
|
|
26463
26994
|
console.log(
|
|
26464
|
-
|
|
26995
|
+
chalk33.bold(` ${totalFiles} file${totalFiles !== 1 ? "s" : ""} ${verb}`) + chalk33.dim(`, ${totalLines} line${totalLines !== 1 ? "s" : ""} redacted`)
|
|
26465
26996
|
);
|
|
26466
|
-
console.log(" Patterns: " +
|
|
26997
|
+
console.log(" Patterns: " + chalk33.yellow(totalPatterns.join(", ")));
|
|
26467
26998
|
if (!dryRun) {
|
|
26468
26999
|
console.log("");
|
|
26469
27000
|
console.log(
|
|
26470
|
-
|
|
27001
|
+
chalk33.dim(
|
|
26471
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."
|
|
26472
27003
|
)
|
|
26473
27004
|
);
|
|
@@ -26480,20 +27011,20 @@ function registerMaskCommand(program2) {
|
|
|
26480
27011
|
// src/cli.ts
|
|
26481
27012
|
init_blast();
|
|
26482
27013
|
var { version } = JSON.parse(
|
|
26483
|
-
|
|
27014
|
+
fs62.readFileSync(path60.join(__dirname, "../package.json"), "utf-8")
|
|
26484
27015
|
);
|
|
26485
27016
|
var program = new Command();
|
|
26486
27017
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
26487
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) => {
|
|
26488
27019
|
const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
|
|
26489
|
-
const credPath =
|
|
26490
|
-
if (!
|
|
26491
|
-
|
|
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 });
|
|
26492
27023
|
const profileName = options.profile || "default";
|
|
26493
27024
|
let existingCreds = {};
|
|
26494
27025
|
try {
|
|
26495
|
-
if (
|
|
26496
|
-
const raw = JSON.parse(
|
|
27026
|
+
if (fs62.existsSync(credPath)) {
|
|
27027
|
+
const raw = JSON.parse(fs62.readFileSync(credPath, "utf-8"));
|
|
26497
27028
|
if (raw.apiKey) {
|
|
26498
27029
|
existingCreds = {
|
|
26499
27030
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
|
|
@@ -26505,14 +27036,14 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
26505
27036
|
} catch {
|
|
26506
27037
|
}
|
|
26507
27038
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
|
|
26508
|
-
|
|
27039
|
+
fs62.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
26509
27040
|
let effectiveCloud = null;
|
|
26510
27041
|
if (profileName === "default") {
|
|
26511
|
-
const configPath2 =
|
|
27042
|
+
const configPath2 = path60.join(os54.homedir(), ".node9", "config.json");
|
|
26512
27043
|
let config = {};
|
|
26513
27044
|
try {
|
|
26514
|
-
if (
|
|
26515
|
-
config = JSON.parse(
|
|
27045
|
+
if (fs62.existsSync(configPath2))
|
|
27046
|
+
config = JSON.parse(fs62.readFileSync(configPath2, "utf-8"));
|
|
26516
27047
|
} catch {
|
|
26517
27048
|
}
|
|
26518
27049
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -26527,35 +27058,35 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
26527
27058
|
approvers.cloud = false;
|
|
26528
27059
|
}
|
|
26529
27060
|
s.approvers = approvers;
|
|
26530
|
-
if (!
|
|
26531
|
-
|
|
26532
|
-
|
|
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 });
|
|
26533
27064
|
effectiveCloud = approvers.cloud === true;
|
|
26534
27065
|
}
|
|
26535
27066
|
if (options.profile && profileName !== "default") {
|
|
26536
|
-
console.log(
|
|
26537
|
-
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`));
|
|
26538
27069
|
} else if (options.local || effectiveCloud === false) {
|
|
26539
|
-
console.log(
|
|
26540
|
-
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.`));
|
|
26541
27072
|
if (!options.local) {
|
|
26542
27073
|
console.log(
|
|
26543
|
-
|
|
27074
|
+
chalk35.yellow(` Your config has cloud approvals OFF (settings.approvers.cloud).`)
|
|
26544
27075
|
);
|
|
26545
27076
|
console.log(
|
|
26546
|
-
|
|
27077
|
+
chalk35.gray(` To enable team policy + dashboard sync: set it to true, or re-init.`)
|
|
26547
27078
|
);
|
|
26548
27079
|
}
|
|
26549
27080
|
} else {
|
|
26550
|
-
console.log(
|
|
26551
|
-
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.`));
|
|
26552
27083
|
}
|
|
26553
27084
|
});
|
|
26554
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) => {
|
|
26555
27086
|
const route = options.login ? "auth/login" : "auth/signup";
|
|
26556
27087
|
const url = `https://node9.ai/${route}?ref=cli_cmd`;
|
|
26557
27088
|
console.log("");
|
|
26558
|
-
console.log(" " +
|
|
27089
|
+
console.log(" " + chalk35.dim("Opening ") + chalk35.cyan.underline(url));
|
|
26559
27090
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
26560
27091
|
try {
|
|
26561
27092
|
const child = spawn9(opener, [url], {
|
|
@@ -26588,7 +27119,7 @@ program.command("addto", { hidden: true }).description("Integrate Node9 with an
|
|
|
26588
27119
|
if (target === "hermes") return setupHermes();
|
|
26589
27120
|
if (target === "hud") return setupHud();
|
|
26590
27121
|
console.error(
|
|
26591
|
-
|
|
27122
|
+
chalk35.red(
|
|
26592
27123
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
26593
27124
|
)
|
|
26594
27125
|
);
|
|
@@ -26602,20 +27133,20 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
|
|
|
26602
27133
|
"The agent to protect: claude | antigravity | copilot | gemini | cursor | codex | windsurf | vscode | hud"
|
|
26603
27134
|
).action(async (target) => {
|
|
26604
27135
|
if (!target) {
|
|
26605
|
-
console.log(
|
|
26606
|
-
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");
|
|
26607
27138
|
console.log(" Targets:");
|
|
26608
|
-
console.log(" " +
|
|
26609
|
-
console.log(" " +
|
|
26610
|
-
console.log(" " +
|
|
26611
|
-
console.log(" " +
|
|
26612
|
-
console.log(" " +
|
|
26613
|
-
console.log(" " +
|
|
26614
|
-
console.log(" " +
|
|
26615
|
-
console.log(" " +
|
|
26616
|
-
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)");
|
|
26617
27148
|
process.stdout.write(
|
|
26618
|
-
" " +
|
|
27149
|
+
" " + chalk35.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
26619
27150
|
);
|
|
26620
27151
|
console.log("");
|
|
26621
27152
|
return;
|
|
@@ -26632,7 +27163,7 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
|
|
|
26632
27163
|
if (t === "hermes") return setupHermes();
|
|
26633
27164
|
if (t === "hud") return setupHud();
|
|
26634
27165
|
console.error(
|
|
26635
|
-
|
|
27166
|
+
chalk35.red(
|
|
26636
27167
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
26637
27168
|
)
|
|
26638
27169
|
);
|
|
@@ -26658,33 +27189,33 @@ program.command("removefrom", { hidden: true }).description("Remove Node9 hooks
|
|
|
26658
27189
|
else if (target === "hud") fn = teardownHud;
|
|
26659
27190
|
else {
|
|
26660
27191
|
console.error(
|
|
26661
|
-
|
|
27192
|
+
chalk35.red(
|
|
26662
27193
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
26663
27194
|
)
|
|
26664
27195
|
);
|
|
26665
27196
|
process.exit(1);
|
|
26666
27197
|
}
|
|
26667
|
-
console.log(
|
|
27198
|
+
console.log(chalk35.cyan(`
|
|
26668
27199
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
26669
27200
|
`));
|
|
26670
27201
|
try {
|
|
26671
27202
|
fn();
|
|
26672
27203
|
} catch (err2) {
|
|
26673
|
-
console.error(
|
|
27204
|
+
console.error(chalk35.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
26674
27205
|
process.exit(1);
|
|
26675
27206
|
}
|
|
26676
|
-
console.log(
|
|
27207
|
+
console.log(chalk35.gray("\n Restart the agent for changes to take effect."));
|
|
26677
27208
|
});
|
|
26678
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) => {
|
|
26679
|
-
console.log(
|
|
26680
|
-
console.log(
|
|
27210
|
+
console.log(chalk35.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
27211
|
+
console.log(chalk35.bold("Stopping daemon..."));
|
|
26681
27212
|
try {
|
|
26682
27213
|
stopDaemon();
|
|
26683
|
-
console.log(
|
|
27214
|
+
console.log(chalk35.green(" \u2705 Daemon stopped"));
|
|
26684
27215
|
} catch {
|
|
26685
|
-
console.log(
|
|
27216
|
+
console.log(chalk35.blue(" \u2139\uFE0F Daemon was not running"));
|
|
26686
27217
|
}
|
|
26687
|
-
console.log(
|
|
27218
|
+
console.log(chalk35.bold("\nRemoving hooks..."));
|
|
26688
27219
|
let teardownFailed = false;
|
|
26689
27220
|
for (const [label2, fn] of [
|
|
26690
27221
|
["Claude", teardownClaude],
|
|
@@ -26700,45 +27231,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
26700
27231
|
} catch (err2) {
|
|
26701
27232
|
teardownFailed = true;
|
|
26702
27233
|
console.error(
|
|
26703
|
-
|
|
27234
|
+
chalk35.red(
|
|
26704
27235
|
` \u26A0\uFE0F Failed to remove ${label2} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
26705
27236
|
)
|
|
26706
27237
|
);
|
|
26707
27238
|
}
|
|
26708
27239
|
}
|
|
26709
27240
|
if (options.purge) {
|
|
26710
|
-
const node9Dir =
|
|
26711
|
-
if (
|
|
27241
|
+
const node9Dir = path60.join(os54.homedir(), ".node9");
|
|
27242
|
+
if (fs62.existsSync(node9Dir)) {
|
|
26712
27243
|
const confirmed = await confirm2({
|
|
26713
27244
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
26714
27245
|
default: false
|
|
26715
27246
|
});
|
|
26716
27247
|
if (confirmed) {
|
|
26717
|
-
|
|
26718
|
-
if (
|
|
27248
|
+
fs62.rmSync(node9Dir, { recursive: true });
|
|
27249
|
+
if (fs62.existsSync(node9Dir)) {
|
|
26719
27250
|
console.error(
|
|
26720
|
-
|
|
27251
|
+
chalk35.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
26721
27252
|
);
|
|
26722
27253
|
} else {
|
|
26723
|
-
console.log(
|
|
27254
|
+
console.log(chalk35.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
26724
27255
|
}
|
|
26725
27256
|
} else {
|
|
26726
|
-
console.log(
|
|
27257
|
+
console.log(chalk35.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
26727
27258
|
}
|
|
26728
27259
|
} else {
|
|
26729
|
-
console.log(
|
|
27260
|
+
console.log(chalk35.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
26730
27261
|
}
|
|
26731
27262
|
} else {
|
|
26732
27263
|
console.log(
|
|
26733
|
-
|
|
27264
|
+
chalk35.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
26734
27265
|
);
|
|
26735
27266
|
}
|
|
26736
27267
|
if (teardownFailed) {
|
|
26737
|
-
console.error(
|
|
27268
|
+
console.error(chalk35.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
26738
27269
|
process.exit(1);
|
|
26739
27270
|
}
|
|
26740
|
-
console.log(
|
|
26741
|
-
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"));
|
|
26742
27273
|
});
|
|
26743
27274
|
registerDoctorCommand(program, version);
|
|
26744
27275
|
program.command("explain").description(
|
|
@@ -26751,7 +27282,7 @@ program.command("explain").description(
|
|
|
26751
27282
|
try {
|
|
26752
27283
|
args = JSON.parse(trimmed);
|
|
26753
27284
|
} catch {
|
|
26754
|
-
console.error(
|
|
27285
|
+
console.error(chalk35.red(`
|
|
26755
27286
|
\u274C Invalid JSON: ${trimmed}
|
|
26756
27287
|
`));
|
|
26757
27288
|
process.exit(1);
|
|
@@ -26762,54 +27293,54 @@ program.command("explain").description(
|
|
|
26762
27293
|
}
|
|
26763
27294
|
const result = await explainPolicy(tool, args);
|
|
26764
27295
|
console.log("");
|
|
26765
|
-
console.log(
|
|
27296
|
+
console.log(chalk35.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
26766
27297
|
console.log("");
|
|
26767
|
-
console.log(` ${
|
|
27298
|
+
console.log(` ${chalk35.bold("Tool:")} ${chalk35.white(result.tool)}`);
|
|
26768
27299
|
if (argsRaw) {
|
|
26769
27300
|
const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
26770
|
-
console.log(` ${
|
|
27301
|
+
console.log(` ${chalk35.bold("Input:")} ${chalk35.gray(preview2)}`);
|
|
26771
27302
|
}
|
|
26772
27303
|
console.log("");
|
|
26773
|
-
console.log(
|
|
27304
|
+
console.log(chalk35.bold("Config Sources (Waterfall):"));
|
|
26774
27305
|
for (const tier of result.waterfall) {
|
|
26775
|
-
const num3 =
|
|
27306
|
+
const num3 = chalk35.gray(` ${tier.tier}.`);
|
|
26776
27307
|
const label2 = tier.label.padEnd(16);
|
|
26777
27308
|
let statusStr;
|
|
26778
27309
|
if (tier.tier === 1) {
|
|
26779
|
-
statusStr =
|
|
27310
|
+
statusStr = chalk35.gray(tier.note ?? "");
|
|
26780
27311
|
} else if (tier.status === "active") {
|
|
26781
|
-
const loc = tier.path ?
|
|
26782
|
-
const note = tier.note ?
|
|
26783
|
-
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 : "");
|
|
26784
27315
|
} else {
|
|
26785
|
-
statusStr =
|
|
27316
|
+
statusStr = chalk35.gray("\u25CB " + (tier.note ?? "not found"));
|
|
26786
27317
|
}
|
|
26787
|
-
console.log(`${num3} ${
|
|
27318
|
+
console.log(`${num3} ${chalk35.white(label2)} ${statusStr}`);
|
|
26788
27319
|
}
|
|
26789
27320
|
console.log("");
|
|
26790
|
-
console.log(
|
|
27321
|
+
console.log(chalk35.bold("Policy Evaluation:"));
|
|
26791
27322
|
for (const step of result.steps) {
|
|
26792
27323
|
const isFinal = step.isFinal;
|
|
26793
27324
|
let icon;
|
|
26794
|
-
if (step.outcome === "allow") icon =
|
|
26795
|
-
else if (step.outcome === "review") icon =
|
|
26796
|
-
else if (step.outcome === "skip") icon =
|
|
26797
|
-
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 ");
|
|
26798
27329
|
const name = step.name.padEnd(18);
|
|
26799
|
-
const nameStr = isFinal ?
|
|
26800
|
-
const detail = isFinal ?
|
|
26801
|
-
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") : "";
|
|
26802
27333
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
26803
27334
|
}
|
|
26804
27335
|
console.log("");
|
|
26805
27336
|
if (result.decision === "allow") {
|
|
26806
|
-
console.log(
|
|
27337
|
+
console.log(chalk35.green.bold(" Decision: \u2705 ALLOW") + chalk35.gray(" \u2014 no approval needed"));
|
|
26807
27338
|
} else {
|
|
26808
27339
|
console.log(
|
|
26809
|
-
|
|
27340
|
+
chalk35.red.bold(" Decision: \u{1F534} REVIEW") + chalk35.gray(" \u2014 human approval required")
|
|
26810
27341
|
);
|
|
26811
27342
|
if (result.blockedByLabel) {
|
|
26812
|
-
console.log(
|
|
27343
|
+
console.log(chalk35.gray(` Reason: ${result.blockedByLabel}`));
|
|
26813
27344
|
}
|
|
26814
27345
|
}
|
|
26815
27346
|
console.log("");
|
|
@@ -26824,18 +27355,18 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
26824
27355
|
try {
|
|
26825
27356
|
await startTail2(options);
|
|
26826
27357
|
} catch (err2) {
|
|
26827
|
-
console.error(
|
|
27358
|
+
console.error(chalk35.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
26828
27359
|
process.exit(1);
|
|
26829
27360
|
}
|
|
26830
27361
|
});
|
|
26831
27362
|
program.command("monitor").description("Live interactive dashboard \u2014 activity feed, approvals, security signals").action(async () => {
|
|
26832
27363
|
try {
|
|
26833
|
-
const dashboardPath =
|
|
27364
|
+
const dashboardPath = path60.join(__dirname, "dashboard.mjs");
|
|
26834
27365
|
const dynamicImport = new Function("id", "return import(id)");
|
|
26835
27366
|
const mod = await dynamicImport(`file://${dashboardPath}`);
|
|
26836
27367
|
await mod.startMonitor();
|
|
26837
27368
|
} catch (err2) {
|
|
26838
|
-
console.error(
|
|
27369
|
+
console.error(chalk35.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
26839
27370
|
process.exit(1);
|
|
26840
27371
|
}
|
|
26841
27372
|
});
|
|
@@ -26868,14 +27399,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
|
26868
27399
|
Run "node9 addto claude" to register it as the statusLine.`
|
|
26869
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) => {
|
|
26870
27401
|
if (subcommand === "debug") {
|
|
26871
|
-
const flagFile =
|
|
27402
|
+
const flagFile = path60.join(os54.homedir(), ".node9", "hud-debug");
|
|
26872
27403
|
if (state === "on") {
|
|
26873
|
-
|
|
26874
|
-
|
|
27404
|
+
fs62.mkdirSync(path60.dirname(flagFile), { recursive: true });
|
|
27405
|
+
fs62.writeFileSync(flagFile, "");
|
|
26875
27406
|
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
26876
27407
|
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
26877
27408
|
} else if (state === "off") {
|
|
26878
|
-
if (
|
|
27409
|
+
if (fs62.existsSync(flagFile)) fs62.unlinkSync(flagFile);
|
|
26879
27410
|
console.log("HUD debug logging disabled.");
|
|
26880
27411
|
} else {
|
|
26881
27412
|
console.error("Usage: node9 hud debug on|off");
|
|
@@ -26890,7 +27421,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
26890
27421
|
const ms = parseDuration(options.duration);
|
|
26891
27422
|
if (ms === null) {
|
|
26892
27423
|
console.error(
|
|
26893
|
-
|
|
27424
|
+
chalk35.red(`
|
|
26894
27425
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
26895
27426
|
`)
|
|
26896
27427
|
);
|
|
@@ -26898,20 +27429,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
26898
27429
|
}
|
|
26899
27430
|
pauseNode9(ms, options.duration);
|
|
26900
27431
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
26901
|
-
console.log(
|
|
27432
|
+
console.log(chalk35.yellow(`
|
|
26902
27433
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
26903
|
-
console.log(
|
|
26904
|
-
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.
|
|
26905
27436
|
`));
|
|
26906
27437
|
});
|
|
26907
27438
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
26908
27439
|
const { paused } = checkPause();
|
|
26909
27440
|
if (!paused) {
|
|
26910
|
-
console.log(
|
|
27441
|
+
console.log(chalk35.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
26911
27442
|
return;
|
|
26912
27443
|
}
|
|
26913
27444
|
resumeNode9();
|
|
26914
|
-
console.log(
|
|
27445
|
+
console.log(chalk35.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
26915
27446
|
});
|
|
26916
27447
|
var HOOK_BASED_AGENTS = {
|
|
26917
27448
|
claude: "claude",
|
|
@@ -26927,15 +27458,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
26927
27458
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
26928
27459
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
26929
27460
|
console.error(
|
|
26930
|
-
|
|
27461
|
+
chalk35.yellow(`
|
|
26931
27462
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
26932
27463
|
);
|
|
26933
|
-
console.error(
|
|
27464
|
+
console.error(chalk35.white(`
|
|
26934
27465
|
"${target}" uses its own hook system. Use:`));
|
|
26935
27466
|
console.error(
|
|
26936
|
-
|
|
27467
|
+
chalk35.green(` node9 addto ${target} `) + chalk35.gray("# one-time setup")
|
|
26937
27468
|
);
|
|
26938
|
-
console.error(
|
|
27469
|
+
console.error(chalk35.green(` ${target} `) + chalk35.gray("# run normally"));
|
|
26939
27470
|
process.exit(1);
|
|
26940
27471
|
}
|
|
26941
27472
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -26952,7 +27483,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
26952
27483
|
}
|
|
26953
27484
|
);
|
|
26954
27485
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
26955
|
-
console.error(
|
|
27486
|
+
console.error(chalk35.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
26956
27487
|
const daemonReady = await autoStartDaemonAndWait();
|
|
26957
27488
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
26958
27489
|
}
|
|
@@ -26965,12 +27496,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
26965
27496
|
}
|
|
26966
27497
|
if (!result.approved) {
|
|
26967
27498
|
console.error(
|
|
26968
|
-
|
|
27499
|
+
chalk35.red(`
|
|
26969
27500
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
26970
27501
|
);
|
|
26971
27502
|
process.exit(1);
|
|
26972
27503
|
}
|
|
26973
|
-
console.error(
|
|
27504
|
+
console.error(chalk35.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
26974
27505
|
await runProxy(fullCommand);
|
|
26975
27506
|
} else {
|
|
26976
27507
|
program.help();
|
|
@@ -26985,6 +27516,7 @@ registerAgentsCommand(program);
|
|
|
26985
27516
|
registerScanCommand(program);
|
|
26986
27517
|
registerPostureCommand(program);
|
|
26987
27518
|
registerEgressCommand(program);
|
|
27519
|
+
registerSandboxCommand(program, version);
|
|
26988
27520
|
registerSessionsCommand(program);
|
|
26989
27521
|
registerSessionTaintCommand(program);
|
|
26990
27522
|
registerDlpCommand(program);
|
|
@@ -26995,9 +27527,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
26995
27527
|
const isCheckHook = process.argv[2] === "check";
|
|
26996
27528
|
if (isCheckHook) {
|
|
26997
27529
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
26998
|
-
const logPath =
|
|
27530
|
+
const logPath = path60.join(os54.homedir(), ".node9", "hook-debug.log");
|
|
26999
27531
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
27000
|
-
|
|
27532
|
+
fs62.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
27001
27533
|
`);
|
|
27002
27534
|
}
|
|
27003
27535
|
process.exit(0);
|