@node9/proxy 1.0.13 → 1.0.15
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 +188 -119
- package/dist/cli.js +2335 -1097
- package/dist/cli.mjs +2315 -1075
- package/dist/index.js +500 -125
- package/dist/index.mjs +500 -125
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import os from "os";
|
|
12
|
-
import pm from "picomatch";
|
|
13
|
-
import { parse } from "sh-syntax";
|
|
14
|
-
|
|
15
|
-
// src/ui/native.ts
|
|
16
|
-
import { spawn } from "child_process";
|
|
17
|
-
import path2 from "path";
|
|
18
|
-
import chalk from "chalk";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
19
11
|
|
|
20
12
|
// src/context-sniper.ts
|
|
21
13
|
import path from "path";
|
|
@@ -48,22 +40,6 @@ function extractContext(text, matchedWord) {
|
|
|
48
40
|
... [${lines.length - end} lines hidden] ...` : "";
|
|
49
41
|
return { snippet: `${head}${snippet}${tail}`, lineIndex };
|
|
50
42
|
}
|
|
51
|
-
var CODE_KEYS = [
|
|
52
|
-
"command",
|
|
53
|
-
"cmd",
|
|
54
|
-
"shell_command",
|
|
55
|
-
"bash_command",
|
|
56
|
-
"script",
|
|
57
|
-
"code",
|
|
58
|
-
"input",
|
|
59
|
-
"sql",
|
|
60
|
-
"query",
|
|
61
|
-
"arguments",
|
|
62
|
-
"args",
|
|
63
|
-
"param",
|
|
64
|
-
"params",
|
|
65
|
-
"text"
|
|
66
|
-
];
|
|
67
43
|
function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
|
|
68
44
|
let intent = "EXEC";
|
|
69
45
|
let contextSnippet;
|
|
@@ -118,11 +94,33 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
118
94
|
...ruleName && { ruleName }
|
|
119
95
|
};
|
|
120
96
|
}
|
|
97
|
+
var CODE_KEYS;
|
|
98
|
+
var init_context_sniper = __esm({
|
|
99
|
+
"src/context-sniper.ts"() {
|
|
100
|
+
"use strict";
|
|
101
|
+
CODE_KEYS = [
|
|
102
|
+
"command",
|
|
103
|
+
"cmd",
|
|
104
|
+
"shell_command",
|
|
105
|
+
"bash_command",
|
|
106
|
+
"script",
|
|
107
|
+
"code",
|
|
108
|
+
"input",
|
|
109
|
+
"sql",
|
|
110
|
+
"query",
|
|
111
|
+
"arguments",
|
|
112
|
+
"args",
|
|
113
|
+
"param",
|
|
114
|
+
"params",
|
|
115
|
+
"text"
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
});
|
|
121
119
|
|
|
122
120
|
// src/ui/native.ts
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
import { spawn } from "child_process";
|
|
122
|
+
import path2 from "path";
|
|
123
|
+
import chalk from "chalk";
|
|
126
124
|
function formatArgs(args, matchedField, matchedWord) {
|
|
127
125
|
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
128
126
|
let parsed = args;
|
|
@@ -332,82 +330,19 @@ end run`;
|
|
|
332
330
|
}
|
|
333
331
|
});
|
|
334
332
|
}
|
|
333
|
+
var isTestEnv;
|
|
334
|
+
var init_native = __esm({
|
|
335
|
+
"src/ui/native.ts"() {
|
|
336
|
+
"use strict";
|
|
337
|
+
init_context_sniper();
|
|
338
|
+
isTestEnv = () => {
|
|
339
|
+
return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
});
|
|
335
343
|
|
|
336
344
|
// src/config-schema.ts
|
|
337
345
|
import { z } from "zod";
|
|
338
|
-
var noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
339
|
-
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
340
|
-
});
|
|
341
|
-
var validRegex = noNewlines.refine(
|
|
342
|
-
(s) => {
|
|
343
|
-
try {
|
|
344
|
-
new RegExp(s);
|
|
345
|
-
return true;
|
|
346
|
-
} catch {
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
{ message: "Value must be a valid regular expression" }
|
|
351
|
-
);
|
|
352
|
-
var SmartConditionSchema = z.object({
|
|
353
|
-
field: z.string().min(1, "Condition field must not be empty"),
|
|
354
|
-
op: z.enum(["matches", "notMatches", "contains", "notContains", "exists", "notExists"], {
|
|
355
|
-
errorMap: () => ({
|
|
356
|
-
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists"
|
|
357
|
-
})
|
|
358
|
-
}),
|
|
359
|
-
value: validRegex.optional(),
|
|
360
|
-
flags: z.string().optional()
|
|
361
|
-
});
|
|
362
|
-
var SmartRuleSchema = z.object({
|
|
363
|
-
name: z.string().optional(),
|
|
364
|
-
tool: z.string().min(1, "Smart rule tool must not be empty"),
|
|
365
|
-
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
366
|
-
conditionMode: z.enum(["all", "any"]).optional(),
|
|
367
|
-
verdict: z.enum(["allow", "review", "block"], {
|
|
368
|
-
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
369
|
-
}),
|
|
370
|
-
reason: z.string().optional()
|
|
371
|
-
});
|
|
372
|
-
var PolicyRuleSchema = z.object({
|
|
373
|
-
action: z.string().min(1),
|
|
374
|
-
allowPaths: z.array(z.string()).optional(),
|
|
375
|
-
blockPaths: z.array(z.string()).optional()
|
|
376
|
-
});
|
|
377
|
-
var ConfigFileSchema = z.object({
|
|
378
|
-
version: z.string().optional(),
|
|
379
|
-
settings: z.object({
|
|
380
|
-
mode: z.enum(["standard", "strict", "audit"]).optional(),
|
|
381
|
-
autoStartDaemon: z.boolean().optional(),
|
|
382
|
-
enableUndo: z.boolean().optional(),
|
|
383
|
-
enableHookLogDebug: z.boolean().optional(),
|
|
384
|
-
approvalTimeoutMs: z.number().nonnegative().optional(),
|
|
385
|
-
approvers: z.object({
|
|
386
|
-
native: z.boolean().optional(),
|
|
387
|
-
browser: z.boolean().optional(),
|
|
388
|
-
cloud: z.boolean().optional(),
|
|
389
|
-
terminal: z.boolean().optional()
|
|
390
|
-
}).optional(),
|
|
391
|
-
environment: z.string().optional(),
|
|
392
|
-
slackEnabled: z.boolean().optional(),
|
|
393
|
-
enableTrustSessions: z.boolean().optional(),
|
|
394
|
-
allowGlobalPause: z.boolean().optional()
|
|
395
|
-
}).optional(),
|
|
396
|
-
policy: z.object({
|
|
397
|
-
sandboxPaths: z.array(z.string()).optional(),
|
|
398
|
-
dangerousWords: z.array(noNewlines).optional(),
|
|
399
|
-
ignoredTools: z.array(z.string()).optional(),
|
|
400
|
-
toolInspection: z.record(z.string()).optional(),
|
|
401
|
-
rules: z.array(PolicyRuleSchema).optional(),
|
|
402
|
-
smartRules: z.array(SmartRuleSchema).optional(),
|
|
403
|
-
snapshot: z.object({
|
|
404
|
-
tools: z.array(z.string()).optional(),
|
|
405
|
-
onlyPaths: z.array(z.string()).optional(),
|
|
406
|
-
ignorePaths: z.array(z.string()).optional()
|
|
407
|
-
}).optional()
|
|
408
|
-
}).optional(),
|
|
409
|
-
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
410
|
-
}).strict({ message: "Config contains unknown top-level keys" });
|
|
411
346
|
function sanitizeConfig(raw) {
|
|
412
347
|
const result = ConfigFileSchema.safeParse(raw);
|
|
413
348
|
if (result.success) {
|
|
@@ -425,8 +360,8 @@ function sanitizeConfig(raw) {
|
|
|
425
360
|
}
|
|
426
361
|
}
|
|
427
362
|
const lines = result.error.issues.map((issue) => {
|
|
428
|
-
const
|
|
429
|
-
return ` \u2022 ${
|
|
363
|
+
const path10 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
364
|
+
return ` \u2022 ${path10}: ${issue.message}`;
|
|
430
365
|
});
|
|
431
366
|
return {
|
|
432
367
|
sanitized,
|
|
@@ -434,19 +369,397 @@ function sanitizeConfig(raw) {
|
|
|
434
369
|
${lines.join("\n")}`
|
|
435
370
|
};
|
|
436
371
|
}
|
|
372
|
+
var noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
|
|
373
|
+
var init_config_schema = __esm({
|
|
374
|
+
"src/config-schema.ts"() {
|
|
375
|
+
"use strict";
|
|
376
|
+
noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
377
|
+
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
378
|
+
});
|
|
379
|
+
SmartConditionSchema = z.object({
|
|
380
|
+
field: z.string().min(1, "Condition field must not be empty"),
|
|
381
|
+
op: z.enum(
|
|
382
|
+
[
|
|
383
|
+
"matches",
|
|
384
|
+
"notMatches",
|
|
385
|
+
"contains",
|
|
386
|
+
"notContains",
|
|
387
|
+
"exists",
|
|
388
|
+
"notExists",
|
|
389
|
+
"matchesGlob",
|
|
390
|
+
"notMatchesGlob"
|
|
391
|
+
],
|
|
392
|
+
{
|
|
393
|
+
errorMap: () => ({
|
|
394
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
),
|
|
398
|
+
value: z.string().optional(),
|
|
399
|
+
flags: z.string().optional()
|
|
400
|
+
});
|
|
401
|
+
SmartRuleSchema = z.object({
|
|
402
|
+
name: z.string().optional(),
|
|
403
|
+
tool: z.string().min(1, "Smart rule tool must not be empty"),
|
|
404
|
+
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
405
|
+
conditionMode: z.enum(["all", "any"]).optional(),
|
|
406
|
+
verdict: z.enum(["allow", "review", "block"], {
|
|
407
|
+
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
408
|
+
}),
|
|
409
|
+
reason: z.string().optional()
|
|
410
|
+
});
|
|
411
|
+
ConfigFileSchema = z.object({
|
|
412
|
+
version: z.string().optional(),
|
|
413
|
+
settings: z.object({
|
|
414
|
+
mode: z.enum(["standard", "strict", "audit"]).optional(),
|
|
415
|
+
autoStartDaemon: z.boolean().optional(),
|
|
416
|
+
enableUndo: z.boolean().optional(),
|
|
417
|
+
enableHookLogDebug: z.boolean().optional(),
|
|
418
|
+
approvalTimeoutMs: z.number().nonnegative().optional(),
|
|
419
|
+
approvers: z.object({
|
|
420
|
+
native: z.boolean().optional(),
|
|
421
|
+
browser: z.boolean().optional(),
|
|
422
|
+
cloud: z.boolean().optional(),
|
|
423
|
+
terminal: z.boolean().optional()
|
|
424
|
+
}).optional(),
|
|
425
|
+
environment: z.string().optional(),
|
|
426
|
+
slackEnabled: z.boolean().optional(),
|
|
427
|
+
enableTrustSessions: z.boolean().optional(),
|
|
428
|
+
allowGlobalPause: z.boolean().optional()
|
|
429
|
+
}).optional(),
|
|
430
|
+
policy: z.object({
|
|
431
|
+
sandboxPaths: z.array(z.string()).optional(),
|
|
432
|
+
dangerousWords: z.array(noNewlines).optional(),
|
|
433
|
+
ignoredTools: z.array(z.string()).optional(),
|
|
434
|
+
toolInspection: z.record(z.string()).optional(),
|
|
435
|
+
smartRules: z.array(SmartRuleSchema).optional(),
|
|
436
|
+
snapshot: z.object({
|
|
437
|
+
tools: z.array(z.string()).optional(),
|
|
438
|
+
onlyPaths: z.array(z.string()).optional(),
|
|
439
|
+
ignorePaths: z.array(z.string()).optional()
|
|
440
|
+
}).optional(),
|
|
441
|
+
dlp: z.object({
|
|
442
|
+
enabled: z.boolean().optional(),
|
|
443
|
+
scanIgnoredTools: z.boolean().optional()
|
|
444
|
+
}).optional()
|
|
445
|
+
}).optional(),
|
|
446
|
+
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
447
|
+
}).strict({ message: "Config contains unknown top-level keys" });
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// src/shields.ts
|
|
452
|
+
import fs from "fs";
|
|
453
|
+
import path3 from "path";
|
|
454
|
+
import os from "os";
|
|
455
|
+
import crypto from "crypto";
|
|
456
|
+
function resolveShieldName(input) {
|
|
457
|
+
const lower = input.toLowerCase();
|
|
458
|
+
if (SHIELDS[lower]) return lower;
|
|
459
|
+
for (const [name, def] of Object.entries(SHIELDS)) {
|
|
460
|
+
if (def.aliases.includes(lower)) return name;
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
function getShield(name) {
|
|
465
|
+
const resolved = resolveShieldName(name);
|
|
466
|
+
return resolved ? SHIELDS[resolved] : null;
|
|
467
|
+
}
|
|
468
|
+
function listShields() {
|
|
469
|
+
return Object.values(SHIELDS);
|
|
470
|
+
}
|
|
471
|
+
function readActiveShields() {
|
|
472
|
+
try {
|
|
473
|
+
const raw = fs.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
474
|
+
if (!raw.trim()) return [];
|
|
475
|
+
const parsed = JSON.parse(raw);
|
|
476
|
+
if (Array.isArray(parsed.active)) {
|
|
477
|
+
return parsed.active.filter(
|
|
478
|
+
(e) => typeof e === "string" && e.length > 0 && e in SHIELDS
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
} catch (err) {
|
|
482
|
+
if (err.code !== "ENOENT") {
|
|
483
|
+
process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
|
|
484
|
+
`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return [];
|
|
488
|
+
}
|
|
489
|
+
function writeActiveShields(active) {
|
|
490
|
+
fs.mkdirSync(path3.dirname(SHIELDS_STATE_FILE), { recursive: true });
|
|
491
|
+
const tmp = `${SHIELDS_STATE_FILE}.${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
492
|
+
fs.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
|
|
493
|
+
fs.renameSync(tmp, SHIELDS_STATE_FILE);
|
|
494
|
+
}
|
|
495
|
+
var SHIELDS, SHIELDS_STATE_FILE;
|
|
496
|
+
var init_shields = __esm({
|
|
497
|
+
"src/shields.ts"() {
|
|
498
|
+
"use strict";
|
|
499
|
+
SHIELDS = {
|
|
500
|
+
postgres: {
|
|
501
|
+
name: "postgres",
|
|
502
|
+
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
503
|
+
aliases: ["pg", "postgresql"],
|
|
504
|
+
smartRules: [
|
|
505
|
+
{
|
|
506
|
+
name: "shield:postgres:block-drop-table",
|
|
507
|
+
tool: "*",
|
|
508
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
509
|
+
verdict: "block",
|
|
510
|
+
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
name: "shield:postgres:block-truncate",
|
|
514
|
+
tool: "*",
|
|
515
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
516
|
+
verdict: "block",
|
|
517
|
+
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: "shield:postgres:block-drop-column",
|
|
521
|
+
tool: "*",
|
|
522
|
+
conditions: [
|
|
523
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
524
|
+
],
|
|
525
|
+
verdict: "block",
|
|
526
|
+
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
name: "shield:postgres:review-grant-revoke",
|
|
530
|
+
tool: "*",
|
|
531
|
+
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
532
|
+
verdict: "review",
|
|
533
|
+
reason: "Permission changes require human approval (Postgres shield)"
|
|
534
|
+
}
|
|
535
|
+
],
|
|
536
|
+
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
537
|
+
},
|
|
538
|
+
github: {
|
|
539
|
+
name: "github",
|
|
540
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
541
|
+
aliases: ["git"],
|
|
542
|
+
smartRules: [
|
|
543
|
+
{
|
|
544
|
+
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
545
|
+
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
546
|
+
name: "shield:github:review-delete-branch-remote",
|
|
547
|
+
tool: "bash",
|
|
548
|
+
conditions: [
|
|
549
|
+
{
|
|
550
|
+
field: "command",
|
|
551
|
+
op: "matches",
|
|
552
|
+
value: "git\\s+push\\s+.*--delete",
|
|
553
|
+
flags: "i"
|
|
554
|
+
}
|
|
555
|
+
],
|
|
556
|
+
verdict: "review",
|
|
557
|
+
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
name: "shield:github:block-delete-repo",
|
|
561
|
+
tool: "*",
|
|
562
|
+
conditions: [
|
|
563
|
+
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
564
|
+
],
|
|
565
|
+
verdict: "block",
|
|
566
|
+
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
567
|
+
}
|
|
568
|
+
],
|
|
569
|
+
dangerousWords: []
|
|
570
|
+
},
|
|
571
|
+
aws: {
|
|
572
|
+
name: "aws",
|
|
573
|
+
description: "Protects AWS infrastructure from destructive AI operations",
|
|
574
|
+
aliases: ["amazon"],
|
|
575
|
+
smartRules: [
|
|
576
|
+
{
|
|
577
|
+
name: "shield:aws:block-delete-s3-bucket",
|
|
578
|
+
tool: "*",
|
|
579
|
+
conditions: [
|
|
580
|
+
{
|
|
581
|
+
field: "command",
|
|
582
|
+
op: "matches",
|
|
583
|
+
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
584
|
+
flags: "i"
|
|
585
|
+
}
|
|
586
|
+
],
|
|
587
|
+
verdict: "block",
|
|
588
|
+
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: "shield:aws:review-iam-changes",
|
|
592
|
+
tool: "*",
|
|
593
|
+
conditions: [
|
|
594
|
+
{
|
|
595
|
+
field: "command",
|
|
596
|
+
op: "matches",
|
|
597
|
+
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
598
|
+
flags: "i"
|
|
599
|
+
}
|
|
600
|
+
],
|
|
601
|
+
verdict: "review",
|
|
602
|
+
reason: "IAM changes require human approval (AWS shield)"
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
name: "shield:aws:block-ec2-terminate",
|
|
606
|
+
tool: "*",
|
|
607
|
+
conditions: [
|
|
608
|
+
{
|
|
609
|
+
field: "command",
|
|
610
|
+
op: "matches",
|
|
611
|
+
value: "aws\\s+ec2\\s+terminate-instances",
|
|
612
|
+
flags: "i"
|
|
613
|
+
}
|
|
614
|
+
],
|
|
615
|
+
verdict: "block",
|
|
616
|
+
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: "shield:aws:review-rds-delete",
|
|
620
|
+
tool: "*",
|
|
621
|
+
conditions: [
|
|
622
|
+
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
623
|
+
],
|
|
624
|
+
verdict: "review",
|
|
625
|
+
reason: "RDS deletion requires human approval (AWS shield)"
|
|
626
|
+
}
|
|
627
|
+
],
|
|
628
|
+
dangerousWords: []
|
|
629
|
+
},
|
|
630
|
+
filesystem: {
|
|
631
|
+
name: "filesystem",
|
|
632
|
+
description: "Protects the local filesystem from dangerous AI operations",
|
|
633
|
+
aliases: ["fs"],
|
|
634
|
+
smartRules: [
|
|
635
|
+
{
|
|
636
|
+
name: "shield:filesystem:review-chmod-777",
|
|
637
|
+
tool: "bash",
|
|
638
|
+
conditions: [
|
|
639
|
+
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
640
|
+
],
|
|
641
|
+
verdict: "review",
|
|
642
|
+
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: "shield:filesystem:review-write-etc",
|
|
646
|
+
tool: "bash",
|
|
647
|
+
conditions: [
|
|
648
|
+
{
|
|
649
|
+
field: "command",
|
|
650
|
+
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
651
|
+
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
652
|
+
op: "matches",
|
|
653
|
+
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
654
|
+
}
|
|
655
|
+
],
|
|
656
|
+
verdict: "review",
|
|
657
|
+
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
658
|
+
}
|
|
659
|
+
],
|
|
660
|
+
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
661
|
+
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
662
|
+
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
663
|
+
dangerousWords: ["wipefs"]
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// src/dlp.ts
|
|
671
|
+
function maskSecret(raw, pattern) {
|
|
672
|
+
const match = raw.match(pattern);
|
|
673
|
+
if (!match) return "****";
|
|
674
|
+
const secret = match[0];
|
|
675
|
+
if (secret.length < 8) return "****";
|
|
676
|
+
const prefix = secret.slice(0, 4);
|
|
677
|
+
const suffix = secret.slice(-4);
|
|
678
|
+
const stars = "*".repeat(Math.min(secret.length - 8, 12));
|
|
679
|
+
return `${prefix}${stars}${suffix}`;
|
|
680
|
+
}
|
|
681
|
+
function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
682
|
+
if (depth > MAX_DEPTH || args === null || args === void 0) return null;
|
|
683
|
+
if (Array.isArray(args)) {
|
|
684
|
+
for (let i = 0; i < args.length; i++) {
|
|
685
|
+
const match = scanArgs(args[i], depth + 1, `${fieldPath}[${i}]`);
|
|
686
|
+
if (match) return match;
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
if (typeof args === "object") {
|
|
691
|
+
for (const [key, value] of Object.entries(args)) {
|
|
692
|
+
const match = scanArgs(value, depth + 1, `${fieldPath}.${key}`);
|
|
693
|
+
if (match) return match;
|
|
694
|
+
}
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
if (typeof args === "string") {
|
|
698
|
+
const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
|
|
699
|
+
for (const pattern of DLP_PATTERNS) {
|
|
700
|
+
if (pattern.regex.test(text)) {
|
|
701
|
+
return {
|
|
702
|
+
patternName: pattern.name,
|
|
703
|
+
fieldPath,
|
|
704
|
+
redactedSample: maskSecret(text, pattern.regex),
|
|
705
|
+
severity: pattern.severity
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (text.length < MAX_JSON_PARSE_BYTES) {
|
|
710
|
+
const trimmed = text.trim();
|
|
711
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
712
|
+
try {
|
|
713
|
+
const parsed = JSON.parse(text);
|
|
714
|
+
const inner = scanArgs(parsed, depth + 1, fieldPath);
|
|
715
|
+
if (inner) return inner;
|
|
716
|
+
} catch {
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
|
|
724
|
+
var init_dlp = __esm({
|
|
725
|
+
"src/dlp.ts"() {
|
|
726
|
+
"use strict";
|
|
727
|
+
DLP_PATTERNS = [
|
|
728
|
+
{ name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
|
|
729
|
+
{ name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
|
|
730
|
+
{ name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
|
|
731
|
+
{ name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
|
|
732
|
+
{ name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
|
|
733
|
+
{
|
|
734
|
+
name: "Private Key (PEM)",
|
|
735
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
736
|
+
severity: "block"
|
|
737
|
+
},
|
|
738
|
+
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
|
|
739
|
+
];
|
|
740
|
+
MAX_DEPTH = 5;
|
|
741
|
+
MAX_STRING_BYTES = 1e5;
|
|
742
|
+
MAX_JSON_PARSE_BYTES = 1e4;
|
|
743
|
+
}
|
|
744
|
+
});
|
|
437
745
|
|
|
438
746
|
// src/core.ts
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
747
|
+
import chalk2 from "chalk";
|
|
748
|
+
import { confirm } from "@inquirer/prompts";
|
|
749
|
+
import fs2 from "fs";
|
|
750
|
+
import path4 from "path";
|
|
751
|
+
import os2 from "os";
|
|
752
|
+
import net from "net";
|
|
753
|
+
import { randomUUID } from "crypto";
|
|
754
|
+
import pm from "picomatch";
|
|
755
|
+
import { parse } from "sh-syntax";
|
|
443
756
|
function checkPause() {
|
|
444
757
|
try {
|
|
445
|
-
if (!
|
|
446
|
-
const state = JSON.parse(
|
|
758
|
+
if (!fs2.existsSync(PAUSED_FILE)) return { paused: false };
|
|
759
|
+
const state = JSON.parse(fs2.readFileSync(PAUSED_FILE, "utf-8"));
|
|
447
760
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
448
761
|
try {
|
|
449
|
-
|
|
762
|
+
fs2.unlinkSync(PAUSED_FILE);
|
|
450
763
|
} catch {
|
|
451
764
|
}
|
|
452
765
|
return { paused: false };
|
|
@@ -457,11 +770,11 @@ function checkPause() {
|
|
|
457
770
|
}
|
|
458
771
|
}
|
|
459
772
|
function atomicWriteSync(filePath, data, options) {
|
|
460
|
-
const dir =
|
|
461
|
-
if (!
|
|
462
|
-
const tmpPath = `${filePath}.${
|
|
463
|
-
|
|
464
|
-
|
|
773
|
+
const dir = path4.dirname(filePath);
|
|
774
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
775
|
+
const tmpPath = `${filePath}.${os2.hostname()}.${process.pid}.tmp`;
|
|
776
|
+
fs2.writeFileSync(tmpPath, data, options);
|
|
777
|
+
fs2.renameSync(tmpPath, filePath);
|
|
465
778
|
}
|
|
466
779
|
function pauseNode9(durationMs, durationStr) {
|
|
467
780
|
const state = { expiry: Date.now() + durationMs, duration: durationStr };
|
|
@@ -469,18 +782,18 @@ function pauseNode9(durationMs, durationStr) {
|
|
|
469
782
|
}
|
|
470
783
|
function resumeNode9() {
|
|
471
784
|
try {
|
|
472
|
-
if (
|
|
785
|
+
if (fs2.existsSync(PAUSED_FILE)) fs2.unlinkSync(PAUSED_FILE);
|
|
473
786
|
} catch {
|
|
474
787
|
}
|
|
475
788
|
}
|
|
476
789
|
function getActiveTrustSession(toolName) {
|
|
477
790
|
try {
|
|
478
|
-
if (!
|
|
479
|
-
const trust = JSON.parse(
|
|
791
|
+
if (!fs2.existsSync(TRUST_FILE)) return false;
|
|
792
|
+
const trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
|
|
480
793
|
const now = Date.now();
|
|
481
794
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
482
795
|
if (active.length !== trust.entries.length) {
|
|
483
|
-
|
|
796
|
+
fs2.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
484
797
|
}
|
|
485
798
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
486
799
|
} catch {
|
|
@@ -491,8 +804,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
491
804
|
try {
|
|
492
805
|
let trust = { entries: [] };
|
|
493
806
|
try {
|
|
494
|
-
if (
|
|
495
|
-
trust = JSON.parse(
|
|
807
|
+
if (fs2.existsSync(TRUST_FILE)) {
|
|
808
|
+
trust = JSON.parse(fs2.readFileSync(TRUST_FILE, "utf-8"));
|
|
496
809
|
}
|
|
497
810
|
} catch {
|
|
498
811
|
}
|
|
@@ -508,9 +821,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
508
821
|
}
|
|
509
822
|
function appendToLog(logPath, entry) {
|
|
510
823
|
try {
|
|
511
|
-
const dir =
|
|
512
|
-
if (!
|
|
513
|
-
|
|
824
|
+
const dir = path4.dirname(logPath);
|
|
825
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
826
|
+
fs2.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
514
827
|
} catch {
|
|
515
828
|
}
|
|
516
829
|
}
|
|
@@ -522,7 +835,7 @@ function appendHookDebug(toolName, args, meta) {
|
|
|
522
835
|
args: safeArgs,
|
|
523
836
|
agent: meta?.agent,
|
|
524
837
|
mcpServer: meta?.mcpServer,
|
|
525
|
-
hostname:
|
|
838
|
+
hostname: os2.hostname(),
|
|
526
839
|
cwd: process.cwd()
|
|
527
840
|
});
|
|
528
841
|
}
|
|
@@ -536,7 +849,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
|
536
849
|
checkedBy,
|
|
537
850
|
agent: meta?.agent,
|
|
538
851
|
mcpServer: meta?.mcpServer,
|
|
539
|
-
hostname:
|
|
852
|
+
hostname: os2.hostname()
|
|
540
853
|
});
|
|
541
854
|
}
|
|
542
855
|
function tokenize(toolName) {
|
|
@@ -552,9 +865,9 @@ function matchesPattern(text, patterns) {
|
|
|
552
865
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
553
866
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
554
867
|
}
|
|
555
|
-
function getNestedValue(obj,
|
|
868
|
+
function getNestedValue(obj, path10) {
|
|
556
869
|
if (!obj || typeof obj !== "object") return null;
|
|
557
|
-
return
|
|
870
|
+
return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
558
871
|
}
|
|
559
872
|
function shouldSnapshot(toolName, args, config) {
|
|
560
873
|
if (!config.settings.enableUndo) return false;
|
|
@@ -599,6 +912,10 @@ function evaluateSmartConditions(args, rule) {
|
|
|
599
912
|
return true;
|
|
600
913
|
}
|
|
601
914
|
}
|
|
915
|
+
case "matchesGlob":
|
|
916
|
+
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
917
|
+
case "notMatchesGlob":
|
|
918
|
+
return val !== null && cond.value ? !pm.isMatch(val, cond.value) : true;
|
|
602
919
|
default:
|
|
603
920
|
return false;
|
|
604
921
|
}
|
|
@@ -620,7 +937,6 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
620
937
|
const fieldName = toolInspection[matchingPattern];
|
|
621
938
|
return fieldName === "sql" || fieldName === "query";
|
|
622
939
|
}
|
|
623
|
-
var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
624
940
|
async function analyzeShellCommand(command) {
|
|
625
941
|
const actions = [];
|
|
626
942
|
const paths = [];
|
|
@@ -702,188 +1018,14 @@ function redactSecrets(text) {
|
|
|
702
1018
|
);
|
|
703
1019
|
return redacted;
|
|
704
1020
|
}
|
|
705
|
-
var DANGEROUS_WORDS = [
|
|
706
|
-
"mkfs",
|
|
707
|
-
// formats/wipes a filesystem partition
|
|
708
|
-
"shred"
|
|
709
|
-
// permanently overwrites file contents (unrecoverable)
|
|
710
|
-
];
|
|
711
|
-
var DEFAULT_CONFIG = {
|
|
712
|
-
settings: {
|
|
713
|
-
mode: "standard",
|
|
714
|
-
autoStartDaemon: true,
|
|
715
|
-
enableUndo: true,
|
|
716
|
-
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
717
|
-
enableHookLogDebug: false,
|
|
718
|
-
approvalTimeoutMs: 0,
|
|
719
|
-
// 0 = disabled; set e.g. 30000 for 30-second auto-deny
|
|
720
|
-
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
721
|
-
},
|
|
722
|
-
policy: {
|
|
723
|
-
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
724
|
-
dangerousWords: DANGEROUS_WORDS,
|
|
725
|
-
ignoredTools: [
|
|
726
|
-
"list_*",
|
|
727
|
-
"get_*",
|
|
728
|
-
"read_*",
|
|
729
|
-
"describe_*",
|
|
730
|
-
"read",
|
|
731
|
-
"glob",
|
|
732
|
-
"grep",
|
|
733
|
-
"ls",
|
|
734
|
-
"notebookread",
|
|
735
|
-
"notebookedit",
|
|
736
|
-
"webfetch",
|
|
737
|
-
"websearch",
|
|
738
|
-
"exitplanmode",
|
|
739
|
-
"askuserquestion",
|
|
740
|
-
"agent",
|
|
741
|
-
"task*",
|
|
742
|
-
"toolsearch",
|
|
743
|
-
"mcp__ide__*",
|
|
744
|
-
"getDiagnostics"
|
|
745
|
-
],
|
|
746
|
-
toolInspection: {
|
|
747
|
-
bash: "command",
|
|
748
|
-
shell: "command",
|
|
749
|
-
run_shell_command: "command",
|
|
750
|
-
"terminal.execute": "command",
|
|
751
|
-
"postgres:query": "sql"
|
|
752
|
-
},
|
|
753
|
-
snapshot: {
|
|
754
|
-
tools: [
|
|
755
|
-
"str_replace_based_edit_tool",
|
|
756
|
-
"write_file",
|
|
757
|
-
"edit_file",
|
|
758
|
-
"create_file",
|
|
759
|
-
"edit",
|
|
760
|
-
"replace"
|
|
761
|
-
],
|
|
762
|
-
onlyPaths: [],
|
|
763
|
-
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
764
|
-
},
|
|
765
|
-
rules: [
|
|
766
|
-
// Only use the legacy rules format for simple path-based rm control.
|
|
767
|
-
// All other command-level enforcement lives in smartRules below.
|
|
768
|
-
{
|
|
769
|
-
action: "rm",
|
|
770
|
-
allowPaths: [
|
|
771
|
-
"**/node_modules/**",
|
|
772
|
-
"dist/**",
|
|
773
|
-
"build/**",
|
|
774
|
-
".next/**",
|
|
775
|
-
"coverage/**",
|
|
776
|
-
".cache/**",
|
|
777
|
-
"tmp/**",
|
|
778
|
-
"temp/**",
|
|
779
|
-
".DS_Store"
|
|
780
|
-
]
|
|
781
|
-
}
|
|
782
|
-
],
|
|
783
|
-
smartRules: [
|
|
784
|
-
// ── SQL safety ────────────────────────────────────────────────────────
|
|
785
|
-
{
|
|
786
|
-
name: "no-delete-without-where",
|
|
787
|
-
tool: "*",
|
|
788
|
-
conditions: [
|
|
789
|
-
{ field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
|
|
790
|
-
{ field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
|
|
791
|
-
],
|
|
792
|
-
conditionMode: "all",
|
|
793
|
-
verdict: "review",
|
|
794
|
-
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
795
|
-
},
|
|
796
|
-
{
|
|
797
|
-
name: "review-drop-truncate-shell",
|
|
798
|
-
tool: "bash",
|
|
799
|
-
conditions: [
|
|
800
|
-
{
|
|
801
|
-
field: "command",
|
|
802
|
-
op: "matches",
|
|
803
|
-
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
804
|
-
flags: "i"
|
|
805
|
-
}
|
|
806
|
-
],
|
|
807
|
-
conditionMode: "all",
|
|
808
|
-
verdict: "review",
|
|
809
|
-
reason: "SQL DDL destructive statement inside a shell command"
|
|
810
|
-
},
|
|
811
|
-
// ── Git safety ────────────────────────────────────────────────────────
|
|
812
|
-
{
|
|
813
|
-
name: "block-force-push",
|
|
814
|
-
tool: "bash",
|
|
815
|
-
conditions: [
|
|
816
|
-
{
|
|
817
|
-
field: "command",
|
|
818
|
-
op: "matches",
|
|
819
|
-
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
820
|
-
flags: "i"
|
|
821
|
-
}
|
|
822
|
-
],
|
|
823
|
-
conditionMode: "all",
|
|
824
|
-
verdict: "block",
|
|
825
|
-
reason: "Force push overwrites remote history and cannot be undone"
|
|
826
|
-
},
|
|
827
|
-
{
|
|
828
|
-
name: "review-git-push",
|
|
829
|
-
tool: "bash",
|
|
830
|
-
conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
|
|
831
|
-
conditionMode: "all",
|
|
832
|
-
verdict: "review",
|
|
833
|
-
reason: "git push sends changes to a shared remote"
|
|
834
|
-
},
|
|
835
|
-
{
|
|
836
|
-
name: "review-git-destructive",
|
|
837
|
-
tool: "bash",
|
|
838
|
-
conditions: [
|
|
839
|
-
{
|
|
840
|
-
field: "command",
|
|
841
|
-
op: "matches",
|
|
842
|
-
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
843
|
-
flags: "i"
|
|
844
|
-
}
|
|
845
|
-
],
|
|
846
|
-
conditionMode: "all",
|
|
847
|
-
verdict: "review",
|
|
848
|
-
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
849
|
-
},
|
|
850
|
-
// ── Shell safety ──────────────────────────────────────────────────────
|
|
851
|
-
{
|
|
852
|
-
name: "review-sudo",
|
|
853
|
-
tool: "bash",
|
|
854
|
-
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
855
|
-
conditionMode: "all",
|
|
856
|
-
verdict: "review",
|
|
857
|
-
reason: "Command requires elevated privileges"
|
|
858
|
-
},
|
|
859
|
-
{
|
|
860
|
-
name: "review-curl-pipe-shell",
|
|
861
|
-
tool: "bash",
|
|
862
|
-
conditions: [
|
|
863
|
-
{
|
|
864
|
-
field: "command",
|
|
865
|
-
op: "matches",
|
|
866
|
-
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
867
|
-
flags: "i"
|
|
868
|
-
}
|
|
869
|
-
],
|
|
870
|
-
conditionMode: "all",
|
|
871
|
-
verdict: "block",
|
|
872
|
-
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
873
|
-
}
|
|
874
|
-
]
|
|
875
|
-
},
|
|
876
|
-
environments: {}
|
|
877
|
-
};
|
|
878
|
-
var cachedConfig = null;
|
|
879
1021
|
function _resetConfigCache() {
|
|
880
1022
|
cachedConfig = null;
|
|
881
1023
|
}
|
|
882
1024
|
function getGlobalSettings() {
|
|
883
1025
|
try {
|
|
884
|
-
const globalConfigPath =
|
|
885
|
-
if (
|
|
886
|
-
const parsed = JSON.parse(
|
|
1026
|
+
const globalConfigPath = path4.join(os2.homedir(), ".node9", "config.json");
|
|
1027
|
+
if (fs2.existsSync(globalConfigPath)) {
|
|
1028
|
+
const parsed = JSON.parse(fs2.readFileSync(globalConfigPath, "utf-8"));
|
|
887
1029
|
const settings = parsed.settings || {};
|
|
888
1030
|
return {
|
|
889
1031
|
mode: settings.mode || "standard",
|
|
@@ -905,9 +1047,9 @@ function getGlobalSettings() {
|
|
|
905
1047
|
}
|
|
906
1048
|
function getInternalToken() {
|
|
907
1049
|
try {
|
|
908
|
-
const pidFile =
|
|
909
|
-
if (!
|
|
910
|
-
const data = JSON.parse(
|
|
1050
|
+
const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
|
|
1051
|
+
if (!fs2.existsSync(pidFile)) return null;
|
|
1052
|
+
const data = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
|
|
911
1053
|
process.kill(data.pid, 0);
|
|
912
1054
|
return data.internalToken ?? null;
|
|
913
1055
|
} catch {
|
|
@@ -922,7 +1064,8 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
922
1064
|
(rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
|
|
923
1065
|
);
|
|
924
1066
|
if (matchedRule) {
|
|
925
|
-
if (matchedRule.verdict === "allow")
|
|
1067
|
+
if (matchedRule.verdict === "allow")
|
|
1068
|
+
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
926
1069
|
return {
|
|
927
1070
|
decision: matchedRule.verdict,
|
|
928
1071
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -933,13 +1076,11 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
933
1076
|
}
|
|
934
1077
|
}
|
|
935
1078
|
let allTokens = [];
|
|
936
|
-
let actionTokens = [];
|
|
937
1079
|
let pathTokens = [];
|
|
938
1080
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
939
1081
|
if (shellCommand) {
|
|
940
1082
|
const analyzed = await analyzeShellCommand(shellCommand);
|
|
941
1083
|
allTokens = analyzed.allTokens;
|
|
942
|
-
actionTokens = analyzed.actions;
|
|
943
1084
|
pathTokens = analyzed.paths;
|
|
944
1085
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
945
1086
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
@@ -947,11 +1088,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
947
1088
|
}
|
|
948
1089
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
949
1090
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
950
|
-
actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
951
1091
|
}
|
|
952
1092
|
} else {
|
|
953
1093
|
allTokens = tokenize(toolName);
|
|
954
|
-
actionTokens = [toolName];
|
|
955
1094
|
if (args && typeof args === "object") {
|
|
956
1095
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
957
1096
|
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
@@ -974,29 +1113,6 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
974
1113
|
const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
|
|
975
1114
|
if (allInSandbox) return { decision: "allow" };
|
|
976
1115
|
}
|
|
977
|
-
for (const action of actionTokens) {
|
|
978
|
-
const rule = config.policy.rules.find(
|
|
979
|
-
(r) => r.action === action || matchesPattern(action, r.action)
|
|
980
|
-
);
|
|
981
|
-
if (rule) {
|
|
982
|
-
if (pathTokens.length > 0) {
|
|
983
|
-
const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
|
|
984
|
-
if (anyBlocked)
|
|
985
|
-
return {
|
|
986
|
-
decision: "review",
|
|
987
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
|
|
988
|
-
tier: 5
|
|
989
|
-
};
|
|
990
|
-
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
991
|
-
if (allAllowed) return { decision: "allow" };
|
|
992
|
-
}
|
|
993
|
-
return {
|
|
994
|
-
decision: "review",
|
|
995
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
|
|
996
|
-
tier: 5
|
|
997
|
-
};
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
1116
|
let matchedDangerousWord;
|
|
1001
1117
|
const isDangerous = allTokens.some(
|
|
1002
1118
|
(token) => config.policy.dangerousWords.some((word) => {
|
|
@@ -1048,9 +1164,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
1048
1164
|
}
|
|
1049
1165
|
async function explainPolicy(toolName, args) {
|
|
1050
1166
|
const steps = [];
|
|
1051
|
-
const globalPath =
|
|
1052
|
-
const projectPath =
|
|
1053
|
-
const credsPath =
|
|
1167
|
+
const globalPath = path4.join(os2.homedir(), ".node9", "config.json");
|
|
1168
|
+
const projectPath = path4.join(process.cwd(), "node9.config.json");
|
|
1169
|
+
const credsPath = path4.join(os2.homedir(), ".node9", "credentials.json");
|
|
1054
1170
|
const waterfall = [
|
|
1055
1171
|
{
|
|
1056
1172
|
tier: 1,
|
|
@@ -1061,19 +1177,19 @@ async function explainPolicy(toolName, args) {
|
|
|
1061
1177
|
{
|
|
1062
1178
|
tier: 2,
|
|
1063
1179
|
label: "Cloud policy",
|
|
1064
|
-
status:
|
|
1065
|
-
note:
|
|
1180
|
+
status: fs2.existsSync(credsPath) ? "active" : "missing",
|
|
1181
|
+
note: fs2.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
|
|
1066
1182
|
},
|
|
1067
1183
|
{
|
|
1068
1184
|
tier: 3,
|
|
1069
1185
|
label: "Project config",
|
|
1070
|
-
status:
|
|
1186
|
+
status: fs2.existsSync(projectPath) ? "active" : "missing",
|
|
1071
1187
|
path: projectPath
|
|
1072
1188
|
},
|
|
1073
1189
|
{
|
|
1074
1190
|
tier: 4,
|
|
1075
1191
|
label: "Global config",
|
|
1076
|
-
status:
|
|
1192
|
+
status: fs2.existsSync(globalPath) ? "active" : "missing",
|
|
1077
1193
|
path: globalPath
|
|
1078
1194
|
},
|
|
1079
1195
|
{
|
|
@@ -1084,7 +1200,28 @@ async function explainPolicy(toolName, args) {
|
|
|
1084
1200
|
}
|
|
1085
1201
|
];
|
|
1086
1202
|
const config = getConfig();
|
|
1087
|
-
|
|
1203
|
+
const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
|
|
1204
|
+
if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
|
|
1205
|
+
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
1206
|
+
if (dlpMatch) {
|
|
1207
|
+
steps.push({
|
|
1208
|
+
name: "DLP Content Scanner",
|
|
1209
|
+
outcome: dlpMatch.severity === "block" ? "block" : "review",
|
|
1210
|
+
detail: `\u{1F6A8} ${dlpMatch.patternName} detected in ${dlpMatch.fieldPath} \u2014 sample: ${dlpMatch.redactedSample}`,
|
|
1211
|
+
isFinal: dlpMatch.severity === "block"
|
|
1212
|
+
});
|
|
1213
|
+
if (dlpMatch.severity === "block") {
|
|
1214
|
+
return { tool: toolName, args, waterfall, steps, decision: "block" };
|
|
1215
|
+
}
|
|
1216
|
+
} else {
|
|
1217
|
+
steps.push({
|
|
1218
|
+
name: "DLP Content Scanner",
|
|
1219
|
+
outcome: "checked",
|
|
1220
|
+
detail: "No sensitive credentials detected in args"
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (wouldBeIgnored) {
|
|
1088
1225
|
steps.push({
|
|
1089
1226
|
name: "Ignored tools",
|
|
1090
1227
|
outcome: "allow",
|
|
@@ -1137,13 +1274,11 @@ async function explainPolicy(toolName, args) {
|
|
|
1137
1274
|
steps.push({ name: "Smart rules", outcome: "skip", detail: "No smart rules configured" });
|
|
1138
1275
|
}
|
|
1139
1276
|
let allTokens = [];
|
|
1140
|
-
let actionTokens = [];
|
|
1141
1277
|
let pathTokens = [];
|
|
1142
1278
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
1143
1279
|
if (shellCommand) {
|
|
1144
1280
|
const analyzed = await analyzeShellCommand(shellCommand);
|
|
1145
1281
|
allTokens = analyzed.allTokens;
|
|
1146
|
-
actionTokens = analyzed.actions;
|
|
1147
1282
|
pathTokens = analyzed.paths;
|
|
1148
1283
|
const patterns = Object.keys(config.policy.toolInspection);
|
|
1149
1284
|
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
@@ -1177,7 +1312,6 @@ async function explainPolicy(toolName, args) {
|
|
|
1177
1312
|
});
|
|
1178
1313
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
1179
1314
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
1180
|
-
actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
1181
1315
|
steps.push({
|
|
1182
1316
|
name: "SQL token stripping",
|
|
1183
1317
|
outcome: "checked",
|
|
@@ -1186,7 +1320,6 @@ async function explainPolicy(toolName, args) {
|
|
|
1186
1320
|
}
|
|
1187
1321
|
} else {
|
|
1188
1322
|
allTokens = tokenize(toolName);
|
|
1189
|
-
actionTokens = [toolName];
|
|
1190
1323
|
let detail = `No toolInspection match for "${toolName}" \u2014 tokens: [${allTokens.join(", ")}]`;
|
|
1191
1324
|
if (args && typeof args === "object") {
|
|
1192
1325
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
@@ -1227,74 +1360,15 @@ async function explainPolicy(toolName, args) {
|
|
|
1227
1360
|
detail: pathTokens.length === 0 ? "No path tokens found in input" : "No sandbox paths configured"
|
|
1228
1361
|
});
|
|
1229
1362
|
}
|
|
1230
|
-
let
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
if (anyBlocked) {
|
|
1240
|
-
steps.push({
|
|
1241
|
-
name: "Policy rules",
|
|
1242
|
-
outcome: "review",
|
|
1243
|
-
detail: `Rule "${rule.action}" matched + path is in blockPaths`,
|
|
1244
|
-
isFinal: true
|
|
1245
|
-
});
|
|
1246
|
-
return {
|
|
1247
|
-
tool: toolName,
|
|
1248
|
-
args,
|
|
1249
|
-
waterfall,
|
|
1250
|
-
steps,
|
|
1251
|
-
decision: "review",
|
|
1252
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
|
|
1253
|
-
};
|
|
1254
|
-
}
|
|
1255
|
-
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
1256
|
-
if (allAllowed) {
|
|
1257
|
-
steps.push({
|
|
1258
|
-
name: "Policy rules",
|
|
1259
|
-
outcome: "allow",
|
|
1260
|
-
detail: `Rule "${rule.action}" matched + all paths are in allowPaths`,
|
|
1261
|
-
isFinal: true
|
|
1262
|
-
});
|
|
1263
|
-
return { tool: toolName, args, waterfall, steps, decision: "allow" };
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
steps.push({
|
|
1267
|
-
name: "Policy rules",
|
|
1268
|
-
outcome: "review",
|
|
1269
|
-
detail: `Rule "${rule.action}" matched \u2014 default block (no path exception)`,
|
|
1270
|
-
isFinal: true
|
|
1271
|
-
});
|
|
1272
|
-
return {
|
|
1273
|
-
tool: toolName,
|
|
1274
|
-
args,
|
|
1275
|
-
waterfall,
|
|
1276
|
-
steps,
|
|
1277
|
-
decision: "review",
|
|
1278
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
if (!ruleMatched) {
|
|
1283
|
-
steps.push({
|
|
1284
|
-
name: "Policy rules",
|
|
1285
|
-
outcome: "skip",
|
|
1286
|
-
detail: config.policy.rules.length === 0 ? "No rules configured" : `No rule matched [${actionTokens.join(", ")}]`
|
|
1287
|
-
});
|
|
1288
|
-
}
|
|
1289
|
-
let matchedDangerousWord;
|
|
1290
|
-
const isDangerous = uniqueTokens.some(
|
|
1291
|
-
(token) => config.policy.dangerousWords.some((word) => {
|
|
1292
|
-
const w = word.toLowerCase();
|
|
1293
|
-
const hit = token === w || (() => {
|
|
1294
|
-
try {
|
|
1295
|
-
return new RegExp(`\\b${w}\\b`, "i").test(token);
|
|
1296
|
-
} catch {
|
|
1297
|
-
return false;
|
|
1363
|
+
let matchedDangerousWord;
|
|
1364
|
+
const isDangerous = uniqueTokens.some(
|
|
1365
|
+
(token) => config.policy.dangerousWords.some((word) => {
|
|
1366
|
+
const w = word.toLowerCase();
|
|
1367
|
+
const hit = token === w || (() => {
|
|
1368
|
+
try {
|
|
1369
|
+
return new RegExp(`\\b${w}\\b`, "i").test(token);
|
|
1370
|
+
} catch {
|
|
1371
|
+
return false;
|
|
1298
1372
|
}
|
|
1299
1373
|
})();
|
|
1300
1374
|
if (hit && !matchedDangerousWord) matchedDangerousWord = word;
|
|
@@ -1350,13 +1424,11 @@ function isIgnoredTool(toolName) {
|
|
|
1350
1424
|
const config = getConfig();
|
|
1351
1425
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
1352
1426
|
}
|
|
1353
|
-
var DAEMON_PORT = 7391;
|
|
1354
|
-
var DAEMON_HOST = "127.0.0.1";
|
|
1355
1427
|
function isDaemonRunning() {
|
|
1356
1428
|
try {
|
|
1357
|
-
const pidFile =
|
|
1358
|
-
if (!
|
|
1359
|
-
const { pid, port } = JSON.parse(
|
|
1429
|
+
const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
|
|
1430
|
+
if (!fs2.existsSync(pidFile)) return false;
|
|
1431
|
+
const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
|
|
1360
1432
|
if (port !== DAEMON_PORT) return false;
|
|
1361
1433
|
process.kill(pid, 0);
|
|
1362
1434
|
return true;
|
|
@@ -1366,16 +1438,16 @@ function isDaemonRunning() {
|
|
|
1366
1438
|
}
|
|
1367
1439
|
function getPersistentDecision(toolName) {
|
|
1368
1440
|
try {
|
|
1369
|
-
const file =
|
|
1370
|
-
if (!
|
|
1371
|
-
const decisions = JSON.parse(
|
|
1441
|
+
const file = path4.join(os2.homedir(), ".node9", "decisions.json");
|
|
1442
|
+
if (!fs2.existsSync(file)) return null;
|
|
1443
|
+
const decisions = JSON.parse(fs2.readFileSync(file, "utf-8"));
|
|
1372
1444
|
const d = decisions[toolName];
|
|
1373
1445
|
if (d === "allow" || d === "deny") return d;
|
|
1374
1446
|
} catch {
|
|
1375
1447
|
}
|
|
1376
1448
|
return null;
|
|
1377
1449
|
}
|
|
1378
|
-
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
1450
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
|
|
1379
1451
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1380
1452
|
const checkCtrl = new AbortController();
|
|
1381
1453
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -1390,6 +1462,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
|
1390
1462
|
args,
|
|
1391
1463
|
agent: meta?.agent,
|
|
1392
1464
|
mcpServer: meta?.mcpServer,
|
|
1465
|
+
fromCLI: true,
|
|
1466
|
+
// Pass the flight-recorder ID so the daemon uses the same UUID for
|
|
1467
|
+
// activity-result as the CLI used for the pending activity event.
|
|
1468
|
+
// Without this, the two UUIDs never match and tail.ts never resolves
|
|
1469
|
+
// the pending item.
|
|
1470
|
+
activityId,
|
|
1393
1471
|
...riskMetadata && { riskMetadata }
|
|
1394
1472
|
}),
|
|
1395
1473
|
signal: checkCtrl.signal
|
|
@@ -1444,7 +1522,44 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1444
1522
|
signal: AbortSignal.timeout(3e3)
|
|
1445
1523
|
});
|
|
1446
1524
|
}
|
|
1525
|
+
function notifyActivity(data) {
|
|
1526
|
+
return new Promise((resolve) => {
|
|
1527
|
+
try {
|
|
1528
|
+
const payload = JSON.stringify(data);
|
|
1529
|
+
const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
|
|
1530
|
+
sock.on("connect", () => {
|
|
1531
|
+
sock.on("close", resolve);
|
|
1532
|
+
sock.end(payload);
|
|
1533
|
+
});
|
|
1534
|
+
sock.on("error", resolve);
|
|
1535
|
+
} catch {
|
|
1536
|
+
resolve();
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1447
1540
|
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1541
|
+
if (!options?.calledFromDaemon) {
|
|
1542
|
+
const actId = randomUUID();
|
|
1543
|
+
const actTs = Date.now();
|
|
1544
|
+
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
|
|
1545
|
+
const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
|
|
1546
|
+
...options,
|
|
1547
|
+
activityId: actId
|
|
1548
|
+
});
|
|
1549
|
+
if (!result.noApprovalMechanism) {
|
|
1550
|
+
await notifyActivity({
|
|
1551
|
+
id: actId,
|
|
1552
|
+
tool: toolName,
|
|
1553
|
+
ts: actTs,
|
|
1554
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
1555
|
+
label: result.blockedByLabel
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
return result;
|
|
1559
|
+
}
|
|
1560
|
+
return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
|
|
1561
|
+
}
|
|
1562
|
+
async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1448
1563
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1449
1564
|
const pauseState = checkPause();
|
|
1450
1565
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
@@ -1467,6 +1582,23 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1467
1582
|
let policyMatchedField;
|
|
1468
1583
|
let policyMatchedWord;
|
|
1469
1584
|
let riskMetadata;
|
|
1585
|
+
if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
1586
|
+
const dlpMatch = scanArgs(args);
|
|
1587
|
+
if (dlpMatch) {
|
|
1588
|
+
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
1589
|
+
if (dlpMatch.severity === "block") {
|
|
1590
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
|
|
1591
|
+
return {
|
|
1592
|
+
approved: false,
|
|
1593
|
+
reason: dlpReason,
|
|
1594
|
+
blockedBy: "local-config",
|
|
1595
|
+
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
|
|
1599
|
+
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1470
1602
|
if (config.settings.mode === "audit") {
|
|
1471
1603
|
if (!isIgnoredTool(toolName)) {
|
|
1472
1604
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
@@ -1686,7 +1818,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1686
1818
|
console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
1687
1819
|
`));
|
|
1688
1820
|
}
|
|
1689
|
-
const daemonDecision = await askDaemon(
|
|
1821
|
+
const daemonDecision = await askDaemon(
|
|
1822
|
+
toolName,
|
|
1823
|
+
args,
|
|
1824
|
+
meta,
|
|
1825
|
+
signal,
|
|
1826
|
+
riskMetadata,
|
|
1827
|
+
options?.activityId
|
|
1828
|
+
);
|
|
1690
1829
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1691
1830
|
const isApproved = daemonDecision === "allow";
|
|
1692
1831
|
return {
|
|
@@ -1706,7 +1845,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1706
1845
|
racePromises.push(
|
|
1707
1846
|
(async () => {
|
|
1708
1847
|
try {
|
|
1709
|
-
|
|
1848
|
+
if (explainableLabel.includes("DLP")) {
|
|
1849
|
+
console.log(chalk2.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
1850
|
+
console.log(
|
|
1851
|
+
chalk2.red.bold(` A sensitive secret was detected in the tool arguments!`)
|
|
1852
|
+
);
|
|
1853
|
+
} else {
|
|
1854
|
+
console.log(chalk2.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
1855
|
+
}
|
|
1710
1856
|
console.log(`${chalk2.bold("Action:")} ${chalk2.red(toolName)}`);
|
|
1711
1857
|
console.log(`${chalk2.bold("Flagged By:")} ${chalk2.yellow(explainableLabel)}`);
|
|
1712
1858
|
if (isRemoteLocked) {
|
|
@@ -1811,8 +1957,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1811
1957
|
}
|
|
1812
1958
|
function getConfig() {
|
|
1813
1959
|
if (cachedConfig) return cachedConfig;
|
|
1814
|
-
const globalPath =
|
|
1815
|
-
const projectPath =
|
|
1960
|
+
const globalPath = path4.join(os2.homedir(), ".node9", "config.json");
|
|
1961
|
+
const projectPath = path4.join(process.cwd(), "node9.config.json");
|
|
1816
1962
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1817
1963
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1818
1964
|
const mergedSettings = {
|
|
@@ -1824,13 +1970,13 @@ function getConfig() {
|
|
|
1824
1970
|
dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
|
|
1825
1971
|
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
1826
1972
|
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
1827
|
-
rules: [...DEFAULT_CONFIG.policy.rules],
|
|
1828
1973
|
smartRules: [...DEFAULT_CONFIG.policy.smartRules],
|
|
1829
1974
|
snapshot: {
|
|
1830
1975
|
tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
|
|
1831
1976
|
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
1832
1977
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1833
|
-
}
|
|
1978
|
+
},
|
|
1979
|
+
dlp: { ...DEFAULT_CONFIG.policy.dlp }
|
|
1834
1980
|
};
|
|
1835
1981
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
1836
1982
|
const applyLayer = (source) => {
|
|
@@ -1850,7 +1996,6 @@ function getConfig() {
|
|
|
1850
1996
|
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
1851
1997
|
if (p.toolInspection)
|
|
1852
1998
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
1853
|
-
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
1854
1999
|
if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
|
|
1855
2000
|
if (p.snapshot) {
|
|
1856
2001
|
const s2 = p.snapshot;
|
|
@@ -1858,6 +2003,11 @@ function getConfig() {
|
|
|
1858
2003
|
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1859
2004
|
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1860
2005
|
}
|
|
2006
|
+
if (p.dlp) {
|
|
2007
|
+
const d = p.dlp;
|
|
2008
|
+
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
2009
|
+
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
2010
|
+
}
|
|
1861
2011
|
const envs = source.environments || {};
|
|
1862
2012
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
1863
2013
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -1872,6 +2022,22 @@ function getConfig() {
|
|
|
1872
2022
|
};
|
|
1873
2023
|
applyLayer(globalConfig);
|
|
1874
2024
|
applyLayer(projectConfig);
|
|
2025
|
+
for (const shieldName of readActiveShields()) {
|
|
2026
|
+
const shield = getShield(shieldName);
|
|
2027
|
+
if (!shield) continue;
|
|
2028
|
+
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2029
|
+
for (const rule of shield.smartRules) {
|
|
2030
|
+
if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
2031
|
+
}
|
|
2032
|
+
const existingWords = new Set(mergedPolicy.dangerousWords);
|
|
2033
|
+
for (const word of shield.dangerousWords) {
|
|
2034
|
+
if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2038
|
+
for (const rule of ADVISORY_SMART_RULES) {
|
|
2039
|
+
if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
2040
|
+
}
|
|
1875
2041
|
if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
|
|
1876
2042
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
1877
2043
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
@@ -1887,10 +2053,10 @@ function getConfig() {
|
|
|
1887
2053
|
return cachedConfig;
|
|
1888
2054
|
}
|
|
1889
2055
|
function tryLoadConfig(filePath) {
|
|
1890
|
-
if (!
|
|
2056
|
+
if (!fs2.existsSync(filePath)) return null;
|
|
1891
2057
|
let raw;
|
|
1892
2058
|
try {
|
|
1893
|
-
raw = JSON.parse(
|
|
2059
|
+
raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
1894
2060
|
} catch (err) {
|
|
1895
2061
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1896
2062
|
process.stderr.write(
|
|
@@ -1952,9 +2118,9 @@ function getCredentials() {
|
|
|
1952
2118
|
};
|
|
1953
2119
|
}
|
|
1954
2120
|
try {
|
|
1955
|
-
const credPath =
|
|
1956
|
-
if (
|
|
1957
|
-
const creds = JSON.parse(
|
|
2121
|
+
const credPath = path4.join(os2.homedir(), ".node9", "credentials.json");
|
|
2122
|
+
if (fs2.existsSync(credPath)) {
|
|
2123
|
+
const creds = JSON.parse(fs2.readFileSync(credPath, "utf-8"));
|
|
1958
2124
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
1959
2125
|
const profile = creds[profileName];
|
|
1960
2126
|
if (profile?.apiKey) {
|
|
@@ -1985,9 +2151,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
1985
2151
|
context: {
|
|
1986
2152
|
agent: meta?.agent,
|
|
1987
2153
|
mcpServer: meta?.mcpServer,
|
|
1988
|
-
hostname:
|
|
2154
|
+
hostname: os2.hostname(),
|
|
1989
2155
|
cwd: process.cwd(),
|
|
1990
|
-
platform:
|
|
2156
|
+
platform: os2.platform()
|
|
1991
2157
|
}
|
|
1992
2158
|
}),
|
|
1993
2159
|
signal: AbortSignal.timeout(5e3)
|
|
@@ -2008,9 +2174,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
2008
2174
|
context: {
|
|
2009
2175
|
agent: meta?.agent,
|
|
2010
2176
|
mcpServer: meta?.mcpServer,
|
|
2011
|
-
hostname:
|
|
2177
|
+
hostname: os2.hostname(),
|
|
2012
2178
|
cwd: process.cwd(),
|
|
2013
|
-
platform:
|
|
2179
|
+
platform: os2.platform()
|
|
2014
2180
|
},
|
|
2015
2181
|
...riskMetadata && { riskMetadata }
|
|
2016
2182
|
}),
|
|
@@ -2067,270 +2233,233 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
2067
2233
|
} catch {
|
|
2068
2234
|
}
|
|
2069
2235
|
}
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2236
|
+
var PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
|
|
2237
|
+
var init_core = __esm({
|
|
2238
|
+
"src/core.ts"() {
|
|
2239
|
+
"use strict";
|
|
2240
|
+
init_native();
|
|
2241
|
+
init_context_sniper();
|
|
2242
|
+
init_config_schema();
|
|
2243
|
+
init_shields();
|
|
2244
|
+
init_dlp();
|
|
2245
|
+
PAUSED_FILE = path4.join(os2.homedir(), ".node9", "PAUSED");
|
|
2246
|
+
TRUST_FILE = path4.join(os2.homedir(), ".node9", "trust.json");
|
|
2247
|
+
LOCAL_AUDIT_LOG = path4.join(os2.homedir(), ".node9", "audit.log");
|
|
2248
|
+
HOOK_DEBUG_LOG = path4.join(os2.homedir(), ".node9", "hook-debug.log");
|
|
2249
|
+
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2250
|
+
DANGEROUS_WORDS = [
|
|
2251
|
+
"mkfs",
|
|
2252
|
+
// formats/wipes a filesystem partition
|
|
2253
|
+
"shred"
|
|
2254
|
+
// permanently overwrites file contents (unrecoverable)
|
|
2255
|
+
];
|
|
2256
|
+
DEFAULT_CONFIG = {
|
|
2257
|
+
settings: {
|
|
2258
|
+
mode: "standard",
|
|
2259
|
+
autoStartDaemon: true,
|
|
2260
|
+
enableUndo: true,
|
|
2261
|
+
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
2262
|
+
enableHookLogDebug: false,
|
|
2263
|
+
approvalTimeoutMs: 0,
|
|
2264
|
+
// 0 = disabled; set e.g. 30000 for 30-second auto-deny
|
|
2265
|
+
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
2266
|
+
},
|
|
2267
|
+
policy: {
|
|
2268
|
+
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
2269
|
+
dangerousWords: DANGEROUS_WORDS,
|
|
2270
|
+
ignoredTools: [
|
|
2271
|
+
"list_*",
|
|
2272
|
+
"get_*",
|
|
2273
|
+
"read_*",
|
|
2274
|
+
"describe_*",
|
|
2275
|
+
"read",
|
|
2276
|
+
"glob",
|
|
2277
|
+
"grep",
|
|
2278
|
+
"ls",
|
|
2279
|
+
"notebookread",
|
|
2280
|
+
"notebookedit",
|
|
2281
|
+
"webfetch",
|
|
2282
|
+
"websearch",
|
|
2283
|
+
"exitplanmode",
|
|
2284
|
+
"askuserquestion",
|
|
2285
|
+
"agent",
|
|
2286
|
+
"task*",
|
|
2287
|
+
"toolsearch",
|
|
2288
|
+
"mcp__ide__*",
|
|
2289
|
+
"getDiagnostics"
|
|
2290
|
+
],
|
|
2291
|
+
toolInspection: {
|
|
2292
|
+
bash: "command",
|
|
2293
|
+
shell: "command",
|
|
2294
|
+
run_shell_command: "command",
|
|
2295
|
+
"terminal.execute": "command",
|
|
2296
|
+
"postgres:query": "sql"
|
|
2297
|
+
},
|
|
2298
|
+
snapshot: {
|
|
2299
|
+
tools: [
|
|
2300
|
+
"str_replace_based_edit_tool",
|
|
2301
|
+
"write_file",
|
|
2302
|
+
"edit_file",
|
|
2303
|
+
"create_file",
|
|
2304
|
+
"edit",
|
|
2305
|
+
"replace"
|
|
2306
|
+
],
|
|
2307
|
+
onlyPaths: [],
|
|
2308
|
+
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
2309
|
+
},
|
|
2310
|
+
smartRules: [
|
|
2311
|
+
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
2312
|
+
{
|
|
2313
|
+
name: "block-rm-rf-home",
|
|
2314
|
+
tool: "bash",
|
|
2315
|
+
conditionMode: "all",
|
|
2316
|
+
conditions: [
|
|
2317
|
+
{
|
|
2318
|
+
field: "command",
|
|
2319
|
+
op: "matches",
|
|
2320
|
+
value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
|
|
2321
|
+
},
|
|
2322
|
+
{
|
|
2323
|
+
field: "command",
|
|
2324
|
+
op: "matches",
|
|
2325
|
+
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
2326
|
+
}
|
|
2327
|
+
],
|
|
2328
|
+
verdict: "block",
|
|
2329
|
+
reason: "Recursive delete of home directory is irreversible"
|
|
2330
|
+
},
|
|
2331
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
2332
|
+
{
|
|
2333
|
+
name: "no-delete-without-where",
|
|
2334
|
+
tool: "*",
|
|
2335
|
+
conditions: [
|
|
2336
|
+
{ field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
|
|
2337
|
+
{ field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
|
|
2338
|
+
],
|
|
2339
|
+
conditionMode: "all",
|
|
2340
|
+
verdict: "review",
|
|
2341
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
name: "review-drop-truncate-shell",
|
|
2345
|
+
tool: "bash",
|
|
2346
|
+
conditions: [
|
|
2347
|
+
{
|
|
2348
|
+
field: "command",
|
|
2349
|
+
op: "matches",
|
|
2350
|
+
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
2351
|
+
flags: "i"
|
|
2352
|
+
}
|
|
2353
|
+
],
|
|
2354
|
+
conditionMode: "all",
|
|
2355
|
+
verdict: "review",
|
|
2356
|
+
reason: "SQL DDL destructive statement inside a shell command"
|
|
2357
|
+
},
|
|
2358
|
+
// ── Git safety ────────────────────────────────────────────────────────
|
|
2359
|
+
{
|
|
2360
|
+
name: "block-force-push",
|
|
2361
|
+
tool: "bash",
|
|
2362
|
+
conditions: [
|
|
2363
|
+
{
|
|
2364
|
+
field: "command",
|
|
2365
|
+
op: "matches",
|
|
2366
|
+
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
2367
|
+
flags: "i"
|
|
2368
|
+
}
|
|
2369
|
+
],
|
|
2370
|
+
conditionMode: "all",
|
|
2371
|
+
verdict: "block",
|
|
2372
|
+
reason: "Force push overwrites remote history and cannot be undone"
|
|
2373
|
+
},
|
|
2374
|
+
{
|
|
2375
|
+
name: "review-git-push",
|
|
2376
|
+
tool: "bash",
|
|
2377
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
|
|
2378
|
+
conditionMode: "all",
|
|
2379
|
+
verdict: "review",
|
|
2380
|
+
reason: "git push sends changes to a shared remote"
|
|
2381
|
+
},
|
|
2382
|
+
{
|
|
2383
|
+
name: "review-git-destructive",
|
|
2384
|
+
tool: "bash",
|
|
2385
|
+
conditions: [
|
|
2386
|
+
{
|
|
2387
|
+
field: "command",
|
|
2388
|
+
op: "matches",
|
|
2389
|
+
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
2390
|
+
flags: "i"
|
|
2391
|
+
}
|
|
2392
|
+
],
|
|
2393
|
+
conditionMode: "all",
|
|
2394
|
+
verdict: "review",
|
|
2395
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
2396
|
+
},
|
|
2397
|
+
// ── Shell safety ──────────────────────────────────────────────────────
|
|
2398
|
+
{
|
|
2399
|
+
name: "review-sudo",
|
|
2400
|
+
tool: "bash",
|
|
2401
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
2402
|
+
conditionMode: "all",
|
|
2403
|
+
verdict: "review",
|
|
2404
|
+
reason: "Command requires elevated privileges"
|
|
2405
|
+
},
|
|
2406
|
+
{
|
|
2407
|
+
name: "review-curl-pipe-shell",
|
|
2408
|
+
tool: "bash",
|
|
2409
|
+
conditions: [
|
|
2410
|
+
{
|
|
2411
|
+
field: "command",
|
|
2412
|
+
op: "matches",
|
|
2413
|
+
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
2414
|
+
flags: "i"
|
|
2415
|
+
}
|
|
2416
|
+
],
|
|
2417
|
+
conditionMode: "all",
|
|
2418
|
+
verdict: "block",
|
|
2419
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
2420
|
+
}
|
|
2421
|
+
],
|
|
2422
|
+
dlp: { enabled: true, scanIgnoredTools: true }
|
|
2423
|
+
},
|
|
2424
|
+
environments: {}
|
|
2425
|
+
};
|
|
2426
|
+
ADVISORY_SMART_RULES = [
|
|
2427
|
+
{
|
|
2428
|
+
name: "allow-rm-safe-paths",
|
|
2429
|
+
tool: "*",
|
|
2430
|
+
conditionMode: "all",
|
|
2431
|
+
conditions: [
|
|
2432
|
+
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
2433
|
+
{
|
|
2434
|
+
field: "command",
|
|
2435
|
+
op: "matches",
|
|
2436
|
+
// Matches known-safe build artifact paths in the command.
|
|
2437
|
+
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
2438
|
+
}
|
|
2439
|
+
],
|
|
2440
|
+
verdict: "allow",
|
|
2441
|
+
reason: "Deleting a known-safe build artifact path"
|
|
2442
|
+
},
|
|
2443
|
+
{
|
|
2444
|
+
name: "review-rm",
|
|
2445
|
+
tool: "*",
|
|
2446
|
+
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
2447
|
+
verdict: "review",
|
|
2448
|
+
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
2156
2449
|
}
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
} else {
|
|
2163
|
-
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
2164
|
-
}
|
|
2165
|
-
console.log("");
|
|
2166
|
-
}
|
|
2167
|
-
if (!anythingChanged && serversToWrap.length === 0) {
|
|
2168
|
-
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
2169
|
-
printDaemonTip();
|
|
2170
|
-
return;
|
|
2171
|
-
}
|
|
2172
|
-
if (anythingChanged) {
|
|
2173
|
-
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
2174
|
-
console.log(chalk3.gray(" Restart Claude Code for changes to take effect."));
|
|
2175
|
-
printDaemonTip();
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
2178
|
-
async function setupGemini() {
|
|
2179
|
-
const homeDir2 = os2.homedir();
|
|
2180
|
-
const settingsPath = path4.join(homeDir2, ".gemini", "settings.json");
|
|
2181
|
-
const settings = readJson(settingsPath) ?? {};
|
|
2182
|
-
const servers = settings.mcpServers ?? {};
|
|
2183
|
-
let anythingChanged = false;
|
|
2184
|
-
if (!settings.hooks) settings.hooks = {};
|
|
2185
|
-
const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
|
|
2186
|
-
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
2187
|
-
);
|
|
2188
|
-
if (!hasBeforeHook) {
|
|
2189
|
-
if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
|
|
2190
|
-
if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
|
|
2191
|
-
settings.hooks.BeforeTool.push({
|
|
2192
|
-
matcher: ".*",
|
|
2193
|
-
hooks: [
|
|
2194
|
-
{
|
|
2195
|
-
name: "node9-check",
|
|
2196
|
-
type: "command",
|
|
2197
|
-
command: fullPathCommand("check"),
|
|
2198
|
-
timeout: 6e5
|
|
2199
|
-
}
|
|
2200
|
-
]
|
|
2201
|
-
});
|
|
2202
|
-
console.log(chalk3.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
2203
|
-
anythingChanged = true;
|
|
2204
|
-
}
|
|
2205
|
-
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
2206
|
-
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
2207
|
-
);
|
|
2208
|
-
if (!hasAfterHook) {
|
|
2209
|
-
if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
|
|
2210
|
-
if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
|
|
2211
|
-
settings.hooks.AfterTool.push({
|
|
2212
|
-
matcher: ".*",
|
|
2213
|
-
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
2214
|
-
});
|
|
2215
|
-
console.log(chalk3.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
2216
|
-
anythingChanged = true;
|
|
2217
|
-
}
|
|
2218
|
-
if (anythingChanged) {
|
|
2219
|
-
writeJson(settingsPath, settings);
|
|
2220
|
-
console.log("");
|
|
2221
|
-
}
|
|
2222
|
-
const serversToWrap = [];
|
|
2223
|
-
for (const [name, server] of Object.entries(servers)) {
|
|
2224
|
-
if (!server.command || server.command === "node9") continue;
|
|
2225
|
-
const parts = [server.command, ...server.args ?? []];
|
|
2226
|
-
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
2450
|
+
];
|
|
2451
|
+
cachedConfig = null;
|
|
2452
|
+
DAEMON_PORT = 7391;
|
|
2453
|
+
DAEMON_HOST = "127.0.0.1";
|
|
2454
|
+
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path4.join(os2.tmpdir(), "node9-activity.sock");
|
|
2227
2455
|
}
|
|
2228
|
-
|
|
2229
|
-
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
2230
|
-
console.log(chalk3.white(` ${settingsPath} (mcpServers)`));
|
|
2231
|
-
for (const { name, originalCmd } of serversToWrap) {
|
|
2232
|
-
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
2233
|
-
}
|
|
2234
|
-
console.log("");
|
|
2235
|
-
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
2236
|
-
if (proceed) {
|
|
2237
|
-
for (const { name, parts } of serversToWrap) {
|
|
2238
|
-
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
2239
|
-
}
|
|
2240
|
-
settings.mcpServers = servers;
|
|
2241
|
-
writeJson(settingsPath, settings);
|
|
2242
|
-
console.log(chalk3.green(`
|
|
2243
|
-
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
2244
|
-
anythingChanged = true;
|
|
2245
|
-
} else {
|
|
2246
|
-
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
2247
|
-
}
|
|
2248
|
-
console.log("");
|
|
2249
|
-
}
|
|
2250
|
-
if (!anythingChanged && serversToWrap.length === 0) {
|
|
2251
|
-
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
|
|
2252
|
-
printDaemonTip();
|
|
2253
|
-
return;
|
|
2254
|
-
}
|
|
2255
|
-
if (anythingChanged) {
|
|
2256
|
-
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
2257
|
-
console.log(chalk3.gray(" Restart Gemini CLI for changes to take effect."));
|
|
2258
|
-
printDaemonTip();
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
|
-
async function setupCursor() {
|
|
2262
|
-
const homeDir2 = os2.homedir();
|
|
2263
|
-
const mcpPath = path4.join(homeDir2, ".cursor", "mcp.json");
|
|
2264
|
-
const hooksPath = path4.join(homeDir2, ".cursor", "hooks.json");
|
|
2265
|
-
const mcpConfig = readJson(mcpPath) ?? {};
|
|
2266
|
-
const hooksFile = readJson(hooksPath) ?? { version: 1 };
|
|
2267
|
-
const servers = mcpConfig.mcpServers ?? {};
|
|
2268
|
-
let anythingChanged = false;
|
|
2269
|
-
if (!hooksFile.hooks) hooksFile.hooks = {};
|
|
2270
|
-
const hasPreHook = hooksFile.hooks.preToolUse?.some(
|
|
2271
|
-
(h) => h.command === "node9" && h.args?.includes("check") || h.command?.includes("cli.js")
|
|
2272
|
-
);
|
|
2273
|
-
if (!hasPreHook) {
|
|
2274
|
-
if (!hooksFile.hooks.preToolUse) hooksFile.hooks.preToolUse = [];
|
|
2275
|
-
hooksFile.hooks.preToolUse.push({ command: fullPathCommand("check") });
|
|
2276
|
-
console.log(chalk3.green(" \u2705 preToolUse hook added \u2192 node9 check"));
|
|
2277
|
-
anythingChanged = true;
|
|
2278
|
-
}
|
|
2279
|
-
const hasPostHook = hooksFile.hooks.postToolUse?.some(
|
|
2280
|
-
(h) => h.command === "node9" && h.args?.includes("log") || h.command?.includes("cli.js")
|
|
2281
|
-
);
|
|
2282
|
-
if (!hasPostHook) {
|
|
2283
|
-
if (!hooksFile.hooks.postToolUse) hooksFile.hooks.postToolUse = [];
|
|
2284
|
-
hooksFile.hooks.postToolUse.push({ command: fullPathCommand("log") });
|
|
2285
|
-
console.log(chalk3.green(" \u2705 postToolUse hook added \u2192 node9 log"));
|
|
2286
|
-
anythingChanged = true;
|
|
2287
|
-
}
|
|
2288
|
-
if (anythingChanged) {
|
|
2289
|
-
writeJson(hooksPath, hooksFile);
|
|
2290
|
-
console.log("");
|
|
2291
|
-
}
|
|
2292
|
-
const serversToWrap = [];
|
|
2293
|
-
for (const [name, server] of Object.entries(servers)) {
|
|
2294
|
-
if (!server.command || server.command === "node9") continue;
|
|
2295
|
-
const parts = [server.command, ...server.args ?? []];
|
|
2296
|
-
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
2297
|
-
}
|
|
2298
|
-
if (serversToWrap.length > 0) {
|
|
2299
|
-
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
2300
|
-
console.log(chalk3.white(` ${mcpPath}`));
|
|
2301
|
-
for (const { name, originalCmd } of serversToWrap) {
|
|
2302
|
-
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
2303
|
-
}
|
|
2304
|
-
console.log("");
|
|
2305
|
-
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
2306
|
-
if (proceed) {
|
|
2307
|
-
for (const { name, parts } of serversToWrap) {
|
|
2308
|
-
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
2309
|
-
}
|
|
2310
|
-
mcpConfig.mcpServers = servers;
|
|
2311
|
-
writeJson(mcpPath, mcpConfig);
|
|
2312
|
-
console.log(chalk3.green(`
|
|
2313
|
-
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
2314
|
-
anythingChanged = true;
|
|
2315
|
-
} else {
|
|
2316
|
-
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
2317
|
-
}
|
|
2318
|
-
console.log("");
|
|
2319
|
-
}
|
|
2320
|
-
if (!anythingChanged && serversToWrap.length === 0) {
|
|
2321
|
-
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Cursor."));
|
|
2322
|
-
printDaemonTip();
|
|
2323
|
-
return;
|
|
2324
|
-
}
|
|
2325
|
-
if (anythingChanged) {
|
|
2326
|
-
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor!"));
|
|
2327
|
-
console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
|
|
2328
|
-
printDaemonTip();
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2456
|
+
});
|
|
2331
2457
|
|
|
2332
2458
|
// src/daemon/ui.html
|
|
2333
|
-
var ui_default
|
|
2459
|
+
var ui_default;
|
|
2460
|
+
var init_ui = __esm({
|
|
2461
|
+
"src/daemon/ui.html"() {
|
|
2462
|
+
ui_default = `<!doctype html>
|
|
2334
2463
|
<html lang="en">
|
|
2335
2464
|
<head>
|
|
2336
2465
|
<meta charset="UTF-8" />
|
|
@@ -2356,6 +2485,11 @@ var ui_default = `<!doctype html>
|
|
|
2356
2485
|
margin: 0;
|
|
2357
2486
|
padding: 0;
|
|
2358
2487
|
}
|
|
2488
|
+
html,
|
|
2489
|
+
body {
|
|
2490
|
+
height: 100%;
|
|
2491
|
+
overflow: hidden;
|
|
2492
|
+
}
|
|
2359
2493
|
body {
|
|
2360
2494
|
background: var(--bg);
|
|
2361
2495
|
color: var(--text);
|
|
@@ -2363,16 +2497,17 @@ var ui_default = `<!doctype html>
|
|
|
2363
2497
|
'Inter',
|
|
2364
2498
|
-apple-system,
|
|
2365
2499
|
sans-serif;
|
|
2366
|
-
min-height: 100vh;
|
|
2367
2500
|
}
|
|
2368
2501
|
|
|
2369
2502
|
.shell {
|
|
2370
|
-
max-width:
|
|
2503
|
+
max-width: 1440px;
|
|
2504
|
+
height: 100vh;
|
|
2371
2505
|
margin: 0 auto;
|
|
2372
|
-
padding:
|
|
2506
|
+
padding: 16px 20px 16px;
|
|
2373
2507
|
display: grid;
|
|
2374
2508
|
grid-template-rows: auto 1fr;
|
|
2375
|
-
gap:
|
|
2509
|
+
gap: 16px;
|
|
2510
|
+
overflow: hidden;
|
|
2376
2511
|
}
|
|
2377
2512
|
header {
|
|
2378
2513
|
display: flex;
|
|
@@ -2409,9 +2544,10 @@ var ui_default = `<!doctype html>
|
|
|
2409
2544
|
|
|
2410
2545
|
.body {
|
|
2411
2546
|
display: grid;
|
|
2412
|
-
grid-template-columns: 1fr
|
|
2413
|
-
gap:
|
|
2414
|
-
|
|
2547
|
+
grid-template-columns: 360px 1fr 270px;
|
|
2548
|
+
gap: 16px;
|
|
2549
|
+
min-height: 0;
|
|
2550
|
+
overflow: hidden;
|
|
2415
2551
|
}
|
|
2416
2552
|
|
|
2417
2553
|
.warning-banner {
|
|
@@ -2431,6 +2567,10 @@ var ui_default = `<!doctype html>
|
|
|
2431
2567
|
|
|
2432
2568
|
.main {
|
|
2433
2569
|
min-width: 0;
|
|
2570
|
+
min-height: 0;
|
|
2571
|
+
overflow-y: auto;
|
|
2572
|
+
scrollbar-width: thin;
|
|
2573
|
+
scrollbar-color: var(--border) transparent;
|
|
2434
2574
|
}
|
|
2435
2575
|
.section-title {
|
|
2436
2576
|
font-size: 11px;
|
|
@@ -2461,14 +2601,64 @@ var ui_default = `<!doctype html>
|
|
|
2461
2601
|
background: var(--card);
|
|
2462
2602
|
border: 1px solid var(--border);
|
|
2463
2603
|
border-radius: 14px;
|
|
2464
|
-
padding:
|
|
2465
|
-
margin-bottom:
|
|
2604
|
+
padding: 20px;
|
|
2605
|
+
margin-bottom: 14px;
|
|
2466
2606
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
2467
2607
|
animation: pop 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
2468
2608
|
}
|
|
2469
2609
|
.card.slack-viewer {
|
|
2470
2610
|
border-color: rgba(83, 155, 245, 0.3);
|
|
2471
2611
|
}
|
|
2612
|
+
.card-header {
|
|
2613
|
+
display: flex;
|
|
2614
|
+
align-items: center;
|
|
2615
|
+
gap: 8px;
|
|
2616
|
+
margin-bottom: 12px;
|
|
2617
|
+
padding-bottom: 12px;
|
|
2618
|
+
border-bottom: 1px solid var(--border);
|
|
2619
|
+
}
|
|
2620
|
+
.card-header-icon {
|
|
2621
|
+
font-size: 16px;
|
|
2622
|
+
}
|
|
2623
|
+
.card-header-title {
|
|
2624
|
+
font-size: 12px;
|
|
2625
|
+
font-weight: 700;
|
|
2626
|
+
color: var(--text-bright);
|
|
2627
|
+
text-transform: uppercase;
|
|
2628
|
+
letter-spacing: 0.5px;
|
|
2629
|
+
}
|
|
2630
|
+
.card-timer {
|
|
2631
|
+
margin-left: auto;
|
|
2632
|
+
font-size: 11px;
|
|
2633
|
+
font-family: 'Fira Code', monospace;
|
|
2634
|
+
color: var(--muted);
|
|
2635
|
+
background: rgba(48, 54, 61, 0.6);
|
|
2636
|
+
padding: 2px 8px;
|
|
2637
|
+
border-radius: 5px;
|
|
2638
|
+
}
|
|
2639
|
+
.card-timer.urgent {
|
|
2640
|
+
color: var(--danger);
|
|
2641
|
+
background: rgba(201, 60, 55, 0.1);
|
|
2642
|
+
}
|
|
2643
|
+
.btn-allow {
|
|
2644
|
+
background: var(--success);
|
|
2645
|
+
color: #fff;
|
|
2646
|
+
grid-column: span 2;
|
|
2647
|
+
font-size: 14px;
|
|
2648
|
+
padding: 13px 14px;
|
|
2649
|
+
}
|
|
2650
|
+
.btn-deny {
|
|
2651
|
+
background: rgba(201, 60, 55, 0.15);
|
|
2652
|
+
color: #e5534b;
|
|
2653
|
+
border: 1px solid rgba(201, 60, 55, 0.3);
|
|
2654
|
+
grid-column: span 2;
|
|
2655
|
+
}
|
|
2656
|
+
.btn-deny:hover:not(:disabled) {
|
|
2657
|
+
background: var(--danger);
|
|
2658
|
+
color: #fff;
|
|
2659
|
+
border-color: transparent;
|
|
2660
|
+
filter: none;
|
|
2661
|
+
}
|
|
2472
2662
|
@keyframes pop {
|
|
2473
2663
|
from {
|
|
2474
2664
|
opacity: 0;
|
|
@@ -2676,24 +2866,178 @@ var ui_default = `<!doctype html>
|
|
|
2676
2866
|
cursor: not-allowed;
|
|
2677
2867
|
}
|
|
2678
2868
|
|
|
2869
|
+
.flight-col {
|
|
2870
|
+
display: flex;
|
|
2871
|
+
flex-direction: column;
|
|
2872
|
+
min-height: 0;
|
|
2873
|
+
overflow: hidden;
|
|
2874
|
+
}
|
|
2875
|
+
.flight-panel {
|
|
2876
|
+
flex: 1;
|
|
2877
|
+
min-height: 0;
|
|
2878
|
+
display: flex;
|
|
2879
|
+
flex-direction: column;
|
|
2880
|
+
overflow: hidden;
|
|
2881
|
+
}
|
|
2679
2882
|
.sidebar {
|
|
2680
2883
|
display: flex;
|
|
2681
2884
|
flex-direction: column;
|
|
2682
2885
|
gap: 12px;
|
|
2683
|
-
|
|
2684
|
-
|
|
2886
|
+
min-height: 0;
|
|
2887
|
+
overflow-y: auto;
|
|
2888
|
+
scrollbar-width: thin;
|
|
2889
|
+
scrollbar-color: var(--border) transparent;
|
|
2685
2890
|
}
|
|
2686
2891
|
.panel {
|
|
2687
2892
|
background: var(--panel);
|
|
2688
2893
|
border: 1px solid var(--border);
|
|
2689
2894
|
border-radius: 12px;
|
|
2690
|
-
padding:
|
|
2895
|
+
padding: 14px;
|
|
2896
|
+
}
|
|
2897
|
+
/* \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2898
|
+
#activity-feed {
|
|
2899
|
+
display: flex;
|
|
2900
|
+
flex-direction: column;
|
|
2901
|
+
gap: 4px;
|
|
2902
|
+
margin-top: 4px;
|
|
2903
|
+
flex: 1;
|
|
2904
|
+
min-height: 0;
|
|
2905
|
+
overflow-y: auto;
|
|
2906
|
+
scrollbar-width: thin;
|
|
2907
|
+
scrollbar-color: var(--border) transparent;
|
|
2908
|
+
}
|
|
2909
|
+
.feed-row {
|
|
2910
|
+
display: grid;
|
|
2911
|
+
grid-template-columns: 58px 20px 1fr 48px;
|
|
2912
|
+
align-items: start;
|
|
2913
|
+
gap: 6px;
|
|
2914
|
+
background: rgba(22, 27, 34, 0.6);
|
|
2915
|
+
border: 1px solid var(--border);
|
|
2916
|
+
padding: 7px 10px;
|
|
2917
|
+
border-radius: 7px;
|
|
2918
|
+
font-size: 11px;
|
|
2919
|
+
animation: frSlideIn 0.15s ease-out;
|
|
2920
|
+
transition: background 0.1s;
|
|
2921
|
+
cursor: default;
|
|
2922
|
+
}
|
|
2923
|
+
.feed-row:hover {
|
|
2924
|
+
background: rgba(30, 38, 48, 0.9);
|
|
2925
|
+
border-color: rgba(83, 155, 245, 0.2);
|
|
2926
|
+
}
|
|
2927
|
+
@keyframes frSlideIn {
|
|
2928
|
+
from {
|
|
2929
|
+
opacity: 0;
|
|
2930
|
+
transform: translateX(-4px);
|
|
2931
|
+
}
|
|
2932
|
+
to {
|
|
2933
|
+
opacity: 1;
|
|
2934
|
+
transform: none;
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
.feed-ts {
|
|
2938
|
+
color: var(--muted);
|
|
2939
|
+
font-family: monospace;
|
|
2940
|
+
font-size: 9px;
|
|
2941
|
+
}
|
|
2942
|
+
.feed-icon {
|
|
2943
|
+
text-align: center;
|
|
2944
|
+
font-size: 13px;
|
|
2945
|
+
}
|
|
2946
|
+
.feed-content {
|
|
2947
|
+
min-width: 0;
|
|
2948
|
+
color: var(--text-bright);
|
|
2949
|
+
word-break: break-all;
|
|
2950
|
+
}
|
|
2951
|
+
.feed-args {
|
|
2952
|
+
display: block;
|
|
2953
|
+
color: var(--muted);
|
|
2954
|
+
font-family: monospace;
|
|
2955
|
+
margin-top: 2px;
|
|
2956
|
+
font-size: 10px;
|
|
2957
|
+
word-break: break-all;
|
|
2958
|
+
}
|
|
2959
|
+
.feed-badge {
|
|
2960
|
+
text-align: right;
|
|
2961
|
+
font-weight: 700;
|
|
2962
|
+
font-size: 9px;
|
|
2963
|
+
letter-spacing: 0.03em;
|
|
2964
|
+
}
|
|
2965
|
+
.fr-pending {
|
|
2966
|
+
color: var(--muted);
|
|
2967
|
+
}
|
|
2968
|
+
.fr-allow {
|
|
2969
|
+
color: #57ab5a;
|
|
2970
|
+
}
|
|
2971
|
+
.fr-block {
|
|
2972
|
+
color: var(--danger);
|
|
2973
|
+
}
|
|
2974
|
+
.fr-dlp {
|
|
2975
|
+
color: var(--primary);
|
|
2976
|
+
animation: frBlink 1s infinite;
|
|
2977
|
+
}
|
|
2978
|
+
@keyframes frBlink {
|
|
2979
|
+
50% {
|
|
2980
|
+
opacity: 0.4;
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
.fr-dlp-row {
|
|
2984
|
+
border-color: var(--primary) !important;
|
|
2985
|
+
}
|
|
2986
|
+
.feed-clear-btn {
|
|
2987
|
+
background: transparent;
|
|
2988
|
+
border: none;
|
|
2989
|
+
color: var(--muted);
|
|
2990
|
+
font-size: 10px;
|
|
2991
|
+
padding: 0;
|
|
2992
|
+
cursor: pointer;
|
|
2993
|
+
margin-left: auto;
|
|
2994
|
+
font-family: inherit;
|
|
2995
|
+
font-weight: 500;
|
|
2996
|
+
transition: color 0.15s;
|
|
2997
|
+
}
|
|
2998
|
+
.feed-clear-btn:hover {
|
|
2999
|
+
color: var(--text);
|
|
3000
|
+
filter: none;
|
|
3001
|
+
transform: none;
|
|
3002
|
+
}
|
|
3003
|
+
/* \u2500\u2500 Shields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
3004
|
+
.shield-row {
|
|
3005
|
+
display: flex;
|
|
3006
|
+
align-items: flex-start;
|
|
3007
|
+
gap: 10px;
|
|
3008
|
+
padding: 8px 0;
|
|
3009
|
+
border-bottom: 1px solid var(--border);
|
|
3010
|
+
}
|
|
3011
|
+
.shield-row:last-child {
|
|
3012
|
+
border-bottom: none;
|
|
3013
|
+
padding-bottom: 0;
|
|
2691
3014
|
}
|
|
3015
|
+
.shield-row:first-child {
|
|
3016
|
+
padding-top: 0;
|
|
3017
|
+
}
|
|
3018
|
+
.shield-info {
|
|
3019
|
+
flex: 1;
|
|
3020
|
+
min-width: 0;
|
|
3021
|
+
}
|
|
3022
|
+
.shield-name {
|
|
3023
|
+
font-size: 12px;
|
|
3024
|
+
color: var(--text-bright);
|
|
3025
|
+
font-weight: 600;
|
|
3026
|
+
font-family: 'Fira Code', monospace;
|
|
3027
|
+
}
|
|
3028
|
+
.shield-desc {
|
|
3029
|
+
font-size: 10px;
|
|
3030
|
+
color: var(--muted);
|
|
3031
|
+
margin-top: 2px;
|
|
3032
|
+
line-height: 1.4;
|
|
3033
|
+
}
|
|
3034
|
+
|
|
2692
3035
|
.panel-title {
|
|
2693
3036
|
font-size: 12px;
|
|
2694
3037
|
font-weight: 700;
|
|
2695
3038
|
color: var(--text-bright);
|
|
2696
3039
|
margin-bottom: 12px;
|
|
3040
|
+
flex-shrink: 0;
|
|
2697
3041
|
display: flex;
|
|
2698
3042
|
align-items: center;
|
|
2699
3043
|
gap: 6px;
|
|
@@ -2701,8 +3045,8 @@ var ui_default = `<!doctype html>
|
|
|
2701
3045
|
.setting-row {
|
|
2702
3046
|
display: flex;
|
|
2703
3047
|
align-items: flex-start;
|
|
2704
|
-
gap:
|
|
2705
|
-
margin-bottom:
|
|
3048
|
+
gap: 10px;
|
|
3049
|
+
margin-bottom: 8px;
|
|
2706
3050
|
}
|
|
2707
3051
|
.setting-row:last-child {
|
|
2708
3052
|
margin-bottom: 0;
|
|
@@ -2711,20 +3055,21 @@ var ui_default = `<!doctype html>
|
|
|
2711
3055
|
flex: 1;
|
|
2712
3056
|
}
|
|
2713
3057
|
.setting-label {
|
|
2714
|
-
font-size:
|
|
3058
|
+
font-size: 11px;
|
|
2715
3059
|
color: var(--text-bright);
|
|
2716
|
-
margin-bottom:
|
|
3060
|
+
margin-bottom: 2px;
|
|
3061
|
+
font-weight: 600;
|
|
2717
3062
|
}
|
|
2718
3063
|
.setting-desc {
|
|
2719
|
-
font-size:
|
|
3064
|
+
font-size: 10px;
|
|
2720
3065
|
color: var(--muted);
|
|
2721
|
-
line-height: 1.
|
|
3066
|
+
line-height: 1.4;
|
|
2722
3067
|
}
|
|
2723
3068
|
.toggle {
|
|
2724
3069
|
position: relative;
|
|
2725
3070
|
display: inline-block;
|
|
2726
|
-
width:
|
|
2727
|
-
height:
|
|
3071
|
+
width: 34px;
|
|
3072
|
+
height: 19px;
|
|
2728
3073
|
flex-shrink: 0;
|
|
2729
3074
|
margin-top: 1px;
|
|
2730
3075
|
}
|
|
@@ -2744,8 +3089,8 @@ var ui_default = `<!doctype html>
|
|
|
2744
3089
|
.slider:before {
|
|
2745
3090
|
content: '';
|
|
2746
3091
|
position: absolute;
|
|
2747
|
-
width:
|
|
2748
|
-
height:
|
|
3092
|
+
width: 13px;
|
|
3093
|
+
height: 13px;
|
|
2749
3094
|
left: 3px;
|
|
2750
3095
|
bottom: 3px;
|
|
2751
3096
|
background: #fff;
|
|
@@ -2756,7 +3101,7 @@ var ui_default = `<!doctype html>
|
|
|
2756
3101
|
background: var(--success);
|
|
2757
3102
|
}
|
|
2758
3103
|
input:checked + .slider:before {
|
|
2759
|
-
transform: translateX(
|
|
3104
|
+
transform: translateX(15px);
|
|
2760
3105
|
}
|
|
2761
3106
|
input:disabled + .slider {
|
|
2762
3107
|
opacity: 0.4;
|
|
@@ -2915,12 +3260,17 @@ var ui_default = `<!doctype html>
|
|
|
2915
3260
|
border: 1px solid var(--border);
|
|
2916
3261
|
}
|
|
2917
3262
|
|
|
2918
|
-
@media (max-width:
|
|
3263
|
+
@media (max-width: 960px) {
|
|
2919
3264
|
.body {
|
|
2920
|
-
grid-template-columns: 1fr;
|
|
3265
|
+
grid-template-columns: 1fr 220px;
|
|
3266
|
+
}
|
|
3267
|
+
.flight-col {
|
|
3268
|
+
display: none;
|
|
2921
3269
|
}
|
|
2922
|
-
|
|
2923
|
-
|
|
3270
|
+
}
|
|
3271
|
+
@media (max-width: 640px) {
|
|
3272
|
+
.body {
|
|
3273
|
+
grid-template-columns: 1fr;
|
|
2924
3274
|
}
|
|
2925
3275
|
}
|
|
2926
3276
|
</style>
|
|
@@ -2934,6 +3284,19 @@ var ui_default = `<!doctype html>
|
|
|
2934
3284
|
</header>
|
|
2935
3285
|
|
|
2936
3286
|
<div class="body">
|
|
3287
|
+
<div class="flight-col">
|
|
3288
|
+
<div class="panel flight-panel">
|
|
3289
|
+
<div class="panel-title">
|
|
3290
|
+
\u{1F6F0}\uFE0F Flight Recorder
|
|
3291
|
+
<span style="font-weight: 400; color: var(--muted); font-size: 11px">live</span>
|
|
3292
|
+
<button class="feed-clear-btn" onclick="clearFeed()">clear</button>
|
|
3293
|
+
</div>
|
|
3294
|
+
<div id="activity-feed">
|
|
3295
|
+
<span class="decisions-empty">Waiting for agent activity\u2026</span>
|
|
3296
|
+
</div>
|
|
3297
|
+
</div>
|
|
3298
|
+
</div>
|
|
3299
|
+
|
|
2937
3300
|
<div class="main">
|
|
2938
3301
|
<div id="warnBanner" class="warning-banner">
|
|
2939
3302
|
\u26A0\uFE0F Auto-start is off \u2014 daemon started manually. Run "node9 daemon stop" to stop it, or
|
|
@@ -3014,6 +3377,11 @@ var ui_default = `<!doctype html>
|
|
|
3014
3377
|
<div id="slackStatusLine" class="slack-status-line">No key saved</div>
|
|
3015
3378
|
</div>
|
|
3016
3379
|
|
|
3380
|
+
<div class="panel">
|
|
3381
|
+
<div class="panel-title">\u{1F6E1}\uFE0F Active Shields</div>
|
|
3382
|
+
<div id="shieldsList"><span class="decisions-empty">Loading\u2026</span></div>
|
|
3383
|
+
</div>
|
|
3384
|
+
|
|
3017
3385
|
<div class="panel">
|
|
3018
3386
|
<div class="panel-title">\u{1F4CB} Persistent Decisions</div>
|
|
3019
3387
|
<div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
|
|
@@ -3059,14 +3427,23 @@ var ui_default = `<!doctype html>
|
|
|
3059
3427
|
|
|
3060
3428
|
function updateDenyButton(id, timestamp) {
|
|
3061
3429
|
const btn = document.querySelector('#c-' + id + ' .btn-deny');
|
|
3430
|
+
const timer = document.querySelector('#timer-' + id);
|
|
3062
3431
|
if (!btn) return;
|
|
3063
3432
|
const elapsed = Date.now() - timestamp;
|
|
3064
3433
|
const remaining = Math.max(0, Math.ceil((autoDenyMs - elapsed) / 1000));
|
|
3065
3434
|
if (remaining <= 0) {
|
|
3066
|
-
btn.textContent = 'Auto-Denying
|
|
3435
|
+
btn.textContent = '\u23F3 Auto-Denying\u2026';
|
|
3067
3436
|
btn.disabled = true;
|
|
3437
|
+
if (timer) {
|
|
3438
|
+
timer.textContent = 'auto-deny';
|
|
3439
|
+
timer.className = 'card-timer urgent';
|
|
3440
|
+
}
|
|
3068
3441
|
} else {
|
|
3069
|
-
btn.textContent = 'Block Action
|
|
3442
|
+
btn.textContent = '\u{1F6AB} Block this Action';
|
|
3443
|
+
if (timer) {
|
|
3444
|
+
timer.textContent = remaining + 's';
|
|
3445
|
+
timer.className = 'card-timer' + (remaining < 15 ? ' urgent' : '');
|
|
3446
|
+
}
|
|
3070
3447
|
setTimeout(() => updateDenyButton(id, timestamp), 1000);
|
|
3071
3448
|
}
|
|
3072
3449
|
}
|
|
@@ -3082,34 +3459,61 @@ var ui_default = `<!doctype html>
|
|
|
3082
3459
|
empty.style.display = requests.size === 0 ? 'block' : 'none';
|
|
3083
3460
|
}
|
|
3084
3461
|
|
|
3085
|
-
function
|
|
3462
|
+
function setCardBusy(card, busy) {
|
|
3463
|
+
if (!card) return;
|
|
3464
|
+
card.querySelectorAll('button').forEach((b) => (b.disabled = busy));
|
|
3465
|
+
card.style.opacity = busy ? '0.5' : '1';
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
function showCardError(card, msg) {
|
|
3469
|
+
if (!card) return;
|
|
3470
|
+
card.style.outline = '2px solid #f87171';
|
|
3471
|
+
let err = card.querySelector('.card-error');
|
|
3472
|
+
if (!err) {
|
|
3473
|
+
err = document.createElement('p');
|
|
3474
|
+
err.className = 'card-error';
|
|
3475
|
+
err.style.cssText = 'color:#f87171;font-size:11px;margin:6px 0 0;';
|
|
3476
|
+
card.appendChild(err);
|
|
3477
|
+
}
|
|
3478
|
+
err.textContent = '\u26A0 ' + msg + ' \u2014 please try again or refresh.';
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
async function sendDecision(id, decision, persist) {
|
|
3086
3482
|
const card = document.getElementById('c-' + id);
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3483
|
+
setCardBusy(card, true);
|
|
3484
|
+
try {
|
|
3485
|
+
const res = await fetch('/decision/' + id, {
|
|
3486
|
+
method: 'POST',
|
|
3487
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3488
|
+
body: JSON.stringify({ decision, persist: !!persist }),
|
|
3489
|
+
});
|
|
3490
|
+
if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
|
|
3094
3491
|
card?.remove();
|
|
3095
3492
|
requests.delete(id);
|
|
3096
3493
|
refresh();
|
|
3097
|
-
}
|
|
3494
|
+
} catch (err) {
|
|
3495
|
+
setCardBusy(card, false);
|
|
3496
|
+
showCardError(card, err.message || 'Network error');
|
|
3497
|
+
}
|
|
3098
3498
|
}
|
|
3099
3499
|
|
|
3100
|
-
function sendTrust(id, duration) {
|
|
3500
|
+
async function sendTrust(id, duration) {
|
|
3101
3501
|
const card = document.getElementById('c-' + id);
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3502
|
+
setCardBusy(card, true);
|
|
3503
|
+
try {
|
|
3504
|
+
const res = await fetch('/decision/' + id, {
|
|
3505
|
+
method: 'POST',
|
|
3506
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3507
|
+
body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
|
|
3508
|
+
});
|
|
3509
|
+
if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
|
|
3109
3510
|
card?.remove();
|
|
3110
3511
|
requests.delete(id);
|
|
3111
3512
|
refresh();
|
|
3112
|
-
}
|
|
3513
|
+
} catch (err) {
|
|
3514
|
+
setCardBusy(card, false);
|
|
3515
|
+
showCardError(card, err.message || 'Network error');
|
|
3516
|
+
}
|
|
3113
3517
|
}
|
|
3114
3518
|
|
|
3115
3519
|
function renderPayload(req) {
|
|
@@ -3160,16 +3564,21 @@ var ui_default = `<!doctype html>
|
|
|
3160
3564
|
const mcpLabel = req.mcpServer ? esc(req.mcpServer) : null;
|
|
3161
3565
|
const dis = isSlack ? 'disabled' : '';
|
|
3162
3566
|
card.innerHTML = \`
|
|
3567
|
+
<div class="card-header">
|
|
3568
|
+
<span class="card-header-icon">\${isSlack ? '\u26A1' : '\u26A0\uFE0F'}</span>
|
|
3569
|
+
<span class="card-header-title">\${isSlack ? 'Awaiting Cloud Approval' : 'Action Required'}</span>
|
|
3570
|
+
<span class="card-timer" id="timer-\${req.id}">\${autoDenyMs > 0 ? Math.ceil(autoDenyMs / 1000) + 's' : ''}</span>
|
|
3571
|
+
</div>
|
|
3163
3572
|
<div class="source-row">
|
|
3164
3573
|
<span class="agent-badge">\${agentLabel}</span>
|
|
3165
3574
|
\${mcpLabel ? \`<span class="source-arrow">\u2192</span><span class="mcp-badge">mcp::\${mcpLabel}</span>\` : ''}
|
|
3166
3575
|
</div>
|
|
3167
3576
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
3168
|
-
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting
|
|
3577
|
+
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
|
|
3169
3578
|
\${renderPayload(req)}
|
|
3170
3579
|
<div class="actions" id="act-\${req.id}">
|
|
3171
|
-
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}
|
|
3172
|
-
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}
|
|
3580
|
+
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
|
|
3581
|
+
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>\u{1F6AB} Block this Action</button>
|
|
3173
3582
|
<div class="trust-row\${trustEnabled ? ' show' : ''}" id="tr-\${req.id}">
|
|
3174
3583
|
<button class="btn-trust" onclick="sendTrust('\${req.id}','30m')" \${dis}>\u23F1 Trust 30m</button>
|
|
3175
3584
|
<button class="btn-trust" onclick="sendTrust('\${req.id}','1h')" \${dis}>\u23F1 Trust 1h</button>
|
|
@@ -3229,11 +3638,86 @@ var ui_default = `<!doctype html>
|
|
|
3229
3638
|
ev.addEventListener('slack-status', (e) => {
|
|
3230
3639
|
applySlackStatus(JSON.parse(e.data));
|
|
3231
3640
|
});
|
|
3232
|
-
|
|
3233
|
-
|
|
3641
|
+
ev.addEventListener('shields-status', (e) => {
|
|
3642
|
+
renderShields(JSON.parse(e.data).shields);
|
|
3643
|
+
});
|
|
3234
3644
|
|
|
3235
|
-
|
|
3236
|
-
|
|
3645
|
+
// \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3646
|
+
ev.addEventListener('activity', (e) => {
|
|
3647
|
+
const data = JSON.parse(e.data);
|
|
3648
|
+
const feed = document.getElementById('activity-feed');
|
|
3649
|
+
// Remove placeholder on first item
|
|
3650
|
+
const placeholder = feed.querySelector('.decisions-empty');
|
|
3651
|
+
if (placeholder) placeholder.remove();
|
|
3652
|
+
|
|
3653
|
+
const time = new Date(data.ts).toLocaleTimeString([], {
|
|
3654
|
+
hour12: false,
|
|
3655
|
+
hour: '2-digit',
|
|
3656
|
+
minute: '2-digit',
|
|
3657
|
+
second: '2-digit',
|
|
3658
|
+
});
|
|
3659
|
+
const icon = frIcon(data.tool);
|
|
3660
|
+
const argsStr = JSON.stringify(data.args ?? {});
|
|
3661
|
+
const argsPreview = esc(argsStr.length > 120 ? argsStr.slice(0, 120) + '\u2026' : argsStr);
|
|
3662
|
+
|
|
3663
|
+
const row = document.createElement('div');
|
|
3664
|
+
row.className = 'feed-row';
|
|
3665
|
+
row.id = 'fr-' + data.id;
|
|
3666
|
+
row.innerHTML = \`
|
|
3667
|
+
<span class="feed-ts">\${time}</span>
|
|
3668
|
+
<span class="feed-icon">\${icon}</span>
|
|
3669
|
+
<span class="feed-content"><strong>\${esc(data.tool)}</strong><span class="feed-args">\${argsPreview}</span></span>
|
|
3670
|
+
<span class="feed-badge fr-pending">\u25CF</span>
|
|
3671
|
+
\`;
|
|
3672
|
+
feed.prepend(row);
|
|
3673
|
+
if (feed.children.length > 100) feed.lastChild.remove();
|
|
3674
|
+
});
|
|
3675
|
+
|
|
3676
|
+
ev.addEventListener('activity-result', (e) => {
|
|
3677
|
+
const { id, status, label } = JSON.parse(e.data);
|
|
3678
|
+
const row = document.getElementById('fr-' + id);
|
|
3679
|
+
if (!row) return;
|
|
3680
|
+
const badge = row.querySelector('.feed-badge');
|
|
3681
|
+
if (status === 'allow') {
|
|
3682
|
+
badge.textContent = 'ALLOW';
|
|
3683
|
+
badge.className = 'feed-badge fr-allow';
|
|
3684
|
+
} else if (status === 'dlp') {
|
|
3685
|
+
badge.textContent = '\u{1F6E1}\uFE0F DLP';
|
|
3686
|
+
badge.className = 'feed-badge fr-dlp';
|
|
3687
|
+
row.classList.add('fr-dlp-row');
|
|
3688
|
+
} else {
|
|
3689
|
+
badge.textContent = 'BLOCK';
|
|
3690
|
+
badge.className = 'feed-badge fr-block';
|
|
3691
|
+
}
|
|
3692
|
+
});
|
|
3693
|
+
}
|
|
3694
|
+
connect();
|
|
3695
|
+
|
|
3696
|
+
const FR_ICONS = {
|
|
3697
|
+
bash: '\u{1F4BB}',
|
|
3698
|
+
read: '\u{1F4D6}',
|
|
3699
|
+
edit: '\u270F\uFE0F',
|
|
3700
|
+
write: '\u270F\uFE0F',
|
|
3701
|
+
glob: '\u{1F4C2}',
|
|
3702
|
+
grep: '\u{1F50D}',
|
|
3703
|
+
agent: '\u{1F916}',
|
|
3704
|
+
search: '\u{1F50D}',
|
|
3705
|
+
sql: '\u{1F5C4}\uFE0F',
|
|
3706
|
+
query: '\u{1F5C4}\uFE0F',
|
|
3707
|
+
list: '\u{1F4C2}',
|
|
3708
|
+
delete: '\u{1F5D1}\uFE0F',
|
|
3709
|
+
web: '\u{1F310}',
|
|
3710
|
+
};
|
|
3711
|
+
function frIcon(tool) {
|
|
3712
|
+
const t = (tool || '').toLowerCase();
|
|
3713
|
+
for (const [k, v] of Object.entries(FR_ICONS)) {
|
|
3714
|
+
if (t.includes(k)) return v;
|
|
3715
|
+
}
|
|
3716
|
+
return '\u{1F6E0}\uFE0F';
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
function saveSetting(key, value) {
|
|
3720
|
+
fetch('/settings', {
|
|
3237
3721
|
method: 'POST',
|
|
3238
3722
|
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3239
3723
|
body: JSON.stringify({ [key]: value }),
|
|
@@ -3321,6 +3805,49 @@ var ui_default = `<!doctype html>
|
|
|
3321
3805
|
}
|
|
3322
3806
|
}
|
|
3323
3807
|
|
|
3808
|
+
function clearFeed() {
|
|
3809
|
+
const feed = document.getElementById('activity-feed');
|
|
3810
|
+
feed.innerHTML = '<span class="decisions-empty">Feed cleared.</span>';
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
function renderShields(shields) {
|
|
3814
|
+
const list = document.getElementById('shieldsList');
|
|
3815
|
+
if (!shields || shields.length === 0) {
|
|
3816
|
+
list.innerHTML = '<span class="decisions-empty">No shields available.</span>';
|
|
3817
|
+
return;
|
|
3818
|
+
}
|
|
3819
|
+
list.innerHTML = shields
|
|
3820
|
+
.map(
|
|
3821
|
+
(s) => \`
|
|
3822
|
+
<div class="shield-row">
|
|
3823
|
+
<div class="shield-info">
|
|
3824
|
+
<div class="shield-name">\${esc(s.name)}</div>
|
|
3825
|
+
<div class="shield-desc">\${esc(s.description)}</div>
|
|
3826
|
+
</div>
|
|
3827
|
+
<label class="toggle">
|
|
3828
|
+
<input type="checkbox" \${s.active ? 'checked' : ''}
|
|
3829
|
+
onchange="toggleShield('\${esc(s.name)}', this.checked)" />
|
|
3830
|
+
<span class="slider"></span>
|
|
3831
|
+
</label>
|
|
3832
|
+
</div>
|
|
3833
|
+
\`
|
|
3834
|
+
)
|
|
3835
|
+
.join('');
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
function toggleShield(name, active) {
|
|
3839
|
+
fetch('/shields', {
|
|
3840
|
+
method: 'POST',
|
|
3841
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3842
|
+
body: JSON.stringify({ name, active }),
|
|
3843
|
+
}).catch(() => {});
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3846
|
+
fetch('/shields')
|
|
3847
|
+
.then((r) => r.json())
|
|
3848
|
+
.then(({ shields }) => renderShields(shields))
|
|
3849
|
+
.catch(() => {});
|
|
3850
|
+
|
|
3324
3851
|
function renderDecisions(decisions) {
|
|
3325
3852
|
const dl = document.getElementById('decisionsList');
|
|
3326
3853
|
const entries = Object.entries(decisions);
|
|
@@ -3367,40 +3894,41 @@ var ui_default = `<!doctype html>
|
|
|
3367
3894
|
</body>
|
|
3368
3895
|
</html>
|
|
3369
3896
|
`;
|
|
3897
|
+
}
|
|
3898
|
+
});
|
|
3370
3899
|
|
|
3371
3900
|
// src/daemon/ui.ts
|
|
3372
|
-
var UI_HTML_TEMPLATE
|
|
3901
|
+
var UI_HTML_TEMPLATE;
|
|
3902
|
+
var init_ui2 = __esm({
|
|
3903
|
+
"src/daemon/ui.ts"() {
|
|
3904
|
+
"use strict";
|
|
3905
|
+
init_ui();
|
|
3906
|
+
UI_HTML_TEMPLATE = ui_default;
|
|
3907
|
+
}
|
|
3908
|
+
});
|
|
3373
3909
|
|
|
3374
3910
|
// src/daemon/index.ts
|
|
3375
3911
|
import http from "http";
|
|
3376
|
-
import
|
|
3377
|
-
import
|
|
3378
|
-
import
|
|
3912
|
+
import net2 from "net";
|
|
3913
|
+
import fs4 from "fs";
|
|
3914
|
+
import path6 from "path";
|
|
3915
|
+
import os4 from "os";
|
|
3379
3916
|
import { spawn as spawn2 } from "child_process";
|
|
3380
|
-
import { randomUUID } from "crypto";
|
|
3917
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3381
3918
|
import chalk4 from "chalk";
|
|
3382
|
-
var DAEMON_PORT2 = 7391;
|
|
3383
|
-
var DAEMON_HOST2 = "127.0.0.1";
|
|
3384
|
-
var homeDir = os3.homedir();
|
|
3385
|
-
var DAEMON_PID_FILE = path5.join(homeDir, ".node9", "daemon.pid");
|
|
3386
|
-
var DECISIONS_FILE = path5.join(homeDir, ".node9", "decisions.json");
|
|
3387
|
-
var GLOBAL_CONFIG_FILE = path5.join(homeDir, ".node9", "config.json");
|
|
3388
|
-
var CREDENTIALS_FILE = path5.join(homeDir, ".node9", "credentials.json");
|
|
3389
|
-
var AUDIT_LOG_FILE = path5.join(homeDir, ".node9", "audit.log");
|
|
3390
|
-
var TRUST_FILE2 = path5.join(homeDir, ".node9", "trust.json");
|
|
3391
3919
|
function atomicWriteSync2(filePath, data, options) {
|
|
3392
|
-
const dir =
|
|
3393
|
-
if (!
|
|
3394
|
-
const tmpPath = `${filePath}.${
|
|
3395
|
-
|
|
3396
|
-
|
|
3920
|
+
const dir = path6.dirname(filePath);
|
|
3921
|
+
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
3922
|
+
const tmpPath = `${filePath}.${randomUUID2()}.tmp`;
|
|
3923
|
+
fs4.writeFileSync(tmpPath, data, options);
|
|
3924
|
+
fs4.renameSync(tmpPath, filePath);
|
|
3397
3925
|
}
|
|
3398
3926
|
function writeTrustEntry(toolName, durationMs) {
|
|
3399
3927
|
try {
|
|
3400
3928
|
let trust = { entries: [] };
|
|
3401
3929
|
try {
|
|
3402
|
-
if (
|
|
3403
|
-
trust = JSON.parse(
|
|
3930
|
+
if (fs4.existsSync(TRUST_FILE2))
|
|
3931
|
+
trust = JSON.parse(fs4.readFileSync(TRUST_FILE2, "utf-8"));
|
|
3404
3932
|
} catch {
|
|
3405
3933
|
}
|
|
3406
3934
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
@@ -3409,12 +3937,6 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
3409
3937
|
} catch {
|
|
3410
3938
|
}
|
|
3411
3939
|
}
|
|
3412
|
-
var TRUST_DURATIONS = {
|
|
3413
|
-
"30m": 30 * 6e4,
|
|
3414
|
-
"1h": 60 * 6e4,
|
|
3415
|
-
"2h": 2 * 60 * 6e4
|
|
3416
|
-
};
|
|
3417
|
-
var SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
3418
3940
|
function redactArgs(value) {
|
|
3419
3941
|
if (!value || typeof value !== "object") return value;
|
|
3420
3942
|
if (Array.isArray(value)) return value.map(redactArgs);
|
|
@@ -3433,41 +3955,39 @@ function appendAuditLog(data) {
|
|
|
3433
3955
|
decision: data.decision,
|
|
3434
3956
|
source: "daemon"
|
|
3435
3957
|
};
|
|
3436
|
-
const dir =
|
|
3437
|
-
if (!
|
|
3438
|
-
|
|
3958
|
+
const dir = path6.dirname(AUDIT_LOG_FILE);
|
|
3959
|
+
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
3960
|
+
fs4.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
3439
3961
|
} catch {
|
|
3440
3962
|
}
|
|
3441
3963
|
}
|
|
3442
3964
|
function getAuditHistory(limit = 20) {
|
|
3443
3965
|
try {
|
|
3444
|
-
if (!
|
|
3445
|
-
const lines =
|
|
3966
|
+
if (!fs4.existsSync(AUDIT_LOG_FILE)) return [];
|
|
3967
|
+
const lines = fs4.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
|
|
3446
3968
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
3447
3969
|
return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
|
|
3448
3970
|
} catch {
|
|
3449
3971
|
return [];
|
|
3450
3972
|
}
|
|
3451
3973
|
}
|
|
3452
|
-
var AUTO_DENY_MS = 12e4;
|
|
3453
3974
|
function getOrgName() {
|
|
3454
3975
|
try {
|
|
3455
|
-
if (
|
|
3976
|
+
if (fs4.existsSync(CREDENTIALS_FILE)) {
|
|
3456
3977
|
return "Node9 Cloud";
|
|
3457
3978
|
}
|
|
3458
3979
|
} catch {
|
|
3459
3980
|
}
|
|
3460
3981
|
return null;
|
|
3461
3982
|
}
|
|
3462
|
-
var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
3463
3983
|
function hasStoredSlackKey() {
|
|
3464
|
-
return
|
|
3984
|
+
return fs4.existsSync(CREDENTIALS_FILE);
|
|
3465
3985
|
}
|
|
3466
3986
|
function writeGlobalSetting(key, value) {
|
|
3467
3987
|
let config = {};
|
|
3468
3988
|
try {
|
|
3469
|
-
if (
|
|
3470
|
-
config = JSON.parse(
|
|
3989
|
+
if (fs4.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
3990
|
+
config = JSON.parse(fs4.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
3471
3991
|
}
|
|
3472
3992
|
} catch {
|
|
3473
3993
|
}
|
|
@@ -3475,11 +3995,6 @@ function writeGlobalSetting(key, value) {
|
|
|
3475
3995
|
config.settings[key] = value;
|
|
3476
3996
|
atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3477
3997
|
}
|
|
3478
|
-
var pending = /* @__PURE__ */ new Map();
|
|
3479
|
-
var sseClients = /* @__PURE__ */ new Set();
|
|
3480
|
-
var abandonTimer = null;
|
|
3481
|
-
var daemonServer = null;
|
|
3482
|
-
var hadBrowserClient = false;
|
|
3483
3998
|
function abandonPending() {
|
|
3484
3999
|
abandonTimer = null;
|
|
3485
4000
|
pending.forEach((entry, id) => {
|
|
@@ -3491,7 +4006,7 @@ function abandonPending() {
|
|
|
3491
4006
|
});
|
|
3492
4007
|
if (autoStarted) {
|
|
3493
4008
|
try {
|
|
3494
|
-
|
|
4009
|
+
fs4.unlinkSync(DAEMON_PID_FILE);
|
|
3495
4010
|
} catch {
|
|
3496
4011
|
}
|
|
3497
4012
|
setTimeout(() => {
|
|
@@ -3501,6 +4016,18 @@ function abandonPending() {
|
|
|
3501
4016
|
}
|
|
3502
4017
|
}
|
|
3503
4018
|
function broadcast(event, data) {
|
|
4019
|
+
if (event === "activity") {
|
|
4020
|
+
activityRing.push({ event, data });
|
|
4021
|
+
if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
|
|
4022
|
+
} else if (event === "activity-result") {
|
|
4023
|
+
const { id, status, label } = data;
|
|
4024
|
+
for (let i = activityRing.length - 1; i >= 0; i--) {
|
|
4025
|
+
if (activityRing[i].data.id === id) {
|
|
4026
|
+
Object.assign(activityRing[i].data, { status, label });
|
|
4027
|
+
break;
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
3504
4031
|
const msg = `event: ${event}
|
|
3505
4032
|
data: ${JSON.stringify(data)}
|
|
3506
4033
|
|
|
@@ -3529,8 +4056,8 @@ function readBody(req) {
|
|
|
3529
4056
|
}
|
|
3530
4057
|
function readPersistentDecisions() {
|
|
3531
4058
|
try {
|
|
3532
|
-
if (
|
|
3533
|
-
return JSON.parse(
|
|
4059
|
+
if (fs4.existsSync(DECISIONS_FILE)) {
|
|
4060
|
+
return JSON.parse(fs4.readFileSync(DECISIONS_FILE, "utf-8"));
|
|
3534
4061
|
}
|
|
3535
4062
|
} catch {
|
|
3536
4063
|
}
|
|
@@ -3546,18 +4073,20 @@ function writePersistentDecision(toolName, decision) {
|
|
|
3546
4073
|
}
|
|
3547
4074
|
}
|
|
3548
4075
|
function startDaemon() {
|
|
3549
|
-
const csrfToken =
|
|
3550
|
-
const internalToken =
|
|
4076
|
+
const csrfToken = randomUUID2();
|
|
4077
|
+
const internalToken = randomUUID2();
|
|
3551
4078
|
const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
|
|
3552
4079
|
const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
|
|
3553
4080
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
4081
|
+
const watchMode = process.env.NODE9_WATCH_MODE === "1";
|
|
3554
4082
|
let idleTimer;
|
|
3555
4083
|
function resetIdleTimer() {
|
|
4084
|
+
if (watchMode) return;
|
|
3556
4085
|
if (idleTimer) clearTimeout(idleTimer);
|
|
3557
4086
|
idleTimer = setTimeout(() => {
|
|
3558
4087
|
if (autoStarted) {
|
|
3559
4088
|
try {
|
|
3560
|
-
|
|
4089
|
+
fs4.unlinkSync(DAEMON_PID_FILE);
|
|
3561
4090
|
} catch {
|
|
3562
4091
|
}
|
|
3563
4092
|
}
|
|
@@ -3607,6 +4136,12 @@ data: ${JSON.stringify({
|
|
|
3607
4136
|
data: ${JSON.stringify(readPersistentDecisions())}
|
|
3608
4137
|
|
|
3609
4138
|
`);
|
|
4139
|
+
for (const item of activityRing) {
|
|
4140
|
+
res.write(`event: ${item.event}
|
|
4141
|
+
data: ${JSON.stringify(item.data)}
|
|
4142
|
+
|
|
4143
|
+
`);
|
|
4144
|
+
}
|
|
3610
4145
|
return req.on("close", () => {
|
|
3611
4146
|
sseClients.delete(res);
|
|
3612
4147
|
if (sseClients.size === 0 && pending.size > 0) {
|
|
@@ -3626,9 +4161,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3626
4161
|
slackDelegated = false,
|
|
3627
4162
|
agent,
|
|
3628
4163
|
mcpServer,
|
|
3629
|
-
riskMetadata
|
|
4164
|
+
riskMetadata,
|
|
4165
|
+
fromCLI = false,
|
|
4166
|
+
activityId
|
|
3630
4167
|
} = JSON.parse(body);
|
|
3631
|
-
const id =
|
|
4168
|
+
const id = fromCLI && typeof activityId === "string" && activityId || randomUUID2();
|
|
3632
4169
|
const entry = {
|
|
3633
4170
|
id,
|
|
3634
4171
|
toolName,
|
|
@@ -3659,6 +4196,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3659
4196
|
}, AUTO_DENY_MS)
|
|
3660
4197
|
};
|
|
3661
4198
|
pending.set(id, entry);
|
|
4199
|
+
if (!fromCLI) {
|
|
4200
|
+
broadcast("activity", {
|
|
4201
|
+
id,
|
|
4202
|
+
ts: entry.timestamp,
|
|
4203
|
+
tool: toolName,
|
|
4204
|
+
args: redactArgs(args),
|
|
4205
|
+
status: "pending"
|
|
4206
|
+
});
|
|
4207
|
+
}
|
|
3662
4208
|
const browserEnabled = getConfig().settings.approvers?.browser !== false;
|
|
3663
4209
|
if (browserEnabled) {
|
|
3664
4210
|
broadcast("add", {
|
|
@@ -3688,6 +4234,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3688
4234
|
const e = pending.get(id);
|
|
3689
4235
|
if (!e) return;
|
|
3690
4236
|
if (result.noApprovalMechanism) return;
|
|
4237
|
+
broadcast("activity-result", {
|
|
4238
|
+
id,
|
|
4239
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
4240
|
+
label: result.blockedByLabel
|
|
4241
|
+
});
|
|
3691
4242
|
clearTimeout(e.timer);
|
|
3692
4243
|
const decision = result.approved ? "allow" : "deny";
|
|
3693
4244
|
appendAuditLog({ toolName: e.toolName, args: e.args, decision });
|
|
@@ -3722,8 +4273,8 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3722
4273
|
const entry = pending.get(id);
|
|
3723
4274
|
if (!entry) return res.writeHead(404).end();
|
|
3724
4275
|
if (entry.earlyDecision) {
|
|
4276
|
+
clearTimeout(entry.timer);
|
|
3725
4277
|
pending.delete(id);
|
|
3726
|
-
broadcast("remove", { id });
|
|
3727
4278
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3728
4279
|
const body = { decision: entry.earlyDecision };
|
|
3729
4280
|
if (entry.earlyReason) body.reason = entry.earlyReason;
|
|
@@ -3753,10 +4304,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3753
4304
|
decision: `trust:${trustDuration}`
|
|
3754
4305
|
});
|
|
3755
4306
|
clearTimeout(entry.timer);
|
|
3756
|
-
if (entry.waiter)
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
4307
|
+
if (entry.waiter) {
|
|
4308
|
+
entry.waiter("allow");
|
|
4309
|
+
pending.delete(id);
|
|
4310
|
+
broadcast("remove", { id });
|
|
4311
|
+
} else {
|
|
4312
|
+
entry.earlyDecision = "allow";
|
|
4313
|
+
broadcast("remove", { id });
|
|
4314
|
+
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
4315
|
+
}
|
|
3760
4316
|
res.writeHead(200);
|
|
3761
4317
|
return res.end(JSON.stringify({ ok: true }));
|
|
3762
4318
|
}
|
|
@@ -3768,13 +4324,16 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3768
4324
|
decision: resolvedDecision
|
|
3769
4325
|
});
|
|
3770
4326
|
clearTimeout(entry.timer);
|
|
3771
|
-
if (entry.waiter)
|
|
3772
|
-
|
|
4327
|
+
if (entry.waiter) {
|
|
4328
|
+
entry.waiter(resolvedDecision, reason);
|
|
4329
|
+
pending.delete(id);
|
|
4330
|
+
broadcast("remove", { id });
|
|
4331
|
+
} else {
|
|
3773
4332
|
entry.earlyDecision = resolvedDecision;
|
|
3774
4333
|
entry.earlyReason = reason;
|
|
4334
|
+
broadcast("remove", { id });
|
|
4335
|
+
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
3775
4336
|
}
|
|
3776
|
-
pending.delete(id);
|
|
3777
|
-
broadcast("remove", { id });
|
|
3778
4337
|
res.writeHead(200);
|
|
3779
4338
|
return res.end(JSON.stringify({ ok: true }));
|
|
3780
4339
|
} catch {
|
|
@@ -3865,99 +4424,658 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3865
4424
|
res.writeHead(400).end();
|
|
3866
4425
|
}
|
|
3867
4426
|
}
|
|
3868
|
-
if (req.method === "GET" && pathname === "/audit") {
|
|
3869
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3870
|
-
return res.end(JSON.stringify(getAuditHistory()));
|
|
4427
|
+
if (req.method === "GET" && pathname === "/audit") {
|
|
4428
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4429
|
+
return res.end(JSON.stringify(getAuditHistory()));
|
|
4430
|
+
}
|
|
4431
|
+
if (req.method === "GET" && pathname === "/shields") {
|
|
4432
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4433
|
+
const active = readActiveShields();
|
|
4434
|
+
const shields = Object.values(SHIELDS).map((s) => ({
|
|
4435
|
+
name: s.name,
|
|
4436
|
+
description: s.description,
|
|
4437
|
+
active: active.includes(s.name)
|
|
4438
|
+
}));
|
|
4439
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4440
|
+
return res.end(JSON.stringify({ shields }));
|
|
4441
|
+
}
|
|
4442
|
+
if (req.method === "POST" && pathname === "/shields") {
|
|
4443
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4444
|
+
try {
|
|
4445
|
+
const { name, active } = JSON.parse(await readBody(req));
|
|
4446
|
+
if (!SHIELDS[name]) return res.writeHead(400).end();
|
|
4447
|
+
const current = readActiveShields();
|
|
4448
|
+
const updated = active ? [.../* @__PURE__ */ new Set([...current, name])] : current.filter((n) => n !== name);
|
|
4449
|
+
writeActiveShields(updated);
|
|
4450
|
+
_resetConfigCache();
|
|
4451
|
+
const shieldsPayload = Object.values(SHIELDS).map((s) => ({
|
|
4452
|
+
name: s.name,
|
|
4453
|
+
description: s.description,
|
|
4454
|
+
active: updated.includes(s.name)
|
|
4455
|
+
}));
|
|
4456
|
+
broadcast("shields-status", { shields: shieldsPayload });
|
|
4457
|
+
res.writeHead(200);
|
|
4458
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4459
|
+
} catch {
|
|
4460
|
+
res.writeHead(400).end();
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
res.writeHead(404).end();
|
|
4464
|
+
});
|
|
4465
|
+
daemonServer = server;
|
|
4466
|
+
server.on("error", (e) => {
|
|
4467
|
+
if (e.code === "EADDRINUSE") {
|
|
4468
|
+
try {
|
|
4469
|
+
if (fs4.existsSync(DAEMON_PID_FILE)) {
|
|
4470
|
+
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4471
|
+
process.kill(pid, 0);
|
|
4472
|
+
return process.exit(0);
|
|
4473
|
+
}
|
|
4474
|
+
} catch {
|
|
4475
|
+
try {
|
|
4476
|
+
fs4.unlinkSync(DAEMON_PID_FILE);
|
|
4477
|
+
} catch {
|
|
4478
|
+
}
|
|
4479
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
4480
|
+
return;
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
console.error(chalk4.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
|
|
4484
|
+
process.exit(1);
|
|
4485
|
+
});
|
|
4486
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
|
|
4487
|
+
atomicWriteSync2(
|
|
4488
|
+
DAEMON_PID_FILE,
|
|
4489
|
+
JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
|
|
4490
|
+
{ mode: 384 }
|
|
4491
|
+
);
|
|
4492
|
+
console.log(chalk4.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
|
|
4493
|
+
});
|
|
4494
|
+
if (watchMode) {
|
|
4495
|
+
console.log(chalk4.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
|
|
4496
|
+
}
|
|
4497
|
+
try {
|
|
4498
|
+
fs4.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4499
|
+
} catch {
|
|
4500
|
+
}
|
|
4501
|
+
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
4502
|
+
const unixServer = net2.createServer((socket) => {
|
|
4503
|
+
const chunks = [];
|
|
4504
|
+
let bytesReceived = 0;
|
|
4505
|
+
socket.on("data", (chunk) => {
|
|
4506
|
+
bytesReceived += chunk.length;
|
|
4507
|
+
if (bytesReceived > ACTIVITY_MAX_BYTES) {
|
|
4508
|
+
socket.destroy();
|
|
4509
|
+
return;
|
|
4510
|
+
}
|
|
4511
|
+
chunks.push(chunk);
|
|
4512
|
+
});
|
|
4513
|
+
socket.on("end", () => {
|
|
4514
|
+
try {
|
|
4515
|
+
const data = JSON.parse(Buffer.concat(chunks).toString());
|
|
4516
|
+
if (data.status === "pending") {
|
|
4517
|
+
broadcast("activity", {
|
|
4518
|
+
id: data.id,
|
|
4519
|
+
ts: data.ts,
|
|
4520
|
+
tool: data.tool,
|
|
4521
|
+
args: redactArgs(data.args),
|
|
4522
|
+
status: "pending"
|
|
4523
|
+
});
|
|
4524
|
+
} else {
|
|
4525
|
+
broadcast("activity-result", {
|
|
4526
|
+
id: data.id,
|
|
4527
|
+
status: data.status,
|
|
4528
|
+
label: data.label
|
|
4529
|
+
});
|
|
4530
|
+
}
|
|
4531
|
+
} catch {
|
|
4532
|
+
}
|
|
4533
|
+
});
|
|
4534
|
+
socket.on("error", () => {
|
|
4535
|
+
});
|
|
4536
|
+
});
|
|
4537
|
+
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
4538
|
+
process.on("exit", () => {
|
|
4539
|
+
try {
|
|
4540
|
+
fs4.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4541
|
+
} catch {
|
|
4542
|
+
}
|
|
4543
|
+
});
|
|
4544
|
+
}
|
|
4545
|
+
function stopDaemon() {
|
|
4546
|
+
if (!fs4.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
|
|
4547
|
+
try {
|
|
4548
|
+
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4549
|
+
process.kill(pid, "SIGTERM");
|
|
4550
|
+
console.log(chalk4.green("\u2705 Stopped."));
|
|
4551
|
+
} catch {
|
|
4552
|
+
console.log(chalk4.gray("Cleaned up stale PID file."));
|
|
4553
|
+
} finally {
|
|
4554
|
+
try {
|
|
4555
|
+
fs4.unlinkSync(DAEMON_PID_FILE);
|
|
4556
|
+
} catch {
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
function daemonStatus() {
|
|
4561
|
+
if (!fs4.existsSync(DAEMON_PID_FILE))
|
|
4562
|
+
return console.log(chalk4.yellow("Node9 daemon: not running"));
|
|
4563
|
+
try {
|
|
4564
|
+
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4565
|
+
process.kill(pid, 0);
|
|
4566
|
+
console.log(chalk4.green("Node9 daemon: running"));
|
|
4567
|
+
} catch {
|
|
4568
|
+
console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
var ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
|
|
4572
|
+
var init_daemon = __esm({
|
|
4573
|
+
"src/daemon/index.ts"() {
|
|
4574
|
+
"use strict";
|
|
4575
|
+
init_ui2();
|
|
4576
|
+
init_core();
|
|
4577
|
+
init_shields();
|
|
4578
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path6.join(os4.tmpdir(), "node9-activity.sock");
|
|
4579
|
+
DAEMON_PORT2 = 7391;
|
|
4580
|
+
DAEMON_HOST2 = "127.0.0.1";
|
|
4581
|
+
homeDir = os4.homedir();
|
|
4582
|
+
DAEMON_PID_FILE = path6.join(homeDir, ".node9", "daemon.pid");
|
|
4583
|
+
DECISIONS_FILE = path6.join(homeDir, ".node9", "decisions.json");
|
|
4584
|
+
GLOBAL_CONFIG_FILE = path6.join(homeDir, ".node9", "config.json");
|
|
4585
|
+
CREDENTIALS_FILE = path6.join(homeDir, ".node9", "credentials.json");
|
|
4586
|
+
AUDIT_LOG_FILE = path6.join(homeDir, ".node9", "audit.log");
|
|
4587
|
+
TRUST_FILE2 = path6.join(homeDir, ".node9", "trust.json");
|
|
4588
|
+
TRUST_DURATIONS = {
|
|
4589
|
+
"30m": 30 * 6e4,
|
|
4590
|
+
"1h": 60 * 6e4,
|
|
4591
|
+
"2h": 2 * 60 * 6e4
|
|
4592
|
+
};
|
|
4593
|
+
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
4594
|
+
AUTO_DENY_MS = 12e4;
|
|
4595
|
+
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
4596
|
+
pending = /* @__PURE__ */ new Map();
|
|
4597
|
+
sseClients = /* @__PURE__ */ new Set();
|
|
4598
|
+
abandonTimer = null;
|
|
4599
|
+
daemonServer = null;
|
|
4600
|
+
hadBrowserClient = false;
|
|
4601
|
+
ACTIVITY_RING_SIZE = 100;
|
|
4602
|
+
activityRing = [];
|
|
4603
|
+
}
|
|
4604
|
+
});
|
|
4605
|
+
|
|
4606
|
+
// src/tui/tail.ts
|
|
4607
|
+
var tail_exports = {};
|
|
4608
|
+
__export(tail_exports, {
|
|
4609
|
+
startTail: () => startTail
|
|
4610
|
+
});
|
|
4611
|
+
import http2 from "http";
|
|
4612
|
+
import chalk5 from "chalk";
|
|
4613
|
+
import fs6 from "fs";
|
|
4614
|
+
import os6 from "os";
|
|
4615
|
+
import path8 from "path";
|
|
4616
|
+
import readline from "readline";
|
|
4617
|
+
import { spawn as spawn3 } from "child_process";
|
|
4618
|
+
function getIcon(tool) {
|
|
4619
|
+
const t = tool.toLowerCase();
|
|
4620
|
+
for (const [k, v] of Object.entries(ICONS)) {
|
|
4621
|
+
if (t.includes(k)) return v;
|
|
4622
|
+
}
|
|
4623
|
+
return "\u{1F6E0}\uFE0F";
|
|
4624
|
+
}
|
|
4625
|
+
function formatBase(activity) {
|
|
4626
|
+
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
4627
|
+
const icon = getIcon(activity.tool);
|
|
4628
|
+
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
4629
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
4630
|
+
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
4631
|
+
return `${chalk5.gray(time)} ${icon} ${chalk5.white.bold(toolName)} ${chalk5.dim(argsPreview)}`;
|
|
4632
|
+
}
|
|
4633
|
+
function renderResult(activity, result) {
|
|
4634
|
+
const base = formatBase(activity);
|
|
4635
|
+
let status;
|
|
4636
|
+
if (result.status === "allow") {
|
|
4637
|
+
status = chalk5.green("\u2713 ALLOW");
|
|
4638
|
+
} else if (result.status === "dlp") {
|
|
4639
|
+
status = chalk5.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
4640
|
+
} else {
|
|
4641
|
+
status = chalk5.red("\u2717 BLOCK");
|
|
4642
|
+
}
|
|
4643
|
+
if (process.stdout.isTTY) {
|
|
4644
|
+
readline.clearLine(process.stdout, 0);
|
|
4645
|
+
readline.cursorTo(process.stdout, 0);
|
|
4646
|
+
}
|
|
4647
|
+
console.log(`${base} ${status}`);
|
|
4648
|
+
}
|
|
4649
|
+
function renderPending(activity) {
|
|
4650
|
+
if (!process.stdout.isTTY) return;
|
|
4651
|
+
process.stdout.write(`${formatBase(activity)} ${chalk5.yellow("\u25CF \u2026")}\r`);
|
|
4652
|
+
}
|
|
4653
|
+
async function ensureDaemon() {
|
|
4654
|
+
if (fs6.existsSync(PID_FILE)) {
|
|
4655
|
+
try {
|
|
4656
|
+
const { port } = JSON.parse(fs6.readFileSync(PID_FILE, "utf-8"));
|
|
4657
|
+
return port;
|
|
4658
|
+
} catch {
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
console.log(chalk5.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
4662
|
+
const child = spawn3(process.execPath, [process.argv[1], "daemon"], {
|
|
4663
|
+
detached: true,
|
|
4664
|
+
stdio: "ignore",
|
|
4665
|
+
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
4666
|
+
});
|
|
4667
|
+
child.unref();
|
|
4668
|
+
for (let i = 0; i < 20; i++) {
|
|
4669
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
4670
|
+
if (!fs6.existsSync(PID_FILE)) continue;
|
|
4671
|
+
try {
|
|
4672
|
+
const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
|
|
4673
|
+
signal: AbortSignal.timeout(500)
|
|
4674
|
+
});
|
|
4675
|
+
if (res.ok) {
|
|
4676
|
+
const { port } = JSON.parse(fs6.readFileSync(PID_FILE, "utf-8"));
|
|
4677
|
+
return port;
|
|
4678
|
+
}
|
|
4679
|
+
} catch {
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4682
|
+
console.error(chalk5.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
4683
|
+
process.exit(1);
|
|
4684
|
+
}
|
|
4685
|
+
async function startTail(options = {}) {
|
|
4686
|
+
const port = await ensureDaemon();
|
|
4687
|
+
const connectionTime = Date.now();
|
|
4688
|
+
const pending2 = /* @__PURE__ */ new Map();
|
|
4689
|
+
console.log(chalk5.cyan.bold(`
|
|
4690
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk5.dim(`\u2192 localhost:${port}`));
|
|
4691
|
+
if (options.history) {
|
|
4692
|
+
console.log(chalk5.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
4693
|
+
} else {
|
|
4694
|
+
console.log(
|
|
4695
|
+
chalk5.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
4696
|
+
);
|
|
4697
|
+
}
|
|
4698
|
+
process.on("SIGINT", () => {
|
|
4699
|
+
if (process.stdout.isTTY) {
|
|
4700
|
+
readline.clearLine(process.stdout, 0);
|
|
4701
|
+
readline.cursorTo(process.stdout, 0);
|
|
4702
|
+
}
|
|
4703
|
+
console.log(chalk5.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
4704
|
+
process.exit(0);
|
|
4705
|
+
});
|
|
4706
|
+
const req = http2.get(`http://127.0.0.1:${port}/events`, (res) => {
|
|
4707
|
+
if (res.statusCode !== 200) {
|
|
4708
|
+
console.error(chalk5.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
4709
|
+
process.exit(1);
|
|
4710
|
+
}
|
|
4711
|
+
let currentEvent = "";
|
|
4712
|
+
let currentData = "";
|
|
4713
|
+
res.on("error", () => {
|
|
4714
|
+
});
|
|
4715
|
+
const rl = readline.createInterface({ input: res, crlfDelay: Infinity });
|
|
4716
|
+
rl.on("error", () => {
|
|
4717
|
+
});
|
|
4718
|
+
rl.on("line", (line) => {
|
|
4719
|
+
if (line.startsWith("event:")) {
|
|
4720
|
+
currentEvent = line.slice(6).trim();
|
|
4721
|
+
} else if (line.startsWith("data:")) {
|
|
4722
|
+
currentData = line.slice(5).trim();
|
|
4723
|
+
} else if (line === "") {
|
|
4724
|
+
if (currentEvent && currentData) {
|
|
4725
|
+
handleMessage(currentEvent, currentData);
|
|
4726
|
+
}
|
|
4727
|
+
currentEvent = "";
|
|
4728
|
+
currentData = "";
|
|
4729
|
+
}
|
|
4730
|
+
});
|
|
4731
|
+
rl.on("close", () => {
|
|
4732
|
+
if (process.stdout.isTTY) {
|
|
4733
|
+
readline.clearLine(process.stdout, 0);
|
|
4734
|
+
readline.cursorTo(process.stdout, 0);
|
|
4735
|
+
}
|
|
4736
|
+
console.log(chalk5.red("\n\u274C Daemon disconnected."));
|
|
4737
|
+
process.exit(1);
|
|
4738
|
+
});
|
|
4739
|
+
});
|
|
4740
|
+
function handleMessage(event, rawData) {
|
|
4741
|
+
let data;
|
|
4742
|
+
try {
|
|
4743
|
+
data = JSON.parse(rawData);
|
|
4744
|
+
} catch {
|
|
4745
|
+
return;
|
|
4746
|
+
}
|
|
4747
|
+
if (event === "activity") {
|
|
4748
|
+
if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
|
|
4749
|
+
if (data.status && data.status !== "pending") {
|
|
4750
|
+
renderResult(data, data);
|
|
4751
|
+
return;
|
|
4752
|
+
}
|
|
4753
|
+
pending2.set(data.id, data);
|
|
4754
|
+
const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
|
|
4755
|
+
if (slowTool) renderPending(data);
|
|
4756
|
+
}
|
|
4757
|
+
if (event === "activity-result") {
|
|
4758
|
+
const original = pending2.get(data.id);
|
|
4759
|
+
if (original) {
|
|
4760
|
+
renderResult(original, data);
|
|
4761
|
+
pending2.delete(data.id);
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
req.on("error", (err) => {
|
|
4766
|
+
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
4767
|
+
console.error(chalk5.red(`
|
|
4768
|
+
\u274C ${msg}`));
|
|
4769
|
+
process.exit(1);
|
|
4770
|
+
});
|
|
4771
|
+
}
|
|
4772
|
+
var PID_FILE, ICONS;
|
|
4773
|
+
var init_tail = __esm({
|
|
4774
|
+
"src/tui/tail.ts"() {
|
|
4775
|
+
"use strict";
|
|
4776
|
+
init_daemon();
|
|
4777
|
+
PID_FILE = path8.join(os6.homedir(), ".node9", "daemon.pid");
|
|
4778
|
+
ICONS = {
|
|
4779
|
+
bash: "\u{1F4BB}",
|
|
4780
|
+
shell: "\u{1F4BB}",
|
|
4781
|
+
terminal: "\u{1F4BB}",
|
|
4782
|
+
read: "\u{1F4D6}",
|
|
4783
|
+
edit: "\u270F\uFE0F",
|
|
4784
|
+
write: "\u270F\uFE0F",
|
|
4785
|
+
glob: "\u{1F4C2}",
|
|
4786
|
+
grep: "\u{1F50D}",
|
|
4787
|
+
agent: "\u{1F916}",
|
|
4788
|
+
search: "\u{1F50D}",
|
|
4789
|
+
sql: "\u{1F5C4}\uFE0F",
|
|
4790
|
+
query: "\u{1F5C4}\uFE0F",
|
|
4791
|
+
list: "\u{1F4C2}",
|
|
4792
|
+
delete: "\u{1F5D1}\uFE0F",
|
|
4793
|
+
web: "\u{1F310}"
|
|
4794
|
+
};
|
|
4795
|
+
}
|
|
4796
|
+
});
|
|
4797
|
+
|
|
4798
|
+
// src/cli.ts
|
|
4799
|
+
init_core();
|
|
4800
|
+
import { Command } from "commander";
|
|
4801
|
+
|
|
4802
|
+
// src/setup.ts
|
|
4803
|
+
import fs3 from "fs";
|
|
4804
|
+
import path5 from "path";
|
|
4805
|
+
import os3 from "os";
|
|
4806
|
+
import chalk3 from "chalk";
|
|
4807
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
4808
|
+
function printDaemonTip() {
|
|
4809
|
+
console.log(
|
|
4810
|
+
chalk3.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk3.white("\n To view your history or manage persistent rules, run:") + chalk3.green("\n node9 daemon --openui")
|
|
4811
|
+
);
|
|
4812
|
+
}
|
|
4813
|
+
function fullPathCommand(subcommand) {
|
|
4814
|
+
if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
|
|
4815
|
+
const nodeExec = process.execPath;
|
|
4816
|
+
const cliScript = process.argv[1];
|
|
4817
|
+
return `${nodeExec} ${cliScript} ${subcommand}`;
|
|
4818
|
+
}
|
|
4819
|
+
function readJson(filePath) {
|
|
4820
|
+
try {
|
|
4821
|
+
if (fs3.existsSync(filePath)) {
|
|
4822
|
+
return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
4823
|
+
}
|
|
4824
|
+
} catch {
|
|
4825
|
+
}
|
|
4826
|
+
return null;
|
|
4827
|
+
}
|
|
4828
|
+
function writeJson(filePath, data) {
|
|
4829
|
+
const dir = path5.dirname(filePath);
|
|
4830
|
+
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
4831
|
+
fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
4832
|
+
}
|
|
4833
|
+
async function setupClaude() {
|
|
4834
|
+
const homeDir2 = os3.homedir();
|
|
4835
|
+
const mcpPath = path5.join(homeDir2, ".claude.json");
|
|
4836
|
+
const hooksPath = path5.join(homeDir2, ".claude", "settings.json");
|
|
4837
|
+
const claudeConfig = readJson(mcpPath) ?? {};
|
|
4838
|
+
const settings = readJson(hooksPath) ?? {};
|
|
4839
|
+
const servers = claudeConfig.mcpServers ?? {};
|
|
4840
|
+
let anythingChanged = false;
|
|
4841
|
+
if (!settings.hooks) settings.hooks = {};
|
|
4842
|
+
const hasPreHook = settings.hooks.PreToolUse?.some(
|
|
4843
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
4844
|
+
);
|
|
4845
|
+
if (!hasPreHook) {
|
|
4846
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
4847
|
+
settings.hooks.PreToolUse.push({
|
|
4848
|
+
matcher: ".*",
|
|
4849
|
+
hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
|
|
4850
|
+
});
|
|
4851
|
+
console.log(chalk3.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
|
|
4852
|
+
anythingChanged = true;
|
|
4853
|
+
}
|
|
4854
|
+
const hasPostHook = settings.hooks.PostToolUse?.some(
|
|
4855
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
4856
|
+
);
|
|
4857
|
+
if (!hasPostHook) {
|
|
4858
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
4859
|
+
settings.hooks.PostToolUse.push({
|
|
4860
|
+
matcher: ".*",
|
|
4861
|
+
hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
|
|
4862
|
+
});
|
|
4863
|
+
console.log(chalk3.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
|
|
4864
|
+
anythingChanged = true;
|
|
4865
|
+
}
|
|
4866
|
+
if (anythingChanged) {
|
|
4867
|
+
writeJson(hooksPath, settings);
|
|
4868
|
+
console.log("");
|
|
4869
|
+
}
|
|
4870
|
+
const serversToWrap = [];
|
|
4871
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
4872
|
+
if (!server.command || server.command === "node9") continue;
|
|
4873
|
+
const parts = [server.command, ...server.args ?? []];
|
|
4874
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
4875
|
+
}
|
|
4876
|
+
if (serversToWrap.length > 0) {
|
|
4877
|
+
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
4878
|
+
console.log(chalk3.white(` ${mcpPath}`));
|
|
4879
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
4880
|
+
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
4881
|
+
}
|
|
4882
|
+
console.log("");
|
|
4883
|
+
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
4884
|
+
if (proceed) {
|
|
4885
|
+
for (const { name, parts } of serversToWrap) {
|
|
4886
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
4887
|
+
}
|
|
4888
|
+
claudeConfig.mcpServers = servers;
|
|
4889
|
+
writeJson(mcpPath, claudeConfig);
|
|
4890
|
+
console.log(chalk3.green(`
|
|
4891
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
4892
|
+
anythingChanged = true;
|
|
4893
|
+
} else {
|
|
4894
|
+
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
3871
4895
|
}
|
|
3872
|
-
|
|
3873
|
-
}
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
4896
|
+
console.log("");
|
|
4897
|
+
}
|
|
4898
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
4899
|
+
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
4900
|
+
printDaemonTip();
|
|
4901
|
+
return;
|
|
4902
|
+
}
|
|
4903
|
+
if (anythingChanged) {
|
|
4904
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
4905
|
+
console.log(chalk3.gray(" Restart Claude Code for changes to take effect."));
|
|
4906
|
+
printDaemonTip();
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
async function setupGemini() {
|
|
4910
|
+
const homeDir2 = os3.homedir();
|
|
4911
|
+
const settingsPath = path5.join(homeDir2, ".gemini", "settings.json");
|
|
4912
|
+
const settings = readJson(settingsPath) ?? {};
|
|
4913
|
+
const servers = settings.mcpServers ?? {};
|
|
4914
|
+
let anythingChanged = false;
|
|
4915
|
+
if (!settings.hooks) settings.hooks = {};
|
|
4916
|
+
const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
|
|
4917
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
4918
|
+
);
|
|
4919
|
+
if (!hasBeforeHook) {
|
|
4920
|
+
if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
|
|
4921
|
+
if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
|
|
4922
|
+
settings.hooks.BeforeTool.push({
|
|
4923
|
+
matcher: ".*",
|
|
4924
|
+
hooks: [
|
|
4925
|
+
{
|
|
4926
|
+
name: "node9-check",
|
|
4927
|
+
type: "command",
|
|
4928
|
+
command: fullPathCommand("check"),
|
|
4929
|
+
timeout: 6e5
|
|
3887
4930
|
}
|
|
3888
|
-
|
|
3889
|
-
|
|
4931
|
+
]
|
|
4932
|
+
});
|
|
4933
|
+
console.log(chalk3.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
4934
|
+
anythingChanged = true;
|
|
4935
|
+
}
|
|
4936
|
+
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
4937
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
4938
|
+
);
|
|
4939
|
+
if (!hasAfterHook) {
|
|
4940
|
+
if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
|
|
4941
|
+
if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
|
|
4942
|
+
settings.hooks.AfterTool.push({
|
|
4943
|
+
matcher: ".*",
|
|
4944
|
+
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
4945
|
+
});
|
|
4946
|
+
console.log(chalk3.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
4947
|
+
anythingChanged = true;
|
|
4948
|
+
}
|
|
4949
|
+
if (anythingChanged) {
|
|
4950
|
+
writeJson(settingsPath, settings);
|
|
4951
|
+
console.log("");
|
|
4952
|
+
}
|
|
4953
|
+
const serversToWrap = [];
|
|
4954
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
4955
|
+
if (!server.command || server.command === "node9") continue;
|
|
4956
|
+
const parts = [server.command, ...server.args ?? []];
|
|
4957
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
4958
|
+
}
|
|
4959
|
+
if (serversToWrap.length > 0) {
|
|
4960
|
+
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
4961
|
+
console.log(chalk3.white(` ${settingsPath} (mcpServers)`));
|
|
4962
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
4963
|
+
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
4964
|
+
}
|
|
4965
|
+
console.log("");
|
|
4966
|
+
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
4967
|
+
if (proceed) {
|
|
4968
|
+
for (const { name, parts } of serversToWrap) {
|
|
4969
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
3890
4970
|
}
|
|
4971
|
+
settings.mcpServers = servers;
|
|
4972
|
+
writeJson(settingsPath, settings);
|
|
4973
|
+
console.log(chalk3.green(`
|
|
4974
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
4975
|
+
anythingChanged = true;
|
|
4976
|
+
} else {
|
|
4977
|
+
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
3891
4978
|
}
|
|
3892
|
-
console.
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
);
|
|
3901
|
-
console.log(
|
|
3902
|
-
|
|
4979
|
+
console.log("");
|
|
4980
|
+
}
|
|
4981
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
4982
|
+
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
|
|
4983
|
+
printDaemonTip();
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
if (anythingChanged) {
|
|
4987
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
4988
|
+
console.log(chalk3.gray(" Restart Gemini CLI for changes to take effect."));
|
|
4989
|
+
printDaemonTip();
|
|
4990
|
+
}
|
|
3903
4991
|
}
|
|
3904
|
-
function
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
4992
|
+
async function setupCursor() {
|
|
4993
|
+
const homeDir2 = os3.homedir();
|
|
4994
|
+
const mcpPath = path5.join(homeDir2, ".cursor", "mcp.json");
|
|
4995
|
+
const mcpConfig = readJson(mcpPath) ?? {};
|
|
4996
|
+
const servers = mcpConfig.mcpServers ?? {};
|
|
4997
|
+
let anythingChanged = false;
|
|
4998
|
+
const serversToWrap = [];
|
|
4999
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
5000
|
+
if (!server.command || server.command === "node9") continue;
|
|
5001
|
+
const parts = [server.command, ...server.args ?? []];
|
|
5002
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
5003
|
+
}
|
|
5004
|
+
if (serversToWrap.length > 0) {
|
|
5005
|
+
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
5006
|
+
console.log(chalk3.white(` ${mcpPath}`));
|
|
5007
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
5008
|
+
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
5009
|
+
}
|
|
5010
|
+
console.log("");
|
|
5011
|
+
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
5012
|
+
if (proceed) {
|
|
5013
|
+
for (const { name, parts } of serversToWrap) {
|
|
5014
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
5015
|
+
}
|
|
5016
|
+
mcpConfig.mcpServers = servers;
|
|
5017
|
+
writeJson(mcpPath, mcpConfig);
|
|
5018
|
+
console.log(chalk3.green(`
|
|
5019
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
5020
|
+
anythingChanged = true;
|
|
5021
|
+
} else {
|
|
5022
|
+
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
3916
5023
|
}
|
|
5024
|
+
console.log("");
|
|
3917
5025
|
}
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
console.log(
|
|
3926
|
-
|
|
3927
|
-
|
|
5026
|
+
console.log(
|
|
5027
|
+
chalk3.yellow(
|
|
5028
|
+
" \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
|
|
5029
|
+
)
|
|
5030
|
+
);
|
|
5031
|
+
console.log("");
|
|
5032
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
5033
|
+
console.log(
|
|
5034
|
+
chalk3.blue(
|
|
5035
|
+
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
|
|
5036
|
+
)
|
|
5037
|
+
);
|
|
5038
|
+
printDaemonTip();
|
|
5039
|
+
return;
|
|
5040
|
+
}
|
|
5041
|
+
if (anythingChanged) {
|
|
5042
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
|
|
5043
|
+
console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
|
|
5044
|
+
printDaemonTip();
|
|
3928
5045
|
}
|
|
3929
5046
|
}
|
|
3930
5047
|
|
|
3931
5048
|
// src/cli.ts
|
|
3932
|
-
|
|
5049
|
+
init_daemon();
|
|
5050
|
+
import { spawn as spawn4, execSync } from "child_process";
|
|
3933
5051
|
import { parseCommandString } from "execa";
|
|
3934
5052
|
import { execa } from "execa";
|
|
3935
|
-
import
|
|
3936
|
-
import
|
|
3937
|
-
import
|
|
3938
|
-
import
|
|
3939
|
-
import
|
|
5053
|
+
import chalk6 from "chalk";
|
|
5054
|
+
import readline2 from "readline";
|
|
5055
|
+
import fs7 from "fs";
|
|
5056
|
+
import path9 from "path";
|
|
5057
|
+
import os7 from "os";
|
|
3940
5058
|
|
|
3941
5059
|
// src/undo.ts
|
|
3942
5060
|
import { spawnSync } from "child_process";
|
|
3943
|
-
import
|
|
3944
|
-
import
|
|
3945
|
-
import
|
|
3946
|
-
var SNAPSHOT_STACK_PATH =
|
|
3947
|
-
var UNDO_LATEST_PATH =
|
|
5061
|
+
import fs5 from "fs";
|
|
5062
|
+
import path7 from "path";
|
|
5063
|
+
import os5 from "os";
|
|
5064
|
+
var SNAPSHOT_STACK_PATH = path7.join(os5.homedir(), ".node9", "snapshots.json");
|
|
5065
|
+
var UNDO_LATEST_PATH = path7.join(os5.homedir(), ".node9", "undo_latest.txt");
|
|
3948
5066
|
var MAX_SNAPSHOTS = 10;
|
|
3949
5067
|
function readStack() {
|
|
3950
5068
|
try {
|
|
3951
|
-
if (
|
|
3952
|
-
return JSON.parse(
|
|
5069
|
+
if (fs5.existsSync(SNAPSHOT_STACK_PATH))
|
|
5070
|
+
return JSON.parse(fs5.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
3953
5071
|
} catch {
|
|
3954
5072
|
}
|
|
3955
5073
|
return [];
|
|
3956
5074
|
}
|
|
3957
5075
|
function writeStack(stack) {
|
|
3958
|
-
const dir =
|
|
3959
|
-
if (!
|
|
3960
|
-
|
|
5076
|
+
const dir = path7.dirname(SNAPSHOT_STACK_PATH);
|
|
5077
|
+
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
5078
|
+
fs5.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
3961
5079
|
}
|
|
3962
5080
|
function buildArgsSummary(tool, args) {
|
|
3963
5081
|
if (!args || typeof args !== "object") return "";
|
|
@@ -3973,13 +5091,13 @@ function buildArgsSummary(tool, args) {
|
|
|
3973
5091
|
async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
3974
5092
|
try {
|
|
3975
5093
|
const cwd = process.cwd();
|
|
3976
|
-
if (!
|
|
3977
|
-
const tempIndex =
|
|
5094
|
+
if (!fs5.existsSync(path7.join(cwd, ".git"))) return null;
|
|
5095
|
+
const tempIndex = path7.join(cwd, ".git", `node9_index_${Date.now()}`);
|
|
3978
5096
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
3979
5097
|
spawnSync("git", ["add", "-A"], { env });
|
|
3980
5098
|
const treeRes = spawnSync("git", ["write-tree"], { env });
|
|
3981
5099
|
const treeHash = treeRes.stdout.toString().trim();
|
|
3982
|
-
if (
|
|
5100
|
+
if (fs5.existsSync(tempIndex)) fs5.unlinkSync(tempIndex);
|
|
3983
5101
|
if (!treeHash || treeRes.status !== 0) return null;
|
|
3984
5102
|
const commitRes = spawnSync("git", [
|
|
3985
5103
|
"commit-tree",
|
|
@@ -4000,7 +5118,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
|
4000
5118
|
stack.push(entry);
|
|
4001
5119
|
if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
4002
5120
|
writeStack(stack);
|
|
4003
|
-
|
|
5121
|
+
fs5.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
4004
5122
|
return commitHash;
|
|
4005
5123
|
} catch (err) {
|
|
4006
5124
|
if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
|
|
@@ -4038,9 +5156,9 @@ function applyUndo(hash, cwd) {
|
|
|
4038
5156
|
const tracked = spawnSync("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
4039
5157
|
const untracked = spawnSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
4040
5158
|
for (const file of [...tracked, ...untracked]) {
|
|
4041
|
-
const fullPath =
|
|
4042
|
-
if (!snapshotFiles.has(file) &&
|
|
4043
|
-
|
|
5159
|
+
const fullPath = path7.join(dir, file);
|
|
5160
|
+
if (!snapshotFiles.has(file) && fs5.existsSync(fullPath)) {
|
|
5161
|
+
fs5.unlinkSync(fullPath);
|
|
4044
5162
|
}
|
|
4045
5163
|
}
|
|
4046
5164
|
return true;
|
|
@@ -4050,9 +5168,10 @@ function applyUndo(hash, cwd) {
|
|
|
4050
5168
|
}
|
|
4051
5169
|
|
|
4052
5170
|
// src/cli.ts
|
|
5171
|
+
init_shields();
|
|
4053
5172
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
4054
5173
|
var { version } = JSON.parse(
|
|
4055
|
-
|
|
5174
|
+
fs7.readFileSync(path9.join(__dirname, "../package.json"), "utf-8")
|
|
4056
5175
|
);
|
|
4057
5176
|
function parseDuration(str) {
|
|
4058
5177
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -4084,6 +5203,15 @@ INSTRUCTIONS:
|
|
|
4084
5203
|
- If you believe this action is critical, explain your reasoning and ask them to run "node9 pause 15m" to proceed.`;
|
|
4085
5204
|
}
|
|
4086
5205
|
const label = blockedByLabel.toLowerCase();
|
|
5206
|
+
if (label.includes("dlp") || label.includes("secret detected") || label.includes("credential review")) {
|
|
5207
|
+
return `NODE9 SECURITY ALERT: A sensitive credential (API key, token, or private key) was found in your tool call arguments.
|
|
5208
|
+
CRITICAL INSTRUCTION: Do NOT retry this action.
|
|
5209
|
+
REQUIRED ACTIONS:
|
|
5210
|
+
1. Remove the hardcoded credential from your command or code.
|
|
5211
|
+
2. Use an environment variable or a dedicated secrets manager instead.
|
|
5212
|
+
3. Treat the leaked credential as compromised and rotate it immediately.
|
|
5213
|
+
Do NOT attempt to bypass this check or pass the credential through another tool.`;
|
|
5214
|
+
}
|
|
4087
5215
|
if (label.includes("sql safety") && label.includes("delete without where")) {
|
|
4088
5216
|
return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
|
|
4089
5217
|
INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
|
|
@@ -4139,7 +5267,7 @@ function openBrowserLocal() {
|
|
|
4139
5267
|
}
|
|
4140
5268
|
async function autoStartDaemonAndWait() {
|
|
4141
5269
|
try {
|
|
4142
|
-
const child =
|
|
5270
|
+
const child = spawn4("node9", ["daemon"], {
|
|
4143
5271
|
detached: true,
|
|
4144
5272
|
stdio: "ignore",
|
|
4145
5273
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -4175,14 +5303,14 @@ async function runProxy(targetCommand) {
|
|
|
4175
5303
|
if (stdout) executable = stdout.trim();
|
|
4176
5304
|
} catch {
|
|
4177
5305
|
}
|
|
4178
|
-
console.log(
|
|
4179
|
-
const child =
|
|
5306
|
+
console.log(chalk6.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
5307
|
+
const child = spawn4(executable, args, {
|
|
4180
5308
|
stdio: ["pipe", "pipe", "inherit"],
|
|
4181
5309
|
// We control STDIN and STDOUT
|
|
4182
5310
|
shell: false,
|
|
4183
5311
|
env: { ...process.env, FORCE_COLOR: "1" }
|
|
4184
5312
|
});
|
|
4185
|
-
const agentIn =
|
|
5313
|
+
const agentIn = readline2.createInterface({ input: process.stdin, terminal: false });
|
|
4186
5314
|
agentIn.on("line", async (line) => {
|
|
4187
5315
|
let message;
|
|
4188
5316
|
try {
|
|
@@ -4200,10 +5328,10 @@ async function runProxy(targetCommand) {
|
|
|
4200
5328
|
agent: "Proxy/MCP"
|
|
4201
5329
|
});
|
|
4202
5330
|
if (!result.approved) {
|
|
4203
|
-
console.error(
|
|
5331
|
+
console.error(chalk6.red(`
|
|
4204
5332
|
\u{1F6D1} Node9 Sudo: Action Blocked`));
|
|
4205
|
-
console.error(
|
|
4206
|
-
console.error(
|
|
5333
|
+
console.error(chalk6.gray(` Tool: ${name}`));
|
|
5334
|
+
console.error(chalk6.gray(` Reason: ${result.reason || "Security Policy"}
|
|
4207
5335
|
`));
|
|
4208
5336
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
4209
5337
|
const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -4245,14 +5373,14 @@ async function runProxy(targetCommand) {
|
|
|
4245
5373
|
}
|
|
4246
5374
|
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) => {
|
|
4247
5375
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
4248
|
-
const credPath =
|
|
4249
|
-
if (!
|
|
4250
|
-
|
|
5376
|
+
const credPath = path9.join(os7.homedir(), ".node9", "credentials.json");
|
|
5377
|
+
if (!fs7.existsSync(path9.dirname(credPath)))
|
|
5378
|
+
fs7.mkdirSync(path9.dirname(credPath), { recursive: true });
|
|
4251
5379
|
const profileName = options.profile || "default";
|
|
4252
5380
|
let existingCreds = {};
|
|
4253
5381
|
try {
|
|
4254
|
-
if (
|
|
4255
|
-
const raw = JSON.parse(
|
|
5382
|
+
if (fs7.existsSync(credPath)) {
|
|
5383
|
+
const raw = JSON.parse(fs7.readFileSync(credPath, "utf-8"));
|
|
4256
5384
|
if (raw.apiKey) {
|
|
4257
5385
|
existingCreds = {
|
|
4258
5386
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -4264,13 +5392,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4264
5392
|
} catch {
|
|
4265
5393
|
}
|
|
4266
5394
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
4267
|
-
|
|
5395
|
+
fs7.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
4268
5396
|
if (profileName === "default") {
|
|
4269
|
-
const configPath =
|
|
5397
|
+
const configPath = path9.join(os7.homedir(), ".node9", "config.json");
|
|
4270
5398
|
let config = {};
|
|
4271
5399
|
try {
|
|
4272
|
-
if (
|
|
4273
|
-
config = JSON.parse(
|
|
5400
|
+
if (fs7.existsSync(configPath))
|
|
5401
|
+
config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
|
|
4274
5402
|
} catch {
|
|
4275
5403
|
}
|
|
4276
5404
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -4285,36 +5413,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4285
5413
|
approvers.cloud = false;
|
|
4286
5414
|
}
|
|
4287
5415
|
s.approvers = approvers;
|
|
4288
|
-
if (!
|
|
4289
|
-
|
|
4290
|
-
|
|
5416
|
+
if (!fs7.existsSync(path9.dirname(configPath)))
|
|
5417
|
+
fs7.mkdirSync(path9.dirname(configPath), { recursive: true });
|
|
5418
|
+
fs7.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
4291
5419
|
}
|
|
4292
5420
|
if (options.profile && profileName !== "default") {
|
|
4293
|
-
console.log(
|
|
4294
|
-
console.log(
|
|
5421
|
+
console.log(chalk6.green(`\u2705 Profile "${profileName}" saved`));
|
|
5422
|
+
console.log(chalk6.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
4295
5423
|
} else if (options.local) {
|
|
4296
|
-
console.log(
|
|
4297
|
-
console.log(
|
|
5424
|
+
console.log(chalk6.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
5425
|
+
console.log(chalk6.gray(` All decisions stay on this machine.`));
|
|
4298
5426
|
} else {
|
|
4299
|
-
console.log(
|
|
4300
|
-
console.log(
|
|
5427
|
+
console.log(chalk6.green(`\u2705 Logged in \u2014 agent mode`));
|
|
5428
|
+
console.log(chalk6.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
4301
5429
|
}
|
|
4302
5430
|
});
|
|
4303
5431
|
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
4304
5432
|
if (target === "gemini") return await setupGemini();
|
|
4305
5433
|
if (target === "claude") return await setupClaude();
|
|
4306
5434
|
if (target === "cursor") return await setupCursor();
|
|
4307
|
-
console.error(
|
|
5435
|
+
console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
4308
5436
|
process.exit(1);
|
|
4309
5437
|
});
|
|
4310
5438
|
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
4311
5439
|
if (!target) {
|
|
4312
|
-
console.log(
|
|
4313
|
-
console.log(" Usage: " +
|
|
5440
|
+
console.log(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
5441
|
+
console.log(" Usage: " + chalk6.white("node9 setup <target>") + "\n");
|
|
4314
5442
|
console.log(" Targets:");
|
|
4315
|
-
console.log(" " +
|
|
4316
|
-
console.log(" " +
|
|
4317
|
-
console.log(" " +
|
|
5443
|
+
console.log(" " + chalk6.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
5444
|
+
console.log(" " + chalk6.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
5445
|
+
console.log(" " + chalk6.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
4318
5446
|
console.log("");
|
|
4319
5447
|
return;
|
|
4320
5448
|
}
|
|
@@ -4322,28 +5450,28 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
4322
5450
|
if (t === "gemini") return await setupGemini();
|
|
4323
5451
|
if (t === "claude") return await setupClaude();
|
|
4324
5452
|
if (t === "cursor") return await setupCursor();
|
|
4325
|
-
console.error(
|
|
5453
|
+
console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
4326
5454
|
process.exit(1);
|
|
4327
5455
|
});
|
|
4328
5456
|
program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
4329
|
-
const homeDir2 =
|
|
5457
|
+
const homeDir2 = os7.homedir();
|
|
4330
5458
|
let failures = 0;
|
|
4331
5459
|
function pass(msg) {
|
|
4332
|
-
console.log(
|
|
5460
|
+
console.log(chalk6.green(" \u2705 ") + msg);
|
|
4333
5461
|
}
|
|
4334
5462
|
function fail(msg, hint) {
|
|
4335
|
-
console.log(
|
|
4336
|
-
if (hint) console.log(
|
|
5463
|
+
console.log(chalk6.red(" \u274C ") + msg);
|
|
5464
|
+
if (hint) console.log(chalk6.gray(" " + hint));
|
|
4337
5465
|
failures++;
|
|
4338
5466
|
}
|
|
4339
5467
|
function warn(msg, hint) {
|
|
4340
|
-
console.log(
|
|
4341
|
-
if (hint) console.log(
|
|
5468
|
+
console.log(chalk6.yellow(" \u26A0\uFE0F ") + msg);
|
|
5469
|
+
if (hint) console.log(chalk6.gray(" " + hint));
|
|
4342
5470
|
}
|
|
4343
5471
|
function section(title) {
|
|
4344
|
-
console.log("\n" +
|
|
5472
|
+
console.log("\n" + chalk6.bold(title));
|
|
4345
5473
|
}
|
|
4346
|
-
console.log(
|
|
5474
|
+
console.log(chalk6.cyan.bold(`
|
|
4347
5475
|
\u{1F6E1}\uFE0F Node9 Doctor v${version}
|
|
4348
5476
|
`));
|
|
4349
5477
|
section("Binary");
|
|
@@ -4372,10 +5500,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4372
5500
|
);
|
|
4373
5501
|
}
|
|
4374
5502
|
section("Configuration");
|
|
4375
|
-
const globalConfigPath =
|
|
4376
|
-
if (
|
|
5503
|
+
const globalConfigPath = path9.join(homeDir2, ".node9", "config.json");
|
|
5504
|
+
if (fs7.existsSync(globalConfigPath)) {
|
|
4377
5505
|
try {
|
|
4378
|
-
JSON.parse(
|
|
5506
|
+
JSON.parse(fs7.readFileSync(globalConfigPath, "utf-8"));
|
|
4379
5507
|
pass("~/.node9/config.json found and valid");
|
|
4380
5508
|
} catch {
|
|
4381
5509
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -4383,17 +5511,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4383
5511
|
} else {
|
|
4384
5512
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
4385
5513
|
}
|
|
4386
|
-
const projectConfigPath =
|
|
4387
|
-
if (
|
|
5514
|
+
const projectConfigPath = path9.join(process.cwd(), "node9.config.json");
|
|
5515
|
+
if (fs7.existsSync(projectConfigPath)) {
|
|
4388
5516
|
try {
|
|
4389
|
-
JSON.parse(
|
|
5517
|
+
JSON.parse(fs7.readFileSync(projectConfigPath, "utf-8"));
|
|
4390
5518
|
pass("node9.config.json found and valid (project)");
|
|
4391
5519
|
} catch {
|
|
4392
5520
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
4393
5521
|
}
|
|
4394
5522
|
}
|
|
4395
|
-
const credsPath =
|
|
4396
|
-
if (
|
|
5523
|
+
const credsPath = path9.join(homeDir2, ".node9", "credentials.json");
|
|
5524
|
+
if (fs7.existsSync(credsPath)) {
|
|
4397
5525
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
4398
5526
|
} else {
|
|
4399
5527
|
warn(
|
|
@@ -4402,10 +5530,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4402
5530
|
);
|
|
4403
5531
|
}
|
|
4404
5532
|
section("Agent Hooks");
|
|
4405
|
-
const claudeSettingsPath =
|
|
4406
|
-
if (
|
|
5533
|
+
const claudeSettingsPath = path9.join(homeDir2, ".claude", "settings.json");
|
|
5534
|
+
if (fs7.existsSync(claudeSettingsPath)) {
|
|
4407
5535
|
try {
|
|
4408
|
-
const cs = JSON.parse(
|
|
5536
|
+
const cs = JSON.parse(fs7.readFileSync(claudeSettingsPath, "utf-8"));
|
|
4409
5537
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
4410
5538
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4411
5539
|
);
|
|
@@ -4418,10 +5546,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4418
5546
|
} else {
|
|
4419
5547
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
4420
5548
|
}
|
|
4421
|
-
const geminiSettingsPath =
|
|
4422
|
-
if (
|
|
5549
|
+
const geminiSettingsPath = path9.join(homeDir2, ".gemini", "settings.json");
|
|
5550
|
+
if (fs7.existsSync(geminiSettingsPath)) {
|
|
4423
5551
|
try {
|
|
4424
|
-
const gs = JSON.parse(
|
|
5552
|
+
const gs = JSON.parse(fs7.readFileSync(geminiSettingsPath, "utf-8"));
|
|
4425
5553
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
4426
5554
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4427
5555
|
);
|
|
@@ -4434,10 +5562,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4434
5562
|
} else {
|
|
4435
5563
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
4436
5564
|
}
|
|
4437
|
-
const cursorHooksPath =
|
|
4438
|
-
if (
|
|
5565
|
+
const cursorHooksPath = path9.join(homeDir2, ".cursor", "hooks.json");
|
|
5566
|
+
if (fs7.existsSync(cursorHooksPath)) {
|
|
4439
5567
|
try {
|
|
4440
|
-
const cur = JSON.parse(
|
|
5568
|
+
const cur = JSON.parse(fs7.readFileSync(cursorHooksPath, "utf-8"));
|
|
4441
5569
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
4442
5570
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
4443
5571
|
);
|
|
@@ -4458,9 +5586,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4458
5586
|
}
|
|
4459
5587
|
console.log("");
|
|
4460
5588
|
if (failures === 0) {
|
|
4461
|
-
console.log(
|
|
5589
|
+
console.log(chalk6.green.bold(" All checks passed. Node9 is ready.\n"));
|
|
4462
5590
|
} else {
|
|
4463
|
-
console.log(
|
|
5591
|
+
console.log(chalk6.red.bold(` ${failures} check(s) failed. See hints above.
|
|
4464
5592
|
`));
|
|
4465
5593
|
process.exit(1);
|
|
4466
5594
|
}
|
|
@@ -4475,7 +5603,7 @@ program.command("explain").description(
|
|
|
4475
5603
|
try {
|
|
4476
5604
|
args = JSON.parse(trimmed);
|
|
4477
5605
|
} catch {
|
|
4478
|
-
console.error(
|
|
5606
|
+
console.error(chalk6.red(`
|
|
4479
5607
|
\u274C Invalid JSON: ${trimmed}
|
|
4480
5608
|
`));
|
|
4481
5609
|
process.exit(1);
|
|
@@ -4486,63 +5614,63 @@ program.command("explain").description(
|
|
|
4486
5614
|
}
|
|
4487
5615
|
const result = await explainPolicy(tool, args);
|
|
4488
5616
|
console.log("");
|
|
4489
|
-
console.log(
|
|
5617
|
+
console.log(chalk6.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
4490
5618
|
console.log("");
|
|
4491
|
-
console.log(` ${
|
|
5619
|
+
console.log(` ${chalk6.bold("Tool:")} ${chalk6.white(result.tool)}`);
|
|
4492
5620
|
if (argsRaw) {
|
|
4493
5621
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
4494
|
-
console.log(` ${
|
|
5622
|
+
console.log(` ${chalk6.bold("Input:")} ${chalk6.gray(preview)}`);
|
|
4495
5623
|
}
|
|
4496
5624
|
console.log("");
|
|
4497
|
-
console.log(
|
|
5625
|
+
console.log(chalk6.bold("Config Sources (Waterfall):"));
|
|
4498
5626
|
for (const tier of result.waterfall) {
|
|
4499
|
-
const num =
|
|
5627
|
+
const num = chalk6.gray(` ${tier.tier}.`);
|
|
4500
5628
|
const label = tier.label.padEnd(16);
|
|
4501
5629
|
let statusStr;
|
|
4502
5630
|
if (tier.tier === 1) {
|
|
4503
|
-
statusStr =
|
|
5631
|
+
statusStr = chalk6.gray(tier.note ?? "");
|
|
4504
5632
|
} else if (tier.status === "active") {
|
|
4505
|
-
const loc = tier.path ?
|
|
4506
|
-
const note = tier.note ?
|
|
4507
|
-
statusStr =
|
|
5633
|
+
const loc = tier.path ? chalk6.gray(tier.path) : "";
|
|
5634
|
+
const note = tier.note ? chalk6.gray(`(${tier.note})`) : "";
|
|
5635
|
+
statusStr = chalk6.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
4508
5636
|
} else {
|
|
4509
|
-
statusStr =
|
|
5637
|
+
statusStr = chalk6.gray("\u25CB " + (tier.note ?? "not found"));
|
|
4510
5638
|
}
|
|
4511
|
-
console.log(`${num} ${
|
|
5639
|
+
console.log(`${num} ${chalk6.white(label)} ${statusStr}`);
|
|
4512
5640
|
}
|
|
4513
5641
|
console.log("");
|
|
4514
|
-
console.log(
|
|
5642
|
+
console.log(chalk6.bold("Policy Evaluation:"));
|
|
4515
5643
|
for (const step of result.steps) {
|
|
4516
5644
|
const isFinal = step.isFinal;
|
|
4517
5645
|
let icon;
|
|
4518
|
-
if (step.outcome === "allow") icon =
|
|
4519
|
-
else if (step.outcome === "review") icon =
|
|
4520
|
-
else if (step.outcome === "skip") icon =
|
|
4521
|
-
else icon =
|
|
5646
|
+
if (step.outcome === "allow") icon = chalk6.green(" \u2705");
|
|
5647
|
+
else if (step.outcome === "review") icon = chalk6.red(" \u{1F534}");
|
|
5648
|
+
else if (step.outcome === "skip") icon = chalk6.gray(" \u2500 ");
|
|
5649
|
+
else icon = chalk6.gray(" \u25CB ");
|
|
4522
5650
|
const name = step.name.padEnd(18);
|
|
4523
|
-
const nameStr = isFinal ?
|
|
4524
|
-
const detail = isFinal ?
|
|
4525
|
-
const arrow = isFinal ?
|
|
5651
|
+
const nameStr = isFinal ? chalk6.white.bold(name) : chalk6.white(name);
|
|
5652
|
+
const detail = isFinal ? chalk6.white(step.detail) : chalk6.gray(step.detail);
|
|
5653
|
+
const arrow = isFinal ? chalk6.yellow(" \u2190 STOP") : "";
|
|
4526
5654
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
4527
5655
|
}
|
|
4528
5656
|
console.log("");
|
|
4529
5657
|
if (result.decision === "allow") {
|
|
4530
|
-
console.log(
|
|
5658
|
+
console.log(chalk6.green.bold(" Decision: \u2705 ALLOW") + chalk6.gray(" \u2014 no approval needed"));
|
|
4531
5659
|
} else {
|
|
4532
5660
|
console.log(
|
|
4533
|
-
|
|
5661
|
+
chalk6.red.bold(" Decision: \u{1F534} REVIEW") + chalk6.gray(" \u2014 human approval required")
|
|
4534
5662
|
);
|
|
4535
5663
|
if (result.blockedByLabel) {
|
|
4536
|
-
console.log(
|
|
5664
|
+
console.log(chalk6.gray(` Reason: ${result.blockedByLabel}`));
|
|
4537
5665
|
}
|
|
4538
5666
|
}
|
|
4539
5667
|
console.log("");
|
|
4540
5668
|
});
|
|
4541
5669
|
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
|
|
4542
|
-
const configPath =
|
|
4543
|
-
if (
|
|
4544
|
-
console.log(
|
|
4545
|
-
console.log(
|
|
5670
|
+
const configPath = path9.join(os7.homedir(), ".node9", "config.json");
|
|
5671
|
+
if (fs7.existsSync(configPath) && !options.force) {
|
|
5672
|
+
console.log(chalk6.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
5673
|
+
console.log(chalk6.gray(` Run with --force to overwrite.`));
|
|
4546
5674
|
return;
|
|
4547
5675
|
}
|
|
4548
5676
|
const requestedMode = options.mode.toLowerCase();
|
|
@@ -4554,13 +5682,13 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
4554
5682
|
mode: safeMode
|
|
4555
5683
|
}
|
|
4556
5684
|
};
|
|
4557
|
-
const dir =
|
|
4558
|
-
if (!
|
|
4559
|
-
|
|
4560
|
-
console.log(
|
|
4561
|
-
console.log(
|
|
5685
|
+
const dir = path9.dirname(configPath);
|
|
5686
|
+
if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
|
|
5687
|
+
fs7.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
5688
|
+
console.log(chalk6.green(`\u2705 Global config created: ${configPath}`));
|
|
5689
|
+
console.log(chalk6.cyan(` Mode set to: ${safeMode}`));
|
|
4562
5690
|
console.log(
|
|
4563
|
-
|
|
5691
|
+
chalk6.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
4564
5692
|
);
|
|
4565
5693
|
});
|
|
4566
5694
|
function formatRelativeTime(timestamp) {
|
|
@@ -4574,14 +5702,14 @@ function formatRelativeTime(timestamp) {
|
|
|
4574
5702
|
return new Date(timestamp).toLocaleDateString();
|
|
4575
5703
|
}
|
|
4576
5704
|
program.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) => {
|
|
4577
|
-
const logPath =
|
|
4578
|
-
if (!
|
|
5705
|
+
const logPath = path9.join(os7.homedir(), ".node9", "audit.log");
|
|
5706
|
+
if (!fs7.existsSync(logPath)) {
|
|
4579
5707
|
console.log(
|
|
4580
|
-
|
|
5708
|
+
chalk6.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
4581
5709
|
);
|
|
4582
5710
|
return;
|
|
4583
5711
|
}
|
|
4584
|
-
const raw =
|
|
5712
|
+
const raw = fs7.readFileSync(logPath, "utf-8");
|
|
4585
5713
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
4586
5714
|
let entries = lines.flatMap((line) => {
|
|
4587
5715
|
try {
|
|
@@ -4603,31 +5731,31 @@ program.command("audit").description("View local execution audit log").option("-
|
|
|
4603
5731
|
return;
|
|
4604
5732
|
}
|
|
4605
5733
|
if (entries.length === 0) {
|
|
4606
|
-
console.log(
|
|
5734
|
+
console.log(chalk6.yellow("No matching audit entries."));
|
|
4607
5735
|
return;
|
|
4608
5736
|
}
|
|
4609
5737
|
console.log(
|
|
4610
5738
|
`
|
|
4611
|
-
${
|
|
5739
|
+
${chalk6.bold("Node9 Audit Log")} ${chalk6.dim(`(${entries.length} entries)`)}`
|
|
4612
5740
|
);
|
|
4613
|
-
console.log(
|
|
5741
|
+
console.log(chalk6.dim(" " + "\u2500".repeat(65)));
|
|
4614
5742
|
console.log(
|
|
4615
5743
|
` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
|
|
4616
5744
|
);
|
|
4617
|
-
console.log(
|
|
5745
|
+
console.log(chalk6.dim(" " + "\u2500".repeat(65)));
|
|
4618
5746
|
for (const e of entries) {
|
|
4619
5747
|
const time = formatRelativeTime(String(e.ts)).padEnd(12);
|
|
4620
5748
|
const tool = String(e.tool).slice(0, 17).padEnd(18);
|
|
4621
|
-
const result = e.decision === "allow" ?
|
|
5749
|
+
const result = e.decision === "allow" ? chalk6.green("ALLOW".padEnd(10)) : chalk6.red("DENY".padEnd(10));
|
|
4622
5750
|
const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
|
|
4623
5751
|
const agent = String(e.agent || "unknown");
|
|
4624
5752
|
console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
|
|
4625
5753
|
}
|
|
4626
5754
|
const allowed = entries.filter((e) => e.decision === "allow").length;
|
|
4627
5755
|
const denied = entries.filter((e) => e.decision === "deny").length;
|
|
4628
|
-
console.log(
|
|
5756
|
+
console.log(chalk6.dim(" " + "\u2500".repeat(65)));
|
|
4629
5757
|
console.log(
|
|
4630
|
-
` ${entries.length} entries | ${
|
|
5758
|
+
` ${entries.length} entries | ${chalk6.green(allowed + " allowed")} | ${chalk6.red(denied + " denied")}
|
|
4631
5759
|
`
|
|
4632
5760
|
);
|
|
4633
5761
|
});
|
|
@@ -4638,43 +5766,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4638
5766
|
const settings = mergedConfig.settings;
|
|
4639
5767
|
console.log("");
|
|
4640
5768
|
if (creds && settings.approvers.cloud) {
|
|
4641
|
-
console.log(
|
|
5769
|
+
console.log(chalk6.green(" \u25CF Agent mode") + chalk6.gray(" \u2014 cloud team policy enforced"));
|
|
4642
5770
|
} else if (creds && !settings.approvers.cloud) {
|
|
4643
5771
|
console.log(
|
|
4644
|
-
|
|
5772
|
+
chalk6.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk6.gray(" \u2014 all decisions stay on this machine")
|
|
4645
5773
|
);
|
|
4646
5774
|
} else {
|
|
4647
5775
|
console.log(
|
|
4648
|
-
|
|
5776
|
+
chalk6.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk6.gray(" \u2014 no API key (Local rules only)")
|
|
4649
5777
|
);
|
|
4650
5778
|
}
|
|
4651
5779
|
console.log("");
|
|
4652
5780
|
if (daemonRunning) {
|
|
4653
5781
|
console.log(
|
|
4654
|
-
|
|
5782
|
+
chalk6.green(" \u25CF Daemon running") + chalk6.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
|
|
4655
5783
|
);
|
|
4656
5784
|
} else {
|
|
4657
|
-
console.log(
|
|
5785
|
+
console.log(chalk6.gray(" \u25CB Daemon stopped"));
|
|
4658
5786
|
}
|
|
4659
5787
|
if (settings.enableUndo) {
|
|
4660
5788
|
console.log(
|
|
4661
|
-
|
|
5789
|
+
chalk6.magenta(" \u25CF Undo Engine") + chalk6.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
|
|
4662
5790
|
);
|
|
4663
5791
|
}
|
|
4664
5792
|
console.log("");
|
|
4665
|
-
const modeLabel = settings.mode === "audit" ?
|
|
5793
|
+
const modeLabel = settings.mode === "audit" ? chalk6.blue("audit") : settings.mode === "strict" ? chalk6.red("strict") : chalk6.white("standard");
|
|
4666
5794
|
console.log(` Mode: ${modeLabel}`);
|
|
4667
|
-
const projectConfig =
|
|
4668
|
-
const globalConfig =
|
|
5795
|
+
const projectConfig = path9.join(process.cwd(), "node9.config.json");
|
|
5796
|
+
const globalConfig = path9.join(os7.homedir(), ".node9", "config.json");
|
|
4669
5797
|
console.log(
|
|
4670
|
-
` Local: ${
|
|
5798
|
+
` Local: ${fs7.existsSync(projectConfig) ? chalk6.green("Active (node9.config.json)") : chalk6.gray("Not present")}`
|
|
4671
5799
|
);
|
|
4672
5800
|
console.log(
|
|
4673
|
-
` Global: ${
|
|
5801
|
+
` Global: ${fs7.existsSync(globalConfig) ? chalk6.green("Active (~/.node9/config.json)") : chalk6.gray("Not present")}`
|
|
4674
5802
|
);
|
|
4675
5803
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
4676
5804
|
console.log(
|
|
4677
|
-
` Sandbox: ${
|
|
5805
|
+
` Sandbox: ${chalk6.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
4678
5806
|
);
|
|
4679
5807
|
}
|
|
4680
5808
|
const pauseState = checkPause();
|
|
@@ -4682,47 +5810,63 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4682
5810
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
4683
5811
|
console.log("");
|
|
4684
5812
|
console.log(
|
|
4685
|
-
|
|
5813
|
+
chalk6.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk6.gray(" \u2014 all tool calls allowed")
|
|
4686
5814
|
);
|
|
4687
5815
|
}
|
|
4688
5816
|
console.log("");
|
|
4689
5817
|
});
|
|
4690
|
-
program.command("daemon").description("Run the local approval server").argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").
|
|
5818
|
+
program.command("daemon").description("Run the local approval server").argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").option(
|
|
5819
|
+
"-w, --watch",
|
|
5820
|
+
"Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
|
|
5821
|
+
).action(
|
|
4691
5822
|
async (action, options) => {
|
|
4692
5823
|
const cmd = (action ?? "start").toLowerCase();
|
|
4693
5824
|
if (cmd === "stop") return stopDaemon();
|
|
4694
5825
|
if (cmd === "status") return daemonStatus();
|
|
4695
5826
|
if (cmd !== "start" && action !== void 0) {
|
|
4696
|
-
console.error(
|
|
5827
|
+
console.error(chalk6.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
|
|
4697
5828
|
process.exit(1);
|
|
4698
5829
|
}
|
|
5830
|
+
if (options.watch) {
|
|
5831
|
+
process.env.NODE9_WATCH_MODE = "1";
|
|
5832
|
+
setTimeout(() => {
|
|
5833
|
+
openBrowserLocal();
|
|
5834
|
+
console.log(chalk6.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
5835
|
+
}, 600);
|
|
5836
|
+
startDaemon();
|
|
5837
|
+
return;
|
|
5838
|
+
}
|
|
4699
5839
|
if (options.openui) {
|
|
4700
5840
|
if (isDaemonRunning()) {
|
|
4701
5841
|
openBrowserLocal();
|
|
4702
|
-
console.log(
|
|
5842
|
+
console.log(chalk6.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
4703
5843
|
process.exit(0);
|
|
4704
5844
|
}
|
|
4705
|
-
const child =
|
|
5845
|
+
const child = spawn4("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
4706
5846
|
child.unref();
|
|
4707
5847
|
for (let i = 0; i < 12; i++) {
|
|
4708
5848
|
await new Promise((r) => setTimeout(r, 250));
|
|
4709
5849
|
if (isDaemonRunning()) break;
|
|
4710
5850
|
}
|
|
4711
5851
|
openBrowserLocal();
|
|
4712
|
-
console.log(
|
|
5852
|
+
console.log(chalk6.green(`
|
|
4713
5853
|
\u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
|
|
4714
5854
|
process.exit(0);
|
|
4715
5855
|
}
|
|
4716
5856
|
if (options.background) {
|
|
4717
|
-
const child =
|
|
5857
|
+
const child = spawn4("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
4718
5858
|
child.unref();
|
|
4719
|
-
console.log(
|
|
5859
|
+
console.log(chalk6.green(`
|
|
4720
5860
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
4721
5861
|
process.exit(0);
|
|
4722
5862
|
}
|
|
4723
5863
|
startDaemon();
|
|
4724
5864
|
}
|
|
4725
5865
|
);
|
|
5866
|
+
program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).action(async (options) => {
|
|
5867
|
+
const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
|
|
5868
|
+
await startTail2(options);
|
|
5869
|
+
});
|
|
4726
5870
|
program.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
|
|
4727
5871
|
const processPayload = async (raw) => {
|
|
4728
5872
|
try {
|
|
@@ -4733,9 +5877,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
4733
5877
|
} catch (err) {
|
|
4734
5878
|
const tempConfig = getConfig();
|
|
4735
5879
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
4736
|
-
const logPath =
|
|
5880
|
+
const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
4737
5881
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4738
|
-
|
|
5882
|
+
fs7.appendFileSync(
|
|
4739
5883
|
logPath,
|
|
4740
5884
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
4741
5885
|
RAW: ${raw}
|
|
@@ -4753,10 +5897,10 @@ RAW: ${raw}
|
|
|
4753
5897
|
}
|
|
4754
5898
|
const config = getConfig();
|
|
4755
5899
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
4756
|
-
const logPath =
|
|
4757
|
-
if (!
|
|
4758
|
-
|
|
4759
|
-
|
|
5900
|
+
const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
5901
|
+
if (!fs7.existsSync(path9.dirname(logPath)))
|
|
5902
|
+
fs7.mkdirSync(path9.dirname(logPath), { recursive: true });
|
|
5903
|
+
fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
4760
5904
|
`);
|
|
4761
5905
|
}
|
|
4762
5906
|
const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
|
|
@@ -4767,13 +5911,19 @@ RAW: ${raw}
|
|
|
4767
5911
|
const sendBlock = (msg, result2) => {
|
|
4768
5912
|
const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
|
|
4769
5913
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
4770
|
-
|
|
5914
|
+
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
5915
|
+
console.error(chalk6.bgRed.white.bold(`
|
|
5916
|
+
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
5917
|
+
console.error(chalk6.red.bold(` A sensitive secret was found in the tool arguments!`));
|
|
5918
|
+
} else {
|
|
5919
|
+
console.error(chalk6.red(`
|
|
4771
5920
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
4772
|
-
|
|
4773
|
-
|
|
5921
|
+
}
|
|
5922
|
+
console.error(chalk6.gray(` Triggered by: ${blockedByContext}`));
|
|
5923
|
+
if (result2?.changeHint) console.error(chalk6.cyan(` To change: ${result2.changeHint}`));
|
|
4774
5924
|
console.error("");
|
|
4775
5925
|
const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
|
|
4776
|
-
console.error(
|
|
5926
|
+
console.error(chalk6.dim(` (Detailed instructions sent to AI agent)`));
|
|
4777
5927
|
process.stdout.write(
|
|
4778
5928
|
JSON.stringify({
|
|
4779
5929
|
decision: "block",
|
|
@@ -4804,7 +5954,7 @@ RAW: ${raw}
|
|
|
4804
5954
|
process.exit(0);
|
|
4805
5955
|
}
|
|
4806
5956
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
4807
|
-
console.error(
|
|
5957
|
+
console.error(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
4808
5958
|
const daemonReady = await autoStartDaemonAndWait();
|
|
4809
5959
|
if (daemonReady) {
|
|
4810
5960
|
const retry = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
@@ -4827,9 +5977,9 @@ RAW: ${raw}
|
|
|
4827
5977
|
});
|
|
4828
5978
|
} catch (err) {
|
|
4829
5979
|
if (process.env.NODE9_DEBUG === "1") {
|
|
4830
|
-
const logPath =
|
|
5980
|
+
const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
4831
5981
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4832
|
-
|
|
5982
|
+
fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
4833
5983
|
`);
|
|
4834
5984
|
}
|
|
4835
5985
|
process.exit(0);
|
|
@@ -4874,10 +6024,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
4874
6024
|
decision: "allowed",
|
|
4875
6025
|
source: "post-hook"
|
|
4876
6026
|
};
|
|
4877
|
-
const logPath =
|
|
4878
|
-
if (!
|
|
4879
|
-
|
|
4880
|
-
|
|
6027
|
+
const logPath = path9.join(os7.homedir(), ".node9", "audit.log");
|
|
6028
|
+
if (!fs7.existsSync(path9.dirname(logPath)))
|
|
6029
|
+
fs7.mkdirSync(path9.dirname(logPath), { recursive: true });
|
|
6030
|
+
fs7.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
4881
6031
|
const config = getConfig();
|
|
4882
6032
|
if (shouldSnapshot(tool, {}, config)) {
|
|
4883
6033
|
await createShadowSnapshot();
|
|
@@ -4904,7 +6054,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
4904
6054
|
const ms = parseDuration(options.duration);
|
|
4905
6055
|
if (ms === null) {
|
|
4906
6056
|
console.error(
|
|
4907
|
-
|
|
6057
|
+
chalk6.red(`
|
|
4908
6058
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
4909
6059
|
`)
|
|
4910
6060
|
);
|
|
@@ -4912,20 +6062,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
4912
6062
|
}
|
|
4913
6063
|
pauseNode9(ms, options.duration);
|
|
4914
6064
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
4915
|
-
console.log(
|
|
6065
|
+
console.log(chalk6.yellow(`
|
|
4916
6066
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
4917
|
-
console.log(
|
|
4918
|
-
console.log(
|
|
6067
|
+
console.log(chalk6.gray(` All tool calls will be allowed without review.`));
|
|
6068
|
+
console.log(chalk6.gray(` Run "node9 resume" to re-enable early.
|
|
4919
6069
|
`));
|
|
4920
6070
|
});
|
|
4921
6071
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
4922
6072
|
const { paused } = checkPause();
|
|
4923
6073
|
if (!paused) {
|
|
4924
|
-
console.log(
|
|
6074
|
+
console.log(chalk6.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
4925
6075
|
return;
|
|
4926
6076
|
}
|
|
4927
6077
|
resumeNode9();
|
|
4928
|
-
console.log(
|
|
6078
|
+
console.log(chalk6.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
4929
6079
|
});
|
|
4930
6080
|
var HOOK_BASED_AGENTS = {
|
|
4931
6081
|
claude: "claude",
|
|
@@ -4938,15 +6088,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
4938
6088
|
if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
|
|
4939
6089
|
const target = HOOK_BASED_AGENTS[firstArg];
|
|
4940
6090
|
console.error(
|
|
4941
|
-
|
|
6091
|
+
chalk6.yellow(`
|
|
4942
6092
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
4943
6093
|
);
|
|
4944
|
-
console.error(
|
|
6094
|
+
console.error(chalk6.white(`
|
|
4945
6095
|
"${target}" uses its own hook system. Use:`));
|
|
4946
6096
|
console.error(
|
|
4947
|
-
|
|
6097
|
+
chalk6.green(` node9 addto ${target} `) + chalk6.gray("# one-time setup")
|
|
4948
6098
|
);
|
|
4949
|
-
console.error(
|
|
6099
|
+
console.error(chalk6.green(` ${target} `) + chalk6.gray("# run normally"));
|
|
4950
6100
|
process.exit(1);
|
|
4951
6101
|
}
|
|
4952
6102
|
const fullCommand = commandArgs.join(" ");
|
|
@@ -4954,7 +6104,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
4954
6104
|
agent: "Terminal"
|
|
4955
6105
|
});
|
|
4956
6106
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
4957
|
-
console.error(
|
|
6107
|
+
console.error(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
4958
6108
|
const daemonReady = await autoStartDaemonAndWait();
|
|
4959
6109
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
4960
6110
|
}
|
|
@@ -4963,12 +6113,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
4963
6113
|
}
|
|
4964
6114
|
if (!result.approved) {
|
|
4965
6115
|
console.error(
|
|
4966
|
-
|
|
6116
|
+
chalk6.red(`
|
|
4967
6117
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
4968
6118
|
);
|
|
4969
6119
|
process.exit(1);
|
|
4970
6120
|
}
|
|
4971
|
-
console.error(
|
|
6121
|
+
console.error(chalk6.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
4972
6122
|
await runProxy(fullCommand);
|
|
4973
6123
|
} else {
|
|
4974
6124
|
program.help();
|
|
@@ -4983,22 +6133,22 @@ program.command("undo").description(
|
|
|
4983
6133
|
if (history.length === 0) {
|
|
4984
6134
|
if (!options.all && allHistory.length > 0) {
|
|
4985
6135
|
console.log(
|
|
4986
|
-
|
|
6136
|
+
chalk6.yellow(
|
|
4987
6137
|
`
|
|
4988
6138
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
4989
|
-
Run ${
|
|
6139
|
+
Run ${chalk6.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
4990
6140
|
`
|
|
4991
6141
|
)
|
|
4992
6142
|
);
|
|
4993
6143
|
} else {
|
|
4994
|
-
console.log(
|
|
6144
|
+
console.log(chalk6.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
4995
6145
|
}
|
|
4996
6146
|
return;
|
|
4997
6147
|
}
|
|
4998
6148
|
const idx = history.length - steps;
|
|
4999
6149
|
if (idx < 0) {
|
|
5000
6150
|
console.log(
|
|
5001
|
-
|
|
6151
|
+
chalk6.yellow(
|
|
5002
6152
|
`
|
|
5003
6153
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
5004
6154
|
`
|
|
@@ -5009,18 +6159,18 @@ program.command("undo").description(
|
|
|
5009
6159
|
const snapshot = history[idx];
|
|
5010
6160
|
const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
|
|
5011
6161
|
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
|
|
5012
|
-
console.log(
|
|
6162
|
+
console.log(chalk6.magenta.bold(`
|
|
5013
6163
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
|
|
5014
6164
|
console.log(
|
|
5015
|
-
|
|
5016
|
-
` Tool: ${
|
|
6165
|
+
chalk6.white(
|
|
6166
|
+
` Tool: ${chalk6.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk6.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
5017
6167
|
)
|
|
5018
6168
|
);
|
|
5019
|
-
console.log(
|
|
5020
|
-
console.log(
|
|
6169
|
+
console.log(chalk6.white(` When: ${chalk6.gray(ageStr)}`));
|
|
6170
|
+
console.log(chalk6.white(` Dir: ${chalk6.gray(snapshot.cwd)}`));
|
|
5021
6171
|
if (steps > 1)
|
|
5022
6172
|
console.log(
|
|
5023
|
-
|
|
6173
|
+
chalk6.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
5024
6174
|
);
|
|
5025
6175
|
console.log("");
|
|
5026
6176
|
const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
@@ -5028,21 +6178,21 @@ program.command("undo").description(
|
|
|
5028
6178
|
const lines = diff.split("\n");
|
|
5029
6179
|
for (const line of lines) {
|
|
5030
6180
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
5031
|
-
console.log(
|
|
6181
|
+
console.log(chalk6.bold(line));
|
|
5032
6182
|
} else if (line.startsWith("+")) {
|
|
5033
|
-
console.log(
|
|
6183
|
+
console.log(chalk6.green(line));
|
|
5034
6184
|
} else if (line.startsWith("-")) {
|
|
5035
|
-
console.log(
|
|
6185
|
+
console.log(chalk6.red(line));
|
|
5036
6186
|
} else if (line.startsWith("@@")) {
|
|
5037
|
-
console.log(
|
|
6187
|
+
console.log(chalk6.cyan(line));
|
|
5038
6188
|
} else {
|
|
5039
|
-
console.log(
|
|
6189
|
+
console.log(chalk6.gray(line));
|
|
5040
6190
|
}
|
|
5041
6191
|
}
|
|
5042
6192
|
console.log("");
|
|
5043
6193
|
} else {
|
|
5044
6194
|
console.log(
|
|
5045
|
-
|
|
6195
|
+
chalk6.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
5046
6196
|
);
|
|
5047
6197
|
}
|
|
5048
6198
|
const proceed = await confirm3({
|
|
@@ -5051,21 +6201,111 @@ program.command("undo").description(
|
|
|
5051
6201
|
});
|
|
5052
6202
|
if (proceed) {
|
|
5053
6203
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
5054
|
-
console.log(
|
|
6204
|
+
console.log(chalk6.green("\n\u2705 Reverted successfully.\n"));
|
|
5055
6205
|
} else {
|
|
5056
|
-
console.error(
|
|
6206
|
+
console.error(chalk6.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
5057
6207
|
}
|
|
5058
6208
|
} else {
|
|
5059
|
-
console.log(
|
|
6209
|
+
console.log(chalk6.gray("\nCancelled.\n"));
|
|
6210
|
+
}
|
|
6211
|
+
});
|
|
6212
|
+
var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
|
|
6213
|
+
shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
|
|
6214
|
+
const name = resolveShieldName(service);
|
|
6215
|
+
if (!name) {
|
|
6216
|
+
console.error(chalk6.red(`
|
|
6217
|
+
\u274C Unknown shield: "${service}"
|
|
6218
|
+
`));
|
|
6219
|
+
console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
|
|
6220
|
+
`);
|
|
6221
|
+
process.exit(1);
|
|
6222
|
+
}
|
|
6223
|
+
const shield = getShield(name);
|
|
6224
|
+
const active = readActiveShields();
|
|
6225
|
+
if (active.includes(name)) {
|
|
6226
|
+
console.log(chalk6.yellow(`
|
|
6227
|
+
\u2139\uFE0F Shield "${name}" is already active.
|
|
6228
|
+
`));
|
|
6229
|
+
return;
|
|
6230
|
+
}
|
|
6231
|
+
writeActiveShields([...active, name]);
|
|
6232
|
+
console.log(chalk6.green(`
|
|
6233
|
+
\u{1F6E1}\uFE0F Shield "${name}" enabled.`));
|
|
6234
|
+
console.log(chalk6.gray(` ${shield.smartRules.length} smart rules now active.`));
|
|
6235
|
+
if (shield.dangerousWords.length > 0)
|
|
6236
|
+
console.log(chalk6.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
|
|
6237
|
+
if (name === "filesystem") {
|
|
6238
|
+
console.log(
|
|
6239
|
+
chalk6.yellow(
|
|
6240
|
+
`
|
|
6241
|
+
\u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
|
|
6242
|
+
Tools like unlink, find -delete, or language-level file ops are not intercepted.`
|
|
6243
|
+
)
|
|
6244
|
+
);
|
|
6245
|
+
}
|
|
6246
|
+
console.log("");
|
|
6247
|
+
});
|
|
6248
|
+
shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
|
|
6249
|
+
const name = resolveShieldName(service);
|
|
6250
|
+
if (!name) {
|
|
6251
|
+
console.error(chalk6.red(`
|
|
6252
|
+
\u274C Unknown shield: "${service}"
|
|
6253
|
+
`));
|
|
6254
|
+
console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
|
|
6255
|
+
`);
|
|
6256
|
+
process.exit(1);
|
|
5060
6257
|
}
|
|
6258
|
+
const active = readActiveShields();
|
|
6259
|
+
if (!active.includes(name)) {
|
|
6260
|
+
console.log(chalk6.yellow(`
|
|
6261
|
+
\u2139\uFE0F Shield "${name}" is not active.
|
|
6262
|
+
`));
|
|
6263
|
+
return;
|
|
6264
|
+
}
|
|
6265
|
+
writeActiveShields(active.filter((s) => s !== name));
|
|
6266
|
+
console.log(chalk6.green(`
|
|
6267
|
+
\u{1F6E1}\uFE0F Shield "${name}" disabled.
|
|
6268
|
+
`));
|
|
6269
|
+
});
|
|
6270
|
+
shieldCmd.command("list").description("Show all available shields").action(() => {
|
|
6271
|
+
const active = new Set(readActiveShields());
|
|
6272
|
+
console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
|
|
6273
|
+
for (const shield of listShields()) {
|
|
6274
|
+
const status = active.has(shield.name) ? chalk6.green("\u25CF enabled") : chalk6.gray("\u25CB disabled");
|
|
6275
|
+
console.log(` ${status} ${chalk6.cyan(shield.name.padEnd(12))} ${shield.description}`);
|
|
6276
|
+
if (shield.aliases.length > 0)
|
|
6277
|
+
console.log(chalk6.gray(` aliases: ${shield.aliases.join(", ")}`));
|
|
6278
|
+
}
|
|
6279
|
+
console.log("");
|
|
6280
|
+
});
|
|
6281
|
+
shieldCmd.command("status").description("Show which shields are currently active").action(() => {
|
|
6282
|
+
const active = readActiveShields();
|
|
6283
|
+
if (active.length === 0) {
|
|
6284
|
+
console.log(chalk6.yellow("\n\u2139\uFE0F No shields are active.\n"));
|
|
6285
|
+
console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
|
|
6286
|
+
`);
|
|
6287
|
+
return;
|
|
6288
|
+
}
|
|
6289
|
+
console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
|
|
6290
|
+
for (const name of active) {
|
|
6291
|
+
const shield = getShield(name);
|
|
6292
|
+
if (!shield) continue;
|
|
6293
|
+
console.log(` ${chalk6.green("\u25CF")} ${chalk6.cyan(name)}`);
|
|
6294
|
+
console.log(
|
|
6295
|
+
chalk6.gray(
|
|
6296
|
+
` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
|
|
6297
|
+
)
|
|
6298
|
+
);
|
|
6299
|
+
}
|
|
6300
|
+
console.log("");
|
|
5061
6301
|
});
|
|
5062
6302
|
process.on("unhandledRejection", (reason) => {
|
|
5063
6303
|
const isCheckHook = process.argv[2] === "check";
|
|
5064
6304
|
if (isCheckHook) {
|
|
5065
6305
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
5066
|
-
const logPath =
|
|
6306
|
+
const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
5067
6307
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
5068
|
-
|
|
6308
|
+
fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
5069
6309
|
`);
|
|
5070
6310
|
}
|
|
5071
6311
|
process.exit(0);
|