@node9/proxy 1.39.0 → 1.41.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 +1401 -689
- package/dist/cli.mjs +1395 -683
- package/dist/dashboard.mjs +5 -0
- package/dist/index.js +17 -1
- package/dist/index.mjs +17 -1
- 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 path62 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
189
|
+
return ` \u2022 ${path62}: ${issue.message}`;
|
|
190
190
|
});
|
|
191
191
|
return {
|
|
192
192
|
sanitized,
|
|
@@ -271,6 +271,11 @@ var init_config_schema = __esm({
|
|
|
271
271
|
allowGlobalPause: z.boolean().optional(),
|
|
272
272
|
auditHashArgs: z.boolean().optional(),
|
|
273
273
|
agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
|
|
274
|
+
// Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
|
|
275
|
+
// inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
|
|
276
|
+
// node9's own approver (terminal/native/cloud). Unset → smart default
|
|
277
|
+
// (ask for ask-capable agents unless a cloud approver is configured).
|
|
278
|
+
reviewChannel: z.enum(["ask", "approver"]).optional(),
|
|
274
279
|
cloudSyncIntervalHours: z.number().positive().optional(),
|
|
275
280
|
// Outbox shipper (audit.log → SaaS batch ingest). enabled defaults
|
|
276
281
|
// to true; set false to fall back to local-only auditing.
|
|
@@ -1258,9 +1263,9 @@ function matchesPattern(text, patterns) {
|
|
|
1258
1263
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1259
1264
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1260
1265
|
}
|
|
1261
|
-
function getNestedValue(obj,
|
|
1266
|
+
function getNestedValue(obj, path62) {
|
|
1262
1267
|
if (!obj || typeof obj !== "object") return null;
|
|
1263
|
-
const segments =
|
|
1268
|
+
const segments = path62.split(".");
|
|
1264
1269
|
for (const seg of segments) {
|
|
1265
1270
|
if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return null;
|
|
1266
1271
|
}
|
|
@@ -4178,6 +4183,7 @@ function getConfig(cwd) {
|
|
|
4178
4183
|
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
4179
4184
|
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
4180
4185
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
4186
|
+
if (s.reviewChannel !== void 0) mergedSettings.reviewChannel = s.reviewChannel;
|
|
4181
4187
|
if (s.cloudSyncIntervalHours !== void 0)
|
|
4182
4188
|
mergedSettings.cloudSyncIntervalHours = s.cloudSyncIntervalHours;
|
|
4183
4189
|
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
@@ -6330,7 +6336,7 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
6330
6336
|
tool: toolName,
|
|
6331
6337
|
args,
|
|
6332
6338
|
ts: actTs,
|
|
6333
|
-
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
6339
|
+
status: result.review ? "review" : result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
6334
6340
|
label: result.blockedByLabel,
|
|
6335
6341
|
ruleHit: result.ruleHit,
|
|
6336
6342
|
observeWouldBlock: result.observeWouldBlock,
|
|
@@ -6693,6 +6699,16 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
6693
6699
|
taintWarning
|
|
6694
6700
|
);
|
|
6695
6701
|
}
|
|
6702
|
+
const cloudEnforcedForDefer = approvers.cloud && !!creds?.apiKey;
|
|
6703
|
+
if (options?.deferReview && !taintWarning && !cloudEnforcedForDefer) {
|
|
6704
|
+
return {
|
|
6705
|
+
approved: false,
|
|
6706
|
+
review: true,
|
|
6707
|
+
reason: explainableLabel || "Node9 flagged this action for review.",
|
|
6708
|
+
ruleDescription: policyRuleDescription,
|
|
6709
|
+
blockedByLabel: explainableLabel
|
|
6710
|
+
};
|
|
6711
|
+
}
|
|
6696
6712
|
let cloudRequestId = null;
|
|
6697
6713
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
6698
6714
|
const forceReview = localSmartRuleMatched === true || options?.localSmartRuleMatched === true || void 0;
|
|
@@ -7504,6 +7520,18 @@ function removeNode9McpServer(servers) {
|
|
|
7504
7520
|
function printDaemonTip() {
|
|
7505
7521
|
console.log(chalk.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups."));
|
|
7506
7522
|
}
|
|
7523
|
+
function printInlineAskNotice() {
|
|
7524
|
+
console.log(
|
|
7525
|
+
chalk.cyan(
|
|
7526
|
+
" \u{1F4AC} Review prompts appear inline in your agent (approve/deny in the chat) by default."
|
|
7527
|
+
)
|
|
7528
|
+
);
|
|
7529
|
+
console.log(
|
|
7530
|
+
chalk.gray(
|
|
7531
|
+
' Prefer node9\u2019s own approver? Set "reviewChannel": "approver" in config, or add --no-ask to the hook.\n (Inline prompts are auto-disabled when a cloud approver is configured.)'
|
|
7532
|
+
)
|
|
7533
|
+
);
|
|
7534
|
+
}
|
|
7507
7535
|
function fullPathCommand(subcommand) {
|
|
7508
7536
|
if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
|
|
7509
7537
|
const nodeExec = toForwardSlashes(process.execPath);
|
|
@@ -7828,6 +7856,7 @@ async function setupClaude() {
|
|
|
7828
7856
|
console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
7829
7857
|
console.log(chalk.gray(" Restart Claude Code for changes to take effect."));
|
|
7830
7858
|
printDaemonTip();
|
|
7859
|
+
printInlineAskNotice();
|
|
7831
7860
|
}
|
|
7832
7861
|
}
|
|
7833
7862
|
async function setupGemini() {
|
|
@@ -8224,6 +8253,7 @@ async function setupCopilot() {
|
|
|
8224
8253
|
console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting GitHub Copilot CLI!"));
|
|
8225
8254
|
console.log(chalk.gray(" Restart Copilot CLI for changes to take effect."));
|
|
8226
8255
|
printDaemonTip();
|
|
8256
|
+
printInlineAskNotice();
|
|
8227
8257
|
}
|
|
8228
8258
|
function teardownCopilot() {
|
|
8229
8259
|
const homeDir2 = os12.homedir();
|
|
@@ -15510,8 +15540,8 @@ function fileSignature(filePath) {
|
|
|
15510
15540
|
const fd = fs27.openSync(filePath, "r");
|
|
15511
15541
|
try {
|
|
15512
15542
|
const buf = Buffer.alloc(512);
|
|
15513
|
-
const
|
|
15514
|
-
const slice = buf.subarray(0,
|
|
15543
|
+
const read2 = fs27.readSync(fd, buf, 0, 512, 0);
|
|
15544
|
+
const slice = buf.subarray(0, read2);
|
|
15515
15545
|
const nl = slice.indexOf(10);
|
|
15516
15546
|
const firstLine = nl === -1 ? slice : slice.subarray(0, nl);
|
|
15517
15547
|
return crypto5.createHash("sha256").update(firstLine).digest("hex").slice(0, 16);
|
|
@@ -15615,13 +15645,13 @@ async function shipOnce(deps = {}) {
|
|
|
15615
15645
|
const toRead = Math.min(size - offset, MAX_CHUNK_BYTES);
|
|
15616
15646
|
const buf = Buffer.alloc(toRead);
|
|
15617
15647
|
const fd = fs27.openSync(auditLogPath, "r");
|
|
15618
|
-
let
|
|
15648
|
+
let read2;
|
|
15619
15649
|
try {
|
|
15620
|
-
|
|
15650
|
+
read2 = fs27.readSync(fd, buf, 0, toRead, offset);
|
|
15621
15651
|
} finally {
|
|
15622
15652
|
fs27.closeSync(fd);
|
|
15623
15653
|
}
|
|
15624
|
-
const { rows, consumed } = buildWireRows(buf.subarray(0,
|
|
15654
|
+
const { rows, consumed } = buildWireRows(buf.subarray(0, read2));
|
|
15625
15655
|
if (consumed === 0) break;
|
|
15626
15656
|
for (let i = 0; i < rows.length; i += MAX_BATCH) {
|
|
15627
15657
|
const batch = rows.slice(i, i + MAX_BATCH);
|
|
@@ -17198,10 +17228,10 @@ __export(tail_exports, {
|
|
|
17198
17228
|
startTail: () => startTail
|
|
17199
17229
|
});
|
|
17200
17230
|
import http3 from "http";
|
|
17201
|
-
import
|
|
17202
|
-
import
|
|
17203
|
-
import
|
|
17204
|
-
import
|
|
17231
|
+
import chalk34 from "chalk";
|
|
17232
|
+
import fs61 from "fs";
|
|
17233
|
+
import os53 from "os";
|
|
17234
|
+
import path59 from "path";
|
|
17205
17235
|
import readline6 from "readline";
|
|
17206
17236
|
import { spawn as spawn8 } from "child_process";
|
|
17207
17237
|
function shortenPathSummary(s) {
|
|
@@ -17225,20 +17255,20 @@ function getModelContextLimit(model) {
|
|
|
17225
17255
|
return 2e5;
|
|
17226
17256
|
}
|
|
17227
17257
|
function readSessionUsage() {
|
|
17228
|
-
const projectsDir =
|
|
17229
|
-
if (!
|
|
17258
|
+
const projectsDir = path59.join(os53.homedir(), ".claude", "projects");
|
|
17259
|
+
if (!fs61.existsSync(projectsDir)) return null;
|
|
17230
17260
|
let latestFile = null;
|
|
17231
17261
|
let latestMtime = 0;
|
|
17232
17262
|
try {
|
|
17233
|
-
for (const dir of
|
|
17234
|
-
const dirPath =
|
|
17263
|
+
for (const dir of fs61.readdirSync(projectsDir)) {
|
|
17264
|
+
const dirPath = path59.join(projectsDir, dir);
|
|
17235
17265
|
try {
|
|
17236
|
-
if (!
|
|
17237
|
-
for (const file of
|
|
17266
|
+
if (!fs61.statSync(dirPath).isDirectory()) continue;
|
|
17267
|
+
for (const file of fs61.readdirSync(dirPath)) {
|
|
17238
17268
|
if (!file.endsWith(".jsonl") || file.startsWith("agent-")) continue;
|
|
17239
|
-
const filePath =
|
|
17269
|
+
const filePath = path59.join(dirPath, file);
|
|
17240
17270
|
try {
|
|
17241
|
-
const mtime =
|
|
17271
|
+
const mtime = fs61.statSync(filePath).mtimeMs;
|
|
17242
17272
|
if (mtime > latestMtime) {
|
|
17243
17273
|
latestMtime = mtime;
|
|
17244
17274
|
latestFile = filePath;
|
|
@@ -17253,7 +17283,7 @@ function readSessionUsage() {
|
|
|
17253
17283
|
}
|
|
17254
17284
|
if (!latestFile) return null;
|
|
17255
17285
|
try {
|
|
17256
|
-
const lines =
|
|
17286
|
+
const lines = fs61.readFileSync(latestFile, "utf-8").split("\n");
|
|
17257
17287
|
let lastModel = "";
|
|
17258
17288
|
let lastInput = 0;
|
|
17259
17289
|
let lastOutput = 0;
|
|
@@ -17278,10 +17308,10 @@ function readSessionUsage() {
|
|
|
17278
17308
|
}
|
|
17279
17309
|
}
|
|
17280
17310
|
function formatContextStat(stat) {
|
|
17281
|
-
const pctColor = stat.fillPct >= 80 ?
|
|
17311
|
+
const pctColor = stat.fillPct >= 80 ? chalk34.red : stat.fillPct >= 50 ? chalk34.yellow : chalk34.cyan;
|
|
17282
17312
|
const k = (n) => `${Math.round(n / 1e3)}k`;
|
|
17283
17313
|
const modelShort = stat.model.replace(/@.*$/, "").replace(/-\d{8}$/, "").replace(/^claude-/, "");
|
|
17284
|
-
return
|
|
17314
|
+
return chalk34.dim("ctx: ") + pctColor(`${stat.fillPct}%`) + chalk34.dim(
|
|
17285
17315
|
` (${k(stat.inputTokens)}/${k(getModelContextLimit(stat.model))} out ${k(stat.outputTokens)} \xB7 ${modelShort})`
|
|
17286
17316
|
);
|
|
17287
17317
|
}
|
|
@@ -17304,32 +17334,32 @@ function agentLabel(agent, mcpServer, sessionId) {
|
|
|
17304
17334
|
const tag = sessionTag(sessionId);
|
|
17305
17335
|
const tagSuffix = tag ? `\xB7${tag}` : "";
|
|
17306
17336
|
if (!agent || agent === "Terminal") {
|
|
17307
|
-
return mcpServer ?
|
|
17337
|
+
return mcpServer ? chalk34.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
17308
17338
|
}
|
|
17309
17339
|
const short = agent === "Claude Code" ? "Claude" : agent === "Gemini CLI" ? "Gemini" : agent === "Unknown Agent" ? "" : agent.split(" ")[0];
|
|
17310
|
-
if (!short) return mcpServer ?
|
|
17311
|
-
return mcpServer ?
|
|
17340
|
+
if (!short) return mcpServer ? chalk34.dim(`[\u2192 ${mcpServer}] `) : "";
|
|
17341
|
+
return mcpServer ? chalk34.dim(`[${short}${tagSuffix} \u2192 ${mcpServer}] `) : chalk34.dim(`[${short}${tagSuffix}] `);
|
|
17312
17342
|
}
|
|
17313
17343
|
function formatBase(activity) {
|
|
17314
17344
|
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
17315
17345
|
const icon = getIcon(activity.tool);
|
|
17316
17346
|
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
17317
|
-
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(
|
|
17347
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ").replaceAll(os53.homedir(), "~");
|
|
17318
17348
|
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
17319
|
-
return `${
|
|
17349
|
+
return `${chalk34.gray(time)} ${icon} ${agentLabel(activity.agent, activity.mcpServer, activity.sessionId)}${chalk34.white.bold(toolName)} ${chalk34.dim(argsPreview)}`;
|
|
17320
17350
|
}
|
|
17321
17351
|
function renderResult(activity, result) {
|
|
17322
17352
|
const base = formatBase(activity);
|
|
17323
17353
|
let status;
|
|
17324
17354
|
if (result.status === "allow") {
|
|
17325
|
-
status =
|
|
17355
|
+
status = chalk34.green("\u2713 ALLOW");
|
|
17326
17356
|
} else if (result.status === "dlp") {
|
|
17327
|
-
status =
|
|
17357
|
+
status = chalk34.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
17328
17358
|
} else {
|
|
17329
|
-
status =
|
|
17359
|
+
status = chalk34.red("\u2717 BLOCK");
|
|
17330
17360
|
}
|
|
17331
17361
|
const cost = result.costEstimate ?? activity.costEstimate;
|
|
17332
|
-
const costSuffix = cost == null ? "" :
|
|
17362
|
+
const costSuffix = cost == null ? "" : chalk34.dim(` ~$${cost >= 1e-3 ? cost.toFixed(3) : "0.000"}`);
|
|
17333
17363
|
if (process.stdout.isTTY) {
|
|
17334
17364
|
if (pendingShownForId === activity.id && pendingWrappedLines > 1) {
|
|
17335
17365
|
readline6.moveCursor(process.stdout, 0, -(pendingWrappedLines - 1));
|
|
@@ -17346,19 +17376,19 @@ function renderResult(activity, result) {
|
|
|
17346
17376
|
}
|
|
17347
17377
|
function renderPending(activity) {
|
|
17348
17378
|
if (!process.stdout.isTTY) return;
|
|
17349
|
-
const line = `${formatBase(activity)} ${
|
|
17379
|
+
const line = `${formatBase(activity)} ${chalk34.yellow("\u25CF \u2026")}`;
|
|
17350
17380
|
pendingShownForId = activity.id;
|
|
17351
17381
|
pendingWrappedLines = wrappedLineCount(line);
|
|
17352
17382
|
process.stdout.write(`${line}\r`);
|
|
17353
17383
|
}
|
|
17354
17384
|
async function ensureDaemon() {
|
|
17355
17385
|
let pidPort = null;
|
|
17356
|
-
if (
|
|
17386
|
+
if (fs61.existsSync(PID_FILE)) {
|
|
17357
17387
|
try {
|
|
17358
|
-
const { port } = JSON.parse(
|
|
17388
|
+
const { port } = JSON.parse(fs61.readFileSync(PID_FILE, "utf-8"));
|
|
17359
17389
|
pidPort = port;
|
|
17360
17390
|
} catch {
|
|
17361
|
-
console.error(
|
|
17391
|
+
console.error(chalk34.dim("\u26A0\uFE0F Could not read PID file; falling back to default port."));
|
|
17362
17392
|
}
|
|
17363
17393
|
}
|
|
17364
17394
|
const checkPort = pidPort ?? DAEMON_PORT;
|
|
@@ -17369,7 +17399,7 @@ async function ensureDaemon() {
|
|
|
17369
17399
|
if (res.ok) return checkPort;
|
|
17370
17400
|
} catch {
|
|
17371
17401
|
}
|
|
17372
|
-
console.log(
|
|
17402
|
+
console.log(chalk34.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
17373
17403
|
const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
|
|
17374
17404
|
detached: true,
|
|
17375
17405
|
stdio: "ignore",
|
|
@@ -17386,7 +17416,7 @@ async function ensureDaemon() {
|
|
|
17386
17416
|
} catch {
|
|
17387
17417
|
}
|
|
17388
17418
|
}
|
|
17389
|
-
console.error(
|
|
17419
|
+
console.error(chalk34.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
17390
17420
|
process.exit(1);
|
|
17391
17421
|
}
|
|
17392
17422
|
function postDecisionHttp(id, decision, authToken, port, opts) {
|
|
@@ -17455,7 +17485,7 @@ function buildCardLines(req, localCount = 0) {
|
|
|
17455
17485
|
const severityIcon = isBlock ? `${RED}\u{1F6D1}` : `${YELLOW}\u26A0 `;
|
|
17456
17486
|
const rawDesc = req.riskMetadata?.ruleDescription ?? "";
|
|
17457
17487
|
const description = rawDesc ? cleanReason(rawDesc) : "";
|
|
17458
|
-
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${
|
|
17488
|
+
const agentSuffix = req.agent && req.agent !== "Terminal" ? ` ${RESET2}${chalk34.dim(`(${req.agent})`)}` : "";
|
|
17459
17489
|
const lines = [
|
|
17460
17490
|
``,
|
|
17461
17491
|
`${BOLD2}${CYAN}\u2554\u2550\u2550 Node9 Approval Required \u2550\u2550\u2557${RESET2}`,
|
|
@@ -17511,9 +17541,9 @@ function buildRecoveryCardLines(req) {
|
|
|
17511
17541
|
];
|
|
17512
17542
|
}
|
|
17513
17543
|
function readApproversFromDisk() {
|
|
17514
|
-
const configPath2 =
|
|
17544
|
+
const configPath2 = path59.join(os53.homedir(), ".node9", "config.json");
|
|
17515
17545
|
try {
|
|
17516
|
-
const raw = JSON.parse(
|
|
17546
|
+
const raw = JSON.parse(fs61.readFileSync(configPath2, "utf-8"));
|
|
17517
17547
|
const settings = raw.settings ?? {};
|
|
17518
17548
|
return settings.approvers ?? {};
|
|
17519
17549
|
} catch {
|
|
@@ -17524,20 +17554,20 @@ function approverStatusLine() {
|
|
|
17524
17554
|
const a = readApproversFromDisk();
|
|
17525
17555
|
const fmt = (label2, key) => {
|
|
17526
17556
|
const on = a[key] !== false;
|
|
17527
|
-
return `[${key[0]}]${label2.slice(1)} ${on ?
|
|
17557
|
+
return `[${key[0]}]${label2.slice(1)} ${on ? chalk34.green("\u2713") : chalk34.dim("\u2717")}`;
|
|
17528
17558
|
};
|
|
17529
17559
|
return `${fmt("native", "native")} ${fmt("cloud", "cloud")} ${fmt("terminal", "terminal")}`;
|
|
17530
17560
|
}
|
|
17531
17561
|
function toggleApprover(channel) {
|
|
17532
|
-
const configPath2 =
|
|
17562
|
+
const configPath2 = path59.join(os53.homedir(), ".node9", "config.json");
|
|
17533
17563
|
try {
|
|
17534
|
-
const raw = JSON.parse(
|
|
17564
|
+
const raw = JSON.parse(fs61.readFileSync(configPath2, "utf-8"));
|
|
17535
17565
|
const settings = raw.settings ?? {};
|
|
17536
17566
|
const approvers = settings.approvers ?? {};
|
|
17537
17567
|
approvers[channel] = approvers[channel] === false;
|
|
17538
17568
|
settings.approvers = approvers;
|
|
17539
17569
|
raw.settings = settings;
|
|
17540
|
-
|
|
17570
|
+
fs61.writeFileSync(configPath2, JSON.stringify(raw, null, 2) + "\n");
|
|
17541
17571
|
} catch (err2) {
|
|
17542
17572
|
process.stderr.write(`[node9] toggleApprover failed: ${String(err2)}
|
|
17543
17573
|
`);
|
|
@@ -17569,7 +17599,7 @@ async function startTail(options = {}) {
|
|
|
17569
17599
|
req2.end();
|
|
17570
17600
|
});
|
|
17571
17601
|
if (result.ok) {
|
|
17572
|
-
console.log(
|
|
17602
|
+
console.log(chalk34.green("\u2713 Flight Recorder buffer cleared."));
|
|
17573
17603
|
} else if (result.code === "ECONNREFUSED") {
|
|
17574
17604
|
throw new Error("Daemon is not running. Start it with: node9 daemon start");
|
|
17575
17605
|
} else if (result.code === "ETIMEDOUT") {
|
|
@@ -17615,7 +17645,7 @@ async function startTail(options = {}) {
|
|
|
17615
17645
|
const channel = name === "n" ? "native" : name === "c" ? "cloud" : name === "t" ? "terminal" : null;
|
|
17616
17646
|
if (channel) {
|
|
17617
17647
|
toggleApprover(channel);
|
|
17618
|
-
console.log(
|
|
17648
|
+
console.log(chalk34.dim(` Approvers: ${approverStatusLine()}`));
|
|
17619
17649
|
}
|
|
17620
17650
|
};
|
|
17621
17651
|
process.stdin.on("keypress", idleKeypressHandler);
|
|
@@ -17681,7 +17711,7 @@ async function startTail(options = {}) {
|
|
|
17681
17711
|
localAllowCounts.get(req2.toolName) ?? 0
|
|
17682
17712
|
)
|
|
17683
17713
|
);
|
|
17684
|
-
const decisionStamp = action === "always-allow" ?
|
|
17714
|
+
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
17715
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${decisionStamp} ${GRAY}(terminal)${RESET2}`, ``);
|
|
17686
17716
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
17687
17717
|
process.stdout.write(SHOW_CURSOR);
|
|
@@ -17709,8 +17739,8 @@ async function startTail(options = {}) {
|
|
|
17709
17739
|
}
|
|
17710
17740
|
postDecisionHttp(req2.id, httpDecision, authToken, port, httpOpts).catch((err2) => {
|
|
17711
17741
|
try {
|
|
17712
|
-
|
|
17713
|
-
|
|
17742
|
+
fs61.appendFileSync(
|
|
17743
|
+
path59.join(os53.homedir(), ".node9", "hook-debug.log"),
|
|
17714
17744
|
`[tail] POST /decision failed: ${String(err2)}
|
|
17715
17745
|
`
|
|
17716
17746
|
);
|
|
@@ -17732,7 +17762,7 @@ async function startTail(options = {}) {
|
|
|
17732
17762
|
);
|
|
17733
17763
|
const stampedLines = buildCardLines(req2, priorCount);
|
|
17734
17764
|
if (externalDecision) {
|
|
17735
|
-
const source = externalDecision === "allow" ?
|
|
17765
|
+
const source = externalDecision === "allow" ? chalk34.green("\u2713 ALLOWED") : chalk34.red("\u2717 DENIED");
|
|
17736
17766
|
stampedLines.push(` ${BOLD2}\u2192${RESET2} ${source} ${GRAY}(external)${RESET2}`, ``);
|
|
17737
17767
|
}
|
|
17738
17768
|
for (const line of stampedLines) process.stdout.write(line + "\n");
|
|
@@ -17774,31 +17804,31 @@ async function startTail(options = {}) {
|
|
|
17774
17804
|
};
|
|
17775
17805
|
process.stdin.on("keypress", onKeypress);
|
|
17776
17806
|
}
|
|
17777
|
-
const auditLog =
|
|
17807
|
+
const auditLog = path59.join(os53.homedir(), ".node9", "audit.log");
|
|
17778
17808
|
try {
|
|
17779
|
-
const unackedDlp =
|
|
17809
|
+
const unackedDlp = fs61.readFileSync(auditLog, "utf-8").split("\n").filter((l) => l.includes('"response-dlp"')).length;
|
|
17780
17810
|
if (unackedDlp > 0) {
|
|
17781
17811
|
console.log("");
|
|
17782
17812
|
console.log(
|
|
17783
|
-
|
|
17813
|
+
chalk34.bgRed.white.bold(
|
|
17784
17814
|
` \u26A0\uFE0F DLP ALERT: ${unackedDlp} secret${unackedDlp !== 1 ? "s" : ""} found in Claude response text \u2014 run: node9 dlp `
|
|
17785
17815
|
)
|
|
17786
17816
|
);
|
|
17787
17817
|
}
|
|
17788
17818
|
} catch {
|
|
17789
17819
|
}
|
|
17790
|
-
console.log(
|
|
17820
|
+
console.log(chalk34.cyan.bold(`
|
|
17791
17821
|
\u{1F6F0}\uFE0F Node9 tail`));
|
|
17792
17822
|
if (canApprove) {
|
|
17793
|
-
console.log(
|
|
17794
|
-
console.log(
|
|
17823
|
+
console.log(chalk34.dim("Card: [\u21B5/y] Allow [n] Deny [a] Always [t] Trust 30m"));
|
|
17824
|
+
console.log(chalk34.dim(`Approvers (toggle): ${approverStatusLine()} [q] quit`));
|
|
17795
17825
|
}
|
|
17796
17826
|
const ctxStat = readSessionUsage();
|
|
17797
17827
|
if (ctxStat) console.log(" " + formatContextStat(ctxStat));
|
|
17798
17828
|
if (options.history) {
|
|
17799
|
-
console.log(
|
|
17829
|
+
console.log(chalk34.dim("Showing history + live events.\n"));
|
|
17800
17830
|
} else {
|
|
17801
|
-
console.log(
|
|
17831
|
+
console.log(chalk34.dim("Showing live events only. Use --history to include past.\n"));
|
|
17802
17832
|
}
|
|
17803
17833
|
process.on("SIGINT", () => {
|
|
17804
17834
|
exitIdleMode();
|
|
@@ -17808,7 +17838,7 @@ async function startTail(options = {}) {
|
|
|
17808
17838
|
readline6.clearLine(process.stdout, 0);
|
|
17809
17839
|
readline6.cursorTo(process.stdout, 0);
|
|
17810
17840
|
}
|
|
17811
|
-
console.log(
|
|
17841
|
+
console.log(chalk34.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
17812
17842
|
process.exit(0);
|
|
17813
17843
|
});
|
|
17814
17844
|
const STALL_THRESHOLD_MS = 6e4;
|
|
@@ -17816,11 +17846,11 @@ async function startTail(options = {}) {
|
|
|
17816
17846
|
if (stallWarned) return;
|
|
17817
17847
|
if (Date.now() - lastActivityFromDaemon < STALL_THRESHOLD_MS) return;
|
|
17818
17848
|
try {
|
|
17819
|
-
const auditMtime =
|
|
17849
|
+
const auditMtime = fs61.statSync(auditLog).mtimeMs;
|
|
17820
17850
|
if (Date.now() - auditMtime >= STALL_THRESHOLD_MS) return;
|
|
17821
17851
|
console.log("");
|
|
17822
17852
|
console.log(
|
|
17823
|
-
|
|
17853
|
+
chalk34.yellow(
|
|
17824
17854
|
"\u26A0\uFE0F Tail appears stalled \u2014 hooks are firing but no events are arriving. Try: node9 daemon restart"
|
|
17825
17855
|
)
|
|
17826
17856
|
);
|
|
@@ -17837,7 +17867,7 @@ async function startTail(options = {}) {
|
|
|
17837
17867
|
},
|
|
17838
17868
|
(res) => {
|
|
17839
17869
|
if (res.statusCode !== 200) {
|
|
17840
|
-
console.error(
|
|
17870
|
+
console.error(chalk34.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
17841
17871
|
process.exit(1);
|
|
17842
17872
|
}
|
|
17843
17873
|
if (canApprove) enterIdleMode();
|
|
@@ -17868,7 +17898,7 @@ async function startTail(options = {}) {
|
|
|
17868
17898
|
readline6.clearLine(process.stdout, 0);
|
|
17869
17899
|
readline6.cursorTo(process.stdout, 0);
|
|
17870
17900
|
}
|
|
17871
|
-
console.log(
|
|
17901
|
+
console.log(chalk34.red("\n\u274C Daemon disconnected."));
|
|
17872
17902
|
process.exit(1);
|
|
17873
17903
|
});
|
|
17874
17904
|
}
|
|
@@ -17881,7 +17911,7 @@ async function startTail(options = {}) {
|
|
|
17881
17911
|
const parsed = JSON.parse(rawData);
|
|
17882
17912
|
const msg = parsed.message ?? "Flight recorder is down \u2014 run: node9 daemon restart";
|
|
17883
17913
|
console.log("");
|
|
17884
|
-
console.log(
|
|
17914
|
+
console.log(chalk34.bgRed.white.bold(` \u26A0\uFE0F ${msg} `));
|
|
17885
17915
|
} catch {
|
|
17886
17916
|
}
|
|
17887
17917
|
return;
|
|
@@ -17966,9 +17996,9 @@ async function startTail(options = {}) {
|
|
|
17966
17996
|
const rawSummary = data.argsSummary ?? data.tool;
|
|
17967
17997
|
const summary = shortenPathSummary(rawSummary);
|
|
17968
17998
|
const fileCount = data.fileCount ?? 0;
|
|
17969
|
-
const files = fileCount > 0 ?
|
|
17999
|
+
const files = fileCount > 0 ? chalk34.dim(` \xB7 ${fileCount} file${fileCount === 1 ? "" : "s"}`) : "";
|
|
17970
18000
|
process.stdout.write(
|
|
17971
|
-
`${
|
|
18001
|
+
`${chalk34.dim(time)} ${chalk34.cyan("\u{1F4F8} snapshot")} ${chalk34.dim(hash)} ${summary}${files}
|
|
17972
18002
|
`
|
|
17973
18003
|
);
|
|
17974
18004
|
return;
|
|
@@ -17985,18 +18015,18 @@ async function startTail(options = {}) {
|
|
|
17985
18015
|
if (event === "execution-result") {
|
|
17986
18016
|
const exec = data;
|
|
17987
18017
|
const time = new Date(Date.now()).toLocaleTimeString([], { hour12: false });
|
|
17988
|
-
const arrow = exec.isError ?
|
|
18018
|
+
const arrow = exec.isError ? chalk34.red(" \u21B3 \u2717") : chalk34.green(" \u21B3 \u2713");
|
|
17989
18019
|
const label2 = agentLabel(exec.agent, exec.mcpServer);
|
|
17990
18020
|
const tool = (exec.tool ?? "").slice(0, 16);
|
|
17991
|
-
const duration = typeof exec.durationMs === "number" ?
|
|
18021
|
+
const duration = typeof exec.durationMs === "number" ? chalk34.dim(` (${exec.durationMs}ms)`) : "";
|
|
17992
18022
|
console.log(
|
|
17993
|
-
`${
|
|
18023
|
+
`${chalk34.gray(time)} ${arrow} ${label2}${chalk34.dim(tool)}${chalk34.dim(" completed")}${duration}`
|
|
17994
18024
|
);
|
|
17995
18025
|
}
|
|
17996
18026
|
}
|
|
17997
18027
|
req.on("error", (err2) => {
|
|
17998
18028
|
const msg = err2.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err2.message;
|
|
17999
|
-
console.error(
|
|
18029
|
+
console.error(chalk34.red(`
|
|
18000
18030
|
\u274C ${msg}`));
|
|
18001
18031
|
process.exit(1);
|
|
18002
18032
|
});
|
|
@@ -18007,7 +18037,7 @@ var init_tail = __esm({
|
|
|
18007
18037
|
"use strict";
|
|
18008
18038
|
init_daemon2();
|
|
18009
18039
|
init_daemon();
|
|
18010
|
-
PID_FILE =
|
|
18040
|
+
PID_FILE = path59.join(os53.homedir(), ".node9", "daemon.pid");
|
|
18011
18041
|
ICONS = {
|
|
18012
18042
|
bash: "\u{1F4BB}",
|
|
18013
18043
|
shell: "\u{1F4BB}",
|
|
@@ -18055,9 +18085,9 @@ __export(hud_exports, {
|
|
|
18055
18085
|
main: () => main,
|
|
18056
18086
|
renderEnvironmentLine: () => renderEnvironmentLine
|
|
18057
18087
|
});
|
|
18058
|
-
import
|
|
18059
|
-
import
|
|
18060
|
-
import
|
|
18088
|
+
import fs62 from "fs";
|
|
18089
|
+
import path60 from "path";
|
|
18090
|
+
import os54 from "os";
|
|
18061
18091
|
import http4 from "http";
|
|
18062
18092
|
async function readStdin() {
|
|
18063
18093
|
const chunks = [];
|
|
@@ -18133,9 +18163,9 @@ function formatTimeLeft(resetsAt) {
|
|
|
18133
18163
|
return ` (${m}m left)`;
|
|
18134
18164
|
}
|
|
18135
18165
|
function safeReadJson(filePath) {
|
|
18136
|
-
if (!
|
|
18166
|
+
if (!fs62.existsSync(filePath)) return null;
|
|
18137
18167
|
try {
|
|
18138
|
-
return JSON.parse(
|
|
18168
|
+
return JSON.parse(fs62.readFileSync(filePath, "utf-8"));
|
|
18139
18169
|
} catch {
|
|
18140
18170
|
return null;
|
|
18141
18171
|
}
|
|
@@ -18156,12 +18186,12 @@ function countHooksInFile(filePath) {
|
|
|
18156
18186
|
return Object.keys(cfg.hooks).length;
|
|
18157
18187
|
}
|
|
18158
18188
|
function countRulesInDir(rulesDir) {
|
|
18159
|
-
if (!
|
|
18189
|
+
if (!fs62.existsSync(rulesDir)) return 0;
|
|
18160
18190
|
let count = 0;
|
|
18161
18191
|
try {
|
|
18162
|
-
for (const entry of
|
|
18192
|
+
for (const entry of fs62.readdirSync(rulesDir, { withFileTypes: true })) {
|
|
18163
18193
|
if (entry.isDirectory()) {
|
|
18164
|
-
count += countRulesInDir(
|
|
18194
|
+
count += countRulesInDir(path60.join(rulesDir, entry.name));
|
|
18165
18195
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
18166
18196
|
count++;
|
|
18167
18197
|
}
|
|
@@ -18172,46 +18202,46 @@ function countRulesInDir(rulesDir) {
|
|
|
18172
18202
|
}
|
|
18173
18203
|
function isSamePath(a, b) {
|
|
18174
18204
|
try {
|
|
18175
|
-
return
|
|
18205
|
+
return path60.resolve(a) === path60.resolve(b);
|
|
18176
18206
|
} catch {
|
|
18177
18207
|
return false;
|
|
18178
18208
|
}
|
|
18179
18209
|
}
|
|
18180
18210
|
function countConfigs(cwd) {
|
|
18181
|
-
const homeDir2 =
|
|
18182
|
-
const claudeDir =
|
|
18211
|
+
const homeDir2 = os54.homedir();
|
|
18212
|
+
const claudeDir = path60.join(homeDir2, ".claude");
|
|
18183
18213
|
let claudeMdCount = 0;
|
|
18184
18214
|
let rulesCount = 0;
|
|
18185
18215
|
let hooksCount = 0;
|
|
18186
18216
|
const userMcpServers = /* @__PURE__ */ new Set();
|
|
18187
18217
|
const projectMcpServers = /* @__PURE__ */ new Set();
|
|
18188
|
-
if (
|
|
18189
|
-
rulesCount += countRulesInDir(
|
|
18190
|
-
const userSettings =
|
|
18218
|
+
if (fs62.existsSync(path60.join(claudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
18219
|
+
rulesCount += countRulesInDir(path60.join(claudeDir, "rules"));
|
|
18220
|
+
const userSettings = path60.join(claudeDir, "settings.json");
|
|
18191
18221
|
for (const name of getMcpServerNames(userSettings)) userMcpServers.add(name);
|
|
18192
18222
|
hooksCount += countHooksInFile(userSettings);
|
|
18193
|
-
const userClaudeJson =
|
|
18223
|
+
const userClaudeJson = path60.join(homeDir2, ".claude.json");
|
|
18194
18224
|
for (const name of getMcpServerNames(userClaudeJson)) userMcpServers.add(name);
|
|
18195
18225
|
for (const name of getDisabledMcpServers(userClaudeJson, "disabledMcpServers")) {
|
|
18196
18226
|
userMcpServers.delete(name);
|
|
18197
18227
|
}
|
|
18198
18228
|
if (cwd) {
|
|
18199
|
-
if (
|
|
18200
|
-
if (
|
|
18201
|
-
const projectClaudeDir =
|
|
18229
|
+
if (fs62.existsSync(path60.join(cwd, "CLAUDE.md"))) claudeMdCount++;
|
|
18230
|
+
if (fs62.existsSync(path60.join(cwd, "CLAUDE.local.md"))) claudeMdCount++;
|
|
18231
|
+
const projectClaudeDir = path60.join(cwd, ".claude");
|
|
18202
18232
|
const overlapsUserScope = isSamePath(projectClaudeDir, claudeDir);
|
|
18203
18233
|
if (!overlapsUserScope) {
|
|
18204
|
-
if (
|
|
18205
|
-
rulesCount += countRulesInDir(
|
|
18206
|
-
const projSettings =
|
|
18234
|
+
if (fs62.existsSync(path60.join(projectClaudeDir, "CLAUDE.md"))) claudeMdCount++;
|
|
18235
|
+
rulesCount += countRulesInDir(path60.join(projectClaudeDir, "rules"));
|
|
18236
|
+
const projSettings = path60.join(projectClaudeDir, "settings.json");
|
|
18207
18237
|
for (const name of getMcpServerNames(projSettings)) projectMcpServers.add(name);
|
|
18208
18238
|
hooksCount += countHooksInFile(projSettings);
|
|
18209
18239
|
}
|
|
18210
|
-
if (
|
|
18211
|
-
const localSettings =
|
|
18240
|
+
if (fs62.existsSync(path60.join(projectClaudeDir, "CLAUDE.local.md"))) claudeMdCount++;
|
|
18241
|
+
const localSettings = path60.join(projectClaudeDir, "settings.local.json");
|
|
18212
18242
|
for (const name of getMcpServerNames(localSettings)) projectMcpServers.add(name);
|
|
18213
18243
|
hooksCount += countHooksInFile(localSettings);
|
|
18214
|
-
const mcpJsonServers = getMcpServerNames(
|
|
18244
|
+
const mcpJsonServers = getMcpServerNames(path60.join(cwd, ".mcp.json"));
|
|
18215
18245
|
const disabledMcpJson = getDisabledMcpServers(localSettings, "disabledMcpjsonServers");
|
|
18216
18246
|
for (const name of disabledMcpJson) mcpJsonServers.delete(name);
|
|
18217
18247
|
for (const name of mcpJsonServers) projectMcpServers.add(name);
|
|
@@ -18244,12 +18274,12 @@ function readActiveShieldsHud() {
|
|
|
18244
18274
|
return shieldsCache.value;
|
|
18245
18275
|
}
|
|
18246
18276
|
try {
|
|
18247
|
-
const shieldsPath =
|
|
18248
|
-
if (!
|
|
18277
|
+
const shieldsPath = path60.join(os54.homedir(), ".node9", "shields.json");
|
|
18278
|
+
if (!fs62.existsSync(shieldsPath)) {
|
|
18249
18279
|
shieldsCache = { value: [], ts: now };
|
|
18250
18280
|
return [];
|
|
18251
18281
|
}
|
|
18252
|
-
const parsed = JSON.parse(
|
|
18282
|
+
const parsed = JSON.parse(fs62.readFileSync(shieldsPath, "utf-8"));
|
|
18253
18283
|
if (!Array.isArray(parsed.active)) {
|
|
18254
18284
|
shieldsCache = { value: [], ts: now };
|
|
18255
18285
|
return [];
|
|
@@ -18351,17 +18381,17 @@ function renderContextLine(stdin) {
|
|
|
18351
18381
|
async function main() {
|
|
18352
18382
|
try {
|
|
18353
18383
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
18354
|
-
if (
|
|
18384
|
+
if (fs62.existsSync(path60.join(os54.homedir(), ".node9", "hud-debug"))) {
|
|
18355
18385
|
try {
|
|
18356
|
-
const logPath =
|
|
18386
|
+
const logPath = path60.join(os54.homedir(), ".node9", "hud-debug.log");
|
|
18357
18387
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
18358
18388
|
let size = 0;
|
|
18359
18389
|
try {
|
|
18360
|
-
size =
|
|
18390
|
+
size = fs62.statSync(logPath).size;
|
|
18361
18391
|
} catch {
|
|
18362
18392
|
}
|
|
18363
18393
|
if (size < MAX_LOG_SIZE) {
|
|
18364
|
-
|
|
18394
|
+
fs62.appendFileSync(
|
|
18365
18395
|
logPath,
|
|
18366
18396
|
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
18367
18397
|
);
|
|
@@ -18382,11 +18412,11 @@ async function main() {
|
|
|
18382
18412
|
try {
|
|
18383
18413
|
const cwd = stdin.cwd ?? process.cwd();
|
|
18384
18414
|
for (const configPath2 of [
|
|
18385
|
-
|
|
18386
|
-
|
|
18415
|
+
path60.join(cwd, "node9.config.json"),
|
|
18416
|
+
path60.join(os54.homedir(), ".node9", "config.json")
|
|
18387
18417
|
]) {
|
|
18388
|
-
if (!
|
|
18389
|
-
const cfg = JSON.parse(
|
|
18418
|
+
if (!fs62.existsSync(configPath2)) continue;
|
|
18419
|
+
const cfg = JSON.parse(fs62.readFileSync(configPath2, "utf-8"));
|
|
18390
18420
|
const hud = cfg.settings?.hud;
|
|
18391
18421
|
if (hud && "showEnvironmentCounts" in hud) return hud.showEnvironmentCounts !== false;
|
|
18392
18422
|
}
|
|
@@ -18432,10 +18462,10 @@ init_core();
|
|
|
18432
18462
|
init_setup();
|
|
18433
18463
|
init_daemon2();
|
|
18434
18464
|
import { Command } from "commander";
|
|
18435
|
-
import
|
|
18436
|
-
import
|
|
18437
|
-
import
|
|
18438
|
-
import
|
|
18465
|
+
import chalk35 from "chalk";
|
|
18466
|
+
import fs63 from "fs";
|
|
18467
|
+
import path61 from "path";
|
|
18468
|
+
import os55 from "os";
|
|
18439
18469
|
import { spawn as spawn9 } from "child_process";
|
|
18440
18470
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
18441
18471
|
|
|
@@ -18530,6 +18560,10 @@ INSTRUCTIONS:
|
|
|
18530
18560
|
- Do NOT retry this exact command or attempt to bypass the rule.${recovery}
|
|
18531
18561
|
- Inform the user which security rule was triggered and ask how to proceed.`;
|
|
18532
18562
|
}
|
|
18563
|
+
function buildReviewMessage(blockedByLabel, ruleDescription) {
|
|
18564
|
+
const why = ruleDescription || blockedByLabel || "this action needs your review";
|
|
18565
|
+
return `Node9 flagged this for your review: ${why}. Approve to proceed, or deny to cancel.`;
|
|
18566
|
+
}
|
|
18533
18567
|
|
|
18534
18568
|
// src/proxy/index.ts
|
|
18535
18569
|
function sanitize(value) {
|
|
@@ -18660,10 +18694,10 @@ init_daemon();
|
|
|
18660
18694
|
init_config();
|
|
18661
18695
|
init_policy();
|
|
18662
18696
|
import chalk9 from "chalk";
|
|
18663
|
-
import
|
|
18697
|
+
import fs37 from "fs";
|
|
18664
18698
|
import { spawn as spawn5 } from "child_process";
|
|
18665
|
-
import
|
|
18666
|
-
import
|
|
18699
|
+
import path38 from "path";
|
|
18700
|
+
import os33 from "os";
|
|
18667
18701
|
|
|
18668
18702
|
// src/undo.ts
|
|
18669
18703
|
import { spawnSync as spawnSync3, spawn as spawn4 } from "child_process";
|
|
@@ -19201,6 +19235,78 @@ function resolveUserSkillRoot(entry, cwd) {
|
|
|
19201
19235
|
// src/cli/commands/check.ts
|
|
19202
19236
|
init_dlp();
|
|
19203
19237
|
init_audit();
|
|
19238
|
+
|
|
19239
|
+
// src/review-pending.ts
|
|
19240
|
+
init_hasher();
|
|
19241
|
+
import fs36 from "fs";
|
|
19242
|
+
import os32 from "os";
|
|
19243
|
+
import path37 from "path";
|
|
19244
|
+
function storePath() {
|
|
19245
|
+
return process.env.NODE9_PENDING_STORE || path37.join(os32.homedir(), ".node9", "pending-reviews.json");
|
|
19246
|
+
}
|
|
19247
|
+
var TTL_MS2 = 6 * 60 * 60 * 1e3;
|
|
19248
|
+
var MAX_ENTRIES = 500;
|
|
19249
|
+
function reviewCorrelationKey(payload) {
|
|
19250
|
+
if (typeof payload.tool_use_id === "string" && payload.tool_use_id) {
|
|
19251
|
+
return `tuid:${payload.tool_use_id}`;
|
|
19252
|
+
}
|
|
19253
|
+
const sid = payload.session_id ?? payload.conversationId;
|
|
19254
|
+
const tool = payload.tool_name;
|
|
19255
|
+
if (typeof sid === "string" && sid && typeof tool === "string" && tool) {
|
|
19256
|
+
return `h:${sid}|${tool}|${hashArgs(payload.tool_input)}`;
|
|
19257
|
+
}
|
|
19258
|
+
return null;
|
|
19259
|
+
}
|
|
19260
|
+
function read() {
|
|
19261
|
+
try {
|
|
19262
|
+
const parsed = JSON.parse(fs36.readFileSync(storePath(), "utf-8"));
|
|
19263
|
+
if (parsed && Array.isArray(parsed.entries)) return parsed;
|
|
19264
|
+
} catch {
|
|
19265
|
+
}
|
|
19266
|
+
return { entries: [] };
|
|
19267
|
+
}
|
|
19268
|
+
function write(store) {
|
|
19269
|
+
try {
|
|
19270
|
+
const p = storePath();
|
|
19271
|
+
const dir = path37.dirname(p);
|
|
19272
|
+
if (!fs36.existsSync(dir)) fs36.mkdirSync(dir, { recursive: true });
|
|
19273
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
19274
|
+
fs36.writeFileSync(tmp, JSON.stringify(store));
|
|
19275
|
+
fs36.renameSync(tmp, p);
|
|
19276
|
+
} catch {
|
|
19277
|
+
}
|
|
19278
|
+
}
|
|
19279
|
+
function prune(entries, now) {
|
|
19280
|
+
const fresh = entries.filter((e) => now - e.ts < TTL_MS2);
|
|
19281
|
+
return fresh.length > MAX_ENTRIES ? fresh.slice(fresh.length - MAX_ENTRIES) : fresh;
|
|
19282
|
+
}
|
|
19283
|
+
function recordPendingReview(entry) {
|
|
19284
|
+
try {
|
|
19285
|
+
const store = read();
|
|
19286
|
+
store.entries = prune(store.entries, entry.ts);
|
|
19287
|
+
store.entries.push(entry);
|
|
19288
|
+
write(store);
|
|
19289
|
+
} catch {
|
|
19290
|
+
}
|
|
19291
|
+
}
|
|
19292
|
+
function resolvePendingReview(key, now = Date.now()) {
|
|
19293
|
+
try {
|
|
19294
|
+
const store = read();
|
|
19295
|
+
const idx = store.entries.findIndex((e) => e.key === key);
|
|
19296
|
+
if (idx === -1) {
|
|
19297
|
+
const pruned = prune(store.entries, now);
|
|
19298
|
+
if (pruned.length !== store.entries.length) write({ entries: pruned });
|
|
19299
|
+
return null;
|
|
19300
|
+
}
|
|
19301
|
+
const [match] = store.entries.splice(idx, 1);
|
|
19302
|
+
write({ entries: prune(store.entries, now) });
|
|
19303
|
+
return match;
|
|
19304
|
+
} catch {
|
|
19305
|
+
return null;
|
|
19306
|
+
}
|
|
19307
|
+
}
|
|
19308
|
+
|
|
19309
|
+
// src/cli/commands/check.ts
|
|
19204
19310
|
init_hook_payload();
|
|
19205
19311
|
function sanitize2(value) {
|
|
19206
19312
|
return value.replace(/[\x00-\x1F\x7F]/g, "");
|
|
@@ -19249,11 +19355,26 @@ function detectAiAgent(payload) {
|
|
|
19249
19355
|
}
|
|
19250
19356
|
return "Terminal";
|
|
19251
19357
|
}
|
|
19358
|
+
function agentSupportsAsk(agent) {
|
|
19359
|
+
return agent === "Claude Code" || agent === "GitHub Copilot";
|
|
19360
|
+
}
|
|
19361
|
+
function resolveAskMode(agent, opts, config) {
|
|
19362
|
+
if (!agentSupportsAsk(agent)) return false;
|
|
19363
|
+
if (config.settings.approvers.cloud === true) return false;
|
|
19364
|
+
if (opts.ask === true) return true;
|
|
19365
|
+
if (opts.ask === false) return false;
|
|
19366
|
+
if (config.settings.reviewChannel === "ask") return true;
|
|
19367
|
+
if (config.settings.reviewChannel === "approver") return false;
|
|
19368
|
+
return true;
|
|
19369
|
+
}
|
|
19252
19370
|
function registerCheckCommand(program2) {
|
|
19253
19371
|
program2.command("check", { hidden: true }).description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").option(
|
|
19254
19372
|
"--agent <name>",
|
|
19255
19373
|
"Agent identity override, set by node9-authored hook registrations (e.g. antigravity)"
|
|
19256
|
-
).
|
|
19374
|
+
).option(
|
|
19375
|
+
"--ask",
|
|
19376
|
+
"Route review verdicts to the agent\u2019s native inline approve/deny prompt (Claude Code / GitHub Copilot only)"
|
|
19377
|
+
).option("--no-ask", "Force node9\u2019s own approver for review verdicts (override default-on)").action(async (data, opts) => {
|
|
19257
19378
|
const agentOverride = agentLabelFromFlag(opts?.agent);
|
|
19258
19379
|
const processPayload = async (raw) => {
|
|
19259
19380
|
try {
|
|
@@ -19264,9 +19385,9 @@ function registerCheckCommand(program2) {
|
|
|
19264
19385
|
} catch (err2) {
|
|
19265
19386
|
const tempConfig = getConfig();
|
|
19266
19387
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
19267
|
-
const logPath =
|
|
19388
|
+
const logPath = path38.join(os33.homedir(), ".node9", "hook-debug.log");
|
|
19268
19389
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
19269
|
-
|
|
19390
|
+
fs37.appendFileSync(
|
|
19270
19391
|
logPath,
|
|
19271
19392
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
19272
19393
|
RAW: ${raw}
|
|
@@ -19279,14 +19400,14 @@ RAW: ${raw}
|
|
|
19279
19400
|
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
19280
19401
|
if (process.env.NODE9_DEBUG === "1") {
|
|
19281
19402
|
try {
|
|
19282
|
-
const logPath =
|
|
19283
|
-
if (!
|
|
19284
|
-
|
|
19403
|
+
const logPath = path38.join(os33.homedir(), ".node9", "hook-debug.log");
|
|
19404
|
+
if (!fs37.existsSync(path38.dirname(logPath)))
|
|
19405
|
+
fs37.mkdirSync(path38.dirname(logPath), { recursive: true });
|
|
19285
19406
|
const sanitized = JSON.stringify({
|
|
19286
19407
|
...payload,
|
|
19287
19408
|
prompt: `<redacted, ${prompt.length} bytes>`
|
|
19288
19409
|
});
|
|
19289
|
-
|
|
19410
|
+
fs37.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${sanitized}
|
|
19290
19411
|
`);
|
|
19291
19412
|
} catch {
|
|
19292
19413
|
}
|
|
@@ -19306,8 +19427,8 @@ RAW: ${raw}
|
|
|
19306
19427
|
);
|
|
19307
19428
|
const reason = `\u{1F6A8} Node9 DLP: ${dlpMatch.patternName} detected in prompt (${dlpMatch.redactedSample}). Prompt was not submitted \u2014 remove the credential and try again.`;
|
|
19308
19429
|
try {
|
|
19309
|
-
const ttyFd =
|
|
19310
|
-
|
|
19430
|
+
const ttyFd = fs37.openSync("/dev/tty", "w");
|
|
19431
|
+
fs37.writeSync(
|
|
19311
19432
|
ttyFd,
|
|
19312
19433
|
chalk9.bgRed.white.bold(`
|
|
19313
19434
|
\u{1F6A8} NODE9 DLP \u2014 PROMPT BLOCKED
|
|
@@ -19317,7 +19438,7 @@ RAW: ${raw}
|
|
|
19317
19438
|
|
|
19318
19439
|
`)
|
|
19319
19440
|
);
|
|
19320
|
-
|
|
19441
|
+
fs37.closeSync(ttyFd);
|
|
19321
19442
|
} catch {
|
|
19322
19443
|
}
|
|
19323
19444
|
const isCodex = agent2 === "Codex";
|
|
@@ -19336,16 +19457,16 @@ RAW: ${raw}
|
|
|
19336
19457
|
process.exit(2);
|
|
19337
19458
|
}
|
|
19338
19459
|
const payloadCwd = typeof payload.cwd === "string" ? payload.cwd : Array.isArray(payload.workspacePaths) && typeof payload.workspacePaths[0] === "string" ? payload.workspacePaths[0] : void 0;
|
|
19339
|
-
const safeCwdForConfig = typeof payloadCwd === "string" &&
|
|
19460
|
+
const safeCwdForConfig = typeof payloadCwd === "string" && path38.isAbsolute(payloadCwd) ? payloadCwd : void 0;
|
|
19340
19461
|
const config = getConfig(safeCwdForConfig);
|
|
19341
19462
|
if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
|
|
19342
19463
|
try {
|
|
19343
19464
|
const scriptPath = process.argv[1];
|
|
19344
|
-
if (typeof scriptPath !== "string" || !
|
|
19465
|
+
if (typeof scriptPath !== "string" || !path38.isAbsolute(scriptPath))
|
|
19345
19466
|
throw new Error("node9: argv[1] is not an absolute path");
|
|
19346
|
-
const resolvedScript =
|
|
19347
|
-
const packageDist =
|
|
19348
|
-
if (!resolvedScript.startsWith(packageDist +
|
|
19467
|
+
const resolvedScript = fs37.realpathSync(scriptPath);
|
|
19468
|
+
const packageDist = fs37.realpathSync(path38.resolve(__dirname, "../.."));
|
|
19469
|
+
if (!resolvedScript.startsWith(packageDist + path38.sep) && resolvedScript !== packageDist)
|
|
19349
19470
|
throw new Error(
|
|
19350
19471
|
`node9: daemon spawn aborted \u2014 argv[1] (${resolvedScript}) is outside package dist (${packageDist})`
|
|
19351
19472
|
);
|
|
@@ -19367,10 +19488,10 @@ RAW: ${raw}
|
|
|
19367
19488
|
});
|
|
19368
19489
|
d.unref();
|
|
19369
19490
|
} catch (spawnErr) {
|
|
19370
|
-
const logPath =
|
|
19491
|
+
const logPath = path38.join(os33.homedir(), ".node9", "hook-debug.log");
|
|
19371
19492
|
const msg = spawnErr instanceof Error ? spawnErr.message : String(spawnErr);
|
|
19372
19493
|
try {
|
|
19373
|
-
|
|
19494
|
+
fs37.appendFileSync(
|
|
19374
19495
|
logPath,
|
|
19375
19496
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] daemon-autostart-failed: ${msg}
|
|
19376
19497
|
`
|
|
@@ -19380,10 +19501,10 @@ RAW: ${raw}
|
|
|
19380
19501
|
}
|
|
19381
19502
|
}
|
|
19382
19503
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
19383
|
-
const logPath =
|
|
19384
|
-
if (!
|
|
19385
|
-
|
|
19386
|
-
|
|
19504
|
+
const logPath = path38.join(os33.homedir(), ".node9", "hook-debug.log");
|
|
19505
|
+
if (!fs37.existsSync(path38.dirname(logPath)))
|
|
19506
|
+
fs37.mkdirSync(path38.dirname(logPath), { recursive: true });
|
|
19507
|
+
fs37.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
19387
19508
|
`);
|
|
19388
19509
|
}
|
|
19389
19510
|
const rawToolName = sanitize2(extractToolName(payload));
|
|
@@ -19397,8 +19518,8 @@ RAW: ${raw}
|
|
|
19397
19518
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
19398
19519
|
let ttyFd = null;
|
|
19399
19520
|
try {
|
|
19400
|
-
ttyFd =
|
|
19401
|
-
const writeTty = (line) =>
|
|
19521
|
+
ttyFd = fs37.openSync("/dev/tty", "w");
|
|
19522
|
+
const writeTty = (line) => fs37.writeSync(ttyFd, line + "\n");
|
|
19402
19523
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
19403
19524
|
writeTty(chalk9.bgRed.white.bold(`
|
|
19404
19525
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
@@ -19417,7 +19538,7 @@ RAW: ${raw}
|
|
|
19417
19538
|
} finally {
|
|
19418
19539
|
if (ttyFd !== null)
|
|
19419
19540
|
try {
|
|
19420
|
-
|
|
19541
|
+
fs37.closeSync(ttyFd);
|
|
19421
19542
|
} catch {
|
|
19422
19543
|
}
|
|
19423
19544
|
}
|
|
@@ -19456,6 +19577,53 @@ RAW: ${raw}
|
|
|
19456
19577
|
);
|
|
19457
19578
|
process.exit(2);
|
|
19458
19579
|
};
|
|
19580
|
+
const sendAsk = (result2) => {
|
|
19581
|
+
const msg = buildReviewMessage(result2.blockedByLabel, result2.ruleDescription);
|
|
19582
|
+
try {
|
|
19583
|
+
const key = reviewCorrelationKey(payload);
|
|
19584
|
+
if (key) {
|
|
19585
|
+
const sid = typeof payload.session_id === "string" ? payload.session_id : typeof payload.conversationId === "string" ? payload.conversationId : void 0;
|
|
19586
|
+
recordPendingReview({
|
|
19587
|
+
key,
|
|
19588
|
+
agent,
|
|
19589
|
+
tool: toolName,
|
|
19590
|
+
sessionId: sid,
|
|
19591
|
+
ts: Date.now(),
|
|
19592
|
+
label: result2.blockedByLabel
|
|
19593
|
+
});
|
|
19594
|
+
}
|
|
19595
|
+
} catch {
|
|
19596
|
+
}
|
|
19597
|
+
try {
|
|
19598
|
+
const ttyFd = fs37.openSync("/dev/tty", "w");
|
|
19599
|
+
fs37.writeSync(
|
|
19600
|
+
ttyFd,
|
|
19601
|
+
chalk9.yellow(
|
|
19602
|
+
`
|
|
19603
|
+
\u26A0\uFE0F Node9: review requested for "${toolName}" \u2014 answer in the prompt.
|
|
19604
|
+
`
|
|
19605
|
+
)
|
|
19606
|
+
);
|
|
19607
|
+
fs37.closeSync(ttyFd);
|
|
19608
|
+
} catch {
|
|
19609
|
+
}
|
|
19610
|
+
if (agent === "GitHub Copilot") {
|
|
19611
|
+
process.stdout.write(
|
|
19612
|
+
JSON.stringify({ permissionDecision: "ask", permissionDecisionReason: msg }) + "\n"
|
|
19613
|
+
);
|
|
19614
|
+
} else {
|
|
19615
|
+
process.stdout.write(
|
|
19616
|
+
JSON.stringify({
|
|
19617
|
+
hookSpecificOutput: {
|
|
19618
|
+
hookEventName: "PreToolUse",
|
|
19619
|
+
permissionDecision: "ask",
|
|
19620
|
+
permissionDecisionReason: msg
|
|
19621
|
+
}
|
|
19622
|
+
}) + "\n"
|
|
19623
|
+
);
|
|
19624
|
+
}
|
|
19625
|
+
process.exit(0);
|
|
19626
|
+
};
|
|
19459
19627
|
if (!toolName) {
|
|
19460
19628
|
sendBlock("Node9: unrecognised hook payload \u2014 tool name missing.");
|
|
19461
19629
|
return;
|
|
@@ -19468,17 +19636,17 @@ RAW: ${raw}
|
|
|
19468
19636
|
const safeSessionId = /^[A-Za-z0-9_\-]{1,128}$/.test(rawSessionId) ? rawSessionId : "";
|
|
19469
19637
|
if (skillPinCfg.enabled && safeSessionId) {
|
|
19470
19638
|
try {
|
|
19471
|
-
const sessionsDir =
|
|
19472
|
-
const flagPath =
|
|
19639
|
+
const sessionsDir = path38.join(os33.homedir(), ".node9", "skill-sessions");
|
|
19640
|
+
const flagPath = path38.join(sessionsDir, `${safeSessionId}.json`);
|
|
19473
19641
|
let flag = null;
|
|
19474
19642
|
try {
|
|
19475
|
-
flag = JSON.parse(
|
|
19643
|
+
flag = JSON.parse(fs37.readFileSync(flagPath, "utf-8"));
|
|
19476
19644
|
} catch {
|
|
19477
19645
|
}
|
|
19478
19646
|
const writeFlag = (data2) => {
|
|
19479
19647
|
try {
|
|
19480
|
-
|
|
19481
|
-
|
|
19648
|
+
fs37.mkdirSync(sessionsDir, { recursive: true });
|
|
19649
|
+
fs37.writeFileSync(
|
|
19482
19650
|
flagPath,
|
|
19483
19651
|
JSON.stringify({ ...data2, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
|
|
19484
19652
|
{ mode: 384 }
|
|
@@ -19489,8 +19657,8 @@ RAW: ${raw}
|
|
|
19489
19657
|
const sendSkillWarn = (detail, recoveryCmd) => {
|
|
19490
19658
|
let ttyFd = null;
|
|
19491
19659
|
try {
|
|
19492
|
-
ttyFd =
|
|
19493
|
-
const w = (line) =>
|
|
19660
|
+
ttyFd = fs37.openSync("/dev/tty", "w");
|
|
19661
|
+
const w = (line) => fs37.writeSync(ttyFd, line + "\n");
|
|
19494
19662
|
w(chalk9.yellow(`
|
|
19495
19663
|
\u26A0\uFE0F Node9: installed skill drift detected`));
|
|
19496
19664
|
w(chalk9.gray(` ${detail}`));
|
|
@@ -19505,7 +19673,7 @@ RAW: ${raw}
|
|
|
19505
19673
|
} finally {
|
|
19506
19674
|
if (ttyFd !== null)
|
|
19507
19675
|
try {
|
|
19508
|
-
|
|
19676
|
+
fs37.closeSync(ttyFd);
|
|
19509
19677
|
} catch {
|
|
19510
19678
|
}
|
|
19511
19679
|
}
|
|
@@ -19521,7 +19689,7 @@ RAW: ${raw}
|
|
|
19521
19689
|
return;
|
|
19522
19690
|
}
|
|
19523
19691
|
if (!flag || flag.state !== "verified" && flag.state !== "warned") {
|
|
19524
|
-
const absoluteCwd = typeof payloadCwd === "string" &&
|
|
19692
|
+
const absoluteCwd = typeof payloadCwd === "string" && path38.isAbsolute(payloadCwd) ? payloadCwd : void 0;
|
|
19525
19693
|
const extraRoots = skillPinCfg.roots;
|
|
19526
19694
|
const resolvedExtra = extraRoots.map((r) => resolveUserSkillRoot(r, absoluteCwd)).filter((r) => typeof r === "string");
|
|
19527
19695
|
const roots = [...defaultSkillRoots(absoluteCwd), ...resolvedExtra];
|
|
@@ -19562,10 +19730,10 @@ RAW: ${raw}
|
|
|
19562
19730
|
}
|
|
19563
19731
|
try {
|
|
19564
19732
|
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
19565
|
-
for (const name of
|
|
19566
|
-
const p =
|
|
19733
|
+
for (const name of fs37.readdirSync(sessionsDir)) {
|
|
19734
|
+
const p = path38.join(sessionsDir, name);
|
|
19567
19735
|
try {
|
|
19568
|
-
if (
|
|
19736
|
+
if (fs37.statSync(p).mtimeMs < cutoff) fs37.unlinkSync(p);
|
|
19569
19737
|
} catch {
|
|
19570
19738
|
}
|
|
19571
19739
|
}
|
|
@@ -19575,9 +19743,9 @@ RAW: ${raw}
|
|
|
19575
19743
|
} catch (err2) {
|
|
19576
19744
|
if (process.env.NODE9_DEBUG === "1") {
|
|
19577
19745
|
try {
|
|
19578
|
-
const dbg =
|
|
19746
|
+
const dbg = path38.join(os33.homedir(), ".node9", "hook-debug.log");
|
|
19579
19747
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
19580
|
-
|
|
19748
|
+
fs37.appendFileSync(dbg, `[${(/* @__PURE__ */ new Date()).toISOString()}] SKILL_PIN_ERROR: ${msg}
|
|
19581
19749
|
`);
|
|
19582
19750
|
} catch {
|
|
19583
19751
|
}
|
|
@@ -19587,9 +19755,11 @@ RAW: ${raw}
|
|
|
19587
19755
|
if (shouldSnapshot(toolName, toolInput, config)) {
|
|
19588
19756
|
await createShadowSnapshot(toolName, toolInput, config.policy.snapshot.ignorePaths);
|
|
19589
19757
|
}
|
|
19590
|
-
const safeCwdForAuth = typeof payloadCwd === "string" &&
|
|
19758
|
+
const safeCwdForAuth = typeof payloadCwd === "string" && path38.isAbsolute(payloadCwd) ? payloadCwd : void 0;
|
|
19759
|
+
const askMode = resolveAskMode(agent, opts, config);
|
|
19591
19760
|
const result = await authorizeHeadless(toolName, toolInput, meta, {
|
|
19592
|
-
cwd: safeCwdForAuth
|
|
19761
|
+
cwd: safeCwdForAuth,
|
|
19762
|
+
deferReview: askMode
|
|
19593
19763
|
});
|
|
19594
19764
|
if (result.approved) {
|
|
19595
19765
|
if (result.checkedBy && process.env.NODE9_DEBUG === "1")
|
|
@@ -19597,14 +19767,18 @@ RAW: ${raw}
|
|
|
19597
19767
|
`);
|
|
19598
19768
|
process.exit(0);
|
|
19599
19769
|
}
|
|
19770
|
+
if (result.review) {
|
|
19771
|
+
sendAsk(result);
|
|
19772
|
+
return;
|
|
19773
|
+
}
|
|
19600
19774
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
19601
19775
|
try {
|
|
19602
|
-
const tty =
|
|
19603
|
-
|
|
19776
|
+
const tty = fs37.openSync("/dev/tty", "w");
|
|
19777
|
+
fs37.writeSync(
|
|
19604
19778
|
tty,
|
|
19605
19779
|
chalk9.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically...\n")
|
|
19606
19780
|
);
|
|
19607
|
-
|
|
19781
|
+
fs37.closeSync(tty);
|
|
19608
19782
|
} catch {
|
|
19609
19783
|
}
|
|
19610
19784
|
const daemonReady = await autoStartDaemonAndWait();
|
|
@@ -19631,9 +19805,9 @@ RAW: ${raw}
|
|
|
19631
19805
|
});
|
|
19632
19806
|
} catch (err2) {
|
|
19633
19807
|
if (process.env.NODE9_DEBUG === "1") {
|
|
19634
|
-
const logPath =
|
|
19808
|
+
const logPath = path38.join(os33.homedir(), ".node9", "hook-debug.log");
|
|
19635
19809
|
const errMsg = err2 instanceof Error ? err2.message : String(err2);
|
|
19636
|
-
|
|
19810
|
+
fs37.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
19637
19811
|
`);
|
|
19638
19812
|
}
|
|
19639
19813
|
process.exit(0);
|
|
@@ -19669,9 +19843,9 @@ RAW: ${raw}
|
|
|
19669
19843
|
// src/cli/commands/log.ts
|
|
19670
19844
|
init_audit();
|
|
19671
19845
|
init_config();
|
|
19672
|
-
import
|
|
19673
|
-
import
|
|
19674
|
-
import
|
|
19846
|
+
import fs38 from "fs";
|
|
19847
|
+
import path39 from "path";
|
|
19848
|
+
import os34 from "os";
|
|
19675
19849
|
init_daemon();
|
|
19676
19850
|
init_dlp();
|
|
19677
19851
|
|
|
@@ -19762,21 +19936,27 @@ function registerLogCommand(program2) {
|
|
|
19762
19936
|
return void 0;
|
|
19763
19937
|
})();
|
|
19764
19938
|
const agent = agentOverride !== void 0 ? agentOverride : metaTag !== void 0 ? metaTag : payload.turn_id !== void 0 ? "Codex" : payload.toolCall !== void 0 || payload.conversationId !== void 0 ? "Antigravity" : payload.hook_event_name === "pre_tool_call" || payload.hook_event_name === "post_tool_call" ? "Hermes" : payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0 ? "Claude Code" : payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0 ? "Gemini CLI" : process.env.HERMES_SESSION_ID || process.env.HERMES_HOME || process.env.HERMES_INTERACTIVE ? "Hermes" : process.env.ANTIGRAVITY_CONVERSATION_ID ? "Antigravity" : void 0;
|
|
19939
|
+
let reviewApproved = false;
|
|
19940
|
+
try {
|
|
19941
|
+
const key = reviewCorrelationKey(payload);
|
|
19942
|
+
if (key && resolvePendingReview(key)) reviewApproved = true;
|
|
19943
|
+
} catch {
|
|
19944
|
+
}
|
|
19765
19945
|
const entry = {
|
|
19766
19946
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19767
19947
|
tool,
|
|
19768
19948
|
args: JSON.parse(redactSecrets(JSON.stringify(rawInput))),
|
|
19769
19949
|
decision: "allowed",
|
|
19770
|
-
source: "post-hook"
|
|
19950
|
+
source: reviewApproved ? "inline-review-approved" : "post-hook"
|
|
19771
19951
|
};
|
|
19772
19952
|
if (agent) entry.agent = agent;
|
|
19773
19953
|
if (rawToolName !== tool) entry.agentToolName = rawToolName;
|
|
19774
19954
|
const payloadSessionId = payload.session_id ?? payload.conversationId;
|
|
19775
19955
|
if (payloadSessionId) entry.sessionId = payloadSessionId;
|
|
19776
|
-
const logPath =
|
|
19777
|
-
if (!
|
|
19778
|
-
|
|
19779
|
-
|
|
19956
|
+
const logPath = path39.join(os34.homedir(), ".node9", "audit.log");
|
|
19957
|
+
if (!fs38.existsSync(path39.dirname(logPath)))
|
|
19958
|
+
fs38.mkdirSync(path39.dirname(logPath), { recursive: true });
|
|
19959
|
+
fs38.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
19780
19960
|
if ((tool === "Bash" || tool === "bash") && isDaemonRunning()) {
|
|
19781
19961
|
const command = typeof rawInput === "object" && rawInput !== null && "command" in rawInput && typeof rawInput.command === "string" ? rawInput.command : null;
|
|
19782
19962
|
if (command) {
|
|
@@ -19810,7 +19990,7 @@ function registerLogCommand(program2) {
|
|
|
19810
19990
|
}
|
|
19811
19991
|
}
|
|
19812
19992
|
const payloadCwd = typeof payload.cwd === "string" ? payload.cwd : Array.isArray(payload.workspacePaths) && typeof payload.workspacePaths[0] === "string" ? payload.workspacePaths[0] : void 0;
|
|
19813
|
-
const safeCwd = typeof payloadCwd === "string" &&
|
|
19993
|
+
const safeCwd = typeof payloadCwd === "string" && path39.isAbsolute(payloadCwd) ? payloadCwd : void 0;
|
|
19814
19994
|
const config = getConfig(safeCwd);
|
|
19815
19995
|
{
|
|
19816
19996
|
const toolOutput = payload.tool_response?.output;
|
|
@@ -19887,9 +20067,9 @@ function registerLogCommand(program2) {
|
|
|
19887
20067
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
19888
20068
|
process.stderr.write(`[Node9] audit log error: ${msg}
|
|
19889
20069
|
`);
|
|
19890
|
-
const debugPath =
|
|
20070
|
+
const debugPath = path39.join(os34.homedir(), ".node9", "hook-debug.log");
|
|
19891
20071
|
try {
|
|
19892
|
-
|
|
20072
|
+
fs38.appendFileSync(debugPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] LOG_ERROR: ${msg}
|
|
19893
20073
|
`);
|
|
19894
20074
|
} catch {
|
|
19895
20075
|
}
|
|
@@ -20291,22 +20471,22 @@ function registerConfigShowCommand(program2) {
|
|
|
20291
20471
|
init_daemon();
|
|
20292
20472
|
init_config();
|
|
20293
20473
|
import chalk11 from "chalk";
|
|
20294
|
-
import
|
|
20295
|
-
import
|
|
20296
|
-
import
|
|
20474
|
+
import fs40 from "fs";
|
|
20475
|
+
import path41 from "path";
|
|
20476
|
+
import os36 from "os";
|
|
20297
20477
|
import { execSync } from "child_process";
|
|
20298
20478
|
|
|
20299
20479
|
// src/agent-wiring.ts
|
|
20300
20480
|
init_setup();
|
|
20301
|
-
import
|
|
20302
|
-
import
|
|
20303
|
-
import
|
|
20481
|
+
import fs39 from "fs";
|
|
20482
|
+
import path40 from "path";
|
|
20483
|
+
import os35 from "os";
|
|
20304
20484
|
import * as yaml2 from "yaml";
|
|
20305
20485
|
import { parse as parseToml2 } from "smol-toml";
|
|
20306
20486
|
function readJson2(filePath) {
|
|
20307
|
-
if (!
|
|
20487
|
+
if (!fs39.existsSync(filePath)) return null;
|
|
20308
20488
|
try {
|
|
20309
|
-
return JSON.parse(
|
|
20489
|
+
return JSON.parse(fs39.readFileSync(filePath, "utf-8"));
|
|
20310
20490
|
} catch {
|
|
20311
20491
|
return "invalid";
|
|
20312
20492
|
}
|
|
@@ -20318,10 +20498,10 @@ function flatHaveNode9Hook(entries) {
|
|
|
20318
20498
|
return (Array.isArray(entries) ? entries : []).some((h) => isNode9Hook(h.command));
|
|
20319
20499
|
}
|
|
20320
20500
|
function readHookRoot(filePath, format) {
|
|
20321
|
-
if (!
|
|
20501
|
+
if (!fs39.existsSync(filePath)) return "absent";
|
|
20322
20502
|
let raw;
|
|
20323
20503
|
try {
|
|
20324
|
-
raw =
|
|
20504
|
+
raw = fs39.readFileSync(filePath, "utf-8");
|
|
20325
20505
|
} catch {
|
|
20326
20506
|
return "absent";
|
|
20327
20507
|
}
|
|
@@ -20344,10 +20524,10 @@ function detectMcp(servers) {
|
|
|
20344
20524
|
return { wrapped, present };
|
|
20345
20525
|
}
|
|
20346
20526
|
function readMcp(filePath, format) {
|
|
20347
|
-
if (!
|
|
20527
|
+
if (!fs39.existsSync(filePath)) return { wrapped: [], present: false };
|
|
20348
20528
|
try {
|
|
20349
20529
|
if (format === "toml") {
|
|
20350
|
-
const parsed2 = parseToml2(
|
|
20530
|
+
const parsed2 = parseToml2(fs39.readFileSync(filePath, "utf-8"));
|
|
20351
20531
|
return detectMcp(parsed2?.mcp_servers);
|
|
20352
20532
|
}
|
|
20353
20533
|
const parsed = readJson2(filePath);
|
|
@@ -20359,7 +20539,7 @@ function readMcp(filePath, format) {
|
|
|
20359
20539
|
}
|
|
20360
20540
|
var exists = (p) => {
|
|
20361
20541
|
try {
|
|
20362
|
-
return
|
|
20542
|
+
return fs39.existsSync(p);
|
|
20363
20543
|
} catch {
|
|
20364
20544
|
return false;
|
|
20365
20545
|
}
|
|
@@ -20373,52 +20553,52 @@ var AGENT_SPECS = [
|
|
|
20373
20553
|
id: "claude",
|
|
20374
20554
|
label: "Claude Code",
|
|
20375
20555
|
setupCommand: "node9 agents add claude",
|
|
20376
|
-
hookFile: (h) =>
|
|
20556
|
+
hookFile: (h) => path40.join(h, ".claude", "settings.json"),
|
|
20377
20557
|
hookFormat: "matcher",
|
|
20378
20558
|
hookEvents: [ck("PreToolUse"), lg("PostToolUse")],
|
|
20379
|
-
mcpFile: (h) =>
|
|
20380
|
-
present: (h) => exists(
|
|
20559
|
+
mcpFile: (h) => path40.join(h, ".claude.json"),
|
|
20560
|
+
present: (h) => exists(path40.join(h, ".claude", "settings.json")) || exists(path40.join(h, ".claude.json"))
|
|
20381
20561
|
},
|
|
20382
20562
|
{
|
|
20383
20563
|
id: "gemini",
|
|
20384
20564
|
label: "Gemini CLI",
|
|
20385
20565
|
setupCommand: "node9 agents add gemini",
|
|
20386
|
-
hookFile: (h) =>
|
|
20566
|
+
hookFile: (h) => path40.join(h, ".gemini", "settings.json"),
|
|
20387
20567
|
hookFormat: "matcher",
|
|
20388
20568
|
hookEvents: [ck("BeforeTool"), lg("AfterTool")],
|
|
20389
|
-
mcpFile: (h) =>
|
|
20390
|
-
present: (h) => exists(
|
|
20569
|
+
mcpFile: (h) => path40.join(h, ".gemini", "settings.json"),
|
|
20570
|
+
present: (h) => exists(path40.join(h, ".gemini", "settings.json"))
|
|
20391
20571
|
},
|
|
20392
20572
|
{
|
|
20393
20573
|
id: "codex",
|
|
20394
20574
|
label: "Codex",
|
|
20395
20575
|
setupCommand: "node9 agents add codex",
|
|
20396
|
-
hookFile: (h) =>
|
|
20576
|
+
hookFile: (h) => path40.join(h, ".codex", "hooks.json"),
|
|
20397
20577
|
hookFormat: "matcher",
|
|
20398
20578
|
hookEvents: [ck("PreToolUse"), ck("UserPromptSubmit")],
|
|
20399
|
-
mcpFile: (h) =>
|
|
20579
|
+
mcpFile: (h) => path40.join(h, ".codex", "config.toml"),
|
|
20400
20580
|
mcpFormat: "toml",
|
|
20401
|
-
present: (h) => exists(
|
|
20581
|
+
present: (h) => exists(path40.join(h, ".codex"))
|
|
20402
20582
|
},
|
|
20403
20583
|
{
|
|
20404
20584
|
id: "antigravity",
|
|
20405
20585
|
label: "Antigravity",
|
|
20406
20586
|
setupCommand: "node9 agents add antigravity",
|
|
20407
|
-
hookFile: (h) =>
|
|
20587
|
+
hookFile: (h) => path40.join(h, ".gemini", "config", "hooks.json"),
|
|
20408
20588
|
hookFormat: "matcher",
|
|
20409
20589
|
hookEvents: [ck("PreToolUse"), lg("PostToolUse")],
|
|
20410
|
-
mcpFile: (h) =>
|
|
20411
|
-
present: (h) => exists(
|
|
20590
|
+
mcpFile: (h) => path40.join(h, ".gemini", "config", "mcp_config.json"),
|
|
20591
|
+
present: (h) => exists(path40.join(h, ".gemini", "config", "hooks.json")) || exists(path40.join(h, ".gemini", "antigravity-cli")) || exists(path40.join(h, ".gemini", "antigravity-ide"))
|
|
20412
20592
|
},
|
|
20413
20593
|
{
|
|
20414
20594
|
id: "copilot",
|
|
20415
20595
|
label: "GitHub Copilot",
|
|
20416
20596
|
setupCommand: "node9 agents add copilot",
|
|
20417
|
-
hookFile: (h) =>
|
|
20597
|
+
hookFile: (h) => path40.join(h, ".copilot", "hooks", "node9.json"),
|
|
20418
20598
|
hookFormat: "flat",
|
|
20419
20599
|
hookEvents: [ck("PreToolUse"), lg("PostToolUse"), ck("UserPromptSubmit")],
|
|
20420
|
-
mcpFile: (h) =>
|
|
20421
|
-
present: (h) => exists(
|
|
20600
|
+
mcpFile: (h) => path40.join(h, ".copilot", "mcp-config.json"),
|
|
20601
|
+
present: (h) => exists(path40.join(h, ".copilot"))
|
|
20422
20602
|
},
|
|
20423
20603
|
{
|
|
20424
20604
|
id: "cursor",
|
|
@@ -20427,8 +20607,8 @@ var AGENT_SPECS = [
|
|
|
20427
20607
|
// MCP-only — no hook file (see note above).
|
|
20428
20608
|
hookFormat: "flat",
|
|
20429
20609
|
hookEvents: [],
|
|
20430
|
-
mcpFile: (h) =>
|
|
20431
|
-
present: (h) => exists(
|
|
20610
|
+
mcpFile: (h) => path40.join(h, ".cursor", "mcp.json"),
|
|
20611
|
+
present: (h) => exists(path40.join(h, ".cursor", "mcp.json"))
|
|
20432
20612
|
},
|
|
20433
20613
|
{
|
|
20434
20614
|
id: "hermes",
|
|
@@ -20449,8 +20629,8 @@ var AGENT_SPECS = [
|
|
|
20449
20629
|
setupCommand: "node9 agents add opencode",
|
|
20450
20630
|
hookFormat: "flat",
|
|
20451
20631
|
hookEvents: [],
|
|
20452
|
-
shimFile: (h) =>
|
|
20453
|
-
present: (h) => exists(
|
|
20632
|
+
shimFile: (h) => path40.join(h, ".config", "opencode", "plugins", "node9.js"),
|
|
20633
|
+
present: (h) => exists(path40.join(h, ".config", "opencode")) || exists(path40.join(h, ".config", "opencode", "plugins", "node9.js"))
|
|
20454
20634
|
},
|
|
20455
20635
|
{
|
|
20456
20636
|
id: "pi",
|
|
@@ -20458,11 +20638,11 @@ var AGENT_SPECS = [
|
|
|
20458
20638
|
setupCommand: "node9 agents add pi",
|
|
20459
20639
|
hookFormat: "flat",
|
|
20460
20640
|
hookEvents: [],
|
|
20461
|
-
shimFile: (h) =>
|
|
20462
|
-
present: (h) => exists(
|
|
20641
|
+
shimFile: (h) => path40.join(h, ".pi", "agent", "extensions", "node9.js"),
|
|
20642
|
+
present: (h) => exists(path40.join(h, ".pi", "agent")) || exists(path40.join(h, ".pi", "agent", "extensions", "node9.js"))
|
|
20463
20643
|
}
|
|
20464
20644
|
];
|
|
20465
|
-
function getAgentWiring(home =
|
|
20645
|
+
function getAgentWiring(home = os35.homedir()) {
|
|
20466
20646
|
const detected = detectAgents(home);
|
|
20467
20647
|
return AGENT_SPECS.map((spec) => {
|
|
20468
20648
|
const present = spec.present(home);
|
|
@@ -20514,7 +20694,7 @@ function getAgentWiring(home = os34.homedir()) {
|
|
|
20514
20694
|
// src/cli/commands/doctor.ts
|
|
20515
20695
|
function registerDoctorCommand(program2, version2) {
|
|
20516
20696
|
program2.command("doctor").description("Check that Node9 is installed and configured correctly").action(async () => {
|
|
20517
|
-
const homeDir2 =
|
|
20697
|
+
const homeDir2 = os36.homedir();
|
|
20518
20698
|
let failures = 0;
|
|
20519
20699
|
function pass(msg) {
|
|
20520
20700
|
console.log(chalk11.green(" \u2705 ") + msg);
|
|
@@ -20560,10 +20740,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
20560
20740
|
);
|
|
20561
20741
|
}
|
|
20562
20742
|
section("Configuration");
|
|
20563
|
-
const globalConfigPath =
|
|
20564
|
-
if (
|
|
20743
|
+
const globalConfigPath = path41.join(homeDir2, ".node9", "config.json");
|
|
20744
|
+
if (fs40.existsSync(globalConfigPath)) {
|
|
20565
20745
|
try {
|
|
20566
|
-
JSON.parse(
|
|
20746
|
+
JSON.parse(fs40.readFileSync(globalConfigPath, "utf-8"));
|
|
20567
20747
|
pass("~/.node9/config.json found and valid");
|
|
20568
20748
|
} catch {
|
|
20569
20749
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -20571,10 +20751,10 @@ function registerDoctorCommand(program2, version2) {
|
|
|
20571
20751
|
} else {
|
|
20572
20752
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
20573
20753
|
}
|
|
20574
|
-
const projectConfigPath =
|
|
20575
|
-
if (
|
|
20754
|
+
const projectConfigPath = path41.join(process.cwd(), "node9.config.json");
|
|
20755
|
+
if (fs40.existsSync(projectConfigPath)) {
|
|
20576
20756
|
try {
|
|
20577
|
-
JSON.parse(
|
|
20757
|
+
JSON.parse(fs40.readFileSync(projectConfigPath, "utf-8"));
|
|
20578
20758
|
pass("node9.config.json found and valid (project)");
|
|
20579
20759
|
} catch {
|
|
20580
20760
|
fail(
|
|
@@ -20583,8 +20763,8 @@ function registerDoctorCommand(program2, version2) {
|
|
|
20583
20763
|
);
|
|
20584
20764
|
}
|
|
20585
20765
|
}
|
|
20586
|
-
const credsPath =
|
|
20587
|
-
if (
|
|
20766
|
+
const credsPath = path41.join(homeDir2, ".node9", "credentials.json");
|
|
20767
|
+
if (fs40.existsSync(credsPath)) {
|
|
20588
20768
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
20589
20769
|
} else {
|
|
20590
20770
|
warn(
|
|
@@ -20628,7 +20808,7 @@ function registerDoctorCommand(program2, version2) {
|
|
|
20628
20808
|
try {
|
|
20629
20809
|
const { shipLagBytes: shipLagBytes2, readWatermark: readWatermark2, AUDIT_SHIP_WATERMARK: AUDIT_SHIP_WATERMARK2 } = await Promise.resolve().then(() => (init_audit_shipper(), audit_shipper_exports));
|
|
20630
20810
|
const cfg = getConfig();
|
|
20631
|
-
const creds =
|
|
20811
|
+
const creds = fs40.existsSync(path41.join(os36.homedir(), ".node9", "credentials.json"));
|
|
20632
20812
|
if (!creds) {
|
|
20633
20813
|
warn("Not logged in \u2014 audit rows stay local", "Run: node9 login <api-key>");
|
|
20634
20814
|
} else if (!cfg.settings.approvers.cloud) {
|
|
@@ -20678,9 +20858,9 @@ function registerDoctorCommand(program2, version2) {
|
|
|
20678
20858
|
|
|
20679
20859
|
// src/cli/commands/audit.ts
|
|
20680
20860
|
import chalk12 from "chalk";
|
|
20681
|
-
import
|
|
20682
|
-
import
|
|
20683
|
-
import
|
|
20861
|
+
import fs41 from "fs";
|
|
20862
|
+
import path42 from "path";
|
|
20863
|
+
import os37 from "os";
|
|
20684
20864
|
function formatRelativeTime(timestamp) {
|
|
20685
20865
|
const diff = Date.now() - new Date(timestamp).getTime();
|
|
20686
20866
|
const sec = Math.floor(diff / 1e3);
|
|
@@ -20693,14 +20873,14 @@ function formatRelativeTime(timestamp) {
|
|
|
20693
20873
|
}
|
|
20694
20874
|
function registerAuditCommand(program2) {
|
|
20695
20875
|
program2.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
|
|
20696
|
-
const logPath =
|
|
20697
|
-
if (!
|
|
20876
|
+
const logPath = path42.join(os37.homedir(), ".node9", "audit.log");
|
|
20877
|
+
if (!fs41.existsSync(logPath)) {
|
|
20698
20878
|
console.log(
|
|
20699
20879
|
chalk12.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
20700
20880
|
);
|
|
20701
20881
|
return;
|
|
20702
20882
|
}
|
|
20703
|
-
const raw =
|
|
20883
|
+
const raw = fs41.readFileSync(logPath, "utf-8");
|
|
20704
20884
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
20705
20885
|
let entries = lines.flatMap((line) => {
|
|
20706
20886
|
try {
|
|
@@ -20759,9 +20939,9 @@ import chalk13 from "chalk";
|
|
|
20759
20939
|
init_costSync();
|
|
20760
20940
|
init_litellm();
|
|
20761
20941
|
init_cost_codex();
|
|
20762
|
-
import
|
|
20763
|
-
import
|
|
20764
|
-
import
|
|
20942
|
+
import fs42 from "fs";
|
|
20943
|
+
import os38 from "os";
|
|
20944
|
+
import path43 from "path";
|
|
20765
20945
|
var TEST_COMMAND_RE3 = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
|
|
20766
20946
|
function buildTestTimestamps(allEntries) {
|
|
20767
20947
|
const testTs = /* @__PURE__ */ new Set();
|
|
@@ -20841,8 +21021,8 @@ function getDateRange(period, now) {
|
|
|
20841
21021
|
}
|
|
20842
21022
|
}
|
|
20843
21023
|
function parseAuditLog(logPath) {
|
|
20844
|
-
if (!
|
|
20845
|
-
const raw =
|
|
21024
|
+
if (!fs42.existsSync(logPath)) return [];
|
|
21025
|
+
const raw = fs42.readFileSync(logPath, "utf-8");
|
|
20846
21026
|
return raw.split("\n").flatMap((line) => {
|
|
20847
21027
|
if (!line.trim()) return [];
|
|
20848
21028
|
try {
|
|
@@ -20889,25 +21069,25 @@ function freezeClaudeCost(acc) {
|
|
|
20889
21069
|
};
|
|
20890
21070
|
}
|
|
20891
21071
|
function processClaudeCostProject(proj, projectsDir, start, end, acc) {
|
|
20892
|
-
const projPath =
|
|
21072
|
+
const projPath = path43.join(projectsDir, proj);
|
|
20893
21073
|
let files;
|
|
20894
21074
|
try {
|
|
20895
|
-
const stat =
|
|
21075
|
+
const stat = fs42.statSync(projPath);
|
|
20896
21076
|
if (!stat.isDirectory()) return;
|
|
20897
|
-
files =
|
|
21077
|
+
files = fs42.readdirSync(projPath).filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"));
|
|
20898
21078
|
} catch {
|
|
20899
21079
|
return;
|
|
20900
21080
|
}
|
|
20901
21081
|
const startMs = start.getTime();
|
|
20902
21082
|
for (const file of files) {
|
|
20903
|
-
const filePath =
|
|
21083
|
+
const filePath = path43.join(projPath, file);
|
|
20904
21084
|
try {
|
|
20905
|
-
if (
|
|
21085
|
+
if (fs42.statSync(filePath).mtimeMs < startMs) continue;
|
|
20906
21086
|
} catch {
|
|
20907
21087
|
continue;
|
|
20908
21088
|
}
|
|
20909
21089
|
try {
|
|
20910
|
-
const raw =
|
|
21090
|
+
const raw = fs42.readFileSync(filePath, "utf-8");
|
|
20911
21091
|
for (const line of raw.split("\n")) {
|
|
20912
21092
|
if (!line.trim()) continue;
|
|
20913
21093
|
let entry;
|
|
@@ -20957,10 +21137,10 @@ function processClaudeCostProject(proj, projectsDir, start, end, acc) {
|
|
|
20957
21137
|
}
|
|
20958
21138
|
function loadClaudeCost(start, end, projectsDir) {
|
|
20959
21139
|
const acc = emptyClaudeCostAccumulator();
|
|
20960
|
-
if (!
|
|
21140
|
+
if (!fs42.existsSync(projectsDir)) return freezeClaudeCost(acc);
|
|
20961
21141
|
let dirs;
|
|
20962
21142
|
try {
|
|
20963
|
-
dirs =
|
|
21143
|
+
dirs = fs42.readdirSync(projectsDir);
|
|
20964
21144
|
} catch {
|
|
20965
21145
|
return freezeClaudeCost(acc);
|
|
20966
21146
|
}
|
|
@@ -20972,7 +21152,7 @@ function loadClaudeCost(start, end, projectsDir) {
|
|
|
20972
21152
|
function processCodexCostFile(filePath, start, end, acc) {
|
|
20973
21153
|
let lines;
|
|
20974
21154
|
try {
|
|
20975
|
-
lines =
|
|
21155
|
+
lines = fs42.readFileSync(filePath, "utf-8").split("\n");
|
|
20976
21156
|
} catch {
|
|
20977
21157
|
return;
|
|
20978
21158
|
}
|
|
@@ -21027,31 +21207,31 @@ function processCodexCostFile(filePath, start, end, acc) {
|
|
|
21027
21207
|
}
|
|
21028
21208
|
function listCodexSessionFiles2(sessionsBase) {
|
|
21029
21209
|
const jsonlFiles = [];
|
|
21030
|
-
if (!
|
|
21210
|
+
if (!fs42.existsSync(sessionsBase)) return jsonlFiles;
|
|
21031
21211
|
try {
|
|
21032
|
-
for (const year of
|
|
21033
|
-
const yearPath =
|
|
21212
|
+
for (const year of fs42.readdirSync(sessionsBase)) {
|
|
21213
|
+
const yearPath = path43.join(sessionsBase, year);
|
|
21034
21214
|
try {
|
|
21035
|
-
if (!
|
|
21215
|
+
if (!fs42.statSync(yearPath).isDirectory()) continue;
|
|
21036
21216
|
} catch {
|
|
21037
21217
|
continue;
|
|
21038
21218
|
}
|
|
21039
|
-
for (const month of
|
|
21040
|
-
const monthPath =
|
|
21219
|
+
for (const month of fs42.readdirSync(yearPath)) {
|
|
21220
|
+
const monthPath = path43.join(yearPath, month);
|
|
21041
21221
|
try {
|
|
21042
|
-
if (!
|
|
21222
|
+
if (!fs42.statSync(monthPath).isDirectory()) continue;
|
|
21043
21223
|
} catch {
|
|
21044
21224
|
continue;
|
|
21045
21225
|
}
|
|
21046
|
-
for (const day of
|
|
21047
|
-
const dayPath =
|
|
21226
|
+
for (const day of fs42.readdirSync(monthPath)) {
|
|
21227
|
+
const dayPath = path43.join(monthPath, day);
|
|
21048
21228
|
try {
|
|
21049
|
-
if (!
|
|
21229
|
+
if (!fs42.statSync(dayPath).isDirectory()) continue;
|
|
21050
21230
|
} catch {
|
|
21051
21231
|
continue;
|
|
21052
21232
|
}
|
|
21053
|
-
for (const file of
|
|
21054
|
-
if (file.endsWith(".jsonl")) jsonlFiles.push(
|
|
21233
|
+
for (const file of fs42.readdirSync(dayPath)) {
|
|
21234
|
+
if (file.endsWith(".jsonl")) jsonlFiles.push(path43.join(dayPath, file));
|
|
21055
21235
|
}
|
|
21056
21236
|
}
|
|
21057
21237
|
}
|
|
@@ -21116,13 +21296,13 @@ function freezeGeminiCost(acc) {
|
|
|
21116
21296
|
function processGeminiCostFile(filePath, projectKey, start, end, acc) {
|
|
21117
21297
|
const startMs = start.getTime();
|
|
21118
21298
|
try {
|
|
21119
|
-
if (
|
|
21299
|
+
if (fs42.statSync(filePath).mtimeMs < startMs) return;
|
|
21120
21300
|
} catch {
|
|
21121
21301
|
return;
|
|
21122
21302
|
}
|
|
21123
21303
|
let raw;
|
|
21124
21304
|
try {
|
|
21125
|
-
raw =
|
|
21305
|
+
raw = fs42.readFileSync(filePath, "utf-8");
|
|
21126
21306
|
} catch {
|
|
21127
21307
|
return;
|
|
21128
21308
|
}
|
|
@@ -21171,30 +21351,30 @@ function listGeminiSessionFiles2(geminiTmpDir2) {
|
|
|
21171
21351
|
const out = [];
|
|
21172
21352
|
let dirs;
|
|
21173
21353
|
try {
|
|
21174
|
-
if (!
|
|
21175
|
-
dirs =
|
|
21354
|
+
if (!fs42.statSync(geminiTmpDir2).isDirectory()) return out;
|
|
21355
|
+
dirs = fs42.readdirSync(geminiTmpDir2);
|
|
21176
21356
|
} catch {
|
|
21177
21357
|
return out;
|
|
21178
21358
|
}
|
|
21179
21359
|
for (const proj of dirs) {
|
|
21180
|
-
const chatsDir =
|
|
21360
|
+
const chatsDir = path43.join(geminiTmpDir2, proj, "chats");
|
|
21181
21361
|
let files;
|
|
21182
21362
|
try {
|
|
21183
|
-
if (!
|
|
21184
|
-
files =
|
|
21363
|
+
if (!fs42.statSync(chatsDir).isDirectory()) continue;
|
|
21364
|
+
files = fs42.readdirSync(chatsDir);
|
|
21185
21365
|
} catch {
|
|
21186
21366
|
continue;
|
|
21187
21367
|
}
|
|
21188
21368
|
for (const f of files) {
|
|
21189
21369
|
if (!f.endsWith(".jsonl")) continue;
|
|
21190
|
-
out.push({ projectKey: proj, file:
|
|
21370
|
+
out.push({ projectKey: proj, file: path43.join(chatsDir, f) });
|
|
21191
21371
|
}
|
|
21192
21372
|
}
|
|
21193
21373
|
return out;
|
|
21194
21374
|
}
|
|
21195
21375
|
function loadGeminiCost(start, end, geminiTmpDir2) {
|
|
21196
21376
|
const acc = emptyGeminiAccumulator();
|
|
21197
|
-
if (!
|
|
21377
|
+
if (!fs42.existsSync(geminiTmpDir2)) return freezeGeminiCost(acc);
|
|
21198
21378
|
for (const { projectKey, file } of listGeminiSessionFiles2(geminiTmpDir2)) {
|
|
21199
21379
|
processGeminiCostFile(file, projectKey, start, end, acc);
|
|
21200
21380
|
}
|
|
@@ -21202,11 +21382,11 @@ function loadGeminiCost(start, end, geminiTmpDir2) {
|
|
|
21202
21382
|
}
|
|
21203
21383
|
function aggregateReportFromAudit(period, opts = {}) {
|
|
21204
21384
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
21205
|
-
const auditLogPath = opts.auditLogPath ??
|
|
21206
|
-
const claudeProjectsDir = opts.claudeProjectsDir ??
|
|
21207
|
-
const codexSessionsDir2 = opts.codexSessionsDir ??
|
|
21208
|
-
const geminiTmpDir2 = opts.geminiTmpDir ??
|
|
21209
|
-
const hasAuditFile =
|
|
21385
|
+
const auditLogPath = opts.auditLogPath ?? path43.join(os38.homedir(), ".node9", "audit.log");
|
|
21386
|
+
const claudeProjectsDir = opts.claudeProjectsDir ?? path43.join(os38.homedir(), ".claude", "projects");
|
|
21387
|
+
const codexSessionsDir2 = opts.codexSessionsDir ?? path43.join(os38.homedir(), ".codex", "sessions");
|
|
21388
|
+
const geminiTmpDir2 = opts.geminiTmpDir ?? path43.join(os38.homedir(), ".gemini", "tmp");
|
|
21389
|
+
const hasAuditFile = fs42.existsSync(auditLogPath);
|
|
21210
21390
|
const allEntries = opts.preloadedAuditEntries ?? parseAuditLog(auditLogPath);
|
|
21211
21391
|
const unackedDlp = allEntries.filter((e) => e.source === "response-dlp");
|
|
21212
21392
|
const { start, end } = getDateRange(period, now);
|
|
@@ -21904,9 +22084,9 @@ function registerDaemonCommand(program2) {
|
|
|
21904
22084
|
init_core();
|
|
21905
22085
|
init_daemon();
|
|
21906
22086
|
import chalk15 from "chalk";
|
|
21907
|
-
import
|
|
21908
|
-
import
|
|
21909
|
-
import
|
|
22087
|
+
import fs43 from "fs";
|
|
22088
|
+
import path44 from "path";
|
|
22089
|
+
import os39 from "os";
|
|
21910
22090
|
function printAgentSection(label2, hookPairs, wrapped) {
|
|
21911
22091
|
console.log(chalk15.bold(` ${label2}`));
|
|
21912
22092
|
for (const { name, present } of hookPairs) {
|
|
@@ -21960,20 +22140,20 @@ function registerStatusCommand(program2) {
|
|
|
21960
22140
|
console.log("");
|
|
21961
22141
|
const modeLabel = settings.mode === "audit" ? chalk15.blue("audit") : settings.mode === "strict" ? chalk15.red("strict") : chalk15.white("standard");
|
|
21962
22142
|
console.log(` Mode: ${modeLabel}`);
|
|
21963
|
-
const projectConfig =
|
|
21964
|
-
const globalConfig =
|
|
22143
|
+
const projectConfig = path44.join(process.cwd(), "node9.config.json");
|
|
22144
|
+
const globalConfig = path44.join(os39.homedir(), ".node9", "config.json");
|
|
21965
22145
|
console.log(
|
|
21966
|
-
` Local: ${
|
|
22146
|
+
` Local: ${fs43.existsSync(projectConfig) ? chalk15.green("Active (node9.config.json)") : chalk15.gray("Not present")}`
|
|
21967
22147
|
);
|
|
21968
22148
|
console.log(
|
|
21969
|
-
` Global: ${
|
|
22149
|
+
` Global: ${fs43.existsSync(globalConfig) ? chalk15.green("Active (~/.node9/config.json)") : chalk15.gray("Not present")}`
|
|
21970
22150
|
);
|
|
21971
22151
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
21972
22152
|
console.log(
|
|
21973
22153
|
` Sandbox: ${chalk15.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
21974
22154
|
);
|
|
21975
22155
|
}
|
|
21976
|
-
const wiring = getAgentWiring(
|
|
22156
|
+
const wiring = getAgentWiring(os39.homedir()).filter((a) => a.present);
|
|
21977
22157
|
if (wiring.length > 0) {
|
|
21978
22158
|
console.log("");
|
|
21979
22159
|
console.log(chalk15.bold(" Agent Wiring:"));
|
|
@@ -22012,9 +22192,9 @@ init_setup();
|
|
|
22012
22192
|
init_shields();
|
|
22013
22193
|
init_service();
|
|
22014
22194
|
import chalk16 from "chalk";
|
|
22015
|
-
import
|
|
22016
|
-
import
|
|
22017
|
-
import
|
|
22195
|
+
import fs44 from "fs";
|
|
22196
|
+
import path45 from "path";
|
|
22197
|
+
import os40 from "os";
|
|
22018
22198
|
import https4 from "https";
|
|
22019
22199
|
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "project-jail"];
|
|
22020
22200
|
function buildTelemetryPayload(agents, firstInstall) {
|
|
@@ -22100,16 +22280,16 @@ function registerInitCommand(program2) {
|
|
|
22100
22280
|
}
|
|
22101
22281
|
console.log("");
|
|
22102
22282
|
}
|
|
22103
|
-
const configPath2 =
|
|
22104
|
-
const isFirstInstall = !
|
|
22105
|
-
if (
|
|
22283
|
+
const configPath2 = path45.join(os40.homedir(), ".node9", "config.json");
|
|
22284
|
+
const isFirstInstall = !fs44.existsSync(configPath2);
|
|
22285
|
+
if (fs44.existsSync(configPath2) && !options.force) {
|
|
22106
22286
|
try {
|
|
22107
|
-
const existing = JSON.parse(
|
|
22287
|
+
const existing = JSON.parse(fs44.readFileSync(configPath2, "utf-8"));
|
|
22108
22288
|
const settings = existing.settings ?? {};
|
|
22109
22289
|
if (settings.mode !== chosenMode) {
|
|
22110
22290
|
settings.mode = chosenMode;
|
|
22111
22291
|
existing.settings = settings;
|
|
22112
|
-
|
|
22292
|
+
fs44.writeFileSync(configPath2, JSON.stringify(existing, null, 2) + "\n");
|
|
22113
22293
|
console.log(chalk16.green(`\u2705 Mode updated: ${chosenMode}`));
|
|
22114
22294
|
} else {
|
|
22115
22295
|
console.log(chalk16.blue(`\u2139\uFE0F Config already exists: ${configPath2}`));
|
|
@@ -22122,9 +22302,9 @@ function registerInitCommand(program2) {
|
|
|
22122
22302
|
...DEFAULT_CONFIG,
|
|
22123
22303
|
settings: { ...DEFAULT_CONFIG.settings, mode: chosenMode }
|
|
22124
22304
|
};
|
|
22125
|
-
const dir =
|
|
22126
|
-
if (!
|
|
22127
|
-
|
|
22305
|
+
const dir = path45.dirname(configPath2);
|
|
22306
|
+
if (!fs44.existsSync(dir)) fs44.mkdirSync(dir, { recursive: true });
|
|
22307
|
+
fs44.writeFileSync(configPath2, JSON.stringify(configToSave, null, 2) + "\n");
|
|
22128
22308
|
console.log(chalk16.green(`\u2705 Config created: ${configPath2}`));
|
|
22129
22309
|
console.log(chalk16.gray(` Mode: ${chosenMode}`));
|
|
22130
22310
|
}
|
|
@@ -22229,7 +22409,7 @@ function registerInitCommand(program2) {
|
|
|
22229
22409
|
}
|
|
22230
22410
|
|
|
22231
22411
|
// src/cli/commands/undo.ts
|
|
22232
|
-
import
|
|
22412
|
+
import path46 from "path";
|
|
22233
22413
|
import chalk18 from "chalk";
|
|
22234
22414
|
|
|
22235
22415
|
// src/tui/undo-navigator.ts
|
|
@@ -22388,7 +22568,7 @@ function findMatchingCwd(startDir, history) {
|
|
|
22388
22568
|
let dir = startDir;
|
|
22389
22569
|
while (true) {
|
|
22390
22570
|
if (cwds.has(dir)) return dir;
|
|
22391
|
-
const parent =
|
|
22571
|
+
const parent = path46.dirname(dir);
|
|
22392
22572
|
if (parent === dir) return null;
|
|
22393
22573
|
dir = parent;
|
|
22394
22574
|
}
|
|
@@ -23023,9 +23203,9 @@ function registerMcpGatewayCommand(program2) {
|
|
|
23023
23203
|
|
|
23024
23204
|
// src/mcp-server/index.ts
|
|
23025
23205
|
import readline5 from "readline";
|
|
23026
|
-
import
|
|
23027
|
-
import
|
|
23028
|
-
import
|
|
23206
|
+
import fs45 from "fs";
|
|
23207
|
+
import os41 from "os";
|
|
23208
|
+
import path47 from "path";
|
|
23029
23209
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
23030
23210
|
init_core();
|
|
23031
23211
|
init_daemon();
|
|
@@ -23276,13 +23456,13 @@ function handleStatus() {
|
|
|
23276
23456
|
lines.push(`Active shields: ${activeShields.length > 0 ? activeShields.join(", ") : "none"}`);
|
|
23277
23457
|
lines.push(`Smart rules: ${config.policy.smartRules.length} loaded`);
|
|
23278
23458
|
lines.push(`DLP: ${config.policy.dlp?.enabled !== false ? "enabled" : "disabled"}`);
|
|
23279
|
-
const projectConfig =
|
|
23280
|
-
const globalConfig =
|
|
23459
|
+
const projectConfig = path47.join(process.cwd(), "node9.config.json");
|
|
23460
|
+
const globalConfig = path47.join(os41.homedir(), ".node9", "config.json");
|
|
23281
23461
|
lines.push(
|
|
23282
|
-
`Project config (node9.config.json): ${
|
|
23462
|
+
`Project config (node9.config.json): ${fs45.existsSync(projectConfig) ? "present" : "not found"}`
|
|
23283
23463
|
);
|
|
23284
23464
|
lines.push(
|
|
23285
|
-
`Global config (~/.node9/config.json): ${
|
|
23465
|
+
`Global config (~/.node9/config.json): ${fs45.existsSync(globalConfig) ? "present" : "not found"}`
|
|
23286
23466
|
);
|
|
23287
23467
|
return lines.join("\n");
|
|
23288
23468
|
}
|
|
@@ -23356,21 +23536,21 @@ function handleShieldDisable(args) {
|
|
|
23356
23536
|
writeActiveShields(active.filter((s) => s !== name));
|
|
23357
23537
|
return `Shield "${name}" disabled.`;
|
|
23358
23538
|
}
|
|
23359
|
-
var GLOBAL_CONFIG_PATH =
|
|
23539
|
+
var GLOBAL_CONFIG_PATH = path47.join(os41.homedir(), ".node9", "config.json");
|
|
23360
23540
|
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
23361
23541
|
function readGlobalConfigRaw() {
|
|
23362
23542
|
try {
|
|
23363
|
-
if (
|
|
23364
|
-
return JSON.parse(
|
|
23543
|
+
if (fs45.existsSync(GLOBAL_CONFIG_PATH)) {
|
|
23544
|
+
return JSON.parse(fs45.readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
23365
23545
|
}
|
|
23366
23546
|
} catch {
|
|
23367
23547
|
}
|
|
23368
23548
|
return {};
|
|
23369
23549
|
}
|
|
23370
23550
|
function writeGlobalConfigRaw(data) {
|
|
23371
|
-
const dir =
|
|
23372
|
-
if (!
|
|
23373
|
-
|
|
23551
|
+
const dir = path47.dirname(GLOBAL_CONFIG_PATH);
|
|
23552
|
+
if (!fs45.existsSync(dir)) fs45.mkdirSync(dir, { recursive: true });
|
|
23553
|
+
fs45.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(data, null, 2) + "\n");
|
|
23374
23554
|
}
|
|
23375
23555
|
function handleApproverList() {
|
|
23376
23556
|
const config = getConfig();
|
|
@@ -23414,9 +23594,9 @@ function handleApproverSet(args) {
|
|
|
23414
23594
|
function handleAuditGet(args) {
|
|
23415
23595
|
const limit = Math.min(typeof args.limit === "number" ? args.limit : 20, 100);
|
|
23416
23596
|
const filter = typeof args.filter === "string" && args.filter !== "all" ? args.filter : null;
|
|
23417
|
-
const auditPath =
|
|
23418
|
-
if (!
|
|
23419
|
-
const rawLines =
|
|
23597
|
+
const auditPath = path47.join(os41.homedir(), ".node9", "audit.log");
|
|
23598
|
+
if (!fs45.existsSync(auditPath)) return "No audit log found.";
|
|
23599
|
+
const rawLines = fs45.readFileSync(auditPath, "utf-8").trim().split("\n").filter(Boolean);
|
|
23420
23600
|
const parsed = [];
|
|
23421
23601
|
for (const line of rawLines) {
|
|
23422
23602
|
try {
|
|
@@ -23751,7 +23931,7 @@ function registerTrustCommand(program2) {
|
|
|
23751
23931
|
// src/cli/commands/mcp-pin.ts
|
|
23752
23932
|
init_mcp_pin();
|
|
23753
23933
|
import chalk21 from "chalk";
|
|
23754
|
-
import
|
|
23934
|
+
import fs46 from "fs";
|
|
23755
23935
|
function registerMcpPinCommand(program2) {
|
|
23756
23936
|
const pinCmd = program2.command("mcp").description("Manage MCP server tool definition pinning (rug pull defense)");
|
|
23757
23937
|
const pinSubCmd = pinCmd.command("pin").description("Manage pinned MCP server tool definitions");
|
|
@@ -23762,7 +23942,7 @@ function registerMcpPinCommand(program2) {
|
|
|
23762
23942
|
let repoCorrupt = false;
|
|
23763
23943
|
if (found.source === "repo") {
|
|
23764
23944
|
try {
|
|
23765
|
-
const raw =
|
|
23945
|
+
const raw = fs46.readFileSync(found.path, "utf-8");
|
|
23766
23946
|
const parsed = JSON.parse(raw);
|
|
23767
23947
|
repoEntries = parsed.servers ?? {};
|
|
23768
23948
|
} catch {
|
|
@@ -24077,25 +24257,25 @@ init_scan();
|
|
|
24077
24257
|
import chalk25 from "chalk";
|
|
24078
24258
|
|
|
24079
24259
|
// src/posture/index.ts
|
|
24080
|
-
import
|
|
24260
|
+
import os45 from "os";
|
|
24081
24261
|
|
|
24082
24262
|
// src/posture/secrets.ts
|
|
24083
24263
|
init_dist();
|
|
24084
|
-
import
|
|
24085
|
-
import
|
|
24086
|
-
import
|
|
24264
|
+
import fs47 from "fs";
|
|
24265
|
+
import path48 from "path";
|
|
24266
|
+
import os42 from "os";
|
|
24087
24267
|
var MAX_FILE_BYTES = 256 * 1024;
|
|
24088
24268
|
function displayPath(p, home) {
|
|
24089
24269
|
if (p === home) return "~";
|
|
24090
|
-
const prefix = home.endsWith(
|
|
24091
|
-
if (p.startsWith(prefix)) return "~" +
|
|
24270
|
+
const prefix = home.endsWith(path48.sep) ? home : home + path48.sep;
|
|
24271
|
+
if (p.startsWith(prefix)) return "~" + path48.sep + p.slice(prefix.length);
|
|
24092
24272
|
return p;
|
|
24093
24273
|
}
|
|
24094
24274
|
function safeRead(file) {
|
|
24095
24275
|
try {
|
|
24096
|
-
const stat =
|
|
24276
|
+
const stat = fs47.statSync(file);
|
|
24097
24277
|
if (!stat.isFile() || stat.size === 0 || stat.size > MAX_FILE_BYTES) return null;
|
|
24098
|
-
return
|
|
24278
|
+
return fs47.readFileSync(file, "utf8");
|
|
24099
24279
|
} catch {
|
|
24100
24280
|
return null;
|
|
24101
24281
|
}
|
|
@@ -24103,8 +24283,8 @@ function safeRead(file) {
|
|
|
24103
24283
|
function candidateFiles(home, cwd) {
|
|
24104
24284
|
const files = /* @__PURE__ */ new Set();
|
|
24105
24285
|
try {
|
|
24106
|
-
for (const name of
|
|
24107
|
-
if (name === ".env" || name.startsWith(".env.")) files.add(
|
|
24286
|
+
for (const name of fs47.readdirSync(cwd)) {
|
|
24287
|
+
if (name === ".env" || name.startsWith(".env.")) files.add(path48.join(cwd, name));
|
|
24108
24288
|
}
|
|
24109
24289
|
} catch {
|
|
24110
24290
|
}
|
|
@@ -24112,21 +24292,21 @@ function candidateFiles(home, cwd) {
|
|
|
24112
24292
|
if (spec.hookFile) files.add(spec.hookFile(home));
|
|
24113
24293
|
if (spec.mcpFile) files.add(spec.mcpFile(home));
|
|
24114
24294
|
}
|
|
24115
|
-
files.add(
|
|
24295
|
+
files.add(path48.join(home, ".env"));
|
|
24116
24296
|
return [...files];
|
|
24117
24297
|
}
|
|
24118
24298
|
function credentialMaterial(home) {
|
|
24119
24299
|
return [
|
|
24120
|
-
|
|
24121
|
-
|
|
24122
|
-
|
|
24123
|
-
|
|
24124
|
-
|
|
24125
|
-
|
|
24300
|
+
path48.join(home, ".ssh", "id_rsa"),
|
|
24301
|
+
path48.join(home, ".ssh", "id_dsa"),
|
|
24302
|
+
path48.join(home, ".ssh", "id_ecdsa"),
|
|
24303
|
+
path48.join(home, ".ssh", "id_ed25519"),
|
|
24304
|
+
path48.join(home, ".aws", "credentials"),
|
|
24305
|
+
path48.join(home, ".config", "gcloud", "application_default_credentials.json")
|
|
24126
24306
|
];
|
|
24127
24307
|
}
|
|
24128
24308
|
function checkSecrets(ctx) {
|
|
24129
|
-
const home = ctx.home ||
|
|
24309
|
+
const home = ctx.home || os42.homedir();
|
|
24130
24310
|
const findings = [];
|
|
24131
24311
|
const plaintext = [];
|
|
24132
24312
|
const plaintextPaths = [];
|
|
@@ -24159,7 +24339,7 @@ function checkSecrets(ctx) {
|
|
|
24159
24339
|
const credPaths = [];
|
|
24160
24340
|
for (const file of credentialMaterial(home)) {
|
|
24161
24341
|
try {
|
|
24162
|
-
if (
|
|
24342
|
+
if (fs47.statSync(file).isFile()) {
|
|
24163
24343
|
creds.push(displayPath(file, home));
|
|
24164
24344
|
credPaths.push(file);
|
|
24165
24345
|
}
|
|
@@ -24185,6 +24365,132 @@ function checkSecrets(ctx) {
|
|
|
24185
24365
|
|
|
24186
24366
|
// src/posture/egress.ts
|
|
24187
24367
|
init_config();
|
|
24368
|
+
import fs48 from "fs";
|
|
24369
|
+
|
|
24370
|
+
// src/sandbox/templates.ts
|
|
24371
|
+
var AGENT_NPM_PACKAGE = {
|
|
24372
|
+
claude: "@anthropic-ai/claude-code",
|
|
24373
|
+
codex: "@openai/codex"
|
|
24374
|
+
};
|
|
24375
|
+
function pinnedNode9Version(hostVersion) {
|
|
24376
|
+
return hostVersion && /^\d+\.\d+\.\d+$/.test(hostVersion) ? hostVersion : "latest";
|
|
24377
|
+
}
|
|
24378
|
+
var AGENT_BIN = {
|
|
24379
|
+
claude: "claude",
|
|
24380
|
+
codex: "codex"
|
|
24381
|
+
};
|
|
24382
|
+
var RUN_AS_USER = "agent";
|
|
24383
|
+
var ALLOWED_DOMAINS_PATH = "/etc/node9-sandbox/allowed-domains.txt";
|
|
24384
|
+
function renderDockerfile(config, node9Version2) {
|
|
24385
|
+
const agentPkg = AGENT_NPM_PACKAGE[config.agent];
|
|
24386
|
+
return `# Auto-generated by node9 sandbox. Do not edit by hand.
|
|
24387
|
+
FROM node:22-bookworm
|
|
24388
|
+
|
|
24389
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
24390
|
+
|
|
24391
|
+
# Wall + base tooling (iptables/ipset/dig/gosu) \u2014 ported from Isag.
|
|
24392
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
24393
|
+
ca-certificates curl git gosu iproute2 ipset iptables dnsutils jq \\
|
|
24394
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
24395
|
+
|
|
24396
|
+
# The worker (agent CLI).
|
|
24397
|
+
RUN npm install -g ${agentPkg}
|
|
24398
|
+
|
|
24399
|
+
# The guard (node9), pinned to the host version.
|
|
24400
|
+
RUN npm install -g node9-ai@${node9Version2}
|
|
24401
|
+
|
|
24402
|
+
# Non-root runtime user at uid 1000 (matches the typical single-user host so the
|
|
24403
|
+
# mounted ~/.claude / ~/.codex / project are read/writable). The node base image
|
|
24404
|
+
# already claims uid 1000 for the 'node' user \u2014 free it first (cf. Isag/ubuntu).
|
|
24405
|
+
RUN userdel -r node 2>/dev/null || true; \\
|
|
24406
|
+
userdel -r ubuntu 2>/dev/null || true; \\
|
|
24407
|
+
useradd --create-home --uid 1000 --shell /bin/bash ${RUN_AS_USER}
|
|
24408
|
+
|
|
24409
|
+
# Wire the agent's node9 hooks into the runtime user's home (build-time, static).
|
|
24410
|
+
RUN gosu ${RUN_AS_USER} node9 agents add ${config.agent} || true
|
|
24411
|
+
|
|
24412
|
+
RUN mkdir -p /workspace /etc/node9-sandbox \\
|
|
24413
|
+
&& chown ${RUN_AS_USER}:${RUN_AS_USER} /workspace
|
|
24414
|
+
|
|
24415
|
+
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
24416
|
+
RUN chmod +x /usr/local/bin/entrypoint.sh
|
|
24417
|
+
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
|
24418
|
+
`;
|
|
24419
|
+
}
|
|
24420
|
+
function renderEntrypoint(config) {
|
|
24421
|
+
const agentBin = AGENT_BIN[config.agent];
|
|
24422
|
+
return `#!/usr/bin/env bash
|
|
24423
|
+
# Auto-generated by node9 sandbox. Seals the egress wall (root), then drops to the
|
|
24424
|
+
# non-root agent which starts the node9 daemon + execs the agent.
|
|
24425
|
+
set -Eeuo pipefail
|
|
24426
|
+
|
|
24427
|
+
DOMAINS_FILE="${ALLOWED_DOMAINS_PATH}"
|
|
24428
|
+
RUN_AS_USER="${RUN_AS_USER}"
|
|
24429
|
+
|
|
24430
|
+
[[ -s "$DOMAINS_FILE" ]] || { echo "entrypoint: missing/empty $DOMAINS_FILE" >&2; exit 1; }
|
|
24431
|
+
|
|
24432
|
+
# Own the mounted node9 data dir so the agent user can write audit there.
|
|
24433
|
+
mkdir -p "/home/$RUN_AS_USER/.node9"
|
|
24434
|
+
chown -R "$RUN_AS_USER:$RUN_AS_USER" "/home/$RUN_AS_USER/.node9" || true
|
|
24435
|
+
|
|
24436
|
+
# \u2500\u2500 Resolve the allowlist \u2192 ipset (union of every resolver, like Isag) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
24437
|
+
mapfile -t LOCAL_DNS < <(awk '/^nameserver / {print $2}' /etc/resolv.conf)
|
|
24438
|
+
[[ \${#LOCAL_DNS[@]} -gt 0 ]] || { echo "entrypoint: no resolvers in /etc/resolv.conf" >&2; exit 1; }
|
|
24439
|
+
|
|
24440
|
+
ipset create node9_allowed hash:ip family inet -exist
|
|
24441
|
+
ipset flush node9_allowed
|
|
24442
|
+
|
|
24443
|
+
while IFS= read -r domain; do
|
|
24444
|
+
[[ -n "$domain" ]] || continue
|
|
24445
|
+
found=0
|
|
24446
|
+
# union the local resolver + each upstream so CDN/anycast IP rotation is covered
|
|
24447
|
+
for ip in $(getent ahostsv4 "$domain" 2>/dev/null | awk '{print $1}' | sort -u); do
|
|
24448
|
+
ipset add node9_allowed "$ip" -exist; found=1
|
|
24449
|
+
done
|
|
24450
|
+
for r in "\${LOCAL_DNS[@]}"; do
|
|
24451
|
+
for ip in $(dig +short +time=2 +tries=1 @"$r" A "$domain" 2>/dev/null | awk '/^[0-9.]+$/'); do
|
|
24452
|
+
ipset add node9_allowed "$ip" -exist; found=1
|
|
24453
|
+
done
|
|
24454
|
+
done
|
|
24455
|
+
[[ $found -eq 1 ]] || { echo "entrypoint: failed to resolve $domain" >&2; exit 1; }
|
|
24456
|
+
echo "entrypoint: allowed $domain"
|
|
24457
|
+
done < "$DOMAINS_FILE"
|
|
24458
|
+
|
|
24459
|
+
# \u2500\u2500 Seal iptables: deny-by-default except lo, established, DNS, the allowlist \u2500\u2500\u2500\u2500
|
|
24460
|
+
echo "entrypoint: sealing firewall..."
|
|
24461
|
+
iptables -F; iptables -X
|
|
24462
|
+
iptables -P INPUT DROP
|
|
24463
|
+
iptables -P FORWARD DROP
|
|
24464
|
+
iptables -P OUTPUT DROP
|
|
24465
|
+
iptables -A INPUT -i lo -j ACCEPT
|
|
24466
|
+
iptables -A OUTPUT -o lo -j ACCEPT
|
|
24467
|
+
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
|
24468
|
+
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
|
24469
|
+
for r in "\${LOCAL_DNS[@]}"; do
|
|
24470
|
+
iptables -A OUTPUT -p udp -d "$r" --dport 53 -j ACCEPT
|
|
24471
|
+
iptables -A OUTPUT -p tcp -d "$r" --dport 53 -j ACCEPT
|
|
24472
|
+
done
|
|
24473
|
+
iptables -A OUTPUT -m set --match-set node9_allowed dst -j ACCEPT
|
|
24474
|
+
|
|
24475
|
+
# \u2500\u2500 Drop to the agent: start the node9 daemon (as the user), then exec the agent \u2500
|
|
24476
|
+
echo "entrypoint: starting node9 + ${agentBin} as $RUN_AS_USER"
|
|
24477
|
+
exec gosu "$RUN_AS_USER" bash -lc '
|
|
24478
|
+
set -e
|
|
24479
|
+
node9 daemon --background >/dev/null 2>&1 || true
|
|
24480
|
+
cd /workspace
|
|
24481
|
+
exec ${agentBin} "$@"
|
|
24482
|
+
' -- "$@"
|
|
24483
|
+
`;
|
|
24484
|
+
}
|
|
24485
|
+
|
|
24486
|
+
// src/posture/egress.ts
|
|
24487
|
+
function sandboxEgressWallActive() {
|
|
24488
|
+
try {
|
|
24489
|
+
return fs48.existsSync(ALLOWED_DOMAINS_PATH);
|
|
24490
|
+
} catch {
|
|
24491
|
+
return false;
|
|
24492
|
+
}
|
|
24493
|
+
}
|
|
24188
24494
|
function evaluateEgressConfig(egress) {
|
|
24189
24495
|
if (egress.enabled && egress.mode === "block") {
|
|
24190
24496
|
return {
|
|
@@ -24234,6 +24540,21 @@ function evaluateEgressConfig(egress) {
|
|
|
24234
24540
|
};
|
|
24235
24541
|
}
|
|
24236
24542
|
function checkEgress(ctx) {
|
|
24543
|
+
if (sandboxEgressWallActive()) {
|
|
24544
|
+
return [
|
|
24545
|
+
{
|
|
24546
|
+
category: "Egress",
|
|
24547
|
+
severity: "advisory",
|
|
24548
|
+
title: "Egress is hard-blocked by the sandbox kernel wall",
|
|
24549
|
+
what: "Outbound is deny-by-default at the kernel; only the allowlist is reachable.",
|
|
24550
|
+
why: "The sandbox seals egress with an ipset/iptables wall before the agent starts.",
|
|
24551
|
+
who: "Even a compromised agent can only reach the allowlisted hosts.",
|
|
24552
|
+
owner: "node9",
|
|
24553
|
+
detail: [],
|
|
24554
|
+
coverage: { state: "covered", level: "block", via: "sandbox egress wall" }
|
|
24555
|
+
}
|
|
24556
|
+
];
|
|
24557
|
+
}
|
|
24237
24558
|
const config = getConfig(ctx.cwd);
|
|
24238
24559
|
const egress = config.policy.egress;
|
|
24239
24560
|
return [evaluateEgressConfig({ enabled: egress.enabled, mode: egress.mode })];
|
|
@@ -24275,25 +24596,25 @@ async function checkGate(ctx) {
|
|
|
24275
24596
|
|
|
24276
24597
|
// src/posture/supply-chain.ts
|
|
24277
24598
|
init_provenance();
|
|
24278
|
-
import
|
|
24279
|
-
import
|
|
24280
|
-
import
|
|
24599
|
+
import fs49 from "fs";
|
|
24600
|
+
import os43 from "os";
|
|
24601
|
+
import path49 from "path";
|
|
24281
24602
|
import { parse as parseToml3 } from "smol-toml";
|
|
24282
24603
|
var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpx", "bunx", "dlx", "yarn", "pnpm", "bun"]);
|
|
24283
24604
|
function isNode9Managed(command, args = []) {
|
|
24284
24605
|
if (!command) return false;
|
|
24285
|
-
if (
|
|
24286
|
-
if (PACKAGE_RUNNERS.has(
|
|
24287
|
-
return args.some((a) => a === "node9" ||
|
|
24606
|
+
if (path49.basename(command).toLowerCase() === "node9") return true;
|
|
24607
|
+
if (PACKAGE_RUNNERS.has(path49.basename(command).toLowerCase())) {
|
|
24608
|
+
return args.some((a) => a === "node9" || path49.basename(a).toLowerCase() === "node9");
|
|
24288
24609
|
}
|
|
24289
24610
|
return false;
|
|
24290
24611
|
}
|
|
24291
24612
|
var MAX_CONFIG_BYTES = 10 * 1024 * 1024;
|
|
24292
24613
|
function readServers(file, format, agent) {
|
|
24293
24614
|
try {
|
|
24294
|
-
const stat =
|
|
24615
|
+
const stat = fs49.statSync(file);
|
|
24295
24616
|
if (!stat.isFile() || stat.size > MAX_CONFIG_BYTES) return [];
|
|
24296
|
-
const text =
|
|
24617
|
+
const text = fs49.readFileSync(file, "utf8");
|
|
24297
24618
|
const map = format === "toml" ? parseToml3(text)?.mcp_servers : JSON.parse(text)?.mcpServers;
|
|
24298
24619
|
if (!map || typeof map !== "object") return [];
|
|
24299
24620
|
return Object.entries(map).map(([name, v]) => ({
|
|
@@ -24307,7 +24628,7 @@ function readServers(file, format, agent) {
|
|
|
24307
24628
|
}
|
|
24308
24629
|
}
|
|
24309
24630
|
function checkSupplyChain(ctx) {
|
|
24310
|
-
const home = ctx.home ||
|
|
24631
|
+
const home = ctx.home || os43.homedir();
|
|
24311
24632
|
const servers = [];
|
|
24312
24633
|
for (const spec of AGENT_SPECS) {
|
|
24313
24634
|
if (!spec.mcpFile) continue;
|
|
@@ -24389,11 +24710,12 @@ async function checkPrivilege(ctx) {
|
|
|
24389
24710
|
}
|
|
24390
24711
|
|
|
24391
24712
|
// src/posture/containment.ts
|
|
24392
|
-
import
|
|
24713
|
+
import fs50 from "fs";
|
|
24714
|
+
var ISOLATION_WEIGHT = 12;
|
|
24393
24715
|
function inContainer() {
|
|
24394
|
-
if (
|
|
24716
|
+
if (fs50.existsSync("/.dockerenv") || fs50.existsSync("/run/.containerenv")) return true;
|
|
24395
24717
|
try {
|
|
24396
|
-
const cgroup =
|
|
24718
|
+
const cgroup = fs50.readFileSync("/proc/1/cgroup", "utf8");
|
|
24397
24719
|
if (/docker|kubepods|containerd|lxc|libpod/.test(cgroup)) return true;
|
|
24398
24720
|
} catch {
|
|
24399
24721
|
}
|
|
@@ -24412,14 +24734,28 @@ function checkContainment(_ctx) {
|
|
|
24412
24734
|
detail: [],
|
|
24413
24735
|
owner: "os",
|
|
24414
24736
|
node9Reduces: true,
|
|
24415
|
-
|
|
24416
|
-
|
|
24737
|
+
// The single biggest hardening gap, and node9 now fully remedies it
|
|
24738
|
+
// (`node9 sandbox run`). Deducts while open; closing it is the headline
|
|
24739
|
+
// payoff. No coverageProbe → stays OPEN (scored) until adopted; live
|
|
24740
|
+
// partial-credit for the lighter shield path is a fast-follow.
|
|
24741
|
+
scoreWeight: ISOLATION_WEIGHT,
|
|
24742
|
+
gain: "jailed container \xB7 kernel egress wall \xB7 scoped mounts \xB7 governed inside",
|
|
24743
|
+
cost: "the agent works inside /workspace, not your live host",
|
|
24744
|
+
fix: `Two ways to shrink the blast radius \u2014 pick by how much flexibility you need:
|
|
24745
|
+
Strongest \u2014 jail it (closes this gap, +${ISOLATION_WEIGHT}):
|
|
24746
|
+
\u2022 node9 sandbox run <agent>
|
|
24747
|
+
Lighter \u2014 harden in place, keep full host access (about +${Math.round(
|
|
24748
|
+
ISOLATION_WEIGHT / 2
|
|
24749
|
+
)}):
|
|
24750
|
+
\u2022 node9 shield enable project-jail \u2014 block stray credential reads
|
|
24751
|
+
\u2022 node9 egress lock \u2014 block data exfil`
|
|
24417
24752
|
}
|
|
24418
24753
|
];
|
|
24419
24754
|
}
|
|
24420
24755
|
|
|
24421
24756
|
// src/posture/inbound.ts
|
|
24422
|
-
import
|
|
24757
|
+
import fs51 from "fs";
|
|
24758
|
+
var DB_EXPOSURE_WEIGHT = 4;
|
|
24423
24759
|
var KNOWN_SERVICE_PORTS = {
|
|
24424
24760
|
5432: "PostgreSQL",
|
|
24425
24761
|
6379: "Redis",
|
|
@@ -24505,7 +24841,7 @@ function collectListeners() {
|
|
|
24505
24841
|
const byPort = /* @__PURE__ */ new Map();
|
|
24506
24842
|
for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) {
|
|
24507
24843
|
try {
|
|
24508
|
-
for (const l of parseListeners(
|
|
24844
|
+
for (const l of parseListeners(fs51.readFileSync(file, "utf8"))) {
|
|
24509
24845
|
if (!byPort.has(l.port)) byPort.set(l.port, l);
|
|
24510
24846
|
}
|
|
24511
24847
|
} catch {
|
|
@@ -24517,11 +24853,11 @@ function readProc(pid) {
|
|
|
24517
24853
|
let comm = "unknown";
|
|
24518
24854
|
let cmdline = "";
|
|
24519
24855
|
try {
|
|
24520
|
-
comm =
|
|
24856
|
+
comm = fs51.readFileSync(`/proc/${pid}/comm`, "utf8").trim().replace(/-MainThread$/, "") || "unknown";
|
|
24521
24857
|
} catch {
|
|
24522
24858
|
}
|
|
24523
24859
|
try {
|
|
24524
|
-
cmdline =
|
|
24860
|
+
cmdline = fs51.readFileSync(`/proc/${pid}/cmdline`).toString().replace(/\0/g, " ").trim();
|
|
24525
24861
|
} catch {
|
|
24526
24862
|
}
|
|
24527
24863
|
return { comm, cmdline };
|
|
@@ -24531,21 +24867,21 @@ function resolveProcesses(inodes) {
|
|
|
24531
24867
|
if (inodes.size === 0) return map;
|
|
24532
24868
|
let pids;
|
|
24533
24869
|
try {
|
|
24534
|
-
pids =
|
|
24870
|
+
pids = fs51.readdirSync("/proc").filter((d) => /^\d+$/.test(d));
|
|
24535
24871
|
} catch {
|
|
24536
24872
|
return map;
|
|
24537
24873
|
}
|
|
24538
24874
|
for (const pid of pids) {
|
|
24539
24875
|
let fds;
|
|
24540
24876
|
try {
|
|
24541
|
-
fds =
|
|
24877
|
+
fds = fs51.readdirSync(`/proc/${pid}/fd`);
|
|
24542
24878
|
} catch {
|
|
24543
24879
|
continue;
|
|
24544
24880
|
}
|
|
24545
24881
|
for (const fd of fds) {
|
|
24546
24882
|
let link;
|
|
24547
24883
|
try {
|
|
24548
|
-
link =
|
|
24884
|
+
link = fs51.readlinkSync(`/proc/${pid}/fd/${fd}`);
|
|
24549
24885
|
} catch {
|
|
24550
24886
|
continue;
|
|
24551
24887
|
}
|
|
@@ -24600,8 +24936,15 @@ function checkInbound(ctx) {
|
|
|
24600
24936
|
// Only when node9 actually has a shield for an exposed service — otherwise
|
|
24601
24937
|
// (bare dev servers) it stays purely the user's to rebind.
|
|
24602
24938
|
node9Reduces: reduces,
|
|
24603
|
-
|
|
24604
|
-
|
|
24939
|
+
// When a db-shield applies this is real, node9-addressable hardening → it
|
|
24940
|
+
// scores (and stays OPEN, no cantFix probe). Bare dev servers node9 can't
|
|
24941
|
+
// touch stay can't-fix / your-part / unscored.
|
|
24942
|
+
...reduces ? {
|
|
24943
|
+
scoreWeight: DB_EXPOSURE_WEIGHT,
|
|
24944
|
+
gain: "blocks DROP TABLE / TRUNCATE / FLUSHALL on the exposed DB",
|
|
24945
|
+
cost: "you confirm legit destructive migrations"
|
|
24946
|
+
} : { coverageProbe: { kind: "cantFix" } },
|
|
24947
|
+
fix
|
|
24605
24948
|
});
|
|
24606
24949
|
}
|
|
24607
24950
|
return findings;
|
|
@@ -24609,9 +24952,9 @@ function checkInbound(ctx) {
|
|
|
24609
24952
|
|
|
24610
24953
|
// src/posture/coverage.ts
|
|
24611
24954
|
init_config();
|
|
24612
|
-
import
|
|
24955
|
+
import os44 from "os";
|
|
24613
24956
|
function checkCoverage(ctx) {
|
|
24614
|
-
const home = ctx.home ||
|
|
24957
|
+
const home = ctx.home || os44.homedir();
|
|
24615
24958
|
const findings = [];
|
|
24616
24959
|
const protectedAgents = getAgentWiring(home).filter((r) => r.isProtected);
|
|
24617
24960
|
if (protectedAgents.length === 0) {
|
|
@@ -24651,16 +24994,20 @@ function scorePosture(findings, checksRun) {
|
|
|
24651
24994
|
const open = findings.filter(
|
|
24652
24995
|
(f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix"
|
|
24653
24996
|
);
|
|
24654
|
-
const count = (sev) => open.filter((f) => f.severity === sev).length;
|
|
24655
|
-
|
|
24997
|
+
const count = (sev) => open.filter((f) => f.severity === sev && !f.scoreWeight).length;
|
|
24998
|
+
const base = computeSecurityScore({
|
|
24656
24999
|
critical: count("critical"),
|
|
24657
25000
|
high: count("high"),
|
|
24658
25001
|
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
25002
|
total: Math.max(checksRun, 1)
|
|
24663
25003
|
});
|
|
25004
|
+
const headroom = open.reduce((sum, f) => sum + (f.scoreWeight ?? 0), 0);
|
|
25005
|
+
const score = Math.max(0, base.score - headroom);
|
|
25006
|
+
const tier = score >= 80 ? "good" : score >= 50 ? "at-risk" : "critical";
|
|
25007
|
+
return { score, tier };
|
|
25008
|
+
}
|
|
25009
|
+
function openHeadroom(findings) {
|
|
25010
|
+
return findings.filter((f) => f.coverage?.state !== "covered" && f.coverage?.state !== "cant-fix").reduce((sum, f) => sum + (f.scoreWeight ?? 0), 0);
|
|
24664
25011
|
}
|
|
24665
25012
|
|
|
24666
25013
|
// src/posture/headline.ts
|
|
@@ -24815,7 +25162,7 @@ async function runChecks(checks, ctx) {
|
|
|
24815
25162
|
}
|
|
24816
25163
|
async function runPosture(opts = {}) {
|
|
24817
25164
|
const ctx = {
|
|
24818
|
-
home: opts.home ??
|
|
25165
|
+
home: opts.home ?? os45.homedir(),
|
|
24819
25166
|
cwd: opts.cwd ?? process.cwd(),
|
|
24820
25167
|
agent: opts.agent
|
|
24821
25168
|
};
|
|
@@ -24870,9 +25217,10 @@ var LABEL_WIDTH = 14;
|
|
|
24870
25217
|
function label(category) {
|
|
24871
25218
|
return chalk24.bold(category.padEnd(Math.max(LABEL_WIDTH, category.length + 1)));
|
|
24872
25219
|
}
|
|
24873
|
-
function renderFinding(f) {
|
|
25220
|
+
function renderFinding(f, showWeight = false) {
|
|
24874
25221
|
const lines = [];
|
|
24875
|
-
|
|
25222
|
+
const wt = showWeight && f.scoreWeight ? chalk24.cyan.bold(`+${f.scoreWeight} `) : "";
|
|
25223
|
+
lines.push(` ${ICON[f.severity]} ${label(f.category)}${wt}${f.title}`);
|
|
24876
25224
|
const indent = " ".repeat(2 + 3 + LABEL_WIDTH);
|
|
24877
25225
|
const width = 80 - indent.length;
|
|
24878
25226
|
for (const s of [f.what, f.why, f.who]) {
|
|
@@ -24888,6 +25236,16 @@ function renderFinding(f) {
|
|
|
24888
25236
|
}
|
|
24889
25237
|
}
|
|
24890
25238
|
}
|
|
25239
|
+
const tradeoff = [
|
|
25240
|
+
[f.gain, "gain: ", chalk24.green],
|
|
25241
|
+
[f.cost, "cost: ", chalk24.yellow]
|
|
25242
|
+
];
|
|
25243
|
+
for (const [text, lbl, color2] of tradeoff) {
|
|
25244
|
+
if (!text) continue;
|
|
25245
|
+
wrap(text, width - 6).forEach((l, i) => {
|
|
25246
|
+
lines.push(indent + (i === 0 ? color2(lbl) : " ") + chalk24.gray(l));
|
|
25247
|
+
});
|
|
25248
|
+
}
|
|
24891
25249
|
return lines;
|
|
24892
25250
|
}
|
|
24893
25251
|
function renderPosture(result) {
|
|
@@ -24897,15 +25255,11 @@ function renderPosture(result) {
|
|
|
24897
25255
|
lines.push(
|
|
24898
25256
|
chalk24.cyan.bold(`\u{1F6E1}\uFE0F Node9 Posture`) + chalk24.gray(` \u2014 ${result.agent}`) + ` ${chalk24.bold(`Score: ${result.score}/100`)} (${tier})`
|
|
24899
25257
|
);
|
|
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";
|
|
25258
|
+
const headroom = openHeadroom(result.findings);
|
|
25259
|
+
if (headroom > 0) {
|
|
24906
25260
|
lines.push(
|
|
24907
25261
|
" " + chalk24.gray(
|
|
24908
|
-
`${
|
|
25262
|
+
`${headroom} pts of headroom below \u2014 each layer adds security at some cost to flexibility. You choose which to close.`
|
|
24909
25263
|
)
|
|
24910
25264
|
);
|
|
24911
25265
|
}
|
|
@@ -24921,7 +25275,7 @@ function renderPosture(result) {
|
|
|
24921
25275
|
const covered = result.findings.filter((f) => f.coverage?.state === "covered");
|
|
24922
25276
|
const open = result.findings.filter((f) => f.coverage?.state !== "covered");
|
|
24923
25277
|
if (covered.length > 0) {
|
|
24924
|
-
lines.push(" " + chalk24.green("\u{1F7E2} node9 is
|
|
25278
|
+
lines.push(" " + chalk24.green("\u{1F7E2} ON NOW \u2014 node9 is enforcing these (your floor)"));
|
|
24925
25279
|
for (const f of covered) {
|
|
24926
25280
|
const gated = f.coverage?.level === "review" ? "approval-gating" : "blocking";
|
|
24927
25281
|
const via = f.coverage?.via ?? "node9";
|
|
@@ -24936,18 +25290,16 @@ function renderPosture(result) {
|
|
|
24936
25290
|
const osOpen = open.filter((f) => f.owner !== "node9" && !f.node9Reduces);
|
|
24937
25291
|
if (node9Open.length > 0) {
|
|
24938
25292
|
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));
|
|
25293
|
+
for (const f of node9Open) lines.push(...renderFinding(f, true));
|
|
24940
25294
|
}
|
|
24941
25295
|
if (reduceOpen.length > 0) {
|
|
24942
25296
|
if (node9Open.length > 0) lines.push("");
|
|
24943
|
-
lines.push(
|
|
24944
|
-
|
|
24945
|
-
);
|
|
24946
|
-
for (const f of reduceOpen) lines.push(...renderFinding(f));
|
|
25297
|
+
lines.push(" " + chalk24.yellow.bold("\u{1F512} AVAILABLE \u2014 turn on to harden (each has a tradeoff)"));
|
|
25298
|
+
for (const f of reduceOpen) lines.push(...renderFinding(f, true));
|
|
24947
25299
|
}
|
|
24948
25300
|
if (osOpen.length > 0) {
|
|
24949
25301
|
if (node9Open.length > 0 || reduceOpen.length > 0) lines.push("");
|
|
24950
|
-
lines.push(" " + chalk24.bold("\u{1F9F1}
|
|
25302
|
+
lines.push(" " + chalk24.bold("\u{1F9F1} YOUR PART \u2014 node9 can't fix these (OS-level)"));
|
|
24951
25303
|
for (const f of osOpen) lines.push(...renderFinding(f));
|
|
24952
25304
|
}
|
|
24953
25305
|
for (const cat of result.passedCategories) {
|
|
@@ -25002,7 +25354,12 @@ function buildShipBody(result) {
|
|
|
25002
25354
|
// The runnable fix / OS action — commands + advice, never a path.
|
|
25003
25355
|
fix: f.fix,
|
|
25004
25356
|
// Whose job it is. Default 'os' so the SaaS never falsely claims node9 can fix it.
|
|
25005
|
-
owner: f.owner ?? "os"
|
|
25357
|
+
owner: f.owner ?? "os",
|
|
25358
|
+
// Hardening weight + the flexibility tradeoff (generic prose / a number —
|
|
25359
|
+
// no values or paths), so the fleet view can show the same headroom story.
|
|
25360
|
+
scoreWeight: f.scoreWeight,
|
|
25361
|
+
gain: f.gain,
|
|
25362
|
+
cost: f.cost
|
|
25006
25363
|
}))
|
|
25007
25364
|
};
|
|
25008
25365
|
}
|
|
@@ -25075,9 +25432,9 @@ function registerPostureCommand(program2) {
|
|
|
25075
25432
|
init_config();
|
|
25076
25433
|
init_dist();
|
|
25077
25434
|
import chalk26 from "chalk";
|
|
25078
|
-
import
|
|
25079
|
-
import
|
|
25080
|
-
import
|
|
25435
|
+
import fs52 from "fs";
|
|
25436
|
+
import os46 from "os";
|
|
25437
|
+
import path50 from "path";
|
|
25081
25438
|
var DEFAULT_EGRESS = {
|
|
25082
25439
|
enabled: false,
|
|
25083
25440
|
mode: "review",
|
|
@@ -25086,12 +25443,12 @@ var DEFAULT_EGRESS = {
|
|
|
25086
25443
|
allowPrivate: true
|
|
25087
25444
|
};
|
|
25088
25445
|
function configPath() {
|
|
25089
|
-
return
|
|
25446
|
+
return path50.join(os46.homedir(), ".node9", "config.json");
|
|
25090
25447
|
}
|
|
25091
25448
|
function readRawConfig() {
|
|
25092
25449
|
let text;
|
|
25093
25450
|
try {
|
|
25094
|
-
text =
|
|
25451
|
+
text = fs52.readFileSync(configPath(), "utf8");
|
|
25095
25452
|
} catch (err2) {
|
|
25096
25453
|
if (err2.code === "ENOENT") return {};
|
|
25097
25454
|
throw err2;
|
|
@@ -25106,8 +25463,8 @@ function readRawConfig() {
|
|
|
25106
25463
|
}
|
|
25107
25464
|
function writeRawConfig(config) {
|
|
25108
25465
|
const p = configPath();
|
|
25109
|
-
|
|
25110
|
-
|
|
25466
|
+
fs52.mkdirSync(path50.dirname(p), { recursive: true });
|
|
25467
|
+
fs52.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
25111
25468
|
}
|
|
25112
25469
|
function applyEgress(config, change) {
|
|
25113
25470
|
const policy = config.policy = config.policy ?? {};
|
|
@@ -25198,15 +25555,369 @@ function registerEgressCommand(program2) {
|
|
|
25198
25555
|
egress.action(showStatus);
|
|
25199
25556
|
}
|
|
25200
25557
|
|
|
25558
|
+
// src/cli/commands/sandbox.ts
|
|
25559
|
+
init_config();
|
|
25560
|
+
import chalk27 from "chalk";
|
|
25561
|
+
import fs55 from "fs";
|
|
25562
|
+
import path53 from "path";
|
|
25563
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
25564
|
+
|
|
25565
|
+
// src/sandbox/config.ts
|
|
25566
|
+
import fs53 from "fs";
|
|
25567
|
+
import path51 from "path";
|
|
25568
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
25569
|
+
var SANDBOX_CONFIG_FILE = "node9.sandbox.yaml";
|
|
25570
|
+
var FORBIDDEN_ENV = /* @__PURE__ */ new Set(["NODE9_API_KEY", "NODE9_API_URL"]);
|
|
25571
|
+
function defaultSandboxConfig(agent) {
|
|
25572
|
+
return {
|
|
25573
|
+
agent,
|
|
25574
|
+
workspace: { mount: ".", target: "/workspace", mode: "rw" },
|
|
25575
|
+
runtime: { engine: "docker", image: "node9-sandbox:local", rebuild: "auto" },
|
|
25576
|
+
outbound: {
|
|
25577
|
+
mode: "block",
|
|
25578
|
+
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"]
|
|
25579
|
+
},
|
|
25580
|
+
inbound: { expose: [] },
|
|
25581
|
+
// Provider key only — NODE9_API_KEY intentionally absent (fix #1).
|
|
25582
|
+
env: { pass: [agent === "codex" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY"] },
|
|
25583
|
+
// Terminal-only approval in the MVP; cloud/native/browser off (fix #1).
|
|
25584
|
+
node9: {
|
|
25585
|
+
approvals: { terminal: true, native: false, browser: false, cloud: false },
|
|
25586
|
+
// Mount the agent's OAuth/creds dir so it can authenticate in the box.
|
|
25587
|
+
mountAgentCredentials: true
|
|
25588
|
+
}
|
|
25589
|
+
};
|
|
25590
|
+
}
|
|
25591
|
+
function asStringArray(v) {
|
|
25592
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
25593
|
+
}
|
|
25594
|
+
function mergeSandboxConfig(raw, fallbackAgent) {
|
|
25595
|
+
const r = raw && typeof raw === "object" ? raw : {};
|
|
25596
|
+
const agent = typeof r.agent === "string" ? r.agent : fallbackAgent;
|
|
25597
|
+
if (agent !== "claude" && agent !== "codex") {
|
|
25598
|
+
throw new Error(`sandbox: unsupported agent "${String(agent)}" (use claude or codex)`);
|
|
25599
|
+
}
|
|
25600
|
+
const d = defaultSandboxConfig(agent);
|
|
25601
|
+
const ws = r.workspace ?? {};
|
|
25602
|
+
const rt = r.runtime ?? {};
|
|
25603
|
+
const out = r.outbound ?? {};
|
|
25604
|
+
const inb = r.inbound ?? {};
|
|
25605
|
+
const env = r.env ?? {};
|
|
25606
|
+
const n9 = r.node9 ?? {};
|
|
25607
|
+
const appr = n9.approvals ?? {};
|
|
25608
|
+
const pass = asStringArray(env.pass).filter((k) => !FORBIDDEN_ENV.has(k));
|
|
25609
|
+
return {
|
|
25610
|
+
agent,
|
|
25611
|
+
workspace: {
|
|
25612
|
+
mount: typeof ws.mount === "string" ? ws.mount : d.workspace.mount,
|
|
25613
|
+
target: typeof ws.target === "string" ? ws.target : d.workspace.target,
|
|
25614
|
+
mode: ws.mode === "ro" ? "ro" : "rw"
|
|
25615
|
+
},
|
|
25616
|
+
runtime: {
|
|
25617
|
+
engine: rt.engine === "podman" ? "podman" : "docker",
|
|
25618
|
+
image: typeof rt.image === "string" ? rt.image : d.runtime.image,
|
|
25619
|
+
rebuild: rt.rebuild === "never" || rt.rebuild === "always" ? rt.rebuild : d.runtime.rebuild
|
|
25620
|
+
},
|
|
25621
|
+
outbound: { mode: "block", allow: out.allow ? asStringArray(out.allow) : d.outbound.allow },
|
|
25622
|
+
inbound: { expose: inb.expose ? asStringArray(inb.expose) : d.inbound.expose },
|
|
25623
|
+
env: { pass: env.pass ? pass : d.env.pass },
|
|
25624
|
+
node9: {
|
|
25625
|
+
approvals: {
|
|
25626
|
+
terminal: appr.terminal !== false,
|
|
25627
|
+
native: appr.native === true,
|
|
25628
|
+
browser: appr.browser === true,
|
|
25629
|
+
cloud: appr.cloud === true
|
|
25630
|
+
},
|
|
25631
|
+
mountAgentCredentials: n9.mountAgentCredentials !== false
|
|
25632
|
+
}
|
|
25633
|
+
};
|
|
25634
|
+
}
|
|
25635
|
+
function scaffoldSandboxYaml(agent) {
|
|
25636
|
+
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";
|
|
25637
|
+
return header + stringifyYaml(defaultSandboxConfig(agent));
|
|
25638
|
+
}
|
|
25639
|
+
function sandboxConfigPath(cwd = process.cwd()) {
|
|
25640
|
+
return path51.join(cwd, SANDBOX_CONFIG_FILE);
|
|
25641
|
+
}
|
|
25642
|
+
function loadSandboxConfig(cwd = process.cwd(), fallbackAgent = "claude") {
|
|
25643
|
+
const p = sandboxConfigPath(cwd);
|
|
25644
|
+
if (!fs53.existsSync(p)) {
|
|
25645
|
+
throw new Error(`sandbox: ${SANDBOX_CONFIG_FILE} not found \u2014 run \`node9 sandbox new\` first.`);
|
|
25646
|
+
}
|
|
25647
|
+
let raw;
|
|
25648
|
+
try {
|
|
25649
|
+
raw = parseYaml(fs53.readFileSync(p, "utf-8"));
|
|
25650
|
+
} catch (err2) {
|
|
25651
|
+
throw new Error(
|
|
25652
|
+
`sandbox: ${SANDBOX_CONFIG_FILE} is not valid YAML \u2014 ${err2.message}`
|
|
25653
|
+
);
|
|
25654
|
+
}
|
|
25655
|
+
return mergeSandboxConfig(raw, fallbackAgent);
|
|
25656
|
+
}
|
|
25657
|
+
|
|
25658
|
+
// src/sandbox/firewall.ts
|
|
25659
|
+
var AGENT_PROVIDER_HOST = {
|
|
25660
|
+
claude: ["api.anthropic.com"],
|
|
25661
|
+
codex: ["api.openai.com"]
|
|
25662
|
+
};
|
|
25663
|
+
var NODE9_SAAS_HOSTS = ["api.node9.ai", "app.node9.ai", "node9.ai"];
|
|
25664
|
+
function isValidHost2(host) {
|
|
25665
|
+
if (typeof host !== "string") return false;
|
|
25666
|
+
const h = host.trim().toLowerCase();
|
|
25667
|
+
if (!h || h.length > 253) return false;
|
|
25668
|
+
if (/[\s/:@?#\\]/.test(h)) return false;
|
|
25669
|
+
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(
|
|
25670
|
+
h
|
|
25671
|
+
);
|
|
25672
|
+
}
|
|
25673
|
+
function compileAllowlist(input) {
|
|
25674
|
+
const norm = (h) => h.trim().toLowerCase();
|
|
25675
|
+
const denySet = /* @__PURE__ */ new Set([...input.configDeny.map(norm), ...NODE9_SAAS_HOSTS.map(norm)]);
|
|
25676
|
+
const candidates = [
|
|
25677
|
+
...AGENT_PROVIDER_HOST[input.agent],
|
|
25678
|
+
...input.sandboxAllow,
|
|
25679
|
+
...input.configAllow
|
|
25680
|
+
].map(norm);
|
|
25681
|
+
const allow = /* @__PURE__ */ new Set();
|
|
25682
|
+
const rejected = [];
|
|
25683
|
+
const denied = [];
|
|
25684
|
+
for (const host of candidates) {
|
|
25685
|
+
if (!host) continue;
|
|
25686
|
+
if (!isValidHost2(host)) {
|
|
25687
|
+
if (!rejected.includes(host)) rejected.push(host);
|
|
25688
|
+
continue;
|
|
25689
|
+
}
|
|
25690
|
+
if (denySet.has(host)) {
|
|
25691
|
+
if (!denied.includes(host)) denied.push(host);
|
|
25692
|
+
continue;
|
|
25693
|
+
}
|
|
25694
|
+
allow.add(host);
|
|
25695
|
+
}
|
|
25696
|
+
return {
|
|
25697
|
+
allow: [...allow].sort(),
|
|
25698
|
+
rejected: rejected.sort(),
|
|
25699
|
+
denied: denied.sort()
|
|
25700
|
+
};
|
|
25701
|
+
}
|
|
25702
|
+
|
|
25703
|
+
// src/sandbox/runtime.ts
|
|
25704
|
+
import fs54 from "fs";
|
|
25705
|
+
import os47 from "os";
|
|
25706
|
+
import path52 from "path";
|
|
25707
|
+
import crypto8 from "crypto";
|
|
25708
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
25709
|
+
function sandboxDataDir(cwd = process.cwd()) {
|
|
25710
|
+
return path52.join(cwd, ".node9", "sandbox", "data");
|
|
25711
|
+
}
|
|
25712
|
+
function detectEngine(engine) {
|
|
25713
|
+
const r = spawnSync5(engine, ["--version"], { encoding: "utf-8" });
|
|
25714
|
+
if (r.status === 0 && typeof r.stdout === "string") {
|
|
25715
|
+
return { available: true, version: r.stdout.trim() };
|
|
25716
|
+
}
|
|
25717
|
+
return { available: false };
|
|
25718
|
+
}
|
|
25719
|
+
function agentCredentialsMount(agent) {
|
|
25720
|
+
const rel = agent === "codex" ? ".codex/auth.json" : ".claude/.credentials.json";
|
|
25721
|
+
return { hostPath: path52.join(os47.homedir(), rel), target: `/home/${RUN_AS_USER}/${rel}` };
|
|
25722
|
+
}
|
|
25723
|
+
function buildRunArgs(opts) {
|
|
25724
|
+
const { config, workspaceHostPath, dataHostPath, allowlistHostPath, agentArgs } = opts;
|
|
25725
|
+
const args = ["run", "--rm", "-it", "--cap-add=NET_ADMIN"];
|
|
25726
|
+
args.push("-v", `${workspaceHostPath}:${config.workspace.target}:${config.workspace.mode}`);
|
|
25727
|
+
args.push("-v", `${dataHostPath}:/home/${RUN_AS_USER}/.node9`);
|
|
25728
|
+
args.push("-v", `${allowlistHostPath}:${ALLOWED_DOMAINS_PATH}:ro`);
|
|
25729
|
+
if (config.node9.mountAgentCredentials) {
|
|
25730
|
+
const creds = agentCredentialsMount(config.agent);
|
|
25731
|
+
if (fs54.existsSync(creds.hostPath)) {
|
|
25732
|
+
args.push("-v", `${creds.hostPath}:${creds.target}`);
|
|
25733
|
+
}
|
|
25734
|
+
}
|
|
25735
|
+
for (const key of config.env.pass) {
|
|
25736
|
+
if (process.env[key] !== void 0) args.push("-e", key);
|
|
25737
|
+
}
|
|
25738
|
+
for (const port of config.inbound.expose) {
|
|
25739
|
+
args.push("-p", port);
|
|
25740
|
+
}
|
|
25741
|
+
args.push(config.runtime.image);
|
|
25742
|
+
if (agentArgs.length) args.push(...agentArgs);
|
|
25743
|
+
return args;
|
|
25744
|
+
}
|
|
25745
|
+
function imageContentHash(dockerfile, entrypoint) {
|
|
25746
|
+
return crypto8.createHash("sha256").update(dockerfile).update("\0").update(entrypoint).digest("hex").slice(0, 16);
|
|
25747
|
+
}
|
|
25748
|
+
function sandboxBuildDir(cwd = process.cwd()) {
|
|
25749
|
+
return path52.join(cwd, ".node9", "sandbox", "build");
|
|
25750
|
+
}
|
|
25751
|
+
function writeBuildContext(cwd, dockerfile, entrypoint) {
|
|
25752
|
+
const dir = sandboxBuildDir(cwd);
|
|
25753
|
+
fs54.mkdirSync(dir, { recursive: true });
|
|
25754
|
+
fs54.writeFileSync(path52.join(dir, "Dockerfile"), dockerfile);
|
|
25755
|
+
fs54.writeFileSync(path52.join(dir, "entrypoint.sh"), entrypoint);
|
|
25756
|
+
return dir;
|
|
25757
|
+
}
|
|
25758
|
+
function writeAllowlist(cwd, hosts) {
|
|
25759
|
+
const dir = path52.join(cwd, ".node9", "sandbox");
|
|
25760
|
+
fs54.mkdirSync(dir, { recursive: true });
|
|
25761
|
+
const p = path52.join(dir, "allowed-domains.txt");
|
|
25762
|
+
fs54.writeFileSync(p, hosts.join("\n") + "\n");
|
|
25763
|
+
return p;
|
|
25764
|
+
}
|
|
25765
|
+
function resolveHomePath(p) {
|
|
25766
|
+
return p.startsWith("~") ? path52.join(os47.homedir(), p.slice(1)) : path52.resolve(p);
|
|
25767
|
+
}
|
|
25768
|
+
|
|
25769
|
+
// src/cli/commands/sandbox.ts
|
|
25770
|
+
function seedDataDirConfig(dataDir, sandbox) {
|
|
25771
|
+
fs55.mkdirSync(dataDir, { recursive: true });
|
|
25772
|
+
const configPath2 = path53.join(dataDir, "config.json");
|
|
25773
|
+
const seed = {
|
|
25774
|
+
settings: {
|
|
25775
|
+
approvers: {
|
|
25776
|
+
terminal: sandbox.node9.approvals.terminal,
|
|
25777
|
+
native: false,
|
|
25778
|
+
browser: false,
|
|
25779
|
+
cloud: false
|
|
25780
|
+
}
|
|
25781
|
+
}
|
|
25782
|
+
};
|
|
25783
|
+
fs55.writeFileSync(configPath2, JSON.stringify(seed, null, 2), { mode: 384 });
|
|
25784
|
+
}
|
|
25785
|
+
function registerSandboxCommand(program2, version2) {
|
|
25786
|
+
const node9Version2 = pinnedNode9Version(version2);
|
|
25787
|
+
const cmd = program2.command("sandbox").description("Run an agent in a disposable, jailed container \u2014 governed + audited inside");
|
|
25788
|
+
cmd.command("new").description(`Scaffold ${SANDBOX_CONFIG_FILE} in this project`).option("--agent <agent>", "claude (default) or codex", "claude").action((opts) => {
|
|
25789
|
+
const agent = opts.agent === "codex" ? "codex" : "claude";
|
|
25790
|
+
const p = sandboxConfigPath();
|
|
25791
|
+
if (fs55.existsSync(p)) {
|
|
25792
|
+
console.log(
|
|
25793
|
+
chalk27.yellow(` ${SANDBOX_CONFIG_FILE} already exists \u2014 leaving it untouched.`)
|
|
25794
|
+
);
|
|
25795
|
+
return;
|
|
25796
|
+
}
|
|
25797
|
+
fs55.writeFileSync(p, scaffoldSandboxYaml(agent));
|
|
25798
|
+
console.log(
|
|
25799
|
+
chalk27.green(` \u2713 wrote ${SANDBOX_CONFIG_FILE}`) + chalk27.dim(` (agent: ${agent})`)
|
|
25800
|
+
);
|
|
25801
|
+
console.log(
|
|
25802
|
+
chalk27.dim(" Edit it (mounts / allow / expose), then: ") + chalk27.cyan("node9 sandbox run")
|
|
25803
|
+
);
|
|
25804
|
+
});
|
|
25805
|
+
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) => {
|
|
25806
|
+
const cwd = process.cwd();
|
|
25807
|
+
const sandbox = loadSandboxConfig(cwd, agentArg || "claude");
|
|
25808
|
+
if (agentArg === "claude" || agentArg === "codex") sandbox.agent = agentArg;
|
|
25809
|
+
const engine = detectEngine(sandbox.runtime.engine);
|
|
25810
|
+
if (!engine.available) {
|
|
25811
|
+
console.error(
|
|
25812
|
+
chalk27.red(` ${sandbox.runtime.engine} not found.`) + chalk27.dim(` Install it first \u2014 node9 sandbox needs a container runtime.`)
|
|
25813
|
+
);
|
|
25814
|
+
process.exit(1);
|
|
25815
|
+
}
|
|
25816
|
+
const node9Config = getConfig(cwd);
|
|
25817
|
+
const compiled = compileAllowlist({
|
|
25818
|
+
agent: sandbox.agent,
|
|
25819
|
+
sandboxAllow: sandbox.outbound.allow,
|
|
25820
|
+
configAllow: node9Config.policy.egress.allow,
|
|
25821
|
+
configDeny: node9Config.policy.egress.deny
|
|
25822
|
+
});
|
|
25823
|
+
if (compiled.rejected.length) {
|
|
25824
|
+
console.log(
|
|
25825
|
+
chalk27.yellow(` \u26A0 ignoring invalid allow hosts: ${compiled.rejected.join(", ")}`)
|
|
25826
|
+
);
|
|
25827
|
+
}
|
|
25828
|
+
if (compiled.denied.length) {
|
|
25829
|
+
console.log(chalk27.dim(` (denied: ${compiled.denied.join(", ")})`));
|
|
25830
|
+
}
|
|
25831
|
+
const allowlistPath = writeAllowlist(cwd, compiled.allow);
|
|
25832
|
+
const dockerfile = renderDockerfile(sandbox, node9Version2);
|
|
25833
|
+
const entrypoint = renderEntrypoint(sandbox);
|
|
25834
|
+
const buildDir = writeBuildContext(cwd, dockerfile, entrypoint);
|
|
25835
|
+
const hash = imageContentHash(dockerfile, entrypoint);
|
|
25836
|
+
const image = sandbox.runtime.image;
|
|
25837
|
+
const hashFile = path53.join(sandboxBuildDir(cwd), ".image-hash");
|
|
25838
|
+
const lastHash = fs55.existsSync(hashFile) ? fs55.readFileSync(hashFile, "utf-8").trim() : "";
|
|
25839
|
+
const imageExists = spawnSync6(sandbox.runtime.engine, ["image", "inspect", image], { stdio: "ignore" }).status === 0;
|
|
25840
|
+
const needBuild = sandbox.runtime.rebuild === "always" || !imageExists || sandbox.runtime.rebuild !== "never" && lastHash !== hash;
|
|
25841
|
+
if (needBuild) {
|
|
25842
|
+
console.log(chalk27.dim(` building ${image} \u2026`));
|
|
25843
|
+
const b = spawnSync6(sandbox.runtime.engine, ["build", "-t", image, buildDir], {
|
|
25844
|
+
stdio: "inherit"
|
|
25845
|
+
});
|
|
25846
|
+
if (b.status !== 0) {
|
|
25847
|
+
console.error(chalk27.red(" build failed."));
|
|
25848
|
+
process.exit(b.status ?? 1);
|
|
25849
|
+
}
|
|
25850
|
+
fs55.writeFileSync(hashFile, hash);
|
|
25851
|
+
}
|
|
25852
|
+
const dataDir = sandboxDataDir(cwd);
|
|
25853
|
+
seedDataDirConfig(dataDir, sandbox);
|
|
25854
|
+
const passthru = command.args.slice(agentArg ? 1 : 0);
|
|
25855
|
+
const runArgs = buildRunArgs({
|
|
25856
|
+
config: sandbox,
|
|
25857
|
+
workspaceHostPath: resolveHomePath(sandbox.workspace.mount),
|
|
25858
|
+
dataHostPath: dataDir,
|
|
25859
|
+
allowlistHostPath: allowlistPath,
|
|
25860
|
+
agentArgs: passthru
|
|
25861
|
+
});
|
|
25862
|
+
if (sandbox.node9.mountAgentCredentials) {
|
|
25863
|
+
const creds = agentCredentialsMount(sandbox.agent);
|
|
25864
|
+
if (fs55.existsSync(creds.hostPath)) {
|
|
25865
|
+
console.log(chalk27.dim(` mounting ${creds.hostPath} (agent credentials, rw)`));
|
|
25866
|
+
} else {
|
|
25867
|
+
console.log(
|
|
25868
|
+
chalk27.yellow(` \u26A0 ${creds.hostPath} not found \u2014 `) + chalk27.dim(`the agent must auth via an env key in env.pass.`)
|
|
25869
|
+
);
|
|
25870
|
+
}
|
|
25871
|
+
}
|
|
25872
|
+
console.log(
|
|
25873
|
+
chalk27.green(` \u{1F6E1}\uFE0F ${sandbox.agent} jailed \u2014 ${compiled.allow.length} hosts allowed
|
|
25874
|
+
`)
|
|
25875
|
+
);
|
|
25876
|
+
const r = spawnSync6(sandbox.runtime.engine, runArgs, { stdio: "inherit" });
|
|
25877
|
+
process.exit(r.status ?? 0);
|
|
25878
|
+
});
|
|
25879
|
+
cmd.command("tail").description("Stream the sandbox's audit log (host-side)").action(() => {
|
|
25880
|
+
const auditPath = path53.join(sandboxDataDir(), "audit.log");
|
|
25881
|
+
if (!fs55.existsSync(auditPath)) {
|
|
25882
|
+
console.log(chalk27.dim(" no sandbox audit yet."));
|
|
25883
|
+
return;
|
|
25884
|
+
}
|
|
25885
|
+
spawnSync6("tail", ["-f", auditPath], { stdio: "inherit" });
|
|
25886
|
+
});
|
|
25887
|
+
cmd.command("logs").description("Dump the sandbox's audit log").action(() => {
|
|
25888
|
+
const auditPath = path53.join(sandboxDataDir(), "audit.log");
|
|
25889
|
+
if (!fs55.existsSync(auditPath)) {
|
|
25890
|
+
console.log(chalk27.dim(" no sandbox audit yet."));
|
|
25891
|
+
return;
|
|
25892
|
+
}
|
|
25893
|
+
process.stdout.write(fs55.readFileSync(auditPath, "utf-8"));
|
|
25894
|
+
});
|
|
25895
|
+
cmd.command("clean").description("Remove the sandbox image, build context, and data").action(() => {
|
|
25896
|
+
const cwd = process.cwd();
|
|
25897
|
+
let sandbox = null;
|
|
25898
|
+
try {
|
|
25899
|
+
sandbox = loadSandboxConfig(cwd);
|
|
25900
|
+
} catch {
|
|
25901
|
+
}
|
|
25902
|
+
if (sandbox) {
|
|
25903
|
+
spawnSync6(sandbox.runtime.engine, ["image", "rm", "-f", sandbox.runtime.image], {
|
|
25904
|
+
stdio: "ignore"
|
|
25905
|
+
});
|
|
25906
|
+
}
|
|
25907
|
+
fs55.rmSync(path53.join(cwd, ".node9", "sandbox"), { recursive: true, force: true });
|
|
25908
|
+
console.log(chalk27.green(" \u2713 sandbox image + build + data removed."));
|
|
25909
|
+
});
|
|
25910
|
+
}
|
|
25911
|
+
|
|
25201
25912
|
// src/cli/commands/sessions.ts
|
|
25202
25913
|
init_scan_summary();
|
|
25203
25914
|
init_litellm();
|
|
25204
25915
|
init_cost_gemini();
|
|
25205
25916
|
init_cost_codex();
|
|
25206
|
-
import
|
|
25207
|
-
import
|
|
25208
|
-
import
|
|
25209
|
-
import
|
|
25917
|
+
import chalk28 from "chalk";
|
|
25918
|
+
import fs56 from "fs";
|
|
25919
|
+
import path54 from "path";
|
|
25920
|
+
import os48 from "os";
|
|
25210
25921
|
function modelPrice(model) {
|
|
25211
25922
|
const t = pricingFor(model);
|
|
25212
25923
|
if (!t) return null;
|
|
@@ -25223,10 +25934,10 @@ function encodeProjectPath(projectPath) {
|
|
|
25223
25934
|
}
|
|
25224
25935
|
function sessionJsonlPath(projectPath, sessionId) {
|
|
25225
25936
|
const encoded = encodeProjectPath(projectPath);
|
|
25226
|
-
return
|
|
25937
|
+
return path54.join(os48.homedir(), ".claude", "projects", encoded, `${sessionId}.jsonl`);
|
|
25227
25938
|
}
|
|
25228
25939
|
function projectLabel(projectPath) {
|
|
25229
|
-
return projectPath.replace(
|
|
25940
|
+
return projectPath.replace(os48.homedir(), "~");
|
|
25230
25941
|
}
|
|
25231
25942
|
function parseHistoryLines(lines) {
|
|
25232
25943
|
const entries = [];
|
|
@@ -25295,10 +26006,10 @@ function parseSessionLines(lines) {
|
|
|
25295
26006
|
return { toolCalls, costUSD, hasSnapshot, modifiedFiles };
|
|
25296
26007
|
}
|
|
25297
26008
|
function loadAuditEntries(auditPath) {
|
|
25298
|
-
const aPath = auditPath ??
|
|
26009
|
+
const aPath = auditPath ?? path54.join(os48.homedir(), ".node9", "audit.log");
|
|
25299
26010
|
let raw;
|
|
25300
26011
|
try {
|
|
25301
|
-
raw =
|
|
26012
|
+
raw = fs56.readFileSync(aPath, "utf-8");
|
|
25302
26013
|
} catch {
|
|
25303
26014
|
return [];
|
|
25304
26015
|
}
|
|
@@ -25334,8 +26045,8 @@ function auditEntriesInWindow(entries, windowStart, windowEnd) {
|
|
|
25334
26045
|
return result;
|
|
25335
26046
|
}
|
|
25336
26047
|
function buildGeminiSessions(days, allAuditEntries) {
|
|
25337
|
-
const tmpDir =
|
|
25338
|
-
if (!
|
|
26048
|
+
const tmpDir = path54.join(os48.homedir(), ".gemini", "tmp");
|
|
26049
|
+
if (!fs56.existsSync(tmpDir)) return [];
|
|
25339
26050
|
const cutoff = days !== null ? (() => {
|
|
25340
26051
|
const d = /* @__PURE__ */ new Date();
|
|
25341
26052
|
d.setDate(d.getDate() - days);
|
|
@@ -25344,35 +26055,35 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
25344
26055
|
})() : null;
|
|
25345
26056
|
let slugDirs;
|
|
25346
26057
|
try {
|
|
25347
|
-
slugDirs =
|
|
26058
|
+
slugDirs = fs56.readdirSync(tmpDir);
|
|
25348
26059
|
} catch {
|
|
25349
26060
|
return [];
|
|
25350
26061
|
}
|
|
25351
26062
|
const summaries = [];
|
|
25352
26063
|
for (const slug of slugDirs) {
|
|
25353
|
-
const slugPath =
|
|
26064
|
+
const slugPath = path54.join(tmpDir, slug);
|
|
25354
26065
|
try {
|
|
25355
|
-
if (!
|
|
26066
|
+
if (!fs56.statSync(slugPath).isDirectory()) continue;
|
|
25356
26067
|
} catch {
|
|
25357
26068
|
continue;
|
|
25358
26069
|
}
|
|
25359
|
-
let projectRoot =
|
|
26070
|
+
let projectRoot = path54.join(os48.homedir(), slug);
|
|
25360
26071
|
try {
|
|
25361
|
-
projectRoot =
|
|
26072
|
+
projectRoot = fs56.readFileSync(path54.join(slugPath, ".project_root"), "utf-8").trim();
|
|
25362
26073
|
} catch {
|
|
25363
26074
|
}
|
|
25364
|
-
const chatsDir =
|
|
25365
|
-
if (!
|
|
26075
|
+
const chatsDir = path54.join(slugPath, "chats");
|
|
26076
|
+
if (!fs56.existsSync(chatsDir)) continue;
|
|
25366
26077
|
let chatFiles;
|
|
25367
26078
|
try {
|
|
25368
|
-
chatFiles =
|
|
26079
|
+
chatFiles = fs56.readdirSync(chatsDir).filter((f) => f.endsWith(".json"));
|
|
25369
26080
|
} catch {
|
|
25370
26081
|
continue;
|
|
25371
26082
|
}
|
|
25372
26083
|
for (const chatFile of chatFiles) {
|
|
25373
26084
|
let raw;
|
|
25374
26085
|
try {
|
|
25375
|
-
raw =
|
|
26086
|
+
raw = fs56.readFileSync(path54.join(chatsDir, chatFile), "utf-8");
|
|
25376
26087
|
} catch {
|
|
25377
26088
|
continue;
|
|
25378
26089
|
}
|
|
@@ -25452,8 +26163,8 @@ function buildGeminiSessions(days, allAuditEntries) {
|
|
|
25452
26163
|
return summaries;
|
|
25453
26164
|
}
|
|
25454
26165
|
function buildCodexSessions(days, allAuditEntries) {
|
|
25455
|
-
const sessionsBase =
|
|
25456
|
-
if (!
|
|
26166
|
+
const sessionsBase = path54.join(os48.homedir(), ".codex", "sessions");
|
|
26167
|
+
if (!fs56.existsSync(sessionsBase)) return [];
|
|
25457
26168
|
const cutoff = days !== null ? (() => {
|
|
25458
26169
|
const d = /* @__PURE__ */ new Date();
|
|
25459
26170
|
d.setDate(d.getDate() - days);
|
|
@@ -25462,29 +26173,29 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
25462
26173
|
})() : null;
|
|
25463
26174
|
const jsonlFiles = [];
|
|
25464
26175
|
try {
|
|
25465
|
-
for (const year of
|
|
25466
|
-
const yearPath =
|
|
26176
|
+
for (const year of fs56.readdirSync(sessionsBase)) {
|
|
26177
|
+
const yearPath = path54.join(sessionsBase, year);
|
|
25467
26178
|
try {
|
|
25468
|
-
if (!
|
|
26179
|
+
if (!fs56.statSync(yearPath).isDirectory()) continue;
|
|
25469
26180
|
} catch {
|
|
25470
26181
|
continue;
|
|
25471
26182
|
}
|
|
25472
|
-
for (const month of
|
|
25473
|
-
const monthPath =
|
|
26183
|
+
for (const month of fs56.readdirSync(yearPath)) {
|
|
26184
|
+
const monthPath = path54.join(yearPath, month);
|
|
25474
26185
|
try {
|
|
25475
|
-
if (!
|
|
26186
|
+
if (!fs56.statSync(monthPath).isDirectory()) continue;
|
|
25476
26187
|
} catch {
|
|
25477
26188
|
continue;
|
|
25478
26189
|
}
|
|
25479
|
-
for (const day of
|
|
25480
|
-
const dayPath =
|
|
26190
|
+
for (const day of fs56.readdirSync(monthPath)) {
|
|
26191
|
+
const dayPath = path54.join(monthPath, day);
|
|
25481
26192
|
try {
|
|
25482
|
-
if (!
|
|
26193
|
+
if (!fs56.statSync(dayPath).isDirectory()) continue;
|
|
25483
26194
|
} catch {
|
|
25484
26195
|
continue;
|
|
25485
26196
|
}
|
|
25486
|
-
for (const file of
|
|
25487
|
-
if (file.endsWith(".jsonl")) jsonlFiles.push(
|
|
26197
|
+
for (const file of fs56.readdirSync(dayPath)) {
|
|
26198
|
+
if (file.endsWith(".jsonl")) jsonlFiles.push(path54.join(dayPath, file));
|
|
25488
26199
|
}
|
|
25489
26200
|
}
|
|
25490
26201
|
}
|
|
@@ -25496,7 +26207,7 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
25496
26207
|
for (const filePath of jsonlFiles) {
|
|
25497
26208
|
let lines;
|
|
25498
26209
|
try {
|
|
25499
|
-
lines =
|
|
26210
|
+
lines = fs56.readFileSync(filePath, "utf-8").split("\n");
|
|
25500
26211
|
} catch {
|
|
25501
26212
|
continue;
|
|
25502
26213
|
}
|
|
@@ -25582,10 +26293,10 @@ function buildCodexSessions(days, allAuditEntries) {
|
|
|
25582
26293
|
return summaries;
|
|
25583
26294
|
}
|
|
25584
26295
|
function buildSessions(days, historyPath) {
|
|
25585
|
-
const hPath = historyPath ??
|
|
26296
|
+
const hPath = historyPath ?? path54.join(os48.homedir(), ".claude", "history.jsonl");
|
|
25586
26297
|
let historyRaw = "";
|
|
25587
26298
|
try {
|
|
25588
|
-
historyRaw =
|
|
26299
|
+
historyRaw = fs56.readFileSync(hPath, "utf-8");
|
|
25589
26300
|
} catch {
|
|
25590
26301
|
}
|
|
25591
26302
|
const cutoff = days !== null ? (() => {
|
|
@@ -25609,7 +26320,7 @@ function buildSessions(days, historyPath) {
|
|
|
25609
26320
|
const jsonlFile = sessionJsonlPath(entry.project, entry.sessionId);
|
|
25610
26321
|
let sessionLines = [];
|
|
25611
26322
|
try {
|
|
25612
|
-
sessionLines =
|
|
26323
|
+
sessionLines = fs56.readFileSync(jsonlFile, "utf-8").split("\n");
|
|
25613
26324
|
} catch {
|
|
25614
26325
|
}
|
|
25615
26326
|
const { toolCalls, costUSD, hasSnapshot, modifiedFiles } = parseSessionLines(sessionLines);
|
|
@@ -25695,11 +26406,11 @@ function toolInputSummary(tool, input) {
|
|
|
25695
26406
|
}
|
|
25696
26407
|
function toolColor(tool) {
|
|
25697
26408
|
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
|
|
26409
|
+
if (t === "bash" || t === "execute_bash") return chalk28.red;
|
|
26410
|
+
if (t === "write") return chalk28.green;
|
|
26411
|
+
if (t === "edit" || t === "notebookedit") return chalk28.yellow;
|
|
26412
|
+
if (t === "read") return chalk28.cyan;
|
|
26413
|
+
return chalk28.gray;
|
|
25703
26414
|
}
|
|
25704
26415
|
function barStr2(value, max, width) {
|
|
25705
26416
|
if (max === 0 || width <= 0) return "\u2591".repeat(width);
|
|
@@ -25709,7 +26420,7 @@ function barStr2(value, max, width) {
|
|
|
25709
26420
|
function colorBar2(value, max, width) {
|
|
25710
26421
|
const s = barStr2(value, max, width);
|
|
25711
26422
|
const filled = Math.max(1, Math.round(max > 0 ? value / max * width : 0));
|
|
25712
|
-
return
|
|
26423
|
+
return chalk28.cyan(s.slice(0, filled)) + chalk28.dim(s.slice(filled));
|
|
25713
26424
|
}
|
|
25714
26425
|
function renderSummary(summaries) {
|
|
25715
26426
|
const totalTools = summaries.reduce((n, s) => n + s.toolCalls.length, 0);
|
|
@@ -25739,45 +26450,45 @@ function renderSummary(summaries) {
|
|
|
25739
26450
|
}
|
|
25740
26451
|
const topProjects = [...projCosts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3);
|
|
25741
26452
|
const W = 20;
|
|
25742
|
-
console.log(
|
|
26453
|
+
console.log(chalk28.dim(" " + "\u2500".repeat(70)));
|
|
25743
26454
|
console.log(
|
|
25744
|
-
" " +
|
|
26455
|
+
" " + 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
26456
|
);
|
|
25746
26457
|
console.log(
|
|
25747
|
-
" " +
|
|
26458
|
+
" " + 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
26459
|
);
|
|
25749
26460
|
console.log("");
|
|
25750
|
-
console.log(" " +
|
|
26461
|
+
console.log(" " + chalk28.dim("Tool breakdown:"));
|
|
25751
26462
|
const maxGroup = Math.max(...Object.values(groups));
|
|
25752
26463
|
for (const [label2, count] of Object.entries(groups)) {
|
|
25753
26464
|
if (count === 0) continue;
|
|
25754
26465
|
const pct = totalTools > 0 ? Math.round(count / totalTools * 100) : 0;
|
|
25755
26466
|
console.log(
|
|
25756
|
-
" " + label2.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " +
|
|
26467
|
+
" " + label2.padEnd(6) + " " + colorBar2(count, maxGroup, W) + " " + chalk28.white(String(count).padStart(4)) + chalk28.dim(` (${String(pct)}%)`)
|
|
25757
26468
|
);
|
|
25758
26469
|
}
|
|
25759
26470
|
console.log("");
|
|
25760
26471
|
if (topProjects.length > 1) {
|
|
25761
|
-
console.log(" " +
|
|
26472
|
+
console.log(" " + chalk28.dim("Cost by project:"));
|
|
25762
26473
|
const maxProjCost = topProjects[0][1];
|
|
25763
26474
|
for (const [proj, cost] of topProjects) {
|
|
25764
26475
|
console.log(
|
|
25765
|
-
" " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " +
|
|
26476
|
+
" " + proj.slice(0, 28).padEnd(28) + " " + colorBar2(cost, maxProjCost, W) + " " + chalk28.yellow(fmtCost3(cost))
|
|
25766
26477
|
);
|
|
25767
26478
|
}
|
|
25768
26479
|
console.log("");
|
|
25769
26480
|
}
|
|
25770
|
-
console.log(
|
|
26481
|
+
console.log(chalk28.dim(" " + "\u2500".repeat(70)));
|
|
25771
26482
|
console.log("");
|
|
25772
26483
|
}
|
|
25773
26484
|
function renderList(summaries, totalCost) {
|
|
25774
26485
|
if (summaries.length === 0) {
|
|
25775
|
-
console.log(
|
|
26486
|
+
console.log(chalk28.yellow(" No sessions found in the requested range.\n"));
|
|
25776
26487
|
return;
|
|
25777
26488
|
}
|
|
25778
|
-
const totalLabel = totalCost > 0 ?
|
|
26489
|
+
const totalLabel = totalCost > 0 ? chalk28.dim(" ~" + fmtCost3(totalCost) + " total") : "";
|
|
25779
26490
|
console.log(
|
|
25780
|
-
" " +
|
|
26491
|
+
" " + chalk28.white(String(summaries.length)) + chalk28.dim(` session${summaries.length !== 1 ? "s" : ""}`) + totalLabel
|
|
25781
26492
|
);
|
|
25782
26493
|
console.log("");
|
|
25783
26494
|
let lastGroup = "";
|
|
@@ -25785,51 +26496,51 @@ function renderList(summaries, totalCost) {
|
|
|
25785
26496
|
const activeDate = fmtDate2(s.lastActiveTime);
|
|
25786
26497
|
const group = activeDate + " " + s.projectLabel;
|
|
25787
26498
|
if (group !== lastGroup) {
|
|
25788
|
-
console.log(
|
|
26499
|
+
console.log(chalk28.dim(" \u2500\u2500\u2500 ") + chalk28.bold(activeDate) + chalk28.dim(" " + s.projectLabel));
|
|
25789
26500
|
lastGroup = group;
|
|
25790
26501
|
}
|
|
25791
26502
|
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 =
|
|
26503
|
+
const dateRange = startDate !== activeDate ? chalk28.dim(" (" + startDate + " \u2192 " + activeDate + ")") : "";
|
|
26504
|
+
const timeStr = chalk28.dim(fmtTime(s.startTime));
|
|
26505
|
+
const prompt = chalk28.white(truncate(s.firstPrompt.replace(/\n/g, " "), 50).padEnd(50));
|
|
26506
|
+
const tools = s.toolCalls.length > 0 ? chalk28.dim(String(s.toolCalls.length).padStart(3) + " tools") : chalk28.dim(" 0 tools");
|
|
26507
|
+
const cost = s.costUSD > 0 ? chalk28.dim(" " + fmtCost3(s.costUSD).padEnd(8)) : " ";
|
|
26508
|
+
const blocked = s.blockedCalls.length > 0 ? chalk28.red(" \u{1F6D1} " + String(s.blockedCalls.length)) : "";
|
|
26509
|
+
const snap = s.hasSnapshot ? chalk28.green(" \u{1F4F8}") : "";
|
|
26510
|
+
const agentBadge = chalk28[agentColorName(s.agent ?? "claude")](
|
|
25800
26511
|
" " + agentBadgeText(s.agent ?? "claude", 0)
|
|
25801
26512
|
);
|
|
25802
|
-
const sid =
|
|
26513
|
+
const sid = chalk28.dim(" " + s.sessionId.slice(0, 8));
|
|
25803
26514
|
console.log(
|
|
25804
26515
|
` ${timeStr} ${prompt} ${tools}${cost}${blocked}${snap}${agentBadge}${sid}${dateRange}`
|
|
25805
26516
|
);
|
|
25806
26517
|
}
|
|
25807
26518
|
console.log("");
|
|
25808
26519
|
console.log(
|
|
25809
|
-
|
|
26520
|
+
chalk28.dim(" Run") + " " + chalk28.cyan("node9 sessions --detail <session-id>") + chalk28.dim(" for full tool trace.")
|
|
25810
26521
|
);
|
|
25811
26522
|
console.log("");
|
|
25812
26523
|
}
|
|
25813
26524
|
function renderDetail(s) {
|
|
25814
26525
|
console.log("");
|
|
25815
|
-
console.log(
|
|
26526
|
+
console.log(chalk28.bold(" Session ") + chalk28.dim(s.sessionId));
|
|
25816
26527
|
console.log(
|
|
25817
|
-
|
|
26528
|
+
chalk28.bold(" Prompt ") + chalk28.white(s.firstPrompt.replace(/\n/g, " ").slice(0, 120))
|
|
25818
26529
|
);
|
|
25819
|
-
console.log(
|
|
26530
|
+
console.log(chalk28.bold(" Project ") + chalk28.white(s.projectLabel));
|
|
25820
26531
|
if (s.agent) {
|
|
25821
|
-
const agentLabel2 =
|
|
25822
|
-
console.log(
|
|
26532
|
+
const agentLabel2 = chalk28[agentColorName(s.agent)](agentDisplayName(s.agent));
|
|
26533
|
+
console.log(chalk28.bold(" Agent ") + agentLabel2);
|
|
25823
26534
|
}
|
|
25824
|
-
console.log(
|
|
26535
|
+
console.log(chalk28.bold(" When ") + chalk28.white(fmtDateTime(s.startTime)));
|
|
25825
26536
|
if (s.costUSD > 0)
|
|
25826
|
-
console.log(
|
|
26537
|
+
console.log(chalk28.bold(" Cost ") + chalk28.yellow("~" + fmtCost3(s.costUSD)));
|
|
25827
26538
|
console.log(
|
|
25828
|
-
|
|
26539
|
+
chalk28.bold(" Snapshot ") + (s.hasSnapshot ? chalk28.green("\u2713 taken") : chalk28.dim("none"))
|
|
25829
26540
|
);
|
|
25830
26541
|
console.log("");
|
|
25831
26542
|
if (s.toolCalls.length === 0 && s.blockedCalls.length === 0) {
|
|
25832
|
-
console.log(
|
|
26543
|
+
console.log(chalk28.dim(" No tool calls recorded.\n"));
|
|
25833
26544
|
return;
|
|
25834
26545
|
}
|
|
25835
26546
|
const timeline = [
|
|
@@ -25842,32 +26553,32 @@ function renderDetail(s) {
|
|
|
25842
26553
|
});
|
|
25843
26554
|
const headerParts = [`Tool calls (${s.toolCalls.length})`];
|
|
25844
26555
|
if (s.blockedCalls.length > 0)
|
|
25845
|
-
headerParts.push(
|
|
25846
|
-
console.log(
|
|
26556
|
+
headerParts.push(chalk28.red(`${s.blockedCalls.length} blocked by node9`));
|
|
26557
|
+
console.log(chalk28.bold(" " + headerParts.join(" \xB7 ")));
|
|
25847
26558
|
console.log("");
|
|
25848
26559
|
for (const entry of timeline) {
|
|
25849
26560
|
if (entry.kind === "tool") {
|
|
25850
26561
|
const tc = entry.tc;
|
|
25851
26562
|
const colorFn = toolColor(tc.tool);
|
|
25852
26563
|
const toolPad = colorFn(tc.tool.padEnd(16));
|
|
25853
|
-
const detail =
|
|
25854
|
-
const ts = tc.timestamp ?
|
|
26564
|
+
const detail = chalk28.gray(truncate(toolInputSummary(tc.tool, tc.input), 70));
|
|
26565
|
+
const ts = tc.timestamp ? chalk28.dim(fmtTime(tc.timestamp) + " ") : " ";
|
|
25855
26566
|
console.log(` ${ts}${toolPad} ${detail}`);
|
|
25856
26567
|
} else {
|
|
25857
26568
|
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 ?
|
|
26569
|
+
const ts = bc.timestamp ? chalk28.dim(fmtTime(bc.timestamp) + " ") : " ";
|
|
26570
|
+
const label2 = chalk28.red("\u{1F6D1} BLOCKED".padEnd(16));
|
|
26571
|
+
const toolName = chalk28.red(bc.tool.padEnd(10));
|
|
26572
|
+
const argsSummary = bc.args ? chalk28.gray(truncate(toolInputSummary(bc.tool, bc.args), 40)) : chalk28.dim("[args not logged]");
|
|
26573
|
+
const reason = bc.checkedBy ? chalk28.dim(" \u2190 " + bc.checkedBy) : "";
|
|
25863
26574
|
console.log(` ${ts}${label2} ${toolName} ${argsSummary}${reason}`);
|
|
25864
26575
|
}
|
|
25865
26576
|
}
|
|
25866
26577
|
console.log("");
|
|
25867
26578
|
if (s.modifiedFiles.length > 0) {
|
|
25868
|
-
console.log(
|
|
26579
|
+
console.log(chalk28.bold(` Files modified (${s.modifiedFiles.length}):`));
|
|
25869
26580
|
for (const f of s.modifiedFiles) {
|
|
25870
|
-
console.log(" " +
|
|
26581
|
+
console.log(" " + chalk28.yellow(f));
|
|
25871
26582
|
}
|
|
25872
26583
|
console.log("");
|
|
25873
26584
|
}
|
|
@@ -25875,13 +26586,13 @@ function renderDetail(s) {
|
|
|
25875
26586
|
function registerSessionsCommand(program2) {
|
|
25876
26587
|
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
26588
|
console.log("");
|
|
25878
|
-
console.log(
|
|
26589
|
+
console.log(chalk28.cyan.bold("\u{1F4CB} node9 sessions") + chalk28.dim(" \u2014 what your AI agent did"));
|
|
25879
26590
|
console.log("");
|
|
25880
26591
|
const days = options.detail || options.all ? null : Math.max(1, parseInt(options.days, 10) || 7);
|
|
25881
26592
|
const rangeLabel = options.detail ? "all time" : options.all ? "all time" : `last ${String(days)} days`;
|
|
25882
|
-
console.log(
|
|
26593
|
+
console.log(chalk28.dim(" " + rangeLabel));
|
|
25883
26594
|
console.log("");
|
|
25884
|
-
process.stdout.write(
|
|
26595
|
+
process.stdout.write(chalk28.dim(" Loading\u2026"));
|
|
25885
26596
|
const summaries = buildSessions(days);
|
|
25886
26597
|
if (process.stdout.isTTY) {
|
|
25887
26598
|
process.stdout.clearLine(0);
|
|
@@ -25894,8 +26605,8 @@ function registerSessionsCommand(program2) {
|
|
|
25894
26605
|
(s) => s.sessionId === options.detail || s.sessionId.startsWith(options.detail)
|
|
25895
26606
|
);
|
|
25896
26607
|
if (!target) {
|
|
25897
|
-
console.log(
|
|
25898
|
-
console.log(
|
|
26608
|
+
console.log(chalk28.red(` Session not found: ${options.detail}`));
|
|
26609
|
+
console.log(chalk28.dim(" Run `node9 sessions` to list recent sessions.\n"));
|
|
25899
26610
|
return;
|
|
25900
26611
|
}
|
|
25901
26612
|
renderDetail(target);
|
|
@@ -25909,7 +26620,7 @@ function registerSessionsCommand(program2) {
|
|
|
25909
26620
|
|
|
25910
26621
|
// src/cli/commands/session-taint.ts
|
|
25911
26622
|
init_daemon();
|
|
25912
|
-
import
|
|
26623
|
+
import chalk29 from "chalk";
|
|
25913
26624
|
function resolveSessionId(records, query) {
|
|
25914
26625
|
const exact = records.find((r) => r.sessionId === query);
|
|
25915
26626
|
if (exact) return { record: exact };
|
|
@@ -25935,22 +26646,22 @@ function registerSessionTaintCommand(program2) {
|
|
|
25935
26646
|
const records = await listSessionTaints();
|
|
25936
26647
|
console.log("");
|
|
25937
26648
|
if (records.length === 0) {
|
|
25938
|
-
console.log(
|
|
25939
|
-
console.log(
|
|
26649
|
+
console.log(chalk29.dim(" No tainted sessions."));
|
|
26650
|
+
console.log(chalk29.dim(" (Taint lives in the daemon \u2014 a stopped daemon has none.)") + "\n");
|
|
25940
26651
|
return;
|
|
25941
26652
|
}
|
|
25942
26653
|
console.log(
|
|
25943
|
-
" " +
|
|
26654
|
+
" " + chalk29.bold(String(records.length)) + chalk29.dim(` tainted session${records.length !== 1 ? "s" : ""}`)
|
|
25944
26655
|
);
|
|
25945
26656
|
console.log("");
|
|
25946
26657
|
for (const r of records) {
|
|
25947
26658
|
console.log(
|
|
25948
|
-
" " +
|
|
26659
|
+
" " + chalk29.yellow(r.sessionId.slice(0, 8).padEnd(10)) + chalk29.red(r.source) + sourceGap(r.source) + chalk29.dim("clears in " + fmtRemaining(r.expiresAt))
|
|
25949
26660
|
);
|
|
25950
26661
|
}
|
|
25951
26662
|
console.log("");
|
|
25952
26663
|
console.log(
|
|
25953
|
-
|
|
26664
|
+
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
26665
|
);
|
|
25955
26666
|
});
|
|
25956
26667
|
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 +26669,32 @@ function registerSessionTaintCommand(program2) {
|
|
|
25958
26669
|
if (opts.all) {
|
|
25959
26670
|
const res2 = await clearSessionTaint({ all: true });
|
|
25960
26671
|
if (res2.daemonUnavailable) {
|
|
25961
|
-
console.log(
|
|
26672
|
+
console.log(chalk29.dim(" node9 daemon not running \u2014 no active taints to clear.") + "\n");
|
|
25962
26673
|
return;
|
|
25963
26674
|
}
|
|
25964
26675
|
console.log(
|
|
25965
|
-
|
|
26676
|
+
chalk29.green(" \u2713 ") + `Cleared ${chalk29.bold(String(res2.cleared))} session taint${res2.cleared !== 1 ? "s" : ""}.
|
|
25966
26677
|
`
|
|
25967
26678
|
);
|
|
25968
26679
|
return;
|
|
25969
26680
|
}
|
|
25970
26681
|
if (!sessionId) {
|
|
25971
|
-
console.log(
|
|
25972
|
-
console.log(
|
|
26682
|
+
console.log(chalk29.red(" Provide a session id or --all."));
|
|
26683
|
+
console.log(chalk29.dim(" Run `node9 session-taint list` to see tainted sessions.") + "\n");
|
|
25973
26684
|
return;
|
|
25974
26685
|
}
|
|
25975
26686
|
const records = await listSessionTaints();
|
|
25976
26687
|
if (records.length === 0) {
|
|
25977
|
-
console.log(
|
|
26688
|
+
console.log(chalk29.dim(" No tainted sessions \u2014 nothing to clear.") + "\n");
|
|
25978
26689
|
return;
|
|
25979
26690
|
}
|
|
25980
26691
|
const resolved = resolveSessionId(records, sessionId);
|
|
25981
26692
|
if ("error" in resolved) {
|
|
25982
26693
|
if (resolved.error === "not-found") {
|
|
25983
|
-
console.log(
|
|
26694
|
+
console.log(chalk29.red(` No tainted session matches "${sessionId}".`));
|
|
25984
26695
|
} else {
|
|
25985
|
-
console.log(
|
|
25986
|
-
for (const m of resolved.matches) console.log(
|
|
26696
|
+
console.log(chalk29.red(` "${sessionId}" is ambiguous \u2014 matches:`));
|
|
26697
|
+
for (const m of resolved.matches) console.log(chalk29.dim(" " + m));
|
|
25987
26698
|
}
|
|
25988
26699
|
console.log("");
|
|
25989
26700
|
return;
|
|
@@ -25991,24 +26702,24 @@ function registerSessionTaintCommand(program2) {
|
|
|
25991
26702
|
const res = await clearSessionTaint({ sessionId: resolved.record.sessionId });
|
|
25992
26703
|
if (res.cleared > 0) {
|
|
25993
26704
|
console.log(
|
|
25994
|
-
|
|
26705
|
+
chalk29.green(" \u2713 ") + `Cleared taint for ${chalk29.yellow(resolved.record.sessionId.slice(0, 8))} ` + chalk29.dim(`(was flagged by ${resolved.record.source}).`) + "\n"
|
|
25995
26706
|
);
|
|
25996
26707
|
} else {
|
|
25997
26708
|
console.log(
|
|
25998
|
-
|
|
26709
|
+
chalk29.dim(` Session ${resolved.record.sessionId.slice(0, 8)} was already clear.`) + "\n"
|
|
25999
26710
|
);
|
|
26000
26711
|
}
|
|
26001
26712
|
});
|
|
26002
26713
|
}
|
|
26003
26714
|
|
|
26004
26715
|
// src/cli/commands/skill-pin.ts
|
|
26005
|
-
import
|
|
26006
|
-
import
|
|
26007
|
-
import
|
|
26008
|
-
import
|
|
26716
|
+
import chalk30 from "chalk";
|
|
26717
|
+
import fs57 from "fs";
|
|
26718
|
+
import os49 from "os";
|
|
26719
|
+
import path55 from "path";
|
|
26009
26720
|
function wipeSkillSessions() {
|
|
26010
26721
|
try {
|
|
26011
|
-
|
|
26722
|
+
fs57.rmSync(path55.join(os49.homedir(), ".node9", "skill-sessions"), {
|
|
26012
26723
|
recursive: true,
|
|
26013
26724
|
force: true
|
|
26014
26725
|
});
|
|
@@ -26022,29 +26733,29 @@ function registerSkillPinCommand(program2) {
|
|
|
26022
26733
|
const result = readSkillPinsSafe();
|
|
26023
26734
|
if (!result.ok) {
|
|
26024
26735
|
if (result.reason === "missing") {
|
|
26025
|
-
console.log(
|
|
26736
|
+
console.log(chalk30.gray("\nNo skill roots are pinned yet."));
|
|
26026
26737
|
console.log(
|
|
26027
|
-
|
|
26738
|
+
chalk30.gray("Pins are created automatically on the first tool call of each session.\n")
|
|
26028
26739
|
);
|
|
26029
26740
|
return;
|
|
26030
26741
|
}
|
|
26031
|
-
console.error(
|
|
26742
|
+
console.error(chalk30.red(`
|
|
26032
26743
|
\u274C Pin file is corrupt: ${result.detail}`));
|
|
26033
|
-
console.error(
|
|
26744
|
+
console.error(chalk30.yellow(" Run: node9 skill pin reset\n"));
|
|
26034
26745
|
process.exit(1);
|
|
26035
26746
|
}
|
|
26036
26747
|
const entries = Object.entries(result.pins.roots);
|
|
26037
26748
|
if (entries.length === 0) {
|
|
26038
|
-
console.log(
|
|
26749
|
+
console.log(chalk30.gray("\nNo skill roots are pinned yet.\n"));
|
|
26039
26750
|
return;
|
|
26040
26751
|
}
|
|
26041
|
-
console.log(
|
|
26752
|
+
console.log(chalk30.bold("\n\u{1F512} Pinned Skill Roots\n"));
|
|
26042
26753
|
for (const [key, entry] of entries) {
|
|
26043
|
-
const missing = entry.exists ? "" :
|
|
26044
|
-
console.log(` ${
|
|
26754
|
+
const missing = entry.exists ? "" : chalk30.yellow(" (not present at pin time)");
|
|
26755
|
+
console.log(` ${chalk30.cyan(key)} ${chalk30.gray(entry.rootPath)}${missing}`);
|
|
26045
26756
|
console.log(` Files (${entry.fileCount})`);
|
|
26046
|
-
console.log(` Hash: ${
|
|
26047
|
-
console.log(` Pinned: ${
|
|
26757
|
+
console.log(` Hash: ${chalk30.gray(entry.contentHash.slice(0, 16))}...`);
|
|
26758
|
+
console.log(` Pinned: ${chalk30.gray(entry.pinnedAt)}
|
|
26048
26759
|
`);
|
|
26049
26760
|
}
|
|
26050
26761
|
});
|
|
@@ -26053,52 +26764,52 @@ function registerSkillPinCommand(program2) {
|
|
|
26053
26764
|
try {
|
|
26054
26765
|
pins = readSkillPins();
|
|
26055
26766
|
} catch {
|
|
26056
|
-
console.error(
|
|
26057
|
-
console.error(
|
|
26767
|
+
console.error(chalk30.red("\n\u274C Pin file is corrupt."));
|
|
26768
|
+
console.error(chalk30.yellow(" Run: node9 skill pin reset\n"));
|
|
26058
26769
|
process.exit(1);
|
|
26059
26770
|
}
|
|
26060
26771
|
if (!pins.roots[rootKey]) {
|
|
26061
|
-
console.error(
|
|
26772
|
+
console.error(chalk30.red(`
|
|
26062
26773
|
\u274C No pin found for root key "${rootKey}"
|
|
26063
26774
|
`));
|
|
26064
|
-
console.error(`Run ${
|
|
26775
|
+
console.error(`Run ${chalk30.cyan("node9 skill pin list")} to see pinned roots.
|
|
26065
26776
|
`);
|
|
26066
26777
|
process.exit(1);
|
|
26067
26778
|
}
|
|
26068
26779
|
const rootPath = pins.roots[rootKey].rootPath;
|
|
26069
26780
|
removePin2(rootKey);
|
|
26070
26781
|
wipeSkillSessions();
|
|
26071
|
-
console.log(
|
|
26072
|
-
\u{1F513} Pin removed for ${
|
|
26073
|
-
console.log(
|
|
26074
|
-
console.log(
|
|
26782
|
+
console.log(chalk30.green(`
|
|
26783
|
+
\u{1F513} Pin removed for ${chalk30.cyan(rootKey)}`));
|
|
26784
|
+
console.log(chalk30.gray(` ${rootPath}`));
|
|
26785
|
+
console.log(chalk30.gray(" Next session will re-pin with current state.\n"));
|
|
26075
26786
|
});
|
|
26076
26787
|
pinSubCmd.command("reset").description("Clear all skill pins and wipe session verification flags").action(() => {
|
|
26077
26788
|
const result = readSkillPinsSafe();
|
|
26078
26789
|
if (!result.ok && result.reason === "missing") {
|
|
26079
26790
|
wipeSkillSessions();
|
|
26080
|
-
console.log(
|
|
26791
|
+
console.log(chalk30.gray("\nNo pins to clear.\n"));
|
|
26081
26792
|
return;
|
|
26082
26793
|
}
|
|
26083
26794
|
const count = result.ok ? Object.keys(result.pins.roots).length : "?";
|
|
26084
26795
|
clearAllPins2();
|
|
26085
26796
|
wipeSkillSessions();
|
|
26086
|
-
console.log(
|
|
26797
|
+
console.log(chalk30.green(`
|
|
26087
26798
|
\u{1F513} Cleared ${count} skill pin(s).`));
|
|
26088
|
-
console.log(
|
|
26799
|
+
console.log(chalk30.gray(" Next session will re-pin with current state.\n"));
|
|
26089
26800
|
});
|
|
26090
26801
|
}
|
|
26091
26802
|
|
|
26092
26803
|
// src/cli/commands/decisions.ts
|
|
26093
|
-
import
|
|
26094
|
-
import
|
|
26095
|
-
import
|
|
26096
|
-
import
|
|
26097
|
-
var DECISIONS_FILE2 =
|
|
26804
|
+
import fs58 from "fs";
|
|
26805
|
+
import os50 from "os";
|
|
26806
|
+
import path56 from "path";
|
|
26807
|
+
import chalk31 from "chalk";
|
|
26808
|
+
var DECISIONS_FILE2 = path56.join(os50.homedir(), ".node9", "decisions.json");
|
|
26098
26809
|
function readDecisions() {
|
|
26099
26810
|
try {
|
|
26100
|
-
if (!
|
|
26101
|
-
const raw =
|
|
26811
|
+
if (!fs58.existsSync(DECISIONS_FILE2)) return {};
|
|
26812
|
+
const raw = fs58.readFileSync(DECISIONS_FILE2, "utf-8");
|
|
26102
26813
|
const parsed = JSON.parse(raw);
|
|
26103
26814
|
const out = {};
|
|
26104
26815
|
for (const [k, v] of Object.entries(parsed)) {
|
|
@@ -26110,11 +26821,11 @@ function readDecisions() {
|
|
|
26110
26821
|
}
|
|
26111
26822
|
}
|
|
26112
26823
|
function writeDecisions(d) {
|
|
26113
|
-
const dir =
|
|
26114
|
-
if (!
|
|
26824
|
+
const dir = path56.dirname(DECISIONS_FILE2);
|
|
26825
|
+
if (!fs58.existsSync(dir)) fs58.mkdirSync(dir, { recursive: true });
|
|
26115
26826
|
const tmp = `${DECISIONS_FILE2}.${process.pid}.tmp`;
|
|
26116
|
-
|
|
26117
|
-
|
|
26827
|
+
fs58.writeFileSync(tmp, JSON.stringify(d, null, 2));
|
|
26828
|
+
fs58.renameSync(tmp, DECISIONS_FILE2);
|
|
26118
26829
|
}
|
|
26119
26830
|
function registerDecisionsCommand(program2) {
|
|
26120
26831
|
const cmd = program2.command("decisions").description('Manage persistent "Always Allow" / "Always Deny" tool decisions');
|
|
@@ -26122,67 +26833,67 @@ function registerDecisionsCommand(program2) {
|
|
|
26122
26833
|
const decisions = readDecisions();
|
|
26123
26834
|
const entries = Object.entries(decisions);
|
|
26124
26835
|
if (entries.length === 0) {
|
|
26125
|
-
console.log(
|
|
26836
|
+
console.log(chalk31.gray(" No persistent decisions stored."));
|
|
26126
26837
|
console.log(
|
|
26127
|
-
|
|
26128
|
-
`) +
|
|
26838
|
+
chalk31.gray(` File: ${DECISIONS_FILE2}
|
|
26839
|
+
`) + chalk31.gray(' Decisions are written when you click "Always Allow" or')
|
|
26129
26840
|
);
|
|
26130
|
-
console.log(
|
|
26841
|
+
console.log(chalk31.gray(' "Always Deny" in node9 tail or the native popup.'));
|
|
26131
26842
|
return;
|
|
26132
26843
|
}
|
|
26133
|
-
console.log(
|
|
26844
|
+
console.log(chalk31.bold(`
|
|
26134
26845
|
Persistent decisions (${entries.length})
|
|
26135
26846
|
`));
|
|
26136
26847
|
const w = Math.max(...entries.map(([k]) => k.length));
|
|
26137
26848
|
for (const [tool, verdict] of entries.sort()) {
|
|
26138
|
-
const colored = verdict === "allow" ?
|
|
26849
|
+
const colored = verdict === "allow" ? chalk31.green(verdict) : chalk31.red(verdict);
|
|
26139
26850
|
console.log(` ${tool.padEnd(w)} ${colored}`);
|
|
26140
26851
|
}
|
|
26141
26852
|
console.log(
|
|
26142
|
-
|
|
26853
|
+
chalk31.gray(`
|
|
26143
26854
|
Stored in ${DECISIONS_FILE2}
|
|
26144
|
-
`) +
|
|
26855
|
+
`) + chalk31.gray(" Run `node9 decisions clear <tool>` to remove an entry.")
|
|
26145
26856
|
);
|
|
26146
26857
|
});
|
|
26147
26858
|
cmd.command("clear <toolName>").description("Remove a persistent decision for one tool").action((toolName) => {
|
|
26148
26859
|
const decisions = readDecisions();
|
|
26149
26860
|
if (!(toolName in decisions)) {
|
|
26150
|
-
console.log(
|
|
26861
|
+
console.log(chalk31.yellow(` No persistent decision for "${toolName}". Nothing to clear.`));
|
|
26151
26862
|
process.exitCode = 1;
|
|
26152
26863
|
return;
|
|
26153
26864
|
}
|
|
26154
26865
|
delete decisions[toolName];
|
|
26155
26866
|
writeDecisions(decisions);
|
|
26156
|
-
console.log(
|
|
26867
|
+
console.log(chalk31.green(` \u2713 Cleared persistent decision for "${toolName}".`));
|
|
26157
26868
|
});
|
|
26158
26869
|
cmd.command("clear-all").description("Remove every persistent decision (irreversible)").action(() => {
|
|
26159
26870
|
const decisions = readDecisions();
|
|
26160
26871
|
const count = Object.keys(decisions).length;
|
|
26161
26872
|
if (count === 0) {
|
|
26162
|
-
console.log(
|
|
26873
|
+
console.log(chalk31.gray(" Nothing to clear \u2014 no persistent decisions stored."));
|
|
26163
26874
|
return;
|
|
26164
26875
|
}
|
|
26165
26876
|
writeDecisions({});
|
|
26166
26877
|
console.log(
|
|
26167
|
-
|
|
26878
|
+
chalk31.green(` \u2713 Cleared ${count} persistent decision${count === 1 ? "" : "s"}.`)
|
|
26168
26879
|
);
|
|
26169
26880
|
});
|
|
26170
26881
|
}
|
|
26171
26882
|
|
|
26172
26883
|
// src/cli/commands/dlp.ts
|
|
26173
|
-
import
|
|
26174
|
-
import
|
|
26175
|
-
import
|
|
26176
|
-
import
|
|
26177
|
-
var AUDIT_LOG =
|
|
26178
|
-
var RESOLVED_FILE =
|
|
26884
|
+
import chalk32 from "chalk";
|
|
26885
|
+
import fs59 from "fs";
|
|
26886
|
+
import path57 from "path";
|
|
26887
|
+
import os51 from "os";
|
|
26888
|
+
var AUDIT_LOG = path57.join(os51.homedir(), ".node9", "audit.log");
|
|
26889
|
+
var RESOLVED_FILE = path57.join(os51.homedir(), ".node9", "dlp-resolved.json");
|
|
26179
26890
|
var ANSI_RE = /\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g;
|
|
26180
26891
|
function stripAnsi(s) {
|
|
26181
26892
|
return s.replace(ANSI_RE, "");
|
|
26182
26893
|
}
|
|
26183
26894
|
function loadResolved() {
|
|
26184
26895
|
try {
|
|
26185
|
-
const raw = JSON.parse(
|
|
26896
|
+
const raw = JSON.parse(fs59.readFileSync(RESOLVED_FILE, "utf-8"));
|
|
26186
26897
|
return new Set(raw);
|
|
26187
26898
|
} catch {
|
|
26188
26899
|
return /* @__PURE__ */ new Set();
|
|
@@ -26190,13 +26901,13 @@ function loadResolved() {
|
|
|
26190
26901
|
}
|
|
26191
26902
|
function saveResolved(resolved) {
|
|
26192
26903
|
try {
|
|
26193
|
-
|
|
26904
|
+
fs59.writeFileSync(RESOLVED_FILE, JSON.stringify([...resolved], null, 2), { mode: 384 });
|
|
26194
26905
|
} catch {
|
|
26195
26906
|
}
|
|
26196
26907
|
}
|
|
26197
26908
|
function loadDlpFindings() {
|
|
26198
|
-
if (!
|
|
26199
|
-
return
|
|
26909
|
+
if (!fs59.existsSync(AUDIT_LOG)) return [];
|
|
26910
|
+
return fs59.readFileSync(AUDIT_LOG, "utf-8").split("\n").flatMap((line) => {
|
|
26200
26911
|
if (!line.trim()) return [];
|
|
26201
26912
|
try {
|
|
26202
26913
|
const e = JSON.parse(line);
|
|
@@ -26225,14 +26936,14 @@ function registerDlpCommand(program2) {
|
|
|
26225
26936
|
cmd.command("resolve").description("Mark all current DLP findings as resolved").action(() => {
|
|
26226
26937
|
const findings = loadDlpFindings();
|
|
26227
26938
|
if (findings.length === 0) {
|
|
26228
|
-
console.log(
|
|
26939
|
+
console.log(chalk32.green("\n \u2705 No response-DLP findings to resolve.\n"));
|
|
26229
26940
|
return;
|
|
26230
26941
|
}
|
|
26231
26942
|
const resolved = loadResolved();
|
|
26232
26943
|
for (const e of findings) resolved.add(entryKey2(e));
|
|
26233
26944
|
saveResolved(resolved);
|
|
26234
26945
|
console.log(
|
|
26235
|
-
|
|
26946
|
+
chalk32.green(
|
|
26236
26947
|
`
|
|
26237
26948
|
\u2705 ${findings.length} finding${findings.length !== 1 ? "s" : ""} marked as resolved.
|
|
26238
26949
|
`
|
|
@@ -26246,47 +26957,47 @@ function registerDlpCommand(program2) {
|
|
|
26246
26957
|
const resolvedCount = findings.length - open.length;
|
|
26247
26958
|
console.log("");
|
|
26248
26959
|
console.log(
|
|
26249
|
-
|
|
26960
|
+
chalk32.bold.cyan("\u{1F510} node9 dlp") + chalk32.dim(" \u2014 secrets found in Claude response text")
|
|
26250
26961
|
);
|
|
26251
26962
|
console.log("");
|
|
26252
26963
|
if (open.length === 0) {
|
|
26253
26964
|
if (resolvedCount > 0) {
|
|
26254
|
-
console.log(
|
|
26965
|
+
console.log(chalk32.green(` \u2705 No open findings \xB7 ${resolvedCount} previously resolved`));
|
|
26255
26966
|
} else {
|
|
26256
26967
|
console.log(
|
|
26257
|
-
|
|
26968
|
+
chalk32.green(" \u2705 No findings \u2014 Claude has not leaked secrets in response text")
|
|
26258
26969
|
);
|
|
26259
26970
|
}
|
|
26260
26971
|
console.log("");
|
|
26261
26972
|
return;
|
|
26262
26973
|
}
|
|
26263
26974
|
console.log(
|
|
26264
|
-
|
|
26975
|
+
chalk32.bgRed.white.bold(` \u26A0\uFE0F ${open.length} open finding${open.length !== 1 ? "s" : ""} `) + chalk32.dim(resolvedCount > 0 ? ` (${resolvedCount} resolved)` : "")
|
|
26265
26976
|
);
|
|
26266
26977
|
console.log("");
|
|
26267
26978
|
console.log(
|
|
26268
|
-
|
|
26979
|
+
chalk32.dim(" These secrets were included in Claude's response text \u2014 NOT blocked.")
|
|
26269
26980
|
);
|
|
26270
|
-
console.log(
|
|
26981
|
+
console.log(chalk32.dim(" Rotate each affected key immediately.\n"));
|
|
26271
26982
|
for (const e of open) {
|
|
26272
26983
|
console.log(
|
|
26273
|
-
" " +
|
|
26984
|
+
" " + chalk32.red("\u25CF") + " " + chalk32.white(e.dlpPattern ?? "Secret") + chalk32.dim(" " + fmtDate3(e.ts))
|
|
26274
26985
|
);
|
|
26275
26986
|
if (e.dlpSample) {
|
|
26276
|
-
console.log(" " +
|
|
26987
|
+
console.log(" " + chalk32.dim("Sample: ") + chalk32.yellow(stripAnsi(e.dlpSample)));
|
|
26277
26988
|
}
|
|
26278
26989
|
if (e.project) {
|
|
26279
|
-
console.log(" " +
|
|
26990
|
+
console.log(" " + chalk32.dim("Project: ") + chalk32.dim(stripAnsi(e.project)));
|
|
26280
26991
|
}
|
|
26281
26992
|
console.log("");
|
|
26282
26993
|
}
|
|
26283
|
-
console.log(" " +
|
|
26284
|
-
console.log(" " +
|
|
26994
|
+
console.log(" " + chalk32.bold("Next steps:"));
|
|
26995
|
+
console.log(" " + chalk32.cyan("1.") + " Rotate any exposed keys shown above");
|
|
26285
26996
|
console.log(
|
|
26286
|
-
" " +
|
|
26997
|
+
" " + chalk32.cyan("2.") + " Run " + chalk32.white("node9 dlp resolve") + " to acknowledge"
|
|
26287
26998
|
);
|
|
26288
26999
|
console.log(
|
|
26289
|
-
" " +
|
|
27000
|
+
" " + chalk32.cyan("3.") + " Run " + chalk32.white("node9 report") + " for full audit history"
|
|
26290
27001
|
);
|
|
26291
27002
|
console.log("");
|
|
26292
27003
|
});
|
|
@@ -26294,15 +27005,15 @@ function registerDlpCommand(program2) {
|
|
|
26294
27005
|
|
|
26295
27006
|
// src/cli/commands/mask.ts
|
|
26296
27007
|
init_dlp();
|
|
26297
|
-
import
|
|
26298
|
-
import
|
|
26299
|
-
import
|
|
26300
|
-
import
|
|
27008
|
+
import chalk33 from "chalk";
|
|
27009
|
+
import fs60 from "fs";
|
|
27010
|
+
import path58 from "path";
|
|
27011
|
+
import os52 from "os";
|
|
26301
27012
|
function findJsonlFiles(dir) {
|
|
26302
27013
|
const results = [];
|
|
26303
|
-
if (!
|
|
26304
|
-
for (const entry of
|
|
26305
|
-
const full =
|
|
27014
|
+
if (!fs60.existsSync(dir)) return results;
|
|
27015
|
+
for (const entry of fs60.readdirSync(dir, { withFileTypes: true })) {
|
|
27016
|
+
const full = path58.join(dir, entry.name);
|
|
26306
27017
|
if (entry.isDirectory()) results.push(...findJsonlFiles(full));
|
|
26307
27018
|
else if (entry.isFile() && entry.name.endsWith(".jsonl")) results.push(full);
|
|
26308
27019
|
}
|
|
@@ -26345,7 +27056,7 @@ function redactJson(obj) {
|
|
|
26345
27056
|
function processFile(filePath, dryRun) {
|
|
26346
27057
|
let raw;
|
|
26347
27058
|
try {
|
|
26348
|
-
raw =
|
|
27059
|
+
raw = fs60.readFileSync(filePath, "utf-8");
|
|
26349
27060
|
} catch {
|
|
26350
27061
|
return { redactedLines: 0, patterns: [] };
|
|
26351
27062
|
}
|
|
@@ -26377,14 +27088,14 @@ function processFile(filePath, dryRun) {
|
|
|
26377
27088
|
}
|
|
26378
27089
|
}
|
|
26379
27090
|
if (!dryRun && redactedLines > 0) {
|
|
26380
|
-
|
|
27091
|
+
fs60.writeFileSync(filePath, newLines.join("\n"), "utf-8");
|
|
26381
27092
|
}
|
|
26382
27093
|
return { redactedLines, patterns };
|
|
26383
27094
|
}
|
|
26384
27095
|
function processJsonFile(filePath, dryRun) {
|
|
26385
27096
|
let raw;
|
|
26386
27097
|
try {
|
|
26387
|
-
raw =
|
|
27098
|
+
raw = fs60.readFileSync(filePath, "utf-8");
|
|
26388
27099
|
} catch {
|
|
26389
27100
|
return { redactedLines: 0, patterns: [] };
|
|
26390
27101
|
}
|
|
@@ -26397,15 +27108,15 @@ function processJsonFile(filePath, dryRun) {
|
|
|
26397
27108
|
const { value, modified, found } = redactJson(parsed);
|
|
26398
27109
|
if (!modified) return { redactedLines: 0, patterns: [] };
|
|
26399
27110
|
if (!dryRun) {
|
|
26400
|
-
|
|
27111
|
+
fs60.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
|
|
26401
27112
|
}
|
|
26402
27113
|
return { redactedLines: 1, patterns: found };
|
|
26403
27114
|
}
|
|
26404
27115
|
function findJsonFiles(dir) {
|
|
26405
27116
|
const results = [];
|
|
26406
|
-
if (!
|
|
26407
|
-
for (const entry of
|
|
26408
|
-
const full =
|
|
27117
|
+
if (!fs60.existsSync(dir)) return results;
|
|
27118
|
+
for (const entry of fs60.readdirSync(dir, { withFileTypes: true })) {
|
|
27119
|
+
const full = path58.join(dir, entry.name);
|
|
26409
27120
|
if (entry.isDirectory()) results.push(...findJsonFiles(full));
|
|
26410
27121
|
else if (entry.isFile() && entry.name.endsWith(".json")) results.push(full);
|
|
26411
27122
|
}
|
|
@@ -26414,9 +27125,9 @@ function findJsonFiles(dir) {
|
|
|
26414
27125
|
function registerMaskCommand(program2) {
|
|
26415
27126
|
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
27127
|
const dryRun = !!options.dryRun;
|
|
26417
|
-
const home =
|
|
26418
|
-
const claudeDir =
|
|
26419
|
-
const geminiDir =
|
|
27128
|
+
const home = os52.homedir();
|
|
27129
|
+
const claudeDir = path58.join(home, ".claude", "projects");
|
|
27130
|
+
const geminiDir = path58.join(home, ".gemini", "tmp");
|
|
26420
27131
|
const allFiles = [
|
|
26421
27132
|
...findJsonlFiles(claudeDir).map((p) => ({ path: p, type: "jsonl" })),
|
|
26422
27133
|
...findJsonFiles(geminiDir).map((p) => ({ path: p, type: "json" }))
|
|
@@ -26424,18 +27135,18 @@ function registerMaskCommand(program2) {
|
|
|
26424
27135
|
const cutoff = options.all ? null : new Date(Date.now() - 30 * 24 * 60 * 60 * 1e3);
|
|
26425
27136
|
const filtered = cutoff ? allFiles.filter((f) => {
|
|
26426
27137
|
try {
|
|
26427
|
-
return
|
|
27138
|
+
return fs60.statSync(f.path).mtime >= cutoff;
|
|
26428
27139
|
} catch {
|
|
26429
27140
|
return false;
|
|
26430
27141
|
}
|
|
26431
27142
|
}) : allFiles;
|
|
26432
27143
|
if (filtered.length === 0) {
|
|
26433
|
-
console.log(
|
|
27144
|
+
console.log(chalk33.yellow(" No session files found."));
|
|
26434
27145
|
return;
|
|
26435
27146
|
}
|
|
26436
27147
|
console.log("");
|
|
26437
27148
|
if (dryRun) {
|
|
26438
|
-
console.log(
|
|
27149
|
+
console.log(chalk33.dim(" Dry run \u2014 no files will be modified.\n"));
|
|
26439
27150
|
}
|
|
26440
27151
|
let totalFiles = 0;
|
|
26441
27152
|
let totalLines = 0;
|
|
@@ -26451,23 +27162,23 @@ function registerMaskCommand(program2) {
|
|
|
26451
27162
|
});
|
|
26452
27163
|
const verb = dryRun ? "Would redact" : "Redacted";
|
|
26453
27164
|
console.log(
|
|
26454
|
-
" " +
|
|
27165
|
+
" " + chalk33.dim(shortPath.slice(0, 60).padEnd(62)) + chalk33.red(`${verb}: `) + chalk33.yellow(patterns.join(", ")) + chalk33.dim(` (${redactedLines} line${redactedLines !== 1 ? "s" : ""})`)
|
|
26455
27166
|
);
|
|
26456
27167
|
}
|
|
26457
27168
|
}
|
|
26458
27169
|
console.log("");
|
|
26459
27170
|
if (totalFiles === 0) {
|
|
26460
|
-
console.log(
|
|
27171
|
+
console.log(chalk33.green(" No secrets found in session history."));
|
|
26461
27172
|
} else {
|
|
26462
27173
|
const verb = dryRun ? "would be modified" : "modified";
|
|
26463
27174
|
console.log(
|
|
26464
|
-
|
|
27175
|
+
chalk33.bold(` ${totalFiles} file${totalFiles !== 1 ? "s" : ""} ${verb}`) + chalk33.dim(`, ${totalLines} line${totalLines !== 1 ? "s" : ""} redacted`)
|
|
26465
27176
|
);
|
|
26466
|
-
console.log(" Patterns: " +
|
|
27177
|
+
console.log(" Patterns: " + chalk33.yellow(totalPatterns.join(", ")));
|
|
26467
27178
|
if (!dryRun) {
|
|
26468
27179
|
console.log("");
|
|
26469
27180
|
console.log(
|
|
26470
|
-
|
|
27181
|
+
chalk33.dim(
|
|
26471
27182
|
" 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
27183
|
)
|
|
26473
27184
|
);
|
|
@@ -26480,20 +27191,20 @@ function registerMaskCommand(program2) {
|
|
|
26480
27191
|
// src/cli.ts
|
|
26481
27192
|
init_blast();
|
|
26482
27193
|
var { version } = JSON.parse(
|
|
26483
|
-
|
|
27194
|
+
fs63.readFileSync(path61.join(__dirname, "../package.json"), "utf-8")
|
|
26484
27195
|
);
|
|
26485
27196
|
var program = new Command();
|
|
26486
27197
|
program.name("node9").description("The Sudo Command for AI Agents").version(version);
|
|
26487
27198
|
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
27199
|
const DEFAULT_API_URL2 = "https://api.node9.ai/api/v1/intercept";
|
|
26489
|
-
const credPath =
|
|
26490
|
-
if (!
|
|
26491
|
-
|
|
27200
|
+
const credPath = path61.join(os55.homedir(), ".node9", "credentials.json");
|
|
27201
|
+
if (!fs63.existsSync(path61.dirname(credPath)))
|
|
27202
|
+
fs63.mkdirSync(path61.dirname(credPath), { recursive: true });
|
|
26492
27203
|
const profileName = options.profile || "default";
|
|
26493
27204
|
let existingCreds = {};
|
|
26494
27205
|
try {
|
|
26495
|
-
if (
|
|
26496
|
-
const raw = JSON.parse(
|
|
27206
|
+
if (fs63.existsSync(credPath)) {
|
|
27207
|
+
const raw = JSON.parse(fs63.readFileSync(credPath, "utf-8"));
|
|
26497
27208
|
if (raw.apiKey) {
|
|
26498
27209
|
existingCreds = {
|
|
26499
27210
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL2 }
|
|
@@ -26505,14 +27216,14 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
26505
27216
|
} catch {
|
|
26506
27217
|
}
|
|
26507
27218
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL2 };
|
|
26508
|
-
|
|
27219
|
+
fs63.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
26509
27220
|
let effectiveCloud = null;
|
|
26510
27221
|
if (profileName === "default") {
|
|
26511
|
-
const configPath2 =
|
|
27222
|
+
const configPath2 = path61.join(os55.homedir(), ".node9", "config.json");
|
|
26512
27223
|
let config = {};
|
|
26513
27224
|
try {
|
|
26514
|
-
if (
|
|
26515
|
-
config = JSON.parse(
|
|
27225
|
+
if (fs63.existsSync(configPath2))
|
|
27226
|
+
config = JSON.parse(fs63.readFileSync(configPath2, "utf-8"));
|
|
26516
27227
|
} catch {
|
|
26517
27228
|
}
|
|
26518
27229
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -26527,35 +27238,35 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
26527
27238
|
approvers.cloud = false;
|
|
26528
27239
|
}
|
|
26529
27240
|
s.approvers = approvers;
|
|
26530
|
-
if (!
|
|
26531
|
-
|
|
26532
|
-
|
|
27241
|
+
if (!fs63.existsSync(path61.dirname(configPath2)))
|
|
27242
|
+
fs63.mkdirSync(path61.dirname(configPath2), { recursive: true });
|
|
27243
|
+
fs63.writeFileSync(configPath2, JSON.stringify(config, null, 2), { mode: 384 });
|
|
26533
27244
|
effectiveCloud = approvers.cloud === true;
|
|
26534
27245
|
}
|
|
26535
27246
|
if (options.profile && profileName !== "default") {
|
|
26536
|
-
console.log(
|
|
26537
|
-
console.log(
|
|
27247
|
+
console.log(chalk35.green(`\u2705 Profile "${profileName}" saved`));
|
|
27248
|
+
console.log(chalk35.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
26538
27249
|
} else if (options.local || effectiveCloud === false) {
|
|
26539
|
-
console.log(
|
|
26540
|
-
console.log(
|
|
27250
|
+
console.log(chalk35.green(`\u2705 Key saved \u2014 Privacy mode \u{1F6E1}\uFE0F`));
|
|
27251
|
+
console.log(chalk35.gray(` All decisions stay on this machine. Nothing syncs to the cloud.`));
|
|
26541
27252
|
if (!options.local) {
|
|
26542
27253
|
console.log(
|
|
26543
|
-
|
|
27254
|
+
chalk35.yellow(` Your config has cloud approvals OFF (settings.approvers.cloud).`)
|
|
26544
27255
|
);
|
|
26545
27256
|
console.log(
|
|
26546
|
-
|
|
27257
|
+
chalk35.gray(` To enable team policy + dashboard sync: set it to true, or re-init.`)
|
|
26547
27258
|
);
|
|
26548
27259
|
}
|
|
26549
27260
|
} else {
|
|
26550
|
-
console.log(
|
|
26551
|
-
console.log(
|
|
27261
|
+
console.log(chalk35.green(`\u2705 Logged in \u2014 agent mode`));
|
|
27262
|
+
console.log(chalk35.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
26552
27263
|
}
|
|
26553
27264
|
});
|
|
26554
27265
|
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
27266
|
const route = options.login ? "auth/login" : "auth/signup";
|
|
26556
27267
|
const url = `https://node9.ai/${route}?ref=cli_cmd`;
|
|
26557
27268
|
console.log("");
|
|
26558
|
-
console.log(" " +
|
|
27269
|
+
console.log(" " + chalk35.dim("Opening ") + chalk35.cyan.underline(url));
|
|
26559
27270
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
26560
27271
|
try {
|
|
26561
27272
|
const child = spawn9(opener, [url], {
|
|
@@ -26588,7 +27299,7 @@ program.command("addto", { hidden: true }).description("Integrate Node9 with an
|
|
|
26588
27299
|
if (target === "hermes") return setupHermes();
|
|
26589
27300
|
if (target === "hud") return setupHud();
|
|
26590
27301
|
console.error(
|
|
26591
|
-
|
|
27302
|
+
chalk35.red(
|
|
26592
27303
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
26593
27304
|
)
|
|
26594
27305
|
);
|
|
@@ -26602,20 +27313,20 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
|
|
|
26602
27313
|
"The agent to protect: claude | antigravity | copilot | gemini | cursor | codex | windsurf | vscode | hud"
|
|
26603
27314
|
).action(async (target) => {
|
|
26604
27315
|
if (!target) {
|
|
26605
|
-
console.log(
|
|
26606
|
-
console.log(" Usage: " +
|
|
27316
|
+
console.log(chalk35.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
27317
|
+
console.log(" Usage: " + chalk35.white("node9 setup <target>") + "\n");
|
|
26607
27318
|
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(" " +
|
|
27319
|
+
console.log(" " + chalk35.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
27320
|
+
console.log(" " + chalk35.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
27321
|
+
console.log(" " + chalk35.green("antigravity") + " \u2014 Antigravity / agy (hook mode)");
|
|
27322
|
+
console.log(" " + chalk35.green("copilot") + " \u2014 GitHub Copilot CLI (hook mode)");
|
|
27323
|
+
console.log(" " + chalk35.green("cursor") + " \u2014 Cursor (MCP proxy)");
|
|
27324
|
+
console.log(" " + chalk35.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
|
|
27325
|
+
console.log(" " + chalk35.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
|
|
27326
|
+
console.log(" " + chalk35.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
|
|
27327
|
+
console.log(" " + chalk35.green("hermes") + " \u2014 Hermes Agent (hook mode)");
|
|
26617
27328
|
process.stdout.write(
|
|
26618
|
-
" " +
|
|
27329
|
+
" " + chalk35.green("hud") + " \u2014 Claude Code security statusline\n"
|
|
26619
27330
|
);
|
|
26620
27331
|
console.log("");
|
|
26621
27332
|
return;
|
|
@@ -26632,7 +27343,7 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
|
|
|
26632
27343
|
if (t === "hermes") return setupHermes();
|
|
26633
27344
|
if (t === "hud") return setupHud();
|
|
26634
27345
|
console.error(
|
|
26635
|
-
|
|
27346
|
+
chalk35.red(
|
|
26636
27347
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
26637
27348
|
)
|
|
26638
27349
|
);
|
|
@@ -26658,33 +27369,33 @@ program.command("removefrom", { hidden: true }).description("Remove Node9 hooks
|
|
|
26658
27369
|
else if (target === "hud") fn = teardownHud;
|
|
26659
27370
|
else {
|
|
26660
27371
|
console.error(
|
|
26661
|
-
|
|
27372
|
+
chalk35.red(
|
|
26662
27373
|
`Unknown target: "${target}". Supported: claude, antigravity, copilot, gemini, cursor, codex, windsurf, vscode, hermes, hud`
|
|
26663
27374
|
)
|
|
26664
27375
|
);
|
|
26665
27376
|
process.exit(1);
|
|
26666
27377
|
}
|
|
26667
|
-
console.log(
|
|
27378
|
+
console.log(chalk35.cyan(`
|
|
26668
27379
|
\u{1F6E1}\uFE0F Node9: removing hooks from ${target}...
|
|
26669
27380
|
`));
|
|
26670
27381
|
try {
|
|
26671
27382
|
fn();
|
|
26672
27383
|
} catch (err2) {
|
|
26673
|
-
console.error(
|
|
27384
|
+
console.error(chalk35.red(` \u26A0\uFE0F Failed: ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
26674
27385
|
process.exit(1);
|
|
26675
27386
|
}
|
|
26676
|
-
console.log(
|
|
27387
|
+
console.log(chalk35.gray("\n Restart the agent for changes to take effect."));
|
|
26677
27388
|
});
|
|
26678
27389
|
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(
|
|
27390
|
+
console.log(chalk35.cyan("\n\u{1F6E1}\uFE0F Node9 Uninstall\n"));
|
|
27391
|
+
console.log(chalk35.bold("Stopping daemon..."));
|
|
26681
27392
|
try {
|
|
26682
27393
|
stopDaemon();
|
|
26683
|
-
console.log(
|
|
27394
|
+
console.log(chalk35.green(" \u2705 Daemon stopped"));
|
|
26684
27395
|
} catch {
|
|
26685
|
-
console.log(
|
|
27396
|
+
console.log(chalk35.blue(" \u2139\uFE0F Daemon was not running"));
|
|
26686
27397
|
}
|
|
26687
|
-
console.log(
|
|
27398
|
+
console.log(chalk35.bold("\nRemoving hooks..."));
|
|
26688
27399
|
let teardownFailed = false;
|
|
26689
27400
|
for (const [label2, fn] of [
|
|
26690
27401
|
["Claude", teardownClaude],
|
|
@@ -26700,45 +27411,45 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
|
|
|
26700
27411
|
} catch (err2) {
|
|
26701
27412
|
teardownFailed = true;
|
|
26702
27413
|
console.error(
|
|
26703
|
-
|
|
27414
|
+
chalk35.red(
|
|
26704
27415
|
` \u26A0\uFE0F Failed to remove ${label2} hooks: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
26705
27416
|
)
|
|
26706
27417
|
);
|
|
26707
27418
|
}
|
|
26708
27419
|
}
|
|
26709
27420
|
if (options.purge) {
|
|
26710
|
-
const node9Dir =
|
|
26711
|
-
if (
|
|
27421
|
+
const node9Dir = path61.join(os55.homedir(), ".node9");
|
|
27422
|
+
if (fs63.existsSync(node9Dir)) {
|
|
26712
27423
|
const confirmed = await confirm2({
|
|
26713
27424
|
message: `Permanently delete ${node9Dir} (config, audit log, credentials)?`,
|
|
26714
27425
|
default: false
|
|
26715
27426
|
});
|
|
26716
27427
|
if (confirmed) {
|
|
26717
|
-
|
|
26718
|
-
if (
|
|
27428
|
+
fs63.rmSync(node9Dir, { recursive: true });
|
|
27429
|
+
if (fs63.existsSync(node9Dir)) {
|
|
26719
27430
|
console.error(
|
|
26720
|
-
|
|
27431
|
+
chalk35.red("\n \u26A0\uFE0F ~/.node9/ could not be fully deleted \u2014 remove it manually.")
|
|
26721
27432
|
);
|
|
26722
27433
|
} else {
|
|
26723
|
-
console.log(
|
|
27434
|
+
console.log(chalk35.green("\n \u2705 Deleted ~/.node9/ (config, audit log, credentials)"));
|
|
26724
27435
|
}
|
|
26725
27436
|
} else {
|
|
26726
|
-
console.log(
|
|
27437
|
+
console.log(chalk35.yellow("\n Skipped \u2014 ~/.node9/ was not deleted."));
|
|
26727
27438
|
}
|
|
26728
27439
|
} else {
|
|
26729
|
-
console.log(
|
|
27440
|
+
console.log(chalk35.blue("\n \u2139\uFE0F ~/.node9/ not found \u2014 nothing to delete"));
|
|
26730
27441
|
}
|
|
26731
27442
|
} else {
|
|
26732
27443
|
console.log(
|
|
26733
|
-
|
|
27444
|
+
chalk35.gray("\n ~/.node9/ kept \u2014 run with --purge to delete config and audit log")
|
|
26734
27445
|
);
|
|
26735
27446
|
}
|
|
26736
27447
|
if (teardownFailed) {
|
|
26737
|
-
console.error(
|
|
27448
|
+
console.error(chalk35.red("\n \u26A0\uFE0F Some hooks could not be removed \u2014 see errors above."));
|
|
26738
27449
|
process.exit(1);
|
|
26739
27450
|
}
|
|
26740
|
-
console.log(
|
|
26741
|
-
console.log(
|
|
27451
|
+
console.log(chalk35.green.bold("\n\u{1F6E1}\uFE0F Node9 removed. Run: npm uninstall -g node9-ai"));
|
|
27452
|
+
console.log(chalk35.gray(" Restart any open AI agent sessions for changes to take effect.\n"));
|
|
26742
27453
|
});
|
|
26743
27454
|
registerDoctorCommand(program, version);
|
|
26744
27455
|
program.command("explain").description(
|
|
@@ -26751,7 +27462,7 @@ program.command("explain").description(
|
|
|
26751
27462
|
try {
|
|
26752
27463
|
args = JSON.parse(trimmed);
|
|
26753
27464
|
} catch {
|
|
26754
|
-
console.error(
|
|
27465
|
+
console.error(chalk35.red(`
|
|
26755
27466
|
\u274C Invalid JSON: ${trimmed}
|
|
26756
27467
|
`));
|
|
26757
27468
|
process.exit(1);
|
|
@@ -26762,54 +27473,54 @@ program.command("explain").description(
|
|
|
26762
27473
|
}
|
|
26763
27474
|
const result = await explainPolicy(tool, args);
|
|
26764
27475
|
console.log("");
|
|
26765
|
-
console.log(
|
|
27476
|
+
console.log(chalk35.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
26766
27477
|
console.log("");
|
|
26767
|
-
console.log(` ${
|
|
27478
|
+
console.log(` ${chalk35.bold("Tool:")} ${chalk35.white(result.tool)}`);
|
|
26768
27479
|
if (argsRaw) {
|
|
26769
27480
|
const preview2 = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
26770
|
-
console.log(` ${
|
|
27481
|
+
console.log(` ${chalk35.bold("Input:")} ${chalk35.gray(preview2)}`);
|
|
26771
27482
|
}
|
|
26772
27483
|
console.log("");
|
|
26773
|
-
console.log(
|
|
27484
|
+
console.log(chalk35.bold("Config Sources (Waterfall):"));
|
|
26774
27485
|
for (const tier of result.waterfall) {
|
|
26775
|
-
const num3 =
|
|
27486
|
+
const num3 = chalk35.gray(` ${tier.tier}.`);
|
|
26776
27487
|
const label2 = tier.label.padEnd(16);
|
|
26777
27488
|
let statusStr;
|
|
26778
27489
|
if (tier.tier === 1) {
|
|
26779
|
-
statusStr =
|
|
27490
|
+
statusStr = chalk35.gray(tier.note ?? "");
|
|
26780
27491
|
} else if (tier.status === "active") {
|
|
26781
|
-
const loc = tier.path ?
|
|
26782
|
-
const note = tier.note ?
|
|
26783
|
-
statusStr =
|
|
27492
|
+
const loc = tier.path ? chalk35.gray(tier.path) : "";
|
|
27493
|
+
const note = tier.note ? chalk35.gray(`(${tier.note})`) : "";
|
|
27494
|
+
statusStr = chalk35.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
26784
27495
|
} else {
|
|
26785
|
-
statusStr =
|
|
27496
|
+
statusStr = chalk35.gray("\u25CB " + (tier.note ?? "not found"));
|
|
26786
27497
|
}
|
|
26787
|
-
console.log(`${num3} ${
|
|
27498
|
+
console.log(`${num3} ${chalk35.white(label2)} ${statusStr}`);
|
|
26788
27499
|
}
|
|
26789
27500
|
console.log("");
|
|
26790
|
-
console.log(
|
|
27501
|
+
console.log(chalk35.bold("Policy Evaluation:"));
|
|
26791
27502
|
for (const step of result.steps) {
|
|
26792
27503
|
const isFinal = step.isFinal;
|
|
26793
27504
|
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 =
|
|
27505
|
+
if (step.outcome === "allow") icon = chalk35.green(" \u2705");
|
|
27506
|
+
else if (step.outcome === "review") icon = chalk35.red(" \u{1F534}");
|
|
27507
|
+
else if (step.outcome === "skip") icon = chalk35.gray(" \u2500 ");
|
|
27508
|
+
else icon = chalk35.gray(" \u25CB ");
|
|
26798
27509
|
const name = step.name.padEnd(18);
|
|
26799
|
-
const nameStr = isFinal ?
|
|
26800
|
-
const detail = isFinal ?
|
|
26801
|
-
const arrow = isFinal ?
|
|
27510
|
+
const nameStr = isFinal ? chalk35.white.bold(name) : chalk35.white(name);
|
|
27511
|
+
const detail = isFinal ? chalk35.white(step.detail) : chalk35.gray(step.detail);
|
|
27512
|
+
const arrow = isFinal ? chalk35.yellow(" \u2190 STOP") : "";
|
|
26802
27513
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
26803
27514
|
}
|
|
26804
27515
|
console.log("");
|
|
26805
27516
|
if (result.decision === "allow") {
|
|
26806
|
-
console.log(
|
|
27517
|
+
console.log(chalk35.green.bold(" Decision: \u2705 ALLOW") + chalk35.gray(" \u2014 no approval needed"));
|
|
26807
27518
|
} else {
|
|
26808
27519
|
console.log(
|
|
26809
|
-
|
|
27520
|
+
chalk35.red.bold(" Decision: \u{1F534} REVIEW") + chalk35.gray(" \u2014 human approval required")
|
|
26810
27521
|
);
|
|
26811
27522
|
if (result.blockedByLabel) {
|
|
26812
|
-
console.log(
|
|
27523
|
+
console.log(chalk35.gray(` Reason: ${result.blockedByLabel}`));
|
|
26813
27524
|
}
|
|
26814
27525
|
}
|
|
26815
27526
|
console.log("");
|
|
@@ -26824,18 +27535,18 @@ program.command("tail").description("Stream live agent activity to the terminal"
|
|
|
26824
27535
|
try {
|
|
26825
27536
|
await startTail2(options);
|
|
26826
27537
|
} catch (err2) {
|
|
26827
|
-
console.error(
|
|
27538
|
+
console.error(chalk35.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
26828
27539
|
process.exit(1);
|
|
26829
27540
|
}
|
|
26830
27541
|
});
|
|
26831
27542
|
program.command("monitor").description("Live interactive dashboard \u2014 activity feed, approvals, security signals").action(async () => {
|
|
26832
27543
|
try {
|
|
26833
|
-
const dashboardPath =
|
|
27544
|
+
const dashboardPath = path61.join(__dirname, "dashboard.mjs");
|
|
26834
27545
|
const dynamicImport = new Function("id", "return import(id)");
|
|
26835
27546
|
const mod = await dynamicImport(`file://${dashboardPath}`);
|
|
26836
27547
|
await mod.startMonitor();
|
|
26837
27548
|
} catch (err2) {
|
|
26838
|
-
console.error(
|
|
27549
|
+
console.error(chalk35.red(`\u274C ${err2 instanceof Error ? err2.message : String(err2)}`));
|
|
26839
27550
|
process.exit(1);
|
|
26840
27551
|
}
|
|
26841
27552
|
});
|
|
@@ -26868,14 +27579,14 @@ Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
|
26868
27579
|
Run "node9 addto claude" to register it as the statusLine.`
|
|
26869
27580
|
).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
27581
|
if (subcommand === "debug") {
|
|
26871
|
-
const flagFile =
|
|
27582
|
+
const flagFile = path61.join(os55.homedir(), ".node9", "hud-debug");
|
|
26872
27583
|
if (state === "on") {
|
|
26873
|
-
|
|
26874
|
-
|
|
27584
|
+
fs63.mkdirSync(path61.dirname(flagFile), { recursive: true });
|
|
27585
|
+
fs63.writeFileSync(flagFile, "");
|
|
26875
27586
|
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
26876
27587
|
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
26877
27588
|
} else if (state === "off") {
|
|
26878
|
-
if (
|
|
27589
|
+
if (fs63.existsSync(flagFile)) fs63.unlinkSync(flagFile);
|
|
26879
27590
|
console.log("HUD debug logging disabled.");
|
|
26880
27591
|
} else {
|
|
26881
27592
|
console.error("Usage: node9 hud debug on|off");
|
|
@@ -26890,7 +27601,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
26890
27601
|
const ms = parseDuration(options.duration);
|
|
26891
27602
|
if (ms === null) {
|
|
26892
27603
|
console.error(
|
|
26893
|
-
|
|
27604
|
+
chalk35.red(`
|
|
26894
27605
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
26895
27606
|
`)
|
|
26896
27607
|
);
|
|
@@ -26898,20 +27609,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
26898
27609
|
}
|
|
26899
27610
|
pauseNode9(ms, options.duration);
|
|
26900
27611
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
26901
|
-
console.log(
|
|
27612
|
+
console.log(chalk35.yellow(`
|
|
26902
27613
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
26903
|
-
console.log(
|
|
26904
|
-
console.log(
|
|
27614
|
+
console.log(chalk35.gray(` All tool calls will be allowed without review.`));
|
|
27615
|
+
console.log(chalk35.gray(` Run "node9 resume" to re-enable early.
|
|
26905
27616
|
`));
|
|
26906
27617
|
});
|
|
26907
27618
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
26908
27619
|
const { paused } = checkPause();
|
|
26909
27620
|
if (!paused) {
|
|
26910
|
-
console.log(
|
|
27621
|
+
console.log(chalk35.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
26911
27622
|
return;
|
|
26912
27623
|
}
|
|
26913
27624
|
resumeNode9();
|
|
26914
|
-
console.log(
|
|
27625
|
+
console.log(chalk35.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
26915
27626
|
});
|
|
26916
27627
|
var HOOK_BASED_AGENTS = {
|
|
26917
27628
|
claude: "claude",
|
|
@@ -26927,15 +27638,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
26927
27638
|
if (HOOK_BASED_AGENTS[firstArg2] !== void 0) {
|
|
26928
27639
|
const target = HOOK_BASED_AGENTS[firstArg2];
|
|
26929
27640
|
console.error(
|
|
26930
|
-
|
|
27641
|
+
chalk35.yellow(`
|
|
26931
27642
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
26932
27643
|
);
|
|
26933
|
-
console.error(
|
|
27644
|
+
console.error(chalk35.white(`
|
|
26934
27645
|
"${target}" uses its own hook system. Use:`));
|
|
26935
27646
|
console.error(
|
|
26936
|
-
|
|
27647
|
+
chalk35.green(` node9 addto ${target} `) + chalk35.gray("# one-time setup")
|
|
26937
27648
|
);
|
|
26938
|
-
console.error(
|
|
27649
|
+
console.error(chalk35.green(` ${target} `) + chalk35.gray("# run normally"));
|
|
26939
27650
|
process.exit(1);
|
|
26940
27651
|
}
|
|
26941
27652
|
const runArgs = firstArg2 === "shell" ? commandArgs.slice(1) : commandArgs;
|
|
@@ -26952,7 +27663,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
26952
27663
|
}
|
|
26953
27664
|
);
|
|
26954
27665
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
26955
|
-
console.error(
|
|
27666
|
+
console.error(chalk35.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
26956
27667
|
const daemonReady = await autoStartDaemonAndWait();
|
|
26957
27668
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
26958
27669
|
}
|
|
@@ -26965,12 +27676,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
26965
27676
|
}
|
|
26966
27677
|
if (!result.approved) {
|
|
26967
27678
|
console.error(
|
|
26968
|
-
|
|
27679
|
+
chalk35.red(`
|
|
26969
27680
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
26970
27681
|
);
|
|
26971
27682
|
process.exit(1);
|
|
26972
27683
|
}
|
|
26973
|
-
console.error(
|
|
27684
|
+
console.error(chalk35.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
26974
27685
|
await runProxy(fullCommand);
|
|
26975
27686
|
} else {
|
|
26976
27687
|
program.help();
|
|
@@ -26985,6 +27696,7 @@ registerAgentsCommand(program);
|
|
|
26985
27696
|
registerScanCommand(program);
|
|
26986
27697
|
registerPostureCommand(program);
|
|
26987
27698
|
registerEgressCommand(program);
|
|
27699
|
+
registerSandboxCommand(program, version);
|
|
26988
27700
|
registerSessionsCommand(program);
|
|
26989
27701
|
registerSessionTaintCommand(program);
|
|
26990
27702
|
registerDlpCommand(program);
|
|
@@ -26995,9 +27707,9 @@ if (process.argv[2] !== "daemon") {
|
|
|
26995
27707
|
const isCheckHook = process.argv[2] === "check";
|
|
26996
27708
|
if (isCheckHook) {
|
|
26997
27709
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
26998
|
-
const logPath =
|
|
27710
|
+
const logPath = path61.join(os55.homedir(), ".node9", "hook-debug.log");
|
|
26999
27711
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
27000
|
-
|
|
27712
|
+
fs63.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
27001
27713
|
`);
|
|
27002
27714
|
}
|
|
27003
27715
|
process.exit(0);
|