@node9/proxy 1.0.14 → 1.0.16
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 +99 -19
- package/dist/cli.js +2139 -1194
- package/dist/cli.mjs +2120 -1173
- package/dist/index.js +93 -16
- package/dist/index.mjs +93 -16
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
9
16
|
var __copyProps = (to, from, except, desc) => {
|
|
10
17
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
18
|
for (let key of __getOwnPropNames(from))
|
|
@@ -23,25 +30,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
30
|
mod
|
|
24
31
|
));
|
|
25
32
|
|
|
26
|
-
// src/cli.ts
|
|
27
|
-
var import_commander = require("commander");
|
|
28
|
-
|
|
29
|
-
// src/core.ts
|
|
30
|
-
var import_chalk2 = __toESM(require("chalk"));
|
|
31
|
-
var import_prompts = require("@inquirer/prompts");
|
|
32
|
-
var import_fs2 = __toESM(require("fs"));
|
|
33
|
-
var import_path4 = __toESM(require("path"));
|
|
34
|
-
var import_os2 = __toESM(require("os"));
|
|
35
|
-
var import_picomatch = __toESM(require("picomatch"));
|
|
36
|
-
var import_sh_syntax = require("sh-syntax");
|
|
37
|
-
|
|
38
|
-
// src/ui/native.ts
|
|
39
|
-
var import_child_process = require("child_process");
|
|
40
|
-
var import_path2 = __toESM(require("path"));
|
|
41
|
-
var import_chalk = __toESM(require("chalk"));
|
|
42
|
-
|
|
43
33
|
// src/context-sniper.ts
|
|
44
|
-
var import_path = __toESM(require("path"));
|
|
45
34
|
function smartTruncate(str, maxLen = 500) {
|
|
46
35
|
if (str.length <= maxLen) return str;
|
|
47
36
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -71,22 +60,6 @@ function extractContext(text, matchedWord) {
|
|
|
71
60
|
... [${lines.length - end} lines hidden] ...` : "";
|
|
72
61
|
return { snippet: `${head}${snippet}${tail}`, lineIndex };
|
|
73
62
|
}
|
|
74
|
-
var CODE_KEYS = [
|
|
75
|
-
"command",
|
|
76
|
-
"cmd",
|
|
77
|
-
"shell_command",
|
|
78
|
-
"bash_command",
|
|
79
|
-
"script",
|
|
80
|
-
"code",
|
|
81
|
-
"input",
|
|
82
|
-
"sql",
|
|
83
|
-
"query",
|
|
84
|
-
"arguments",
|
|
85
|
-
"args",
|
|
86
|
-
"param",
|
|
87
|
-
"params",
|
|
88
|
-
"text"
|
|
89
|
-
];
|
|
90
63
|
function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
|
|
91
64
|
let intent = "EXEC";
|
|
92
65
|
let contextSnippet;
|
|
@@ -141,11 +114,31 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
141
114
|
...ruleName && { ruleName }
|
|
142
115
|
};
|
|
143
116
|
}
|
|
117
|
+
var import_path, CODE_KEYS;
|
|
118
|
+
var init_context_sniper = __esm({
|
|
119
|
+
"src/context-sniper.ts"() {
|
|
120
|
+
"use strict";
|
|
121
|
+
import_path = __toESM(require("path"));
|
|
122
|
+
CODE_KEYS = [
|
|
123
|
+
"command",
|
|
124
|
+
"cmd",
|
|
125
|
+
"shell_command",
|
|
126
|
+
"bash_command",
|
|
127
|
+
"script",
|
|
128
|
+
"code",
|
|
129
|
+
"input",
|
|
130
|
+
"sql",
|
|
131
|
+
"query",
|
|
132
|
+
"arguments",
|
|
133
|
+
"args",
|
|
134
|
+
"param",
|
|
135
|
+
"params",
|
|
136
|
+
"text"
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
});
|
|
144
140
|
|
|
145
141
|
// src/ui/native.ts
|
|
146
|
-
var isTestEnv = () => {
|
|
147
|
-
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";
|
|
148
|
-
};
|
|
149
142
|
function formatArgs(args, matchedField, matchedWord) {
|
|
150
143
|
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
151
144
|
let parsed = args;
|
|
@@ -355,81 +348,21 @@ end run`;
|
|
|
355
348
|
}
|
|
356
349
|
});
|
|
357
350
|
}
|
|
351
|
+
var import_child_process, import_path2, import_chalk, isTestEnv;
|
|
352
|
+
var init_native = __esm({
|
|
353
|
+
"src/ui/native.ts"() {
|
|
354
|
+
"use strict";
|
|
355
|
+
import_child_process = require("child_process");
|
|
356
|
+
import_path2 = __toESM(require("path"));
|
|
357
|
+
import_chalk = __toESM(require("chalk"));
|
|
358
|
+
init_context_sniper();
|
|
359
|
+
isTestEnv = () => {
|
|
360
|
+
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";
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
});
|
|
358
364
|
|
|
359
365
|
// src/config-schema.ts
|
|
360
|
-
var import_zod = require("zod");
|
|
361
|
-
var noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
362
|
-
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
363
|
-
});
|
|
364
|
-
var SmartConditionSchema = import_zod.z.object({
|
|
365
|
-
field: import_zod.z.string().min(1, "Condition field must not be empty"),
|
|
366
|
-
op: import_zod.z.enum(
|
|
367
|
-
[
|
|
368
|
-
"matches",
|
|
369
|
-
"notMatches",
|
|
370
|
-
"contains",
|
|
371
|
-
"notContains",
|
|
372
|
-
"exists",
|
|
373
|
-
"notExists",
|
|
374
|
-
"matchesGlob",
|
|
375
|
-
"notMatchesGlob"
|
|
376
|
-
],
|
|
377
|
-
{
|
|
378
|
-
errorMap: () => ({
|
|
379
|
-
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
380
|
-
})
|
|
381
|
-
}
|
|
382
|
-
),
|
|
383
|
-
value: import_zod.z.string().optional(),
|
|
384
|
-
flags: import_zod.z.string().optional()
|
|
385
|
-
});
|
|
386
|
-
var SmartRuleSchema = import_zod.z.object({
|
|
387
|
-
name: import_zod.z.string().optional(),
|
|
388
|
-
tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
|
|
389
|
-
conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
390
|
-
conditionMode: import_zod.z.enum(["all", "any"]).optional(),
|
|
391
|
-
verdict: import_zod.z.enum(["allow", "review", "block"], {
|
|
392
|
-
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
393
|
-
}),
|
|
394
|
-
reason: import_zod.z.string().optional()
|
|
395
|
-
});
|
|
396
|
-
var ConfigFileSchema = import_zod.z.object({
|
|
397
|
-
version: import_zod.z.string().optional(),
|
|
398
|
-
settings: import_zod.z.object({
|
|
399
|
-
mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
|
|
400
|
-
autoStartDaemon: import_zod.z.boolean().optional(),
|
|
401
|
-
enableUndo: import_zod.z.boolean().optional(),
|
|
402
|
-
enableHookLogDebug: import_zod.z.boolean().optional(),
|
|
403
|
-
approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
|
|
404
|
-
approvers: import_zod.z.object({
|
|
405
|
-
native: import_zod.z.boolean().optional(),
|
|
406
|
-
browser: import_zod.z.boolean().optional(),
|
|
407
|
-
cloud: import_zod.z.boolean().optional(),
|
|
408
|
-
terminal: import_zod.z.boolean().optional()
|
|
409
|
-
}).optional(),
|
|
410
|
-
environment: import_zod.z.string().optional(),
|
|
411
|
-
slackEnabled: import_zod.z.boolean().optional(),
|
|
412
|
-
enableTrustSessions: import_zod.z.boolean().optional(),
|
|
413
|
-
allowGlobalPause: import_zod.z.boolean().optional()
|
|
414
|
-
}).optional(),
|
|
415
|
-
policy: import_zod.z.object({
|
|
416
|
-
sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
417
|
-
dangerousWords: import_zod.z.array(noNewlines).optional(),
|
|
418
|
-
ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
419
|
-
toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
|
|
420
|
-
smartRules: import_zod.z.array(SmartRuleSchema).optional(),
|
|
421
|
-
snapshot: import_zod.z.object({
|
|
422
|
-
tools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
423
|
-
onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
424
|
-
ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
425
|
-
}).optional(),
|
|
426
|
-
dlp: import_zod.z.object({
|
|
427
|
-
enabled: import_zod.z.boolean().optional(),
|
|
428
|
-
scanIgnoredTools: import_zod.z.boolean().optional()
|
|
429
|
-
}).optional()
|
|
430
|
-
}).optional(),
|
|
431
|
-
environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
432
|
-
}).strict({ message: "Config contains unknown top-level keys" });
|
|
433
366
|
function sanitizeConfig(raw) {
|
|
434
367
|
const result = ConfigFileSchema.safeParse(raw);
|
|
435
368
|
if (result.success) {
|
|
@@ -447,8 +380,8 @@ function sanitizeConfig(raw) {
|
|
|
447
380
|
}
|
|
448
381
|
}
|
|
449
382
|
const lines = result.error.issues.map((issue) => {
|
|
450
|
-
const
|
|
451
|
-
return ` \u2022 ${
|
|
383
|
+
const path10 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
384
|
+
return ` \u2022 ${path10}: ${issue.message}`;
|
|
452
385
|
});
|
|
453
386
|
return {
|
|
454
387
|
sanitized,
|
|
@@ -456,179 +389,94 @@ function sanitizeConfig(raw) {
|
|
|
456
389
|
${lines.join("\n")}`
|
|
457
390
|
};
|
|
458
391
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
name: "shield:postgres:block-truncate",
|
|
480
|
-
tool: "*",
|
|
481
|
-
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
482
|
-
verdict: "block",
|
|
483
|
-
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
484
|
-
},
|
|
485
|
-
{
|
|
486
|
-
name: "shield:postgres:block-drop-column",
|
|
487
|
-
tool: "*",
|
|
488
|
-
conditions: [
|
|
489
|
-
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
490
|
-
],
|
|
491
|
-
verdict: "block",
|
|
492
|
-
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
493
|
-
},
|
|
494
|
-
{
|
|
495
|
-
name: "shield:postgres:review-grant-revoke",
|
|
496
|
-
tool: "*",
|
|
497
|
-
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
498
|
-
verdict: "review",
|
|
499
|
-
reason: "Permission changes require human approval (Postgres shield)"
|
|
500
|
-
}
|
|
501
|
-
],
|
|
502
|
-
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
503
|
-
},
|
|
504
|
-
github: {
|
|
505
|
-
name: "github",
|
|
506
|
-
description: "Protects GitHub repositories from destructive AI operations",
|
|
507
|
-
aliases: ["git"],
|
|
508
|
-
smartRules: [
|
|
509
|
-
{
|
|
510
|
-
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
511
|
-
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
512
|
-
name: "shield:github:review-delete-branch-remote",
|
|
513
|
-
tool: "bash",
|
|
514
|
-
conditions: [
|
|
515
|
-
{
|
|
516
|
-
field: "command",
|
|
517
|
-
op: "matches",
|
|
518
|
-
value: "git\\s+push\\s+.*--delete",
|
|
519
|
-
flags: "i"
|
|
520
|
-
}
|
|
521
|
-
],
|
|
522
|
-
verdict: "review",
|
|
523
|
-
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
524
|
-
},
|
|
525
|
-
{
|
|
526
|
-
name: "shield:github:block-delete-repo",
|
|
527
|
-
tool: "*",
|
|
528
|
-
conditions: [
|
|
529
|
-
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
530
|
-
],
|
|
531
|
-
verdict: "block",
|
|
532
|
-
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
533
|
-
}
|
|
534
|
-
],
|
|
535
|
-
dangerousWords: []
|
|
536
|
-
},
|
|
537
|
-
aws: {
|
|
538
|
-
name: "aws",
|
|
539
|
-
description: "Protects AWS infrastructure from destructive AI operations",
|
|
540
|
-
aliases: ["amazon"],
|
|
541
|
-
smartRules: [
|
|
542
|
-
{
|
|
543
|
-
name: "shield:aws:block-delete-s3-bucket",
|
|
544
|
-
tool: "*",
|
|
545
|
-
conditions: [
|
|
546
|
-
{
|
|
547
|
-
field: "command",
|
|
548
|
-
op: "matches",
|
|
549
|
-
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
550
|
-
flags: "i"
|
|
551
|
-
}
|
|
552
|
-
],
|
|
553
|
-
verdict: "block",
|
|
554
|
-
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
555
|
-
},
|
|
556
|
-
{
|
|
557
|
-
name: "shield:aws:review-iam-changes",
|
|
558
|
-
tool: "*",
|
|
559
|
-
conditions: [
|
|
560
|
-
{
|
|
561
|
-
field: "command",
|
|
562
|
-
op: "matches",
|
|
563
|
-
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
564
|
-
flags: "i"
|
|
565
|
-
}
|
|
566
|
-
],
|
|
567
|
-
verdict: "review",
|
|
568
|
-
reason: "IAM changes require human approval (AWS shield)"
|
|
569
|
-
},
|
|
570
|
-
{
|
|
571
|
-
name: "shield:aws:block-ec2-terminate",
|
|
572
|
-
tool: "*",
|
|
573
|
-
conditions: [
|
|
574
|
-
{
|
|
575
|
-
field: "command",
|
|
576
|
-
op: "matches",
|
|
577
|
-
value: "aws\\s+ec2\\s+terminate-instances",
|
|
578
|
-
flags: "i"
|
|
579
|
-
}
|
|
580
|
-
],
|
|
581
|
-
verdict: "block",
|
|
582
|
-
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
583
|
-
},
|
|
584
|
-
{
|
|
585
|
-
name: "shield:aws:review-rds-delete",
|
|
586
|
-
tool: "*",
|
|
587
|
-
conditions: [
|
|
588
|
-
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
589
|
-
],
|
|
590
|
-
verdict: "review",
|
|
591
|
-
reason: "RDS deletion requires human approval (AWS shield)"
|
|
592
|
-
}
|
|
593
|
-
],
|
|
594
|
-
dangerousWords: []
|
|
595
|
-
},
|
|
596
|
-
filesystem: {
|
|
597
|
-
name: "filesystem",
|
|
598
|
-
description: "Protects the local filesystem from dangerous AI operations",
|
|
599
|
-
aliases: ["fs"],
|
|
600
|
-
smartRules: [
|
|
601
|
-
{
|
|
602
|
-
name: "shield:filesystem:review-chmod-777",
|
|
603
|
-
tool: "bash",
|
|
604
|
-
conditions: [
|
|
605
|
-
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
392
|
+
var import_zod, noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
|
|
393
|
+
var init_config_schema = __esm({
|
|
394
|
+
"src/config-schema.ts"() {
|
|
395
|
+
"use strict";
|
|
396
|
+
import_zod = require("zod");
|
|
397
|
+
noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
398
|
+
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
399
|
+
});
|
|
400
|
+
SmartConditionSchema = import_zod.z.object({
|
|
401
|
+
field: import_zod.z.string().min(1, "Condition field must not be empty"),
|
|
402
|
+
op: import_zod.z.enum(
|
|
403
|
+
[
|
|
404
|
+
"matches",
|
|
405
|
+
"notMatches",
|
|
406
|
+
"contains",
|
|
407
|
+
"notContains",
|
|
408
|
+
"exists",
|
|
409
|
+
"notExists",
|
|
410
|
+
"matchesGlob",
|
|
411
|
+
"notMatchesGlob"
|
|
606
412
|
],
|
|
607
|
-
|
|
608
|
-
|
|
413
|
+
{
|
|
414
|
+
errorMap: () => ({
|
|
415
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
),
|
|
419
|
+
value: import_zod.z.string().optional(),
|
|
420
|
+
flags: import_zod.z.string().optional()
|
|
421
|
+
}).refine(
|
|
422
|
+
(c) => {
|
|
423
|
+
if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
|
|
424
|
+
return true;
|
|
609
425
|
},
|
|
610
|
-
{
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
426
|
+
{ message: "matchesGlob and notMatchesGlob conditions require a value field" }
|
|
427
|
+
);
|
|
428
|
+
SmartRuleSchema = import_zod.z.object({
|
|
429
|
+
name: import_zod.z.string().optional(),
|
|
430
|
+
tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
|
|
431
|
+
conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
432
|
+
conditionMode: import_zod.z.enum(["all", "any"]).optional(),
|
|
433
|
+
verdict: import_zod.z.enum(["allow", "review", "block"], {
|
|
434
|
+
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
435
|
+
}),
|
|
436
|
+
reason: import_zod.z.string().optional()
|
|
437
|
+
});
|
|
438
|
+
ConfigFileSchema = import_zod.z.object({
|
|
439
|
+
version: import_zod.z.string().optional(),
|
|
440
|
+
settings: import_zod.z.object({
|
|
441
|
+
mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
|
|
442
|
+
autoStartDaemon: import_zod.z.boolean().optional(),
|
|
443
|
+
enableUndo: import_zod.z.boolean().optional(),
|
|
444
|
+
enableHookLogDebug: import_zod.z.boolean().optional(),
|
|
445
|
+
approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
|
|
446
|
+
flightRecorder: import_zod.z.boolean().optional(),
|
|
447
|
+
approvers: import_zod.z.object({
|
|
448
|
+
native: import_zod.z.boolean().optional(),
|
|
449
|
+
browser: import_zod.z.boolean().optional(),
|
|
450
|
+
cloud: import_zod.z.boolean().optional(),
|
|
451
|
+
terminal: import_zod.z.boolean().optional()
|
|
452
|
+
}).optional(),
|
|
453
|
+
environment: import_zod.z.string().optional(),
|
|
454
|
+
slackEnabled: import_zod.z.boolean().optional(),
|
|
455
|
+
enableTrustSessions: import_zod.z.boolean().optional(),
|
|
456
|
+
allowGlobalPause: import_zod.z.boolean().optional()
|
|
457
|
+
}).optional(),
|
|
458
|
+
policy: import_zod.z.object({
|
|
459
|
+
sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
460
|
+
dangerousWords: import_zod.z.array(noNewlines).optional(),
|
|
461
|
+
ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
462
|
+
toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
|
|
463
|
+
smartRules: import_zod.z.array(SmartRuleSchema).optional(),
|
|
464
|
+
snapshot: import_zod.z.object({
|
|
465
|
+
tools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
466
|
+
onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
467
|
+
ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
468
|
+
}).optional(),
|
|
469
|
+
dlp: import_zod.z.object({
|
|
470
|
+
enabled: import_zod.z.boolean().optional(),
|
|
471
|
+
scanIgnoredTools: import_zod.z.boolean().optional()
|
|
472
|
+
}).optional()
|
|
473
|
+
}).optional(),
|
|
474
|
+
environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
475
|
+
}).strict({ message: "Config contains unknown top-level keys" });
|
|
630
476
|
}
|
|
631
|
-
};
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// src/shields.ts
|
|
632
480
|
function resolveShieldName(input) {
|
|
633
481
|
const lower = input.toLowerCase();
|
|
634
482
|
if (SHIELDS[lower]) return lower;
|
|
@@ -644,7 +492,6 @@ function getShield(name) {
|
|
|
644
492
|
function listShields() {
|
|
645
493
|
return Object.values(SHIELDS);
|
|
646
494
|
}
|
|
647
|
-
var SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
|
|
648
495
|
function readActiveShields() {
|
|
649
496
|
try {
|
|
650
497
|
const raw = import_fs.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
@@ -669,21 +516,186 @@ function writeActiveShields(active) {
|
|
|
669
516
|
import_fs.default.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
|
|
670
517
|
import_fs.default.renameSync(tmp, SHIELDS_STATE_FILE);
|
|
671
518
|
}
|
|
519
|
+
var import_fs, import_path3, import_os, import_crypto, SHIELDS, SHIELDS_STATE_FILE;
|
|
520
|
+
var init_shields = __esm({
|
|
521
|
+
"src/shields.ts"() {
|
|
522
|
+
"use strict";
|
|
523
|
+
import_fs = __toESM(require("fs"));
|
|
524
|
+
import_path3 = __toESM(require("path"));
|
|
525
|
+
import_os = __toESM(require("os"));
|
|
526
|
+
import_crypto = __toESM(require("crypto"));
|
|
527
|
+
SHIELDS = {
|
|
528
|
+
postgres: {
|
|
529
|
+
name: "postgres",
|
|
530
|
+
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
531
|
+
aliases: ["pg", "postgresql"],
|
|
532
|
+
smartRules: [
|
|
533
|
+
{
|
|
534
|
+
name: "shield:postgres:block-drop-table",
|
|
535
|
+
tool: "*",
|
|
536
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
537
|
+
verdict: "block",
|
|
538
|
+
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "shield:postgres:block-truncate",
|
|
542
|
+
tool: "*",
|
|
543
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
544
|
+
verdict: "block",
|
|
545
|
+
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
name: "shield:postgres:block-drop-column",
|
|
549
|
+
tool: "*",
|
|
550
|
+
conditions: [
|
|
551
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
552
|
+
],
|
|
553
|
+
verdict: "block",
|
|
554
|
+
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
name: "shield:postgres:review-grant-revoke",
|
|
558
|
+
tool: "*",
|
|
559
|
+
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
560
|
+
verdict: "review",
|
|
561
|
+
reason: "Permission changes require human approval (Postgres shield)"
|
|
562
|
+
}
|
|
563
|
+
],
|
|
564
|
+
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
565
|
+
},
|
|
566
|
+
github: {
|
|
567
|
+
name: "github",
|
|
568
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
569
|
+
aliases: ["git"],
|
|
570
|
+
smartRules: [
|
|
571
|
+
{
|
|
572
|
+
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
573
|
+
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
574
|
+
name: "shield:github:review-delete-branch-remote",
|
|
575
|
+
tool: "bash",
|
|
576
|
+
conditions: [
|
|
577
|
+
{
|
|
578
|
+
field: "command",
|
|
579
|
+
op: "matches",
|
|
580
|
+
value: "git\\s+push\\s+.*--delete",
|
|
581
|
+
flags: "i"
|
|
582
|
+
}
|
|
583
|
+
],
|
|
584
|
+
verdict: "review",
|
|
585
|
+
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
name: "shield:github:block-delete-repo",
|
|
589
|
+
tool: "*",
|
|
590
|
+
conditions: [
|
|
591
|
+
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
592
|
+
],
|
|
593
|
+
verdict: "block",
|
|
594
|
+
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
595
|
+
}
|
|
596
|
+
],
|
|
597
|
+
dangerousWords: []
|
|
598
|
+
},
|
|
599
|
+
aws: {
|
|
600
|
+
name: "aws",
|
|
601
|
+
description: "Protects AWS infrastructure from destructive AI operations",
|
|
602
|
+
aliases: ["amazon"],
|
|
603
|
+
smartRules: [
|
|
604
|
+
{
|
|
605
|
+
name: "shield:aws:block-delete-s3-bucket",
|
|
606
|
+
tool: "*",
|
|
607
|
+
conditions: [
|
|
608
|
+
{
|
|
609
|
+
field: "command",
|
|
610
|
+
op: "matches",
|
|
611
|
+
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
612
|
+
flags: "i"
|
|
613
|
+
}
|
|
614
|
+
],
|
|
615
|
+
verdict: "block",
|
|
616
|
+
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: "shield:aws:review-iam-changes",
|
|
620
|
+
tool: "*",
|
|
621
|
+
conditions: [
|
|
622
|
+
{
|
|
623
|
+
field: "command",
|
|
624
|
+
op: "matches",
|
|
625
|
+
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
626
|
+
flags: "i"
|
|
627
|
+
}
|
|
628
|
+
],
|
|
629
|
+
verdict: "review",
|
|
630
|
+
reason: "IAM changes require human approval (AWS shield)"
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: "shield:aws:block-ec2-terminate",
|
|
634
|
+
tool: "*",
|
|
635
|
+
conditions: [
|
|
636
|
+
{
|
|
637
|
+
field: "command",
|
|
638
|
+
op: "matches",
|
|
639
|
+
value: "aws\\s+ec2\\s+terminate-instances",
|
|
640
|
+
flags: "i"
|
|
641
|
+
}
|
|
642
|
+
],
|
|
643
|
+
verdict: "block",
|
|
644
|
+
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: "shield:aws:review-rds-delete",
|
|
648
|
+
tool: "*",
|
|
649
|
+
conditions: [
|
|
650
|
+
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
651
|
+
],
|
|
652
|
+
verdict: "review",
|
|
653
|
+
reason: "RDS deletion requires human approval (AWS shield)"
|
|
654
|
+
}
|
|
655
|
+
],
|
|
656
|
+
dangerousWords: []
|
|
657
|
+
},
|
|
658
|
+
filesystem: {
|
|
659
|
+
name: "filesystem",
|
|
660
|
+
description: "Protects the local filesystem from dangerous AI operations",
|
|
661
|
+
aliases: ["fs"],
|
|
662
|
+
smartRules: [
|
|
663
|
+
{
|
|
664
|
+
name: "shield:filesystem:review-chmod-777",
|
|
665
|
+
tool: "bash",
|
|
666
|
+
conditions: [
|
|
667
|
+
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
668
|
+
],
|
|
669
|
+
verdict: "review",
|
|
670
|
+
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
name: "shield:filesystem:review-write-etc",
|
|
674
|
+
tool: "bash",
|
|
675
|
+
conditions: [
|
|
676
|
+
{
|
|
677
|
+
field: "command",
|
|
678
|
+
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
679
|
+
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
680
|
+
op: "matches",
|
|
681
|
+
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
682
|
+
}
|
|
683
|
+
],
|
|
684
|
+
verdict: "review",
|
|
685
|
+
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
686
|
+
}
|
|
687
|
+
],
|
|
688
|
+
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
689
|
+
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
690
|
+
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
691
|
+
dangerousWords: ["wipefs"]
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
|
|
695
|
+
}
|
|
696
|
+
});
|
|
672
697
|
|
|
673
698
|
// src/dlp.ts
|
|
674
|
-
var DLP_PATTERNS = [
|
|
675
|
-
{ name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
|
|
676
|
-
{ name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
|
|
677
|
-
{ name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
|
|
678
|
-
{ name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
|
|
679
|
-
{ name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
|
|
680
|
-
{
|
|
681
|
-
name: "Private Key (PEM)",
|
|
682
|
-
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
683
|
-
severity: "block"
|
|
684
|
-
},
|
|
685
|
-
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
|
|
686
|
-
];
|
|
687
699
|
function maskSecret(raw, pattern) {
|
|
688
700
|
const match = raw.match(pattern);
|
|
689
701
|
if (!match) return "****";
|
|
@@ -694,9 +706,6 @@ function maskSecret(raw, pattern) {
|
|
|
694
706
|
const stars = "*".repeat(Math.min(secret.length - 8, 12));
|
|
695
707
|
return `${prefix}${stars}${suffix}`;
|
|
696
708
|
}
|
|
697
|
-
var MAX_DEPTH = 5;
|
|
698
|
-
var MAX_STRING_BYTES = 1e5;
|
|
699
|
-
var MAX_JSON_PARSE_BYTES = 1e4;
|
|
700
709
|
function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
701
710
|
if (depth > MAX_DEPTH || args === null || args === void 0) return null;
|
|
702
711
|
if (Array.isArray(args)) {
|
|
@@ -739,12 +748,30 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
739
748
|
}
|
|
740
749
|
return null;
|
|
741
750
|
}
|
|
751
|
+
var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
|
|
752
|
+
var init_dlp = __esm({
|
|
753
|
+
"src/dlp.ts"() {
|
|
754
|
+
"use strict";
|
|
755
|
+
DLP_PATTERNS = [
|
|
756
|
+
{ name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
|
|
757
|
+
{ name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
|
|
758
|
+
{ name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
|
|
759
|
+
{ name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
|
|
760
|
+
{ name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
|
|
761
|
+
{
|
|
762
|
+
name: "Private Key (PEM)",
|
|
763
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
764
|
+
severity: "block"
|
|
765
|
+
},
|
|
766
|
+
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
|
|
767
|
+
];
|
|
768
|
+
MAX_DEPTH = 5;
|
|
769
|
+
MAX_STRING_BYTES = 1e5;
|
|
770
|
+
MAX_JSON_PARSE_BYTES = 1e4;
|
|
771
|
+
}
|
|
772
|
+
});
|
|
742
773
|
|
|
743
774
|
// src/core.ts
|
|
744
|
-
var PAUSED_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
|
|
745
|
-
var TRUST_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "trust.json");
|
|
746
|
-
var LOCAL_AUDIT_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "audit.log");
|
|
747
|
-
var HOOK_DEBUG_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
|
|
748
775
|
function checkPause() {
|
|
749
776
|
try {
|
|
750
777
|
if (!import_fs2.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -857,9 +884,9 @@ function matchesPattern(text, patterns) {
|
|
|
857
884
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
858
885
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
859
886
|
}
|
|
860
|
-
function getNestedValue(obj,
|
|
887
|
+
function getNestedValue(obj, path10) {
|
|
861
888
|
if (!obj || typeof obj !== "object") return null;
|
|
862
|
-
return
|
|
889
|
+
return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
863
890
|
}
|
|
864
891
|
function shouldSnapshot(toolName, args, config) {
|
|
865
892
|
if (!config.settings.enableUndo) return false;
|
|
@@ -907,7 +934,7 @@ function evaluateSmartConditions(args, rule) {
|
|
|
907
934
|
case "matchesGlob":
|
|
908
935
|
return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
|
|
909
936
|
case "notMatchesGlob":
|
|
910
|
-
return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) :
|
|
937
|
+
return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
|
|
911
938
|
default:
|
|
912
939
|
return false;
|
|
913
940
|
}
|
|
@@ -929,7 +956,6 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
929
956
|
const fieldName = toolInspection[matchingPattern];
|
|
930
957
|
return fieldName === "sql" || fieldName === "query";
|
|
931
958
|
}
|
|
932
|
-
var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
933
959
|
async function analyzeShellCommand(command) {
|
|
934
960
|
const actions = [];
|
|
935
961
|
const paths = [];
|
|
@@ -1011,208 +1037,6 @@ function redactSecrets(text) {
|
|
|
1011
1037
|
);
|
|
1012
1038
|
return redacted;
|
|
1013
1039
|
}
|
|
1014
|
-
var DANGEROUS_WORDS = [
|
|
1015
|
-
"mkfs",
|
|
1016
|
-
// formats/wipes a filesystem partition
|
|
1017
|
-
"shred"
|
|
1018
|
-
// permanently overwrites file contents (unrecoverable)
|
|
1019
|
-
];
|
|
1020
|
-
var DEFAULT_CONFIG = {
|
|
1021
|
-
settings: {
|
|
1022
|
-
mode: "standard",
|
|
1023
|
-
autoStartDaemon: true,
|
|
1024
|
-
enableUndo: true,
|
|
1025
|
-
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
1026
|
-
enableHookLogDebug: false,
|
|
1027
|
-
approvalTimeoutMs: 0,
|
|
1028
|
-
// 0 = disabled; set e.g. 30000 for 30-second auto-deny
|
|
1029
|
-
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
1030
|
-
},
|
|
1031
|
-
policy: {
|
|
1032
|
-
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
1033
|
-
dangerousWords: DANGEROUS_WORDS,
|
|
1034
|
-
ignoredTools: [
|
|
1035
|
-
"list_*",
|
|
1036
|
-
"get_*",
|
|
1037
|
-
"read_*",
|
|
1038
|
-
"describe_*",
|
|
1039
|
-
"read",
|
|
1040
|
-
"glob",
|
|
1041
|
-
"grep",
|
|
1042
|
-
"ls",
|
|
1043
|
-
"notebookread",
|
|
1044
|
-
"notebookedit",
|
|
1045
|
-
"webfetch",
|
|
1046
|
-
"websearch",
|
|
1047
|
-
"exitplanmode",
|
|
1048
|
-
"askuserquestion",
|
|
1049
|
-
"agent",
|
|
1050
|
-
"task*",
|
|
1051
|
-
"toolsearch",
|
|
1052
|
-
"mcp__ide__*",
|
|
1053
|
-
"getDiagnostics"
|
|
1054
|
-
],
|
|
1055
|
-
toolInspection: {
|
|
1056
|
-
bash: "command",
|
|
1057
|
-
shell: "command",
|
|
1058
|
-
run_shell_command: "command",
|
|
1059
|
-
"terminal.execute": "command",
|
|
1060
|
-
"postgres:query": "sql"
|
|
1061
|
-
},
|
|
1062
|
-
snapshot: {
|
|
1063
|
-
tools: [
|
|
1064
|
-
"str_replace_based_edit_tool",
|
|
1065
|
-
"write_file",
|
|
1066
|
-
"edit_file",
|
|
1067
|
-
"create_file",
|
|
1068
|
-
"edit",
|
|
1069
|
-
"replace"
|
|
1070
|
-
],
|
|
1071
|
-
onlyPaths: [],
|
|
1072
|
-
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
1073
|
-
},
|
|
1074
|
-
smartRules: [
|
|
1075
|
-
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
1076
|
-
{
|
|
1077
|
-
name: "block-rm-rf-home",
|
|
1078
|
-
tool: "bash",
|
|
1079
|
-
conditionMode: "all",
|
|
1080
|
-
conditions: [
|
|
1081
|
-
{
|
|
1082
|
-
field: "command",
|
|
1083
|
-
op: "matches",
|
|
1084
|
-
value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
|
|
1085
|
-
},
|
|
1086
|
-
{
|
|
1087
|
-
field: "command",
|
|
1088
|
-
op: "matches",
|
|
1089
|
-
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
1090
|
-
}
|
|
1091
|
-
],
|
|
1092
|
-
verdict: "block",
|
|
1093
|
-
reason: "Recursive delete of home directory is irreversible"
|
|
1094
|
-
},
|
|
1095
|
-
// ── SQL safety ────────────────────────────────────────────────────────
|
|
1096
|
-
{
|
|
1097
|
-
name: "no-delete-without-where",
|
|
1098
|
-
tool: "*",
|
|
1099
|
-
conditions: [
|
|
1100
|
-
{ field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
|
|
1101
|
-
{ field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
|
|
1102
|
-
],
|
|
1103
|
-
conditionMode: "all",
|
|
1104
|
-
verdict: "review",
|
|
1105
|
-
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
1106
|
-
},
|
|
1107
|
-
{
|
|
1108
|
-
name: "review-drop-truncate-shell",
|
|
1109
|
-
tool: "bash",
|
|
1110
|
-
conditions: [
|
|
1111
|
-
{
|
|
1112
|
-
field: "command",
|
|
1113
|
-
op: "matches",
|
|
1114
|
-
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
1115
|
-
flags: "i"
|
|
1116
|
-
}
|
|
1117
|
-
],
|
|
1118
|
-
conditionMode: "all",
|
|
1119
|
-
verdict: "review",
|
|
1120
|
-
reason: "SQL DDL destructive statement inside a shell command"
|
|
1121
|
-
},
|
|
1122
|
-
// ── Git safety ────────────────────────────────────────────────────────
|
|
1123
|
-
{
|
|
1124
|
-
name: "block-force-push",
|
|
1125
|
-
tool: "bash",
|
|
1126
|
-
conditions: [
|
|
1127
|
-
{
|
|
1128
|
-
field: "command",
|
|
1129
|
-
op: "matches",
|
|
1130
|
-
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
1131
|
-
flags: "i"
|
|
1132
|
-
}
|
|
1133
|
-
],
|
|
1134
|
-
conditionMode: "all",
|
|
1135
|
-
verdict: "block",
|
|
1136
|
-
reason: "Force push overwrites remote history and cannot be undone"
|
|
1137
|
-
},
|
|
1138
|
-
{
|
|
1139
|
-
name: "review-git-push",
|
|
1140
|
-
tool: "bash",
|
|
1141
|
-
conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
|
|
1142
|
-
conditionMode: "all",
|
|
1143
|
-
verdict: "review",
|
|
1144
|
-
reason: "git push sends changes to a shared remote"
|
|
1145
|
-
},
|
|
1146
|
-
{
|
|
1147
|
-
name: "review-git-destructive",
|
|
1148
|
-
tool: "bash",
|
|
1149
|
-
conditions: [
|
|
1150
|
-
{
|
|
1151
|
-
field: "command",
|
|
1152
|
-
op: "matches",
|
|
1153
|
-
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
1154
|
-
flags: "i"
|
|
1155
|
-
}
|
|
1156
|
-
],
|
|
1157
|
-
conditionMode: "all",
|
|
1158
|
-
verdict: "review",
|
|
1159
|
-
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
1160
|
-
},
|
|
1161
|
-
// ── Shell safety ──────────────────────────────────────────────────────
|
|
1162
|
-
{
|
|
1163
|
-
name: "review-sudo",
|
|
1164
|
-
tool: "bash",
|
|
1165
|
-
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
1166
|
-
conditionMode: "all",
|
|
1167
|
-
verdict: "review",
|
|
1168
|
-
reason: "Command requires elevated privileges"
|
|
1169
|
-
},
|
|
1170
|
-
{
|
|
1171
|
-
name: "review-curl-pipe-shell",
|
|
1172
|
-
tool: "bash",
|
|
1173
|
-
conditions: [
|
|
1174
|
-
{
|
|
1175
|
-
field: "command",
|
|
1176
|
-
op: "matches",
|
|
1177
|
-
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
1178
|
-
flags: "i"
|
|
1179
|
-
}
|
|
1180
|
-
],
|
|
1181
|
-
conditionMode: "all",
|
|
1182
|
-
verdict: "block",
|
|
1183
|
-
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
1184
|
-
}
|
|
1185
|
-
],
|
|
1186
|
-
dlp: { enabled: true, scanIgnoredTools: true }
|
|
1187
|
-
},
|
|
1188
|
-
environments: {}
|
|
1189
|
-
};
|
|
1190
|
-
var ADVISORY_SMART_RULES = [
|
|
1191
|
-
{
|
|
1192
|
-
name: "allow-rm-safe-paths",
|
|
1193
|
-
tool: "*",
|
|
1194
|
-
conditionMode: "all",
|
|
1195
|
-
conditions: [
|
|
1196
|
-
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
1197
|
-
{
|
|
1198
|
-
field: "command",
|
|
1199
|
-
op: "matches",
|
|
1200
|
-
// Matches known-safe build artifact paths in the command.
|
|
1201
|
-
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
1202
|
-
}
|
|
1203
|
-
],
|
|
1204
|
-
verdict: "allow",
|
|
1205
|
-
reason: "Deleting a known-safe build artifact path"
|
|
1206
|
-
},
|
|
1207
|
-
{
|
|
1208
|
-
name: "review-rm",
|
|
1209
|
-
tool: "*",
|
|
1210
|
-
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
1211
|
-
verdict: "review",
|
|
1212
|
-
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
1213
|
-
}
|
|
1214
|
-
];
|
|
1215
|
-
var cachedConfig = null;
|
|
1216
1040
|
function _resetConfigCache() {
|
|
1217
1041
|
cachedConfig = null;
|
|
1218
1042
|
}
|
|
@@ -1223,7 +1047,7 @@ function getGlobalSettings() {
|
|
|
1223
1047
|
const parsed = JSON.parse(import_fs2.default.readFileSync(globalConfigPath, "utf-8"));
|
|
1224
1048
|
const settings = parsed.settings || {};
|
|
1225
1049
|
return {
|
|
1226
|
-
mode: settings.mode || "
|
|
1050
|
+
mode: settings.mode || "audit",
|
|
1227
1051
|
autoStartDaemon: settings.autoStartDaemon !== false,
|
|
1228
1052
|
slackEnabled: settings.slackEnabled !== false,
|
|
1229
1053
|
enableTrustSessions: settings.enableTrustSessions === true,
|
|
@@ -1233,7 +1057,7 @@ function getGlobalSettings() {
|
|
|
1233
1057
|
} catch {
|
|
1234
1058
|
}
|
|
1235
1059
|
return {
|
|
1236
|
-
mode: "
|
|
1060
|
+
mode: "audit",
|
|
1237
1061
|
autoStartDaemon: true,
|
|
1238
1062
|
slackEnabled: true,
|
|
1239
1063
|
enableTrustSessions: false,
|
|
@@ -1619,16 +1443,24 @@ function isIgnoredTool(toolName) {
|
|
|
1619
1443
|
const config = getConfig();
|
|
1620
1444
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
1621
1445
|
}
|
|
1622
|
-
var DAEMON_PORT = 7391;
|
|
1623
|
-
var DAEMON_HOST = "127.0.0.1";
|
|
1624
1446
|
function isDaemonRunning() {
|
|
1447
|
+
const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
|
|
1448
|
+
if (import_fs2.default.existsSync(pidFile)) {
|
|
1449
|
+
try {
|
|
1450
|
+
const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
|
|
1451
|
+
if (port !== DAEMON_PORT) return false;
|
|
1452
|
+
process.kill(pid, 0);
|
|
1453
|
+
return true;
|
|
1454
|
+
} catch {
|
|
1455
|
+
return false;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1625
1458
|
try {
|
|
1626
|
-
const
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
return true;
|
|
1459
|
+
const r = (0, import_child_process2.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
|
|
1460
|
+
encoding: "utf8",
|
|
1461
|
+
timeout: 500
|
|
1462
|
+
});
|
|
1463
|
+
return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
|
|
1632
1464
|
} catch {
|
|
1633
1465
|
return false;
|
|
1634
1466
|
}
|
|
@@ -1644,7 +1476,7 @@ function getPersistentDecision(toolName) {
|
|
|
1644
1476
|
}
|
|
1645
1477
|
return null;
|
|
1646
1478
|
}
|
|
1647
|
-
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
1479
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
|
|
1648
1480
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1649
1481
|
const checkCtrl = new AbortController();
|
|
1650
1482
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -1659,6 +1491,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
|
1659
1491
|
args,
|
|
1660
1492
|
agent: meta?.agent,
|
|
1661
1493
|
mcpServer: meta?.mcpServer,
|
|
1494
|
+
fromCLI: true,
|
|
1495
|
+
// Pass the flight-recorder ID so the daemon uses the same UUID for
|
|
1496
|
+
// activity-result as the CLI used for the pending activity event.
|
|
1497
|
+
// Without this, the two UUIDs never match and tail.ts never resolves
|
|
1498
|
+
// the pending item.
|
|
1499
|
+
activityId,
|
|
1662
1500
|
...riskMetadata && { riskMetadata }
|
|
1663
1501
|
}),
|
|
1664
1502
|
signal: checkCtrl.signal
|
|
@@ -1713,11 +1551,48 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1713
1551
|
signal: AbortSignal.timeout(3e3)
|
|
1714
1552
|
});
|
|
1715
1553
|
}
|
|
1554
|
+
function notifyActivity(data) {
|
|
1555
|
+
return new Promise((resolve) => {
|
|
1556
|
+
try {
|
|
1557
|
+
const payload = JSON.stringify(data);
|
|
1558
|
+
const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
|
|
1559
|
+
sock.on("connect", () => {
|
|
1560
|
+
sock.on("close", resolve);
|
|
1561
|
+
sock.end(payload);
|
|
1562
|
+
});
|
|
1563
|
+
sock.on("error", resolve);
|
|
1564
|
+
} catch {
|
|
1565
|
+
resolve();
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1716
1569
|
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1717
|
-
if (
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1570
|
+
if (!options?.calledFromDaemon) {
|
|
1571
|
+
const actId = (0, import_crypto2.randomUUID)();
|
|
1572
|
+
const actTs = Date.now();
|
|
1573
|
+
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
|
|
1574
|
+
const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
|
|
1575
|
+
...options,
|
|
1576
|
+
activityId: actId
|
|
1577
|
+
});
|
|
1578
|
+
if (!result.noApprovalMechanism) {
|
|
1579
|
+
await notifyActivity({
|
|
1580
|
+
id: actId,
|
|
1581
|
+
tool: toolName,
|
|
1582
|
+
ts: actTs,
|
|
1583
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
1584
|
+
label: result.blockedByLabel
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
return result;
|
|
1588
|
+
}
|
|
1589
|
+
return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
|
|
1590
|
+
}
|
|
1591
|
+
async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1592
|
+
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1593
|
+
const pauseState = checkPause();
|
|
1594
|
+
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
1595
|
+
const creds = getCredentials();
|
|
1721
1596
|
const config = getConfig();
|
|
1722
1597
|
const isTestEnv2 = !!(process.env.VITEST || process.env.NODE_ENV === "test" || process.env.CI || process.env.NODE9_TESTING === "1");
|
|
1723
1598
|
const approvers = {
|
|
@@ -1749,6 +1624,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1749
1624
|
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
1750
1625
|
};
|
|
1751
1626
|
}
|
|
1627
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
|
|
1752
1628
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
1753
1629
|
}
|
|
1754
1630
|
}
|
|
@@ -1971,7 +1847,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1971
1847
|
console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
1972
1848
|
`));
|
|
1973
1849
|
}
|
|
1974
|
-
const daemonDecision = await askDaemon(
|
|
1850
|
+
const daemonDecision = await askDaemon(
|
|
1851
|
+
toolName,
|
|
1852
|
+
args,
|
|
1853
|
+
meta,
|
|
1854
|
+
signal,
|
|
1855
|
+
riskMetadata,
|
|
1856
|
+
options?.activityId
|
|
1857
|
+
);
|
|
1975
1858
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1976
1859
|
const isApproved = daemonDecision === "allow";
|
|
1977
1860
|
return {
|
|
@@ -2175,7 +2058,10 @@ function getConfig() {
|
|
|
2175
2058
|
for (const rule of shield.smartRules) {
|
|
2176
2059
|
if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
2177
2060
|
}
|
|
2178
|
-
|
|
2061
|
+
const existingWords = new Set(mergedPolicy.dangerousWords);
|
|
2062
|
+
for (const word of shield.dangerousWords) {
|
|
2063
|
+
if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
|
|
2064
|
+
}
|
|
2179
2065
|
}
|
|
2180
2066
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2181
2067
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
@@ -2376,280 +2262,275 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
2376
2262
|
} catch {
|
|
2377
2263
|
}
|
|
2378
2264
|
}
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2265
|
+
var import_chalk2, import_prompts, import_fs2, import_path4, import_os2, import_net, import_crypto2, import_child_process2, import_picomatch, import_sh_syntax, 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;
|
|
2266
|
+
var init_core = __esm({
|
|
2267
|
+
"src/core.ts"() {
|
|
2268
|
+
"use strict";
|
|
2269
|
+
import_chalk2 = __toESM(require("chalk"));
|
|
2270
|
+
import_prompts = require("@inquirer/prompts");
|
|
2271
|
+
import_fs2 = __toESM(require("fs"));
|
|
2272
|
+
import_path4 = __toESM(require("path"));
|
|
2273
|
+
import_os2 = __toESM(require("os"));
|
|
2274
|
+
import_net = __toESM(require("net"));
|
|
2275
|
+
import_crypto2 = require("crypto");
|
|
2276
|
+
import_child_process2 = require("child_process");
|
|
2277
|
+
import_picomatch = __toESM(require("picomatch"));
|
|
2278
|
+
import_sh_syntax = require("sh-syntax");
|
|
2279
|
+
init_native();
|
|
2280
|
+
init_context_sniper();
|
|
2281
|
+
init_config_schema();
|
|
2282
|
+
init_shields();
|
|
2283
|
+
init_dlp();
|
|
2284
|
+
PAUSED_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
|
|
2285
|
+
TRUST_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "trust.json");
|
|
2286
|
+
LOCAL_AUDIT_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "audit.log");
|
|
2287
|
+
HOOK_DEBUG_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
|
|
2288
|
+
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2289
|
+
DANGEROUS_WORDS = [
|
|
2290
|
+
"mkfs",
|
|
2291
|
+
// formats/wipes a filesystem partition
|
|
2292
|
+
"shred"
|
|
2293
|
+
// permanently overwrites file contents (unrecoverable)
|
|
2294
|
+
];
|
|
2295
|
+
DEFAULT_CONFIG = {
|
|
2296
|
+
version: "1.0",
|
|
2297
|
+
settings: {
|
|
2298
|
+
mode: "audit",
|
|
2299
|
+
autoStartDaemon: true,
|
|
2300
|
+
enableUndo: true,
|
|
2301
|
+
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
2302
|
+
enableHookLogDebug: true,
|
|
2303
|
+
approvalTimeoutMs: 3e4,
|
|
2304
|
+
// 30-second auto-deny timeout
|
|
2305
|
+
flightRecorder: true,
|
|
2306
|
+
approvers: { native: true, browser: true, cloud: false, terminal: true }
|
|
2307
|
+
},
|
|
2308
|
+
policy: {
|
|
2309
|
+
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
2310
|
+
dangerousWords: DANGEROUS_WORDS,
|
|
2311
|
+
ignoredTools: [
|
|
2312
|
+
"list_*",
|
|
2313
|
+
"get_*",
|
|
2314
|
+
"read_*",
|
|
2315
|
+
"describe_*",
|
|
2316
|
+
"read",
|
|
2317
|
+
"glob",
|
|
2318
|
+
"grep",
|
|
2319
|
+
"ls",
|
|
2320
|
+
"notebookread",
|
|
2321
|
+
"notebookedit",
|
|
2322
|
+
"webfetch",
|
|
2323
|
+
"websearch",
|
|
2324
|
+
"exitplanmode",
|
|
2325
|
+
"askuserquestion",
|
|
2326
|
+
"agent",
|
|
2327
|
+
"task*",
|
|
2328
|
+
"toolsearch",
|
|
2329
|
+
"mcp__ide__*",
|
|
2330
|
+
"getDiagnostics"
|
|
2331
|
+
],
|
|
2332
|
+
toolInspection: {
|
|
2333
|
+
bash: "command",
|
|
2334
|
+
shell: "command",
|
|
2335
|
+
run_shell_command: "command",
|
|
2336
|
+
"terminal.execute": "command",
|
|
2337
|
+
"postgres:query": "sql"
|
|
2338
|
+
},
|
|
2339
|
+
snapshot: {
|
|
2340
|
+
tools: [
|
|
2341
|
+
"str_replace_based_edit_tool",
|
|
2342
|
+
"write_file",
|
|
2343
|
+
"edit_file",
|
|
2344
|
+
"create_file",
|
|
2345
|
+
"edit",
|
|
2346
|
+
"replace"
|
|
2347
|
+
],
|
|
2348
|
+
onlyPaths: [],
|
|
2349
|
+
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
2350
|
+
},
|
|
2351
|
+
smartRules: [
|
|
2352
|
+
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
2353
|
+
{
|
|
2354
|
+
name: "block-rm-rf-home",
|
|
2355
|
+
tool: "bash",
|
|
2356
|
+
conditionMode: "all",
|
|
2357
|
+
conditions: [
|
|
2358
|
+
{
|
|
2359
|
+
field: "command",
|
|
2360
|
+
op: "matches",
|
|
2361
|
+
value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
|
|
2362
|
+
},
|
|
2363
|
+
{
|
|
2364
|
+
field: "command",
|
|
2365
|
+
op: "matches",
|
|
2366
|
+
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
2367
|
+
}
|
|
2368
|
+
],
|
|
2369
|
+
verdict: "block",
|
|
2370
|
+
reason: "Recursive delete of home directory is irreversible"
|
|
2371
|
+
},
|
|
2372
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
2373
|
+
{
|
|
2374
|
+
name: "no-delete-without-where",
|
|
2375
|
+
tool: "*",
|
|
2376
|
+
conditions: [
|
|
2377
|
+
{ field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
|
|
2378
|
+
{ field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
|
|
2379
|
+
],
|
|
2380
|
+
conditionMode: "all",
|
|
2381
|
+
verdict: "review",
|
|
2382
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
2383
|
+
},
|
|
2384
|
+
{
|
|
2385
|
+
name: "review-drop-truncate-shell",
|
|
2386
|
+
tool: "bash",
|
|
2387
|
+
conditions: [
|
|
2388
|
+
{
|
|
2389
|
+
field: "command",
|
|
2390
|
+
op: "matches",
|
|
2391
|
+
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
2392
|
+
flags: "i"
|
|
2393
|
+
}
|
|
2394
|
+
],
|
|
2395
|
+
conditionMode: "all",
|
|
2396
|
+
verdict: "review",
|
|
2397
|
+
reason: "SQL DDL destructive statement inside a shell command"
|
|
2398
|
+
},
|
|
2399
|
+
// ── Git safety ────────────────────────────────────────────────────────
|
|
2400
|
+
{
|
|
2401
|
+
name: "block-force-push",
|
|
2402
|
+
tool: "bash",
|
|
2403
|
+
conditions: [
|
|
2404
|
+
{
|
|
2405
|
+
field: "command",
|
|
2406
|
+
op: "matches",
|
|
2407
|
+
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
2408
|
+
flags: "i"
|
|
2409
|
+
}
|
|
2410
|
+
],
|
|
2411
|
+
conditionMode: "all",
|
|
2412
|
+
verdict: "block",
|
|
2413
|
+
reason: "Force push overwrites remote history and cannot be undone"
|
|
2414
|
+
},
|
|
2415
|
+
{
|
|
2416
|
+
name: "review-git-push",
|
|
2417
|
+
tool: "bash",
|
|
2418
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
|
|
2419
|
+
conditionMode: "all",
|
|
2420
|
+
verdict: "review",
|
|
2421
|
+
reason: "git push sends changes to a shared remote"
|
|
2422
|
+
},
|
|
2423
|
+
{
|
|
2424
|
+
name: "review-git-destructive",
|
|
2425
|
+
tool: "bash",
|
|
2426
|
+
conditions: [
|
|
2427
|
+
{
|
|
2428
|
+
field: "command",
|
|
2429
|
+
op: "matches",
|
|
2430
|
+
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
2431
|
+
flags: "i"
|
|
2432
|
+
}
|
|
2433
|
+
],
|
|
2434
|
+
conditionMode: "all",
|
|
2435
|
+
verdict: "review",
|
|
2436
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
2437
|
+
},
|
|
2438
|
+
// ── Shell safety ──────────────────────────────────────────────────────
|
|
2439
|
+
{
|
|
2440
|
+
name: "review-sudo",
|
|
2441
|
+
tool: "bash",
|
|
2442
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
2443
|
+
conditionMode: "all",
|
|
2444
|
+
verdict: "review",
|
|
2445
|
+
reason: "Command requires elevated privileges"
|
|
2446
|
+
},
|
|
2447
|
+
{
|
|
2448
|
+
name: "review-curl-pipe-shell",
|
|
2449
|
+
tool: "bash",
|
|
2450
|
+
conditions: [
|
|
2451
|
+
{
|
|
2452
|
+
field: "command",
|
|
2453
|
+
op: "matches",
|
|
2454
|
+
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
2455
|
+
flags: "i"
|
|
2456
|
+
}
|
|
2457
|
+
],
|
|
2458
|
+
conditionMode: "all",
|
|
2459
|
+
verdict: "block",
|
|
2460
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
2461
|
+
}
|
|
2462
|
+
],
|
|
2463
|
+
dlp: { enabled: true, scanIgnoredTools: true }
|
|
2464
|
+
},
|
|
2465
|
+
environments: {}
|
|
2466
|
+
};
|
|
2467
|
+
ADVISORY_SMART_RULES = [
|
|
2468
|
+
{
|
|
2469
|
+
name: "allow-rm-safe-paths",
|
|
2470
|
+
tool: "*",
|
|
2471
|
+
conditionMode: "all",
|
|
2472
|
+
conditions: [
|
|
2473
|
+
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
2474
|
+
{
|
|
2475
|
+
field: "command",
|
|
2476
|
+
op: "matches",
|
|
2477
|
+
// Matches known-safe build artifact paths in the command.
|
|
2478
|
+
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
2479
|
+
}
|
|
2480
|
+
],
|
|
2481
|
+
verdict: "allow",
|
|
2482
|
+
reason: "Deleting a known-safe build artifact path"
|
|
2483
|
+
},
|
|
2484
|
+
{
|
|
2485
|
+
name: "review-rm",
|
|
2486
|
+
tool: "*",
|
|
2487
|
+
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
2488
|
+
verdict: "review",
|
|
2489
|
+
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
2465
2490
|
}
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
} else {
|
|
2472
|
-
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
2473
|
-
}
|
|
2474
|
-
console.log("");
|
|
2475
|
-
}
|
|
2476
|
-
if (!anythingChanged && serversToWrap.length === 0) {
|
|
2477
|
-
console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
2478
|
-
printDaemonTip();
|
|
2479
|
-
return;
|
|
2480
|
-
}
|
|
2481
|
-
if (anythingChanged) {
|
|
2482
|
-
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
2483
|
-
console.log(import_chalk3.default.gray(" Restart Claude Code for changes to take effect."));
|
|
2484
|
-
printDaemonTip();
|
|
2485
|
-
}
|
|
2486
|
-
}
|
|
2487
|
-
async function setupGemini() {
|
|
2488
|
-
const homeDir2 = import_os3.default.homedir();
|
|
2489
|
-
const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
|
|
2490
|
-
const settings = readJson(settingsPath) ?? {};
|
|
2491
|
-
const servers = settings.mcpServers ?? {};
|
|
2492
|
-
let anythingChanged = false;
|
|
2493
|
-
if (!settings.hooks) settings.hooks = {};
|
|
2494
|
-
const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
|
|
2495
|
-
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
2496
|
-
);
|
|
2497
|
-
if (!hasBeforeHook) {
|
|
2498
|
-
if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
|
|
2499
|
-
if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
|
|
2500
|
-
settings.hooks.BeforeTool.push({
|
|
2501
|
-
matcher: ".*",
|
|
2502
|
-
hooks: [
|
|
2503
|
-
{
|
|
2504
|
-
name: "node9-check",
|
|
2505
|
-
type: "command",
|
|
2506
|
-
command: fullPathCommand("check"),
|
|
2507
|
-
timeout: 6e5
|
|
2508
|
-
}
|
|
2509
|
-
]
|
|
2510
|
-
});
|
|
2511
|
-
console.log(import_chalk3.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
2512
|
-
anythingChanged = true;
|
|
2513
|
-
}
|
|
2514
|
-
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
2515
|
-
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
2516
|
-
);
|
|
2517
|
-
if (!hasAfterHook) {
|
|
2518
|
-
if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
|
|
2519
|
-
if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
|
|
2520
|
-
settings.hooks.AfterTool.push({
|
|
2521
|
-
matcher: ".*",
|
|
2522
|
-
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
2523
|
-
});
|
|
2524
|
-
console.log(import_chalk3.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
2525
|
-
anythingChanged = true;
|
|
2526
|
-
}
|
|
2527
|
-
if (anythingChanged) {
|
|
2528
|
-
writeJson(settingsPath, settings);
|
|
2529
|
-
console.log("");
|
|
2530
|
-
}
|
|
2531
|
-
const serversToWrap = [];
|
|
2532
|
-
for (const [name, server] of Object.entries(servers)) {
|
|
2533
|
-
if (!server.command || server.command === "node9") continue;
|
|
2534
|
-
const parts = [server.command, ...server.args ?? []];
|
|
2535
|
-
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
2491
|
+
];
|
|
2492
|
+
cachedConfig = null;
|
|
2493
|
+
DAEMON_PORT = 7391;
|
|
2494
|
+
DAEMON_HOST = "127.0.0.1";
|
|
2495
|
+
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path4.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
|
|
2536
2496
|
}
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
}
|
|
2564
|
-
if (anythingChanged) {
|
|
2565
|
-
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
2566
|
-
console.log(import_chalk3.default.gray(" Restart Gemini CLI for changes to take effect."));
|
|
2567
|
-
printDaemonTip();
|
|
2568
|
-
}
|
|
2569
|
-
}
|
|
2570
|
-
async function setupCursor() {
|
|
2571
|
-
const homeDir2 = import_os3.default.homedir();
|
|
2572
|
-
const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
|
|
2573
|
-
const mcpConfig = readJson(mcpPath) ?? {};
|
|
2574
|
-
const servers = mcpConfig.mcpServers ?? {};
|
|
2575
|
-
let anythingChanged = false;
|
|
2576
|
-
const serversToWrap = [];
|
|
2577
|
-
for (const [name, server] of Object.entries(servers)) {
|
|
2578
|
-
if (!server.command || server.command === "node9") continue;
|
|
2579
|
-
const parts = [server.command, ...server.args ?? []];
|
|
2580
|
-
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
2581
|
-
}
|
|
2582
|
-
if (serversToWrap.length > 0) {
|
|
2583
|
-
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
2584
|
-
console.log(import_chalk3.default.white(` ${mcpPath}`));
|
|
2585
|
-
for (const { name, originalCmd } of serversToWrap) {
|
|
2586
|
-
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
2587
|
-
}
|
|
2588
|
-
console.log("");
|
|
2589
|
-
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
2590
|
-
if (proceed) {
|
|
2591
|
-
for (const { name, parts } of serversToWrap) {
|
|
2592
|
-
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
2593
|
-
}
|
|
2594
|
-
mcpConfig.mcpServers = servers;
|
|
2595
|
-
writeJson(mcpPath, mcpConfig);
|
|
2596
|
-
console.log(import_chalk3.default.green(`
|
|
2597
|
-
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
2598
|
-
anythingChanged = true;
|
|
2599
|
-
} else {
|
|
2600
|
-
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
2601
|
-
}
|
|
2602
|
-
console.log("");
|
|
2603
|
-
}
|
|
2604
|
-
console.log(
|
|
2605
|
-
import_chalk3.default.yellow(
|
|
2606
|
-
" \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
|
|
2607
|
-
)
|
|
2608
|
-
);
|
|
2609
|
-
console.log("");
|
|
2610
|
-
if (!anythingChanged && serversToWrap.length === 0) {
|
|
2611
|
-
console.log(
|
|
2612
|
-
import_chalk3.default.blue(
|
|
2613
|
-
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
|
|
2614
|
-
)
|
|
2615
|
-
);
|
|
2616
|
-
printDaemonTip();
|
|
2617
|
-
return;
|
|
2618
|
-
}
|
|
2619
|
-
if (anythingChanged) {
|
|
2620
|
-
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
|
|
2621
|
-
console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
|
|
2622
|
-
printDaemonTip();
|
|
2623
|
-
}
|
|
2624
|
-
}
|
|
2625
|
-
|
|
2626
|
-
// src/daemon/ui.html
|
|
2627
|
-
var ui_default = `<!doctype html>
|
|
2628
|
-
<html lang="en">
|
|
2629
|
-
<head>
|
|
2630
|
-
<meta charset="UTF-8" />
|
|
2631
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2632
|
-
<title>Node9 Security Guard</title>
|
|
2633
|
-
<style>
|
|
2634
|
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Fira+Code:wght@400;500&display=swap');
|
|
2635
|
-
:root {
|
|
2636
|
-
--bg: #0a0c10;
|
|
2637
|
-
--card: #1c2128;
|
|
2638
|
-
--panel: #161b22;
|
|
2639
|
-
--border: #30363d;
|
|
2640
|
-
--text: #adbac7;
|
|
2641
|
-
--text-bright: #cdd9e5;
|
|
2642
|
-
--muted: #768390;
|
|
2643
|
-
--primary: #f0883e;
|
|
2644
|
-
--success: #347d39;
|
|
2645
|
-
--danger: #c93c37;
|
|
2646
|
-
--accent: #539bf5;
|
|
2497
|
+
});
|
|
2498
|
+
|
|
2499
|
+
// src/daemon/ui.html
|
|
2500
|
+
var ui_default;
|
|
2501
|
+
var init_ui = __esm({
|
|
2502
|
+
"src/daemon/ui.html"() {
|
|
2503
|
+
ui_default = `<!doctype html>
|
|
2504
|
+
<html lang="en">
|
|
2505
|
+
<head>
|
|
2506
|
+
<meta charset="UTF-8" />
|
|
2507
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2508
|
+
<title>Node9 Security Guard</title>
|
|
2509
|
+
<style>
|
|
2510
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Fira+Code:wght@400;500&display=swap');
|
|
2511
|
+
:root {
|
|
2512
|
+
--bg: #0a0c10;
|
|
2513
|
+
--card: #1c2128;
|
|
2514
|
+
--panel: #161b22;
|
|
2515
|
+
--border: #30363d;
|
|
2516
|
+
--text: #adbac7;
|
|
2517
|
+
--text-bright: #cdd9e5;
|
|
2518
|
+
--muted: #768390;
|
|
2519
|
+
--primary: #f0883e;
|
|
2520
|
+
--success: #347d39;
|
|
2521
|
+
--danger: #c93c37;
|
|
2522
|
+
--accent: #539bf5;
|
|
2647
2523
|
}
|
|
2648
2524
|
* {
|
|
2649
2525
|
box-sizing: border-box;
|
|
2650
2526
|
margin: 0;
|
|
2651
2527
|
padding: 0;
|
|
2652
2528
|
}
|
|
2529
|
+
html,
|
|
2530
|
+
body {
|
|
2531
|
+
height: 100%;
|
|
2532
|
+
overflow: hidden;
|
|
2533
|
+
}
|
|
2653
2534
|
body {
|
|
2654
2535
|
background: var(--bg);
|
|
2655
2536
|
color: var(--text);
|
|
@@ -2657,16 +2538,17 @@ var ui_default = `<!doctype html>
|
|
|
2657
2538
|
'Inter',
|
|
2658
2539
|
-apple-system,
|
|
2659
2540
|
sans-serif;
|
|
2660
|
-
min-height: 100vh;
|
|
2661
2541
|
}
|
|
2662
2542
|
|
|
2663
2543
|
.shell {
|
|
2664
|
-
max-width:
|
|
2544
|
+
max-width: 1440px;
|
|
2545
|
+
height: 100vh;
|
|
2665
2546
|
margin: 0 auto;
|
|
2666
|
-
padding:
|
|
2547
|
+
padding: 16px 20px 16px;
|
|
2667
2548
|
display: grid;
|
|
2668
2549
|
grid-template-rows: auto 1fr;
|
|
2669
|
-
gap:
|
|
2550
|
+
gap: 16px;
|
|
2551
|
+
overflow: hidden;
|
|
2670
2552
|
}
|
|
2671
2553
|
header {
|
|
2672
2554
|
display: flex;
|
|
@@ -2703,9 +2585,10 @@ var ui_default = `<!doctype html>
|
|
|
2703
2585
|
|
|
2704
2586
|
.body {
|
|
2705
2587
|
display: grid;
|
|
2706
|
-
grid-template-columns: 1fr
|
|
2707
|
-
gap:
|
|
2708
|
-
|
|
2588
|
+
grid-template-columns: 360px 1fr 270px;
|
|
2589
|
+
gap: 16px;
|
|
2590
|
+
min-height: 0;
|
|
2591
|
+
overflow: hidden;
|
|
2709
2592
|
}
|
|
2710
2593
|
|
|
2711
2594
|
.warning-banner {
|
|
@@ -2725,6 +2608,10 @@ var ui_default = `<!doctype html>
|
|
|
2725
2608
|
|
|
2726
2609
|
.main {
|
|
2727
2610
|
min-width: 0;
|
|
2611
|
+
min-height: 0;
|
|
2612
|
+
overflow-y: auto;
|
|
2613
|
+
scrollbar-width: thin;
|
|
2614
|
+
scrollbar-color: var(--border) transparent;
|
|
2728
2615
|
}
|
|
2729
2616
|
.section-title {
|
|
2730
2617
|
font-size: 11px;
|
|
@@ -2755,14 +2642,64 @@ var ui_default = `<!doctype html>
|
|
|
2755
2642
|
background: var(--card);
|
|
2756
2643
|
border: 1px solid var(--border);
|
|
2757
2644
|
border-radius: 14px;
|
|
2758
|
-
padding:
|
|
2759
|
-
margin-bottom:
|
|
2645
|
+
padding: 20px;
|
|
2646
|
+
margin-bottom: 14px;
|
|
2760
2647
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
2761
2648
|
animation: pop 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
2762
2649
|
}
|
|
2763
2650
|
.card.slack-viewer {
|
|
2764
2651
|
border-color: rgba(83, 155, 245, 0.3);
|
|
2765
2652
|
}
|
|
2653
|
+
.card-header {
|
|
2654
|
+
display: flex;
|
|
2655
|
+
align-items: center;
|
|
2656
|
+
gap: 8px;
|
|
2657
|
+
margin-bottom: 12px;
|
|
2658
|
+
padding-bottom: 12px;
|
|
2659
|
+
border-bottom: 1px solid var(--border);
|
|
2660
|
+
}
|
|
2661
|
+
.card-header-icon {
|
|
2662
|
+
font-size: 16px;
|
|
2663
|
+
}
|
|
2664
|
+
.card-header-title {
|
|
2665
|
+
font-size: 12px;
|
|
2666
|
+
font-weight: 700;
|
|
2667
|
+
color: var(--text-bright);
|
|
2668
|
+
text-transform: uppercase;
|
|
2669
|
+
letter-spacing: 0.5px;
|
|
2670
|
+
}
|
|
2671
|
+
.card-timer {
|
|
2672
|
+
margin-left: auto;
|
|
2673
|
+
font-size: 11px;
|
|
2674
|
+
font-family: 'Fira Code', monospace;
|
|
2675
|
+
color: var(--muted);
|
|
2676
|
+
background: rgba(48, 54, 61, 0.6);
|
|
2677
|
+
padding: 2px 8px;
|
|
2678
|
+
border-radius: 5px;
|
|
2679
|
+
}
|
|
2680
|
+
.card-timer.urgent {
|
|
2681
|
+
color: var(--danger);
|
|
2682
|
+
background: rgba(201, 60, 55, 0.1);
|
|
2683
|
+
}
|
|
2684
|
+
.btn-allow {
|
|
2685
|
+
background: var(--success);
|
|
2686
|
+
color: #fff;
|
|
2687
|
+
grid-column: span 2;
|
|
2688
|
+
font-size: 14px;
|
|
2689
|
+
padding: 13px 14px;
|
|
2690
|
+
}
|
|
2691
|
+
.btn-deny {
|
|
2692
|
+
background: rgba(201, 60, 55, 0.15);
|
|
2693
|
+
color: #e5534b;
|
|
2694
|
+
border: 1px solid rgba(201, 60, 55, 0.3);
|
|
2695
|
+
grid-column: span 2;
|
|
2696
|
+
}
|
|
2697
|
+
.btn-deny:hover:not(:disabled) {
|
|
2698
|
+
background: var(--danger);
|
|
2699
|
+
color: #fff;
|
|
2700
|
+
border-color: transparent;
|
|
2701
|
+
filter: none;
|
|
2702
|
+
}
|
|
2766
2703
|
@keyframes pop {
|
|
2767
2704
|
from {
|
|
2768
2705
|
opacity: 0;
|
|
@@ -2970,24 +2907,178 @@ var ui_default = `<!doctype html>
|
|
|
2970
2907
|
cursor: not-allowed;
|
|
2971
2908
|
}
|
|
2972
2909
|
|
|
2910
|
+
.flight-col {
|
|
2911
|
+
display: flex;
|
|
2912
|
+
flex-direction: column;
|
|
2913
|
+
min-height: 0;
|
|
2914
|
+
overflow: hidden;
|
|
2915
|
+
}
|
|
2916
|
+
.flight-panel {
|
|
2917
|
+
flex: 1;
|
|
2918
|
+
min-height: 0;
|
|
2919
|
+
display: flex;
|
|
2920
|
+
flex-direction: column;
|
|
2921
|
+
overflow: hidden;
|
|
2922
|
+
}
|
|
2973
2923
|
.sidebar {
|
|
2974
2924
|
display: flex;
|
|
2975
2925
|
flex-direction: column;
|
|
2976
2926
|
gap: 12px;
|
|
2977
|
-
|
|
2978
|
-
|
|
2927
|
+
min-height: 0;
|
|
2928
|
+
overflow-y: auto;
|
|
2929
|
+
scrollbar-width: thin;
|
|
2930
|
+
scrollbar-color: var(--border) transparent;
|
|
2979
2931
|
}
|
|
2980
2932
|
.panel {
|
|
2981
2933
|
background: var(--panel);
|
|
2982
2934
|
border: 1px solid var(--border);
|
|
2983
2935
|
border-radius: 12px;
|
|
2984
|
-
padding:
|
|
2936
|
+
padding: 14px;
|
|
2937
|
+
}
|
|
2938
|
+
/* \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 */
|
|
2939
|
+
#activity-feed {
|
|
2940
|
+
display: flex;
|
|
2941
|
+
flex-direction: column;
|
|
2942
|
+
gap: 4px;
|
|
2943
|
+
margin-top: 4px;
|
|
2944
|
+
flex: 1;
|
|
2945
|
+
min-height: 0;
|
|
2946
|
+
overflow-y: auto;
|
|
2947
|
+
scrollbar-width: thin;
|
|
2948
|
+
scrollbar-color: var(--border) transparent;
|
|
2949
|
+
}
|
|
2950
|
+
.feed-row {
|
|
2951
|
+
display: grid;
|
|
2952
|
+
grid-template-columns: 58px 20px 1fr 48px;
|
|
2953
|
+
align-items: start;
|
|
2954
|
+
gap: 6px;
|
|
2955
|
+
background: rgba(22, 27, 34, 0.6);
|
|
2956
|
+
border: 1px solid var(--border);
|
|
2957
|
+
padding: 7px 10px;
|
|
2958
|
+
border-radius: 7px;
|
|
2959
|
+
font-size: 11px;
|
|
2960
|
+
animation: frSlideIn 0.15s ease-out;
|
|
2961
|
+
transition: background 0.1s;
|
|
2962
|
+
cursor: default;
|
|
2963
|
+
}
|
|
2964
|
+
.feed-row:hover {
|
|
2965
|
+
background: rgba(30, 38, 48, 0.9);
|
|
2966
|
+
border-color: rgba(83, 155, 245, 0.2);
|
|
2967
|
+
}
|
|
2968
|
+
@keyframes frSlideIn {
|
|
2969
|
+
from {
|
|
2970
|
+
opacity: 0;
|
|
2971
|
+
transform: translateX(-4px);
|
|
2972
|
+
}
|
|
2973
|
+
to {
|
|
2974
|
+
opacity: 1;
|
|
2975
|
+
transform: none;
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
.feed-ts {
|
|
2979
|
+
color: var(--muted);
|
|
2980
|
+
font-family: monospace;
|
|
2981
|
+
font-size: 9px;
|
|
2982
|
+
}
|
|
2983
|
+
.feed-icon {
|
|
2984
|
+
text-align: center;
|
|
2985
|
+
font-size: 13px;
|
|
2986
|
+
}
|
|
2987
|
+
.feed-content {
|
|
2988
|
+
min-width: 0;
|
|
2989
|
+
color: var(--text-bright);
|
|
2990
|
+
word-break: break-all;
|
|
2991
|
+
}
|
|
2992
|
+
.feed-args {
|
|
2993
|
+
display: block;
|
|
2994
|
+
color: var(--muted);
|
|
2995
|
+
font-family: monospace;
|
|
2996
|
+
margin-top: 2px;
|
|
2997
|
+
font-size: 10px;
|
|
2998
|
+
word-break: break-all;
|
|
2999
|
+
}
|
|
3000
|
+
.feed-badge {
|
|
3001
|
+
text-align: right;
|
|
3002
|
+
font-weight: 700;
|
|
3003
|
+
font-size: 9px;
|
|
3004
|
+
letter-spacing: 0.03em;
|
|
3005
|
+
}
|
|
3006
|
+
.fr-pending {
|
|
3007
|
+
color: var(--muted);
|
|
3008
|
+
}
|
|
3009
|
+
.fr-allow {
|
|
3010
|
+
color: #57ab5a;
|
|
3011
|
+
}
|
|
3012
|
+
.fr-block {
|
|
3013
|
+
color: var(--danger);
|
|
3014
|
+
}
|
|
3015
|
+
.fr-dlp {
|
|
3016
|
+
color: var(--primary);
|
|
3017
|
+
animation: frBlink 1s infinite;
|
|
3018
|
+
}
|
|
3019
|
+
@keyframes frBlink {
|
|
3020
|
+
50% {
|
|
3021
|
+
opacity: 0.4;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
.fr-dlp-row {
|
|
3025
|
+
border-color: var(--primary) !important;
|
|
3026
|
+
}
|
|
3027
|
+
.feed-clear-btn {
|
|
3028
|
+
background: transparent;
|
|
3029
|
+
border: none;
|
|
3030
|
+
color: var(--muted);
|
|
3031
|
+
font-size: 10px;
|
|
3032
|
+
padding: 0;
|
|
3033
|
+
cursor: pointer;
|
|
3034
|
+
margin-left: auto;
|
|
3035
|
+
font-family: inherit;
|
|
3036
|
+
font-weight: 500;
|
|
3037
|
+
transition: color 0.15s;
|
|
3038
|
+
}
|
|
3039
|
+
.feed-clear-btn:hover {
|
|
3040
|
+
color: var(--text);
|
|
3041
|
+
filter: none;
|
|
3042
|
+
transform: none;
|
|
3043
|
+
}
|
|
3044
|
+
/* \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 */
|
|
3045
|
+
.shield-row {
|
|
3046
|
+
display: flex;
|
|
3047
|
+
align-items: flex-start;
|
|
3048
|
+
gap: 10px;
|
|
3049
|
+
padding: 8px 0;
|
|
3050
|
+
border-bottom: 1px solid var(--border);
|
|
3051
|
+
}
|
|
3052
|
+
.shield-row:last-child {
|
|
3053
|
+
border-bottom: none;
|
|
3054
|
+
padding-bottom: 0;
|
|
3055
|
+
}
|
|
3056
|
+
.shield-row:first-child {
|
|
3057
|
+
padding-top: 0;
|
|
3058
|
+
}
|
|
3059
|
+
.shield-info {
|
|
3060
|
+
flex: 1;
|
|
3061
|
+
min-width: 0;
|
|
2985
3062
|
}
|
|
3063
|
+
.shield-name {
|
|
3064
|
+
font-size: 12px;
|
|
3065
|
+
color: var(--text-bright);
|
|
3066
|
+
font-weight: 600;
|
|
3067
|
+
font-family: 'Fira Code', monospace;
|
|
3068
|
+
}
|
|
3069
|
+
.shield-desc {
|
|
3070
|
+
font-size: 10px;
|
|
3071
|
+
color: var(--muted);
|
|
3072
|
+
margin-top: 2px;
|
|
3073
|
+
line-height: 1.4;
|
|
3074
|
+
}
|
|
3075
|
+
|
|
2986
3076
|
.panel-title {
|
|
2987
3077
|
font-size: 12px;
|
|
2988
3078
|
font-weight: 700;
|
|
2989
3079
|
color: var(--text-bright);
|
|
2990
3080
|
margin-bottom: 12px;
|
|
3081
|
+
flex-shrink: 0;
|
|
2991
3082
|
display: flex;
|
|
2992
3083
|
align-items: center;
|
|
2993
3084
|
gap: 6px;
|
|
@@ -2995,8 +3086,8 @@ var ui_default = `<!doctype html>
|
|
|
2995
3086
|
.setting-row {
|
|
2996
3087
|
display: flex;
|
|
2997
3088
|
align-items: flex-start;
|
|
2998
|
-
gap:
|
|
2999
|
-
margin-bottom:
|
|
3089
|
+
gap: 10px;
|
|
3090
|
+
margin-bottom: 8px;
|
|
3000
3091
|
}
|
|
3001
3092
|
.setting-row:last-child {
|
|
3002
3093
|
margin-bottom: 0;
|
|
@@ -3005,20 +3096,21 @@ var ui_default = `<!doctype html>
|
|
|
3005
3096
|
flex: 1;
|
|
3006
3097
|
}
|
|
3007
3098
|
.setting-label {
|
|
3008
|
-
font-size:
|
|
3099
|
+
font-size: 11px;
|
|
3009
3100
|
color: var(--text-bright);
|
|
3010
|
-
margin-bottom:
|
|
3101
|
+
margin-bottom: 2px;
|
|
3102
|
+
font-weight: 600;
|
|
3011
3103
|
}
|
|
3012
3104
|
.setting-desc {
|
|
3013
|
-
font-size:
|
|
3105
|
+
font-size: 10px;
|
|
3014
3106
|
color: var(--muted);
|
|
3015
|
-
line-height: 1.
|
|
3107
|
+
line-height: 1.4;
|
|
3016
3108
|
}
|
|
3017
3109
|
.toggle {
|
|
3018
3110
|
position: relative;
|
|
3019
3111
|
display: inline-block;
|
|
3020
|
-
width:
|
|
3021
|
-
height:
|
|
3112
|
+
width: 34px;
|
|
3113
|
+
height: 19px;
|
|
3022
3114
|
flex-shrink: 0;
|
|
3023
3115
|
margin-top: 1px;
|
|
3024
3116
|
}
|
|
@@ -3038,8 +3130,8 @@ var ui_default = `<!doctype html>
|
|
|
3038
3130
|
.slider:before {
|
|
3039
3131
|
content: '';
|
|
3040
3132
|
position: absolute;
|
|
3041
|
-
width:
|
|
3042
|
-
height:
|
|
3133
|
+
width: 13px;
|
|
3134
|
+
height: 13px;
|
|
3043
3135
|
left: 3px;
|
|
3044
3136
|
bottom: 3px;
|
|
3045
3137
|
background: #fff;
|
|
@@ -3050,7 +3142,7 @@ var ui_default = `<!doctype html>
|
|
|
3050
3142
|
background: var(--success);
|
|
3051
3143
|
}
|
|
3052
3144
|
input:checked + .slider:before {
|
|
3053
|
-
transform: translateX(
|
|
3145
|
+
transform: translateX(15px);
|
|
3054
3146
|
}
|
|
3055
3147
|
input:disabled + .slider {
|
|
3056
3148
|
opacity: 0.4;
|
|
@@ -3209,12 +3301,17 @@ var ui_default = `<!doctype html>
|
|
|
3209
3301
|
border: 1px solid var(--border);
|
|
3210
3302
|
}
|
|
3211
3303
|
|
|
3212
|
-
@media (max-width:
|
|
3304
|
+
@media (max-width: 960px) {
|
|
3213
3305
|
.body {
|
|
3214
|
-
grid-template-columns: 1fr;
|
|
3306
|
+
grid-template-columns: 1fr 220px;
|
|
3307
|
+
}
|
|
3308
|
+
.flight-col {
|
|
3309
|
+
display: none;
|
|
3215
3310
|
}
|
|
3216
|
-
|
|
3217
|
-
|
|
3311
|
+
}
|
|
3312
|
+
@media (max-width: 640px) {
|
|
3313
|
+
.body {
|
|
3314
|
+
grid-template-columns: 1fr;
|
|
3218
3315
|
}
|
|
3219
3316
|
}
|
|
3220
3317
|
</style>
|
|
@@ -3228,6 +3325,19 @@ var ui_default = `<!doctype html>
|
|
|
3228
3325
|
</header>
|
|
3229
3326
|
|
|
3230
3327
|
<div class="body">
|
|
3328
|
+
<div class="flight-col">
|
|
3329
|
+
<div class="panel flight-panel">
|
|
3330
|
+
<div class="panel-title">
|
|
3331
|
+
\u{1F6F0}\uFE0F Flight Recorder
|
|
3332
|
+
<span style="font-weight: 400; color: var(--muted); font-size: 11px">live</span>
|
|
3333
|
+
<button class="feed-clear-btn" onclick="clearFeed()">clear</button>
|
|
3334
|
+
</div>
|
|
3335
|
+
<div id="activity-feed">
|
|
3336
|
+
<span class="decisions-empty">Waiting for agent activity\u2026</span>
|
|
3337
|
+
</div>
|
|
3338
|
+
</div>
|
|
3339
|
+
</div>
|
|
3340
|
+
|
|
3231
3341
|
<div class="main">
|
|
3232
3342
|
<div id="warnBanner" class="warning-banner">
|
|
3233
3343
|
\u26A0\uFE0F Auto-start is off \u2014 daemon started manually. Run "node9 daemon stop" to stop it, or
|
|
@@ -3308,6 +3418,11 @@ var ui_default = `<!doctype html>
|
|
|
3308
3418
|
<div id="slackStatusLine" class="slack-status-line">No key saved</div>
|
|
3309
3419
|
</div>
|
|
3310
3420
|
|
|
3421
|
+
<div class="panel">
|
|
3422
|
+
<div class="panel-title">\u{1F6E1}\uFE0F Active Shields</div>
|
|
3423
|
+
<div id="shieldsList"><span class="decisions-empty">Loading\u2026</span></div>
|
|
3424
|
+
</div>
|
|
3425
|
+
|
|
3311
3426
|
<div class="panel">
|
|
3312
3427
|
<div class="panel-title">\u{1F4CB} Persistent Decisions</div>
|
|
3313
3428
|
<div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
|
|
@@ -3353,14 +3468,23 @@ var ui_default = `<!doctype html>
|
|
|
3353
3468
|
|
|
3354
3469
|
function updateDenyButton(id, timestamp) {
|
|
3355
3470
|
const btn = document.querySelector('#c-' + id + ' .btn-deny');
|
|
3471
|
+
const timer = document.querySelector('#timer-' + id);
|
|
3356
3472
|
if (!btn) return;
|
|
3357
3473
|
const elapsed = Date.now() - timestamp;
|
|
3358
3474
|
const remaining = Math.max(0, Math.ceil((autoDenyMs - elapsed) / 1000));
|
|
3359
3475
|
if (remaining <= 0) {
|
|
3360
|
-
btn.textContent = 'Auto-Denying
|
|
3476
|
+
btn.textContent = '\u23F3 Auto-Denying\u2026';
|
|
3361
3477
|
btn.disabled = true;
|
|
3478
|
+
if (timer) {
|
|
3479
|
+
timer.textContent = 'auto-deny';
|
|
3480
|
+
timer.className = 'card-timer urgent';
|
|
3481
|
+
}
|
|
3362
3482
|
} else {
|
|
3363
|
-
btn.textContent = 'Block Action
|
|
3483
|
+
btn.textContent = '\u{1F6AB} Block this Action';
|
|
3484
|
+
if (timer) {
|
|
3485
|
+
timer.textContent = remaining + 's';
|
|
3486
|
+
timer.className = 'card-timer' + (remaining < 15 ? ' urgent' : '');
|
|
3487
|
+
}
|
|
3364
3488
|
setTimeout(() => updateDenyButton(id, timestamp), 1000);
|
|
3365
3489
|
}
|
|
3366
3490
|
}
|
|
@@ -3376,34 +3500,61 @@ var ui_default = `<!doctype html>
|
|
|
3376
3500
|
empty.style.display = requests.size === 0 ? 'block' : 'none';
|
|
3377
3501
|
}
|
|
3378
3502
|
|
|
3379
|
-
function
|
|
3503
|
+
function setCardBusy(card, busy) {
|
|
3504
|
+
if (!card) return;
|
|
3505
|
+
card.querySelectorAll('button').forEach((b) => (b.disabled = busy));
|
|
3506
|
+
card.style.opacity = busy ? '0.5' : '1';
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
function showCardError(card, msg) {
|
|
3510
|
+
if (!card) return;
|
|
3511
|
+
card.style.outline = '2px solid #f87171';
|
|
3512
|
+
let err = card.querySelector('.card-error');
|
|
3513
|
+
if (!err) {
|
|
3514
|
+
err = document.createElement('p');
|
|
3515
|
+
err.className = 'card-error';
|
|
3516
|
+
err.style.cssText = 'color:#f87171;font-size:11px;margin:6px 0 0;';
|
|
3517
|
+
card.appendChild(err);
|
|
3518
|
+
}
|
|
3519
|
+
err.textContent = '\u26A0 ' + msg + ' \u2014 please try again or refresh.';
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
async function sendDecision(id, decision, persist) {
|
|
3380
3523
|
const card = document.getElementById('c-' + id);
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3524
|
+
setCardBusy(card, true);
|
|
3525
|
+
try {
|
|
3526
|
+
const res = await fetch('/decision/' + id, {
|
|
3527
|
+
method: 'POST',
|
|
3528
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3529
|
+
body: JSON.stringify({ decision, persist: !!persist }),
|
|
3530
|
+
});
|
|
3531
|
+
if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
|
|
3388
3532
|
card?.remove();
|
|
3389
3533
|
requests.delete(id);
|
|
3390
3534
|
refresh();
|
|
3391
|
-
}
|
|
3535
|
+
} catch (err) {
|
|
3536
|
+
setCardBusy(card, false);
|
|
3537
|
+
showCardError(card, err.message || 'Network error');
|
|
3538
|
+
}
|
|
3392
3539
|
}
|
|
3393
3540
|
|
|
3394
|
-
function sendTrust(id, duration) {
|
|
3541
|
+
async function sendTrust(id, duration) {
|
|
3395
3542
|
const card = document.getElementById('c-' + id);
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3543
|
+
setCardBusy(card, true);
|
|
3544
|
+
try {
|
|
3545
|
+
const res = await fetch('/decision/' + id, {
|
|
3546
|
+
method: 'POST',
|
|
3547
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3548
|
+
body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
|
|
3549
|
+
});
|
|
3550
|
+
if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
|
|
3403
3551
|
card?.remove();
|
|
3404
3552
|
requests.delete(id);
|
|
3405
3553
|
refresh();
|
|
3406
|
-
}
|
|
3554
|
+
} catch (err) {
|
|
3555
|
+
setCardBusy(card, false);
|
|
3556
|
+
showCardError(card, err.message || 'Network error');
|
|
3557
|
+
}
|
|
3407
3558
|
}
|
|
3408
3559
|
|
|
3409
3560
|
function renderPayload(req) {
|
|
@@ -3454,16 +3605,21 @@ var ui_default = `<!doctype html>
|
|
|
3454
3605
|
const mcpLabel = req.mcpServer ? esc(req.mcpServer) : null;
|
|
3455
3606
|
const dis = isSlack ? 'disabled' : '';
|
|
3456
3607
|
card.innerHTML = \`
|
|
3608
|
+
<div class="card-header">
|
|
3609
|
+
<span class="card-header-icon">\${isSlack ? '\u26A1' : '\u26A0\uFE0F'}</span>
|
|
3610
|
+
<span class="card-header-title">\${isSlack ? 'Awaiting Cloud Approval' : 'Action Required'}</span>
|
|
3611
|
+
<span class="card-timer" id="timer-\${req.id}">\${autoDenyMs > 0 ? Math.ceil(autoDenyMs / 1000) + 's' : ''}</span>
|
|
3612
|
+
</div>
|
|
3457
3613
|
<div class="source-row">
|
|
3458
3614
|
<span class="agent-badge">\${agentLabel}</span>
|
|
3459
3615
|
\${mcpLabel ? \`<span class="source-arrow">\u2192</span><span class="mcp-badge">mcp::\${mcpLabel}</span>\` : ''}
|
|
3460
3616
|
</div>
|
|
3461
3617
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
3462
|
-
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting
|
|
3618
|
+
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
|
|
3463
3619
|
\${renderPayload(req)}
|
|
3464
3620
|
<div class="actions" id="act-\${req.id}">
|
|
3465
|
-
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}
|
|
3466
|
-
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}
|
|
3621
|
+
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
|
|
3622
|
+
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>\u{1F6AB} Block this Action</button>
|
|
3467
3623
|
<div class="trust-row\${trustEnabled ? ' show' : ''}" id="tr-\${req.id}">
|
|
3468
3624
|
<button class="btn-trust" onclick="sendTrust('\${req.id}','30m')" \${dis}>\u23F1 Trust 30m</button>
|
|
3469
3625
|
<button class="btn-trust" onclick="sendTrust('\${req.id}','1h')" \${dis}>\u23F1 Trust 1h</button>
|
|
@@ -3523,9 +3679,84 @@ var ui_default = `<!doctype html>
|
|
|
3523
3679
|
ev.addEventListener('slack-status', (e) => {
|
|
3524
3680
|
applySlackStatus(JSON.parse(e.data));
|
|
3525
3681
|
});
|
|
3682
|
+
ev.addEventListener('shields-status', (e) => {
|
|
3683
|
+
renderShields(JSON.parse(e.data).shields);
|
|
3684
|
+
});
|
|
3685
|
+
|
|
3686
|
+
// \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
|
|
3687
|
+
ev.addEventListener('activity', (e) => {
|
|
3688
|
+
const data = JSON.parse(e.data);
|
|
3689
|
+
const feed = document.getElementById('activity-feed');
|
|
3690
|
+
// Remove placeholder on first item
|
|
3691
|
+
const placeholder = feed.querySelector('.decisions-empty');
|
|
3692
|
+
if (placeholder) placeholder.remove();
|
|
3693
|
+
|
|
3694
|
+
const time = new Date(data.ts).toLocaleTimeString([], {
|
|
3695
|
+
hour12: false,
|
|
3696
|
+
hour: '2-digit',
|
|
3697
|
+
minute: '2-digit',
|
|
3698
|
+
second: '2-digit',
|
|
3699
|
+
});
|
|
3700
|
+
const icon = frIcon(data.tool);
|
|
3701
|
+
const argsStr = JSON.stringify(data.args ?? {});
|
|
3702
|
+
const argsPreview = esc(argsStr.length > 120 ? argsStr.slice(0, 120) + '\u2026' : argsStr);
|
|
3703
|
+
|
|
3704
|
+
const row = document.createElement('div');
|
|
3705
|
+
row.className = 'feed-row';
|
|
3706
|
+
row.id = 'fr-' + data.id;
|
|
3707
|
+
row.innerHTML = \`
|
|
3708
|
+
<span class="feed-ts">\${time}</span>
|
|
3709
|
+
<span class="feed-icon">\${icon}</span>
|
|
3710
|
+
<span class="feed-content"><strong>\${esc(data.tool)}</strong><span class="feed-args">\${argsPreview}</span></span>
|
|
3711
|
+
<span class="feed-badge fr-pending">\u25CF</span>
|
|
3712
|
+
\`;
|
|
3713
|
+
feed.prepend(row);
|
|
3714
|
+
if (feed.children.length > 100) feed.lastChild.remove();
|
|
3715
|
+
});
|
|
3716
|
+
|
|
3717
|
+
ev.addEventListener('activity-result', (e) => {
|
|
3718
|
+
const { id, status, label } = JSON.parse(e.data);
|
|
3719
|
+
const row = document.getElementById('fr-' + id);
|
|
3720
|
+
if (!row) return;
|
|
3721
|
+
const badge = row.querySelector('.feed-badge');
|
|
3722
|
+
if (status === 'allow') {
|
|
3723
|
+
badge.textContent = 'ALLOW';
|
|
3724
|
+
badge.className = 'feed-badge fr-allow';
|
|
3725
|
+
} else if (status === 'dlp') {
|
|
3726
|
+
badge.textContent = '\u{1F6E1}\uFE0F DLP';
|
|
3727
|
+
badge.className = 'feed-badge fr-dlp';
|
|
3728
|
+
row.classList.add('fr-dlp-row');
|
|
3729
|
+
} else {
|
|
3730
|
+
badge.textContent = 'BLOCK';
|
|
3731
|
+
badge.className = 'feed-badge fr-block';
|
|
3732
|
+
}
|
|
3733
|
+
});
|
|
3526
3734
|
}
|
|
3527
3735
|
connect();
|
|
3528
3736
|
|
|
3737
|
+
const FR_ICONS = {
|
|
3738
|
+
bash: '\u{1F4BB}',
|
|
3739
|
+
read: '\u{1F4D6}',
|
|
3740
|
+
edit: '\u270F\uFE0F',
|
|
3741
|
+
write: '\u270F\uFE0F',
|
|
3742
|
+
glob: '\u{1F4C2}',
|
|
3743
|
+
grep: '\u{1F50D}',
|
|
3744
|
+
agent: '\u{1F916}',
|
|
3745
|
+
search: '\u{1F50D}',
|
|
3746
|
+
sql: '\u{1F5C4}\uFE0F',
|
|
3747
|
+
query: '\u{1F5C4}\uFE0F',
|
|
3748
|
+
list: '\u{1F4C2}',
|
|
3749
|
+
delete: '\u{1F5D1}\uFE0F',
|
|
3750
|
+
web: '\u{1F310}',
|
|
3751
|
+
};
|
|
3752
|
+
function frIcon(tool) {
|
|
3753
|
+
const t = (tool || '').toLowerCase();
|
|
3754
|
+
for (const [k, v] of Object.entries(FR_ICONS)) {
|
|
3755
|
+
if (t.includes(k)) return v;
|
|
3756
|
+
}
|
|
3757
|
+
return '\u{1F6E0}\uFE0F';
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3529
3760
|
function saveSetting(key, value) {
|
|
3530
3761
|
fetch('/settings', {
|
|
3531
3762
|
method: 'POST',
|
|
@@ -3615,6 +3846,49 @@ var ui_default = `<!doctype html>
|
|
|
3615
3846
|
}
|
|
3616
3847
|
}
|
|
3617
3848
|
|
|
3849
|
+
function clearFeed() {
|
|
3850
|
+
const feed = document.getElementById('activity-feed');
|
|
3851
|
+
feed.innerHTML = '<span class="decisions-empty">Feed cleared.</span>';
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
function renderShields(shields) {
|
|
3855
|
+
const list = document.getElementById('shieldsList');
|
|
3856
|
+
if (!shields || shields.length === 0) {
|
|
3857
|
+
list.innerHTML = '<span class="decisions-empty">No shields available.</span>';
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3860
|
+
list.innerHTML = shields
|
|
3861
|
+
.map(
|
|
3862
|
+
(s) => \`
|
|
3863
|
+
<div class="shield-row">
|
|
3864
|
+
<div class="shield-info">
|
|
3865
|
+
<div class="shield-name">\${esc(s.name)}</div>
|
|
3866
|
+
<div class="shield-desc">\${esc(s.description)}</div>
|
|
3867
|
+
</div>
|
|
3868
|
+
<label class="toggle">
|
|
3869
|
+
<input type="checkbox" \${s.active ? 'checked' : ''}
|
|
3870
|
+
onchange="toggleShield('\${esc(s.name)}', this.checked)" />
|
|
3871
|
+
<span class="slider"></span>
|
|
3872
|
+
</label>
|
|
3873
|
+
</div>
|
|
3874
|
+
\`
|
|
3875
|
+
)
|
|
3876
|
+
.join('');
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
function toggleShield(name, active) {
|
|
3880
|
+
fetch('/shields', {
|
|
3881
|
+
method: 'POST',
|
|
3882
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3883
|
+
body: JSON.stringify({ name, active }),
|
|
3884
|
+
}).catch(() => {});
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
fetch('/shields')
|
|
3888
|
+
.then((r) => r.json())
|
|
3889
|
+
.then(({ shields }) => renderShields(shields))
|
|
3890
|
+
.catch(() => {});
|
|
3891
|
+
|
|
3618
3892
|
function renderDecisions(decisions) {
|
|
3619
3893
|
const dl = document.getElementById('decisionsList');
|
|
3620
3894
|
const entries = Object.entries(decisions);
|
|
@@ -3661,31 +3935,24 @@ var ui_default = `<!doctype html>
|
|
|
3661
3935
|
</body>
|
|
3662
3936
|
</html>
|
|
3663
3937
|
`;
|
|
3938
|
+
}
|
|
3939
|
+
});
|
|
3664
3940
|
|
|
3665
3941
|
// src/daemon/ui.ts
|
|
3666
|
-
var UI_HTML_TEMPLATE
|
|
3942
|
+
var UI_HTML_TEMPLATE;
|
|
3943
|
+
var init_ui2 = __esm({
|
|
3944
|
+
"src/daemon/ui.ts"() {
|
|
3945
|
+
"use strict";
|
|
3946
|
+
init_ui();
|
|
3947
|
+
UI_HTML_TEMPLATE = ui_default;
|
|
3948
|
+
}
|
|
3949
|
+
});
|
|
3667
3950
|
|
|
3668
3951
|
// src/daemon/index.ts
|
|
3669
|
-
var import_http = __toESM(require("http"));
|
|
3670
|
-
var import_fs4 = __toESM(require("fs"));
|
|
3671
|
-
var import_path6 = __toESM(require("path"));
|
|
3672
|
-
var import_os4 = __toESM(require("os"));
|
|
3673
|
-
var import_child_process2 = require("child_process");
|
|
3674
|
-
var import_crypto2 = require("crypto");
|
|
3675
|
-
var import_chalk4 = __toESM(require("chalk"));
|
|
3676
|
-
var DAEMON_PORT2 = 7391;
|
|
3677
|
-
var DAEMON_HOST2 = "127.0.0.1";
|
|
3678
|
-
var homeDir = import_os4.default.homedir();
|
|
3679
|
-
var DAEMON_PID_FILE = import_path6.default.join(homeDir, ".node9", "daemon.pid");
|
|
3680
|
-
var DECISIONS_FILE = import_path6.default.join(homeDir, ".node9", "decisions.json");
|
|
3681
|
-
var GLOBAL_CONFIG_FILE = import_path6.default.join(homeDir, ".node9", "config.json");
|
|
3682
|
-
var CREDENTIALS_FILE = import_path6.default.join(homeDir, ".node9", "credentials.json");
|
|
3683
|
-
var AUDIT_LOG_FILE = import_path6.default.join(homeDir, ".node9", "audit.log");
|
|
3684
|
-
var TRUST_FILE2 = import_path6.default.join(homeDir, ".node9", "trust.json");
|
|
3685
3952
|
function atomicWriteSync2(filePath, data, options) {
|
|
3686
3953
|
const dir = import_path6.default.dirname(filePath);
|
|
3687
3954
|
if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
3688
|
-
const tmpPath = `${filePath}.${(0,
|
|
3955
|
+
const tmpPath = `${filePath}.${(0, import_crypto3.randomUUID)()}.tmp`;
|
|
3689
3956
|
import_fs4.default.writeFileSync(tmpPath, data, options);
|
|
3690
3957
|
import_fs4.default.renameSync(tmpPath, filePath);
|
|
3691
3958
|
}
|
|
@@ -3703,12 +3970,6 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
3703
3970
|
} catch {
|
|
3704
3971
|
}
|
|
3705
3972
|
}
|
|
3706
|
-
var TRUST_DURATIONS = {
|
|
3707
|
-
"30m": 30 * 6e4,
|
|
3708
|
-
"1h": 60 * 6e4,
|
|
3709
|
-
"2h": 2 * 60 * 6e4
|
|
3710
|
-
};
|
|
3711
|
-
var SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
3712
3973
|
function redactArgs(value) {
|
|
3713
3974
|
if (!value || typeof value !== "object") return value;
|
|
3714
3975
|
if (Array.isArray(value)) return value.map(redactArgs);
|
|
@@ -3743,7 +4004,6 @@ function getAuditHistory(limit = 20) {
|
|
|
3743
4004
|
return [];
|
|
3744
4005
|
}
|
|
3745
4006
|
}
|
|
3746
|
-
var AUTO_DENY_MS = 12e4;
|
|
3747
4007
|
function getOrgName() {
|
|
3748
4008
|
try {
|
|
3749
4009
|
if (import_fs4.default.existsSync(CREDENTIALS_FILE)) {
|
|
@@ -3753,7 +4013,6 @@ function getOrgName() {
|
|
|
3753
4013
|
}
|
|
3754
4014
|
return null;
|
|
3755
4015
|
}
|
|
3756
|
-
var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
3757
4016
|
function hasStoredSlackKey() {
|
|
3758
4017
|
return import_fs4.default.existsSync(CREDENTIALS_FILE);
|
|
3759
4018
|
}
|
|
@@ -3769,11 +4028,6 @@ function writeGlobalSetting(key, value) {
|
|
|
3769
4028
|
config.settings[key] = value;
|
|
3770
4029
|
atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3771
4030
|
}
|
|
3772
|
-
var pending = /* @__PURE__ */ new Map();
|
|
3773
|
-
var sseClients = /* @__PURE__ */ new Set();
|
|
3774
|
-
var abandonTimer = null;
|
|
3775
|
-
var daemonServer = null;
|
|
3776
|
-
var hadBrowserClient = false;
|
|
3777
4031
|
function abandonPending() {
|
|
3778
4032
|
abandonTimer = null;
|
|
3779
4033
|
pending.forEach((entry, id) => {
|
|
@@ -3795,6 +4049,18 @@ function abandonPending() {
|
|
|
3795
4049
|
}
|
|
3796
4050
|
}
|
|
3797
4051
|
function broadcast(event, data) {
|
|
4052
|
+
if (event === "activity") {
|
|
4053
|
+
activityRing.push({ event, data });
|
|
4054
|
+
if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
|
|
4055
|
+
} else if (event === "activity-result") {
|
|
4056
|
+
const { id, status, label } = data;
|
|
4057
|
+
for (let i = activityRing.length - 1; i >= 0; i--) {
|
|
4058
|
+
if (activityRing[i].data.id === id) {
|
|
4059
|
+
Object.assign(activityRing[i].data, { status, label });
|
|
4060
|
+
break;
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
3798
4064
|
const msg = `event: ${event}
|
|
3799
4065
|
data: ${JSON.stringify(data)}
|
|
3800
4066
|
|
|
@@ -3810,7 +4076,7 @@ data: ${JSON.stringify(data)}
|
|
|
3810
4076
|
function openBrowser(url) {
|
|
3811
4077
|
try {
|
|
3812
4078
|
const args = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
|
|
3813
|
-
(0,
|
|
4079
|
+
(0, import_child_process3.spawn)(args[0], args.slice(1), { detached: true, stdio: "ignore" }).unref();
|
|
3814
4080
|
} catch {
|
|
3815
4081
|
}
|
|
3816
4082
|
}
|
|
@@ -3840,13 +4106,15 @@ function writePersistentDecision(toolName, decision) {
|
|
|
3840
4106
|
}
|
|
3841
4107
|
}
|
|
3842
4108
|
function startDaemon() {
|
|
3843
|
-
const csrfToken = (0,
|
|
3844
|
-
const internalToken = (0,
|
|
4109
|
+
const csrfToken = (0, import_crypto3.randomUUID)();
|
|
4110
|
+
const internalToken = (0, import_crypto3.randomUUID)();
|
|
3845
4111
|
const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
|
|
3846
4112
|
const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
|
|
3847
4113
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
4114
|
+
const watchMode = process.env.NODE9_WATCH_MODE === "1";
|
|
3848
4115
|
let idleTimer;
|
|
3849
4116
|
function resetIdleTimer() {
|
|
4117
|
+
if (watchMode) return;
|
|
3850
4118
|
if (idleTimer) clearTimeout(idleTimer);
|
|
3851
4119
|
idleTimer = setTimeout(() => {
|
|
3852
4120
|
if (autoStarted) {
|
|
@@ -3901,6 +4169,12 @@ data: ${JSON.stringify({
|
|
|
3901
4169
|
data: ${JSON.stringify(readPersistentDecisions())}
|
|
3902
4170
|
|
|
3903
4171
|
`);
|
|
4172
|
+
for (const item of activityRing) {
|
|
4173
|
+
res.write(`event: ${item.event}
|
|
4174
|
+
data: ${JSON.stringify(item.data)}
|
|
4175
|
+
|
|
4176
|
+
`);
|
|
4177
|
+
}
|
|
3904
4178
|
return req.on("close", () => {
|
|
3905
4179
|
sseClients.delete(res);
|
|
3906
4180
|
if (sseClients.size === 0 && pending.size > 0) {
|
|
@@ -3920,9 +4194,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3920
4194
|
slackDelegated = false,
|
|
3921
4195
|
agent,
|
|
3922
4196
|
mcpServer,
|
|
3923
|
-
riskMetadata
|
|
4197
|
+
riskMetadata,
|
|
4198
|
+
fromCLI = false,
|
|
4199
|
+
activityId
|
|
3924
4200
|
} = JSON.parse(body);
|
|
3925
|
-
const id = (0,
|
|
4201
|
+
const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto3.randomUUID)();
|
|
3926
4202
|
const entry = {
|
|
3927
4203
|
id,
|
|
3928
4204
|
toolName,
|
|
@@ -3953,6 +4229,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3953
4229
|
}, AUTO_DENY_MS)
|
|
3954
4230
|
};
|
|
3955
4231
|
pending.set(id, entry);
|
|
4232
|
+
if (!fromCLI) {
|
|
4233
|
+
broadcast("activity", {
|
|
4234
|
+
id,
|
|
4235
|
+
ts: entry.timestamp,
|
|
4236
|
+
tool: toolName,
|
|
4237
|
+
args: redactArgs(args),
|
|
4238
|
+
status: "pending"
|
|
4239
|
+
});
|
|
4240
|
+
}
|
|
3956
4241
|
const browserEnabled = getConfig().settings.approvers?.browser !== false;
|
|
3957
4242
|
if (browserEnabled) {
|
|
3958
4243
|
broadcast("add", {
|
|
@@ -3982,6 +4267,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3982
4267
|
const e = pending.get(id);
|
|
3983
4268
|
if (!e) return;
|
|
3984
4269
|
if (result.noApprovalMechanism) return;
|
|
4270
|
+
broadcast("activity-result", {
|
|
4271
|
+
id,
|
|
4272
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
4273
|
+
label: result.blockedByLabel
|
|
4274
|
+
});
|
|
3985
4275
|
clearTimeout(e.timer);
|
|
3986
4276
|
const decision = result.approved ? "allow" : "deny";
|
|
3987
4277
|
appendAuditLog({ toolName: e.toolName, args: e.args, decision });
|
|
@@ -4016,8 +4306,8 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
4016
4306
|
const entry = pending.get(id);
|
|
4017
4307
|
if (!entry) return res.writeHead(404).end();
|
|
4018
4308
|
if (entry.earlyDecision) {
|
|
4309
|
+
clearTimeout(entry.timer);
|
|
4019
4310
|
pending.delete(id);
|
|
4020
|
-
broadcast("remove", { id });
|
|
4021
4311
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4022
4312
|
const body = { decision: entry.earlyDecision };
|
|
4023
4313
|
if (entry.earlyReason) body.reason = entry.earlyReason;
|
|
@@ -4047,10 +4337,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
4047
4337
|
decision: `trust:${trustDuration}`
|
|
4048
4338
|
});
|
|
4049
4339
|
clearTimeout(entry.timer);
|
|
4050
|
-
if (entry.waiter)
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4340
|
+
if (entry.waiter) {
|
|
4341
|
+
entry.waiter("allow");
|
|
4342
|
+
pending.delete(id);
|
|
4343
|
+
broadcast("remove", { id });
|
|
4344
|
+
} else {
|
|
4345
|
+
entry.earlyDecision = "allow";
|
|
4346
|
+
broadcast("remove", { id });
|
|
4347
|
+
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
4348
|
+
}
|
|
4054
4349
|
res.writeHead(200);
|
|
4055
4350
|
return res.end(JSON.stringify({ ok: true }));
|
|
4056
4351
|
}
|
|
@@ -4062,13 +4357,16 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
4062
4357
|
decision: resolvedDecision
|
|
4063
4358
|
});
|
|
4064
4359
|
clearTimeout(entry.timer);
|
|
4065
|
-
if (entry.waiter)
|
|
4066
|
-
|
|
4360
|
+
if (entry.waiter) {
|
|
4361
|
+
entry.waiter(resolvedDecision, reason);
|
|
4362
|
+
pending.delete(id);
|
|
4363
|
+
broadcast("remove", { id });
|
|
4364
|
+
} else {
|
|
4067
4365
|
entry.earlyDecision = resolvedDecision;
|
|
4068
4366
|
entry.earlyReason = reason;
|
|
4367
|
+
broadcast("remove", { id });
|
|
4368
|
+
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
4069
4369
|
}
|
|
4070
|
-
pending.delete(id);
|
|
4071
|
-
broadcast("remove", { id });
|
|
4072
4370
|
res.writeHead(200);
|
|
4073
4371
|
return res.end(JSON.stringify({ ok: true }));
|
|
4074
4372
|
} catch {
|
|
@@ -4121,119 +4419,749 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
4121
4419
|
res.writeHead(400).end();
|
|
4122
4420
|
}
|
|
4123
4421
|
}
|
|
4124
|
-
if (req.method === "DELETE" && pathname.startsWith("/decisions/")) {
|
|
4125
|
-
if (!validToken(req)) return res.writeHead(403).end();
|
|
4126
|
-
try {
|
|
4127
|
-
const toolName = decodeURIComponent(pathname.split("/").pop());
|
|
4128
|
-
const decisions = readPersistentDecisions();
|
|
4129
|
-
delete decisions[toolName];
|
|
4130
|
-
atomicWriteSync2(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
|
|
4131
|
-
broadcast("decisions", decisions);
|
|
4132
|
-
res.writeHead(200);
|
|
4133
|
-
return res.end(JSON.stringify({ ok: true }));
|
|
4134
|
-
} catch {
|
|
4135
|
-
res.writeHead(400).end();
|
|
4422
|
+
if (req.method === "DELETE" && pathname.startsWith("/decisions/")) {
|
|
4423
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4424
|
+
try {
|
|
4425
|
+
const toolName = decodeURIComponent(pathname.split("/").pop());
|
|
4426
|
+
const decisions = readPersistentDecisions();
|
|
4427
|
+
delete decisions[toolName];
|
|
4428
|
+
atomicWriteSync2(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
|
|
4429
|
+
broadcast("decisions", decisions);
|
|
4430
|
+
res.writeHead(200);
|
|
4431
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4432
|
+
} catch {
|
|
4433
|
+
res.writeHead(400).end();
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
if (req.method === "POST" && pathname.startsWith("/resolve/")) {
|
|
4437
|
+
const internalAuth = req.headers["x-node9-internal"];
|
|
4438
|
+
if (internalAuth !== internalToken) return res.writeHead(403).end();
|
|
4439
|
+
try {
|
|
4440
|
+
const id = pathname.split("/").pop();
|
|
4441
|
+
const entry = pending.get(id);
|
|
4442
|
+
if (!entry) return res.writeHead(404).end();
|
|
4443
|
+
const { decision } = JSON.parse(await readBody(req));
|
|
4444
|
+
appendAuditLog({
|
|
4445
|
+
toolName: entry.toolName,
|
|
4446
|
+
args: entry.args,
|
|
4447
|
+
decision
|
|
4448
|
+
});
|
|
4449
|
+
clearTimeout(entry.timer);
|
|
4450
|
+
if (entry.waiter) entry.waiter(decision);
|
|
4451
|
+
else entry.earlyDecision = decision;
|
|
4452
|
+
pending.delete(id);
|
|
4453
|
+
broadcast("remove", { id });
|
|
4454
|
+
res.writeHead(200);
|
|
4455
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4456
|
+
} catch {
|
|
4457
|
+
res.writeHead(400).end();
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
if (req.method === "POST" && pathname === "/events/clear") {
|
|
4461
|
+
activityRing.length = 0;
|
|
4462
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4463
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4464
|
+
}
|
|
4465
|
+
if (req.method === "GET" && pathname === "/audit") {
|
|
4466
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4467
|
+
return res.end(JSON.stringify(getAuditHistory()));
|
|
4468
|
+
}
|
|
4469
|
+
if (req.method === "GET" && pathname === "/shields") {
|
|
4470
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4471
|
+
const active = readActiveShields();
|
|
4472
|
+
const shields = Object.values(SHIELDS).map((s) => ({
|
|
4473
|
+
name: s.name,
|
|
4474
|
+
description: s.description,
|
|
4475
|
+
active: active.includes(s.name)
|
|
4476
|
+
}));
|
|
4477
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4478
|
+
return res.end(JSON.stringify({ shields }));
|
|
4479
|
+
}
|
|
4480
|
+
if (req.method === "POST" && pathname === "/shields") {
|
|
4481
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4482
|
+
try {
|
|
4483
|
+
const { name, active } = JSON.parse(await readBody(req));
|
|
4484
|
+
if (!SHIELDS[name]) return res.writeHead(400).end();
|
|
4485
|
+
const current = readActiveShields();
|
|
4486
|
+
const updated = active ? [.../* @__PURE__ */ new Set([...current, name])] : current.filter((n) => n !== name);
|
|
4487
|
+
writeActiveShields(updated);
|
|
4488
|
+
_resetConfigCache();
|
|
4489
|
+
const shieldsPayload = Object.values(SHIELDS).map((s) => ({
|
|
4490
|
+
name: s.name,
|
|
4491
|
+
description: s.description,
|
|
4492
|
+
active: updated.includes(s.name)
|
|
4493
|
+
}));
|
|
4494
|
+
broadcast("shields-status", { shields: shieldsPayload });
|
|
4495
|
+
res.writeHead(200);
|
|
4496
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4497
|
+
} catch {
|
|
4498
|
+
res.writeHead(400).end();
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
res.writeHead(404).end();
|
|
4502
|
+
});
|
|
4503
|
+
daemonServer = server;
|
|
4504
|
+
server.on("error", (e) => {
|
|
4505
|
+
if (e.code === "EADDRINUSE") {
|
|
4506
|
+
try {
|
|
4507
|
+
if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
|
|
4508
|
+
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4509
|
+
process.kill(pid, 0);
|
|
4510
|
+
return process.exit(0);
|
|
4511
|
+
}
|
|
4512
|
+
} catch {
|
|
4513
|
+
try {
|
|
4514
|
+
import_fs4.default.unlinkSync(DAEMON_PID_FILE);
|
|
4515
|
+
} catch {
|
|
4516
|
+
}
|
|
4517
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
4518
|
+
return;
|
|
4519
|
+
}
|
|
4520
|
+
fetch(`http://${DAEMON_HOST2}:${DAEMON_PORT2}/settings`, {
|
|
4521
|
+
signal: AbortSignal.timeout(1e3)
|
|
4522
|
+
}).then((res) => {
|
|
4523
|
+
if (res.ok) {
|
|
4524
|
+
try {
|
|
4525
|
+
const r = (0, import_child_process3.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT2}`], {
|
|
4526
|
+
encoding: "utf8",
|
|
4527
|
+
timeout: 1e3
|
|
4528
|
+
});
|
|
4529
|
+
const match = r.stdout?.match(/pid=(\d+)/);
|
|
4530
|
+
if (match) {
|
|
4531
|
+
const orphanPid = parseInt(match[1], 10);
|
|
4532
|
+
process.kill(orphanPid, 0);
|
|
4533
|
+
atomicWriteSync2(
|
|
4534
|
+
DAEMON_PID_FILE,
|
|
4535
|
+
JSON.stringify({ pid: orphanPid, port: DAEMON_PORT2, internalToken, autoStarted }),
|
|
4536
|
+
{ mode: 384 }
|
|
4537
|
+
);
|
|
4538
|
+
}
|
|
4539
|
+
} catch {
|
|
4540
|
+
}
|
|
4541
|
+
process.exit(0);
|
|
4542
|
+
} else {
|
|
4543
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
4544
|
+
}
|
|
4545
|
+
}).catch(() => {
|
|
4546
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
4547
|
+
});
|
|
4548
|
+
return;
|
|
4549
|
+
}
|
|
4550
|
+
console.error(import_chalk4.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
|
|
4551
|
+
process.exit(1);
|
|
4552
|
+
});
|
|
4553
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
|
|
4554
|
+
atomicWriteSync2(
|
|
4555
|
+
DAEMON_PID_FILE,
|
|
4556
|
+
JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
|
|
4557
|
+
{ mode: 384 }
|
|
4558
|
+
);
|
|
4559
|
+
console.log(import_chalk4.default.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
|
|
4560
|
+
});
|
|
4561
|
+
if (watchMode) {
|
|
4562
|
+
console.log(import_chalk4.default.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
|
|
4563
|
+
}
|
|
4564
|
+
try {
|
|
4565
|
+
import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4566
|
+
} catch {
|
|
4567
|
+
}
|
|
4568
|
+
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
4569
|
+
const unixServer = import_net2.default.createServer((socket) => {
|
|
4570
|
+
const chunks = [];
|
|
4571
|
+
let bytesReceived = 0;
|
|
4572
|
+
socket.on("data", (chunk) => {
|
|
4573
|
+
bytesReceived += chunk.length;
|
|
4574
|
+
if (bytesReceived > ACTIVITY_MAX_BYTES) {
|
|
4575
|
+
socket.destroy();
|
|
4576
|
+
return;
|
|
4577
|
+
}
|
|
4578
|
+
chunks.push(chunk);
|
|
4579
|
+
});
|
|
4580
|
+
socket.on("end", () => {
|
|
4581
|
+
try {
|
|
4582
|
+
const data = JSON.parse(Buffer.concat(chunks).toString());
|
|
4583
|
+
if (data.status === "pending") {
|
|
4584
|
+
broadcast("activity", {
|
|
4585
|
+
id: data.id,
|
|
4586
|
+
ts: data.ts,
|
|
4587
|
+
tool: data.tool,
|
|
4588
|
+
args: redactArgs(data.args),
|
|
4589
|
+
status: "pending"
|
|
4590
|
+
});
|
|
4591
|
+
} else {
|
|
4592
|
+
broadcast("activity-result", {
|
|
4593
|
+
id: data.id,
|
|
4594
|
+
status: data.status,
|
|
4595
|
+
label: data.label
|
|
4596
|
+
});
|
|
4597
|
+
}
|
|
4598
|
+
} catch {
|
|
4599
|
+
}
|
|
4600
|
+
});
|
|
4601
|
+
socket.on("error", () => {
|
|
4602
|
+
});
|
|
4603
|
+
});
|
|
4604
|
+
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
4605
|
+
process.on("exit", () => {
|
|
4606
|
+
try {
|
|
4607
|
+
import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4608
|
+
} catch {
|
|
4609
|
+
}
|
|
4610
|
+
});
|
|
4611
|
+
}
|
|
4612
|
+
function stopDaemon() {
|
|
4613
|
+
if (!import_fs4.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
|
|
4614
|
+
try {
|
|
4615
|
+
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4616
|
+
process.kill(pid, "SIGTERM");
|
|
4617
|
+
console.log(import_chalk4.default.green("\u2705 Stopped."));
|
|
4618
|
+
} catch {
|
|
4619
|
+
console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
|
|
4620
|
+
} finally {
|
|
4621
|
+
try {
|
|
4622
|
+
import_fs4.default.unlinkSync(DAEMON_PID_FILE);
|
|
4623
|
+
} catch {
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
function daemonStatus() {
|
|
4628
|
+
if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
|
|
4629
|
+
try {
|
|
4630
|
+
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4631
|
+
process.kill(pid, 0);
|
|
4632
|
+
console.log(import_chalk4.default.green("Node9 daemon: running"));
|
|
4633
|
+
return;
|
|
4634
|
+
} catch {
|
|
4635
|
+
console.log(import_chalk4.default.yellow("Node9 daemon: not running (stale PID)"));
|
|
4636
|
+
return;
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4639
|
+
const r = (0, import_child_process3.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT2}`], {
|
|
4640
|
+
encoding: "utf8",
|
|
4641
|
+
timeout: 500
|
|
4642
|
+
});
|
|
4643
|
+
if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT2}`)) {
|
|
4644
|
+
console.log(import_chalk4.default.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
|
|
4645
|
+
} else {
|
|
4646
|
+
console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
var import_http, import_net2, import_fs4, import_path6, import_os4, import_child_process3, import_crypto3, import_chalk4, 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;
|
|
4650
|
+
var init_daemon = __esm({
|
|
4651
|
+
"src/daemon/index.ts"() {
|
|
4652
|
+
"use strict";
|
|
4653
|
+
init_ui2();
|
|
4654
|
+
import_http = __toESM(require("http"));
|
|
4655
|
+
import_net2 = __toESM(require("net"));
|
|
4656
|
+
import_fs4 = __toESM(require("fs"));
|
|
4657
|
+
import_path6 = __toESM(require("path"));
|
|
4658
|
+
import_os4 = __toESM(require("os"));
|
|
4659
|
+
import_child_process3 = require("child_process");
|
|
4660
|
+
import_crypto3 = require("crypto");
|
|
4661
|
+
import_chalk4 = __toESM(require("chalk"));
|
|
4662
|
+
init_core();
|
|
4663
|
+
init_shields();
|
|
4664
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path6.default.join(import_os4.default.tmpdir(), "node9-activity.sock");
|
|
4665
|
+
DAEMON_PORT2 = 7391;
|
|
4666
|
+
DAEMON_HOST2 = "127.0.0.1";
|
|
4667
|
+
homeDir = import_os4.default.homedir();
|
|
4668
|
+
DAEMON_PID_FILE = import_path6.default.join(homeDir, ".node9", "daemon.pid");
|
|
4669
|
+
DECISIONS_FILE = import_path6.default.join(homeDir, ".node9", "decisions.json");
|
|
4670
|
+
GLOBAL_CONFIG_FILE = import_path6.default.join(homeDir, ".node9", "config.json");
|
|
4671
|
+
CREDENTIALS_FILE = import_path6.default.join(homeDir, ".node9", "credentials.json");
|
|
4672
|
+
AUDIT_LOG_FILE = import_path6.default.join(homeDir, ".node9", "audit.log");
|
|
4673
|
+
TRUST_FILE2 = import_path6.default.join(homeDir, ".node9", "trust.json");
|
|
4674
|
+
TRUST_DURATIONS = {
|
|
4675
|
+
"30m": 30 * 6e4,
|
|
4676
|
+
"1h": 60 * 6e4,
|
|
4677
|
+
"2h": 2 * 60 * 6e4
|
|
4678
|
+
};
|
|
4679
|
+
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
4680
|
+
AUTO_DENY_MS = 12e4;
|
|
4681
|
+
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
4682
|
+
pending = /* @__PURE__ */ new Map();
|
|
4683
|
+
sseClients = /* @__PURE__ */ new Set();
|
|
4684
|
+
abandonTimer = null;
|
|
4685
|
+
daemonServer = null;
|
|
4686
|
+
hadBrowserClient = false;
|
|
4687
|
+
ACTIVITY_RING_SIZE = 100;
|
|
4688
|
+
activityRing = [];
|
|
4689
|
+
}
|
|
4690
|
+
});
|
|
4691
|
+
|
|
4692
|
+
// src/tui/tail.ts
|
|
4693
|
+
var tail_exports = {};
|
|
4694
|
+
__export(tail_exports, {
|
|
4695
|
+
startTail: () => startTail
|
|
4696
|
+
});
|
|
4697
|
+
function getIcon(tool) {
|
|
4698
|
+
const t = tool.toLowerCase();
|
|
4699
|
+
for (const [k, v] of Object.entries(ICONS)) {
|
|
4700
|
+
if (t.includes(k)) return v;
|
|
4701
|
+
}
|
|
4702
|
+
return "\u{1F6E0}\uFE0F";
|
|
4703
|
+
}
|
|
4704
|
+
function formatBase(activity) {
|
|
4705
|
+
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
4706
|
+
const icon = getIcon(activity.tool);
|
|
4707
|
+
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
4708
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
4709
|
+
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
4710
|
+
return `${import_chalk5.default.gray(time)} ${icon} ${import_chalk5.default.white.bold(toolName)} ${import_chalk5.default.dim(argsPreview)}`;
|
|
4711
|
+
}
|
|
4712
|
+
function renderResult(activity, result) {
|
|
4713
|
+
const base = formatBase(activity);
|
|
4714
|
+
let status;
|
|
4715
|
+
if (result.status === "allow") {
|
|
4716
|
+
status = import_chalk5.default.green("\u2713 ALLOW");
|
|
4717
|
+
} else if (result.status === "dlp") {
|
|
4718
|
+
status = import_chalk5.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
4719
|
+
} else {
|
|
4720
|
+
status = import_chalk5.default.red("\u2717 BLOCK");
|
|
4721
|
+
}
|
|
4722
|
+
if (process.stdout.isTTY) {
|
|
4723
|
+
import_readline.default.clearLine(process.stdout, 0);
|
|
4724
|
+
import_readline.default.cursorTo(process.stdout, 0);
|
|
4725
|
+
}
|
|
4726
|
+
console.log(`${base} ${status}`);
|
|
4727
|
+
}
|
|
4728
|
+
function renderPending(activity) {
|
|
4729
|
+
if (!process.stdout.isTTY) return;
|
|
4730
|
+
process.stdout.write(`${formatBase(activity)} ${import_chalk5.default.yellow("\u25CF \u2026")}\r`);
|
|
4731
|
+
}
|
|
4732
|
+
async function ensureDaemon() {
|
|
4733
|
+
if (import_fs6.default.existsSync(PID_FILE)) {
|
|
4734
|
+
try {
|
|
4735
|
+
const { port } = JSON.parse(import_fs6.default.readFileSync(PID_FILE, "utf-8"));
|
|
4736
|
+
return port;
|
|
4737
|
+
} catch {
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
try {
|
|
4741
|
+
const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
|
|
4742
|
+
signal: AbortSignal.timeout(500)
|
|
4743
|
+
});
|
|
4744
|
+
if (res.ok) return DAEMON_PORT2;
|
|
4745
|
+
} catch {
|
|
4746
|
+
}
|
|
4747
|
+
console.log(import_chalk5.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
4748
|
+
const child = (0, import_child_process5.spawn)(process.execPath, [process.argv[1], "daemon"], {
|
|
4749
|
+
detached: true,
|
|
4750
|
+
stdio: "ignore",
|
|
4751
|
+
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
4752
|
+
});
|
|
4753
|
+
child.unref();
|
|
4754
|
+
for (let i = 0; i < 20; i++) {
|
|
4755
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
4756
|
+
try {
|
|
4757
|
+
const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
|
|
4758
|
+
signal: AbortSignal.timeout(500)
|
|
4759
|
+
});
|
|
4760
|
+
if (res.ok) return DAEMON_PORT2;
|
|
4761
|
+
} catch {
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
console.error(import_chalk5.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
4765
|
+
process.exit(1);
|
|
4766
|
+
}
|
|
4767
|
+
async function startTail(options = {}) {
|
|
4768
|
+
const port = await ensureDaemon();
|
|
4769
|
+
if (options.clear) {
|
|
4770
|
+
await new Promise((resolve) => {
|
|
4771
|
+
const req2 = import_http2.default.request(
|
|
4772
|
+
{ method: "POST", hostname: "127.0.0.1", port, path: "/events/clear" },
|
|
4773
|
+
(res) => {
|
|
4774
|
+
res.resume();
|
|
4775
|
+
res.on("end", resolve);
|
|
4776
|
+
}
|
|
4777
|
+
);
|
|
4778
|
+
req2.on("error", resolve);
|
|
4779
|
+
req2.end();
|
|
4780
|
+
});
|
|
4781
|
+
}
|
|
4782
|
+
const connectionTime = Date.now();
|
|
4783
|
+
const pending2 = /* @__PURE__ */ new Map();
|
|
4784
|
+
console.log(import_chalk5.default.cyan.bold(`
|
|
4785
|
+
\u{1F6F0}\uFE0F Node9 tail `) + import_chalk5.default.dim(`\u2192 localhost:${port}`));
|
|
4786
|
+
if (options.clear) {
|
|
4787
|
+
console.log(import_chalk5.default.dim("History cleared. Showing live events. Press Ctrl+C to exit.\n"));
|
|
4788
|
+
} else if (options.history) {
|
|
4789
|
+
console.log(import_chalk5.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
4790
|
+
} else {
|
|
4791
|
+
console.log(
|
|
4792
|
+
import_chalk5.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
4793
|
+
);
|
|
4794
|
+
}
|
|
4795
|
+
process.on("SIGINT", () => {
|
|
4796
|
+
if (process.stdout.isTTY) {
|
|
4797
|
+
import_readline.default.clearLine(process.stdout, 0);
|
|
4798
|
+
import_readline.default.cursorTo(process.stdout, 0);
|
|
4799
|
+
}
|
|
4800
|
+
console.log(import_chalk5.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
4801
|
+
process.exit(0);
|
|
4802
|
+
});
|
|
4803
|
+
const req = import_http2.default.get(`http://127.0.0.1:${port}/events`, (res) => {
|
|
4804
|
+
if (res.statusCode !== 200) {
|
|
4805
|
+
console.error(import_chalk5.default.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
4806
|
+
process.exit(1);
|
|
4807
|
+
}
|
|
4808
|
+
let currentEvent = "";
|
|
4809
|
+
let currentData = "";
|
|
4810
|
+
res.on("error", () => {
|
|
4811
|
+
});
|
|
4812
|
+
const rl = import_readline.default.createInterface({ input: res, crlfDelay: Infinity });
|
|
4813
|
+
rl.on("error", () => {
|
|
4814
|
+
});
|
|
4815
|
+
rl.on("line", (line) => {
|
|
4816
|
+
if (line.startsWith("event:")) {
|
|
4817
|
+
currentEvent = line.slice(6).trim();
|
|
4818
|
+
} else if (line.startsWith("data:")) {
|
|
4819
|
+
currentData = line.slice(5).trim();
|
|
4820
|
+
} else if (line === "") {
|
|
4821
|
+
if (currentEvent && currentData) {
|
|
4822
|
+
handleMessage(currentEvent, currentData);
|
|
4823
|
+
}
|
|
4824
|
+
currentEvent = "";
|
|
4825
|
+
currentData = "";
|
|
4826
|
+
}
|
|
4827
|
+
});
|
|
4828
|
+
rl.on("close", () => {
|
|
4829
|
+
if (process.stdout.isTTY) {
|
|
4830
|
+
import_readline.default.clearLine(process.stdout, 0);
|
|
4831
|
+
import_readline.default.cursorTo(process.stdout, 0);
|
|
4832
|
+
}
|
|
4833
|
+
console.log(import_chalk5.default.red("\n\u274C Daemon disconnected."));
|
|
4834
|
+
process.exit(1);
|
|
4835
|
+
});
|
|
4836
|
+
});
|
|
4837
|
+
function handleMessage(event, rawData) {
|
|
4838
|
+
let data;
|
|
4839
|
+
try {
|
|
4840
|
+
data = JSON.parse(rawData);
|
|
4841
|
+
} catch {
|
|
4842
|
+
return;
|
|
4843
|
+
}
|
|
4844
|
+
if (event === "activity") {
|
|
4845
|
+
if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
|
|
4846
|
+
if (data.status && data.status !== "pending") {
|
|
4847
|
+
renderResult(data, data);
|
|
4848
|
+
return;
|
|
4849
|
+
}
|
|
4850
|
+
pending2.set(data.id, data);
|
|
4851
|
+
const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
|
|
4852
|
+
if (slowTool) renderPending(data);
|
|
4853
|
+
}
|
|
4854
|
+
if (event === "activity-result") {
|
|
4855
|
+
const original = pending2.get(data.id);
|
|
4856
|
+
if (original) {
|
|
4857
|
+
renderResult(original, data);
|
|
4858
|
+
pending2.delete(data.id);
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
}
|
|
4862
|
+
req.on("error", (err) => {
|
|
4863
|
+
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
4864
|
+
console.error(import_chalk5.default.red(`
|
|
4865
|
+
\u274C ${msg}`));
|
|
4866
|
+
process.exit(1);
|
|
4867
|
+
});
|
|
4868
|
+
}
|
|
4869
|
+
var import_http2, import_chalk5, import_fs6, import_os6, import_path8, import_readline, import_child_process5, PID_FILE, ICONS;
|
|
4870
|
+
var init_tail = __esm({
|
|
4871
|
+
"src/tui/tail.ts"() {
|
|
4872
|
+
"use strict";
|
|
4873
|
+
import_http2 = __toESM(require("http"));
|
|
4874
|
+
import_chalk5 = __toESM(require("chalk"));
|
|
4875
|
+
import_fs6 = __toESM(require("fs"));
|
|
4876
|
+
import_os6 = __toESM(require("os"));
|
|
4877
|
+
import_path8 = __toESM(require("path"));
|
|
4878
|
+
import_readline = __toESM(require("readline"));
|
|
4879
|
+
import_child_process5 = require("child_process");
|
|
4880
|
+
init_daemon();
|
|
4881
|
+
PID_FILE = import_path8.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
|
|
4882
|
+
ICONS = {
|
|
4883
|
+
bash: "\u{1F4BB}",
|
|
4884
|
+
shell: "\u{1F4BB}",
|
|
4885
|
+
terminal: "\u{1F4BB}",
|
|
4886
|
+
read: "\u{1F4D6}",
|
|
4887
|
+
edit: "\u270F\uFE0F",
|
|
4888
|
+
write: "\u270F\uFE0F",
|
|
4889
|
+
glob: "\u{1F4C2}",
|
|
4890
|
+
grep: "\u{1F50D}",
|
|
4891
|
+
agent: "\u{1F916}",
|
|
4892
|
+
search: "\u{1F50D}",
|
|
4893
|
+
sql: "\u{1F5C4}\uFE0F",
|
|
4894
|
+
query: "\u{1F5C4}\uFE0F",
|
|
4895
|
+
list: "\u{1F4C2}",
|
|
4896
|
+
delete: "\u{1F5D1}\uFE0F",
|
|
4897
|
+
web: "\u{1F310}"
|
|
4898
|
+
};
|
|
4899
|
+
}
|
|
4900
|
+
});
|
|
4901
|
+
|
|
4902
|
+
// src/cli.ts
|
|
4903
|
+
var import_commander = require("commander");
|
|
4904
|
+
init_core();
|
|
4905
|
+
|
|
4906
|
+
// src/setup.ts
|
|
4907
|
+
var import_fs3 = __toESM(require("fs"));
|
|
4908
|
+
var import_path5 = __toESM(require("path"));
|
|
4909
|
+
var import_os3 = __toESM(require("os"));
|
|
4910
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
4911
|
+
var import_prompts2 = require("@inquirer/prompts");
|
|
4912
|
+
function printDaemonTip() {
|
|
4913
|
+
console.log(
|
|
4914
|
+
import_chalk3.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + import_chalk3.default.white("\n To view your history or manage persistent rules, run:") + import_chalk3.default.green("\n node9 daemon --openui")
|
|
4915
|
+
);
|
|
4916
|
+
}
|
|
4917
|
+
function fullPathCommand(subcommand) {
|
|
4918
|
+
if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
|
|
4919
|
+
const nodeExec = process.execPath;
|
|
4920
|
+
const cliScript = process.argv[1];
|
|
4921
|
+
return `${nodeExec} ${cliScript} ${subcommand}`;
|
|
4922
|
+
}
|
|
4923
|
+
function readJson(filePath) {
|
|
4924
|
+
try {
|
|
4925
|
+
if (import_fs3.default.existsSync(filePath)) {
|
|
4926
|
+
return JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
|
|
4927
|
+
}
|
|
4928
|
+
} catch {
|
|
4929
|
+
}
|
|
4930
|
+
return null;
|
|
4931
|
+
}
|
|
4932
|
+
function writeJson(filePath, data) {
|
|
4933
|
+
const dir = import_path5.default.dirname(filePath);
|
|
4934
|
+
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
4935
|
+
import_fs3.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
4936
|
+
}
|
|
4937
|
+
async function setupClaude() {
|
|
4938
|
+
const homeDir2 = import_os3.default.homedir();
|
|
4939
|
+
const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
|
|
4940
|
+
const hooksPath = import_path5.default.join(homeDir2, ".claude", "settings.json");
|
|
4941
|
+
const claudeConfig = readJson(mcpPath) ?? {};
|
|
4942
|
+
const settings = readJson(hooksPath) ?? {};
|
|
4943
|
+
const servers = claudeConfig.mcpServers ?? {};
|
|
4944
|
+
let anythingChanged = false;
|
|
4945
|
+
if (!settings.hooks) settings.hooks = {};
|
|
4946
|
+
const hasPreHook = settings.hooks.PreToolUse?.some(
|
|
4947
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
4948
|
+
);
|
|
4949
|
+
if (!hasPreHook) {
|
|
4950
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
4951
|
+
settings.hooks.PreToolUse.push({
|
|
4952
|
+
matcher: ".*",
|
|
4953
|
+
hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
|
|
4954
|
+
});
|
|
4955
|
+
console.log(import_chalk3.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
|
|
4956
|
+
anythingChanged = true;
|
|
4957
|
+
}
|
|
4958
|
+
const hasPostHook = settings.hooks.PostToolUse?.some(
|
|
4959
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
4960
|
+
);
|
|
4961
|
+
if (!hasPostHook) {
|
|
4962
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
4963
|
+
settings.hooks.PostToolUse.push({
|
|
4964
|
+
matcher: ".*",
|
|
4965
|
+
hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
|
|
4966
|
+
});
|
|
4967
|
+
console.log(import_chalk3.default.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
|
|
4968
|
+
anythingChanged = true;
|
|
4969
|
+
}
|
|
4970
|
+
if (anythingChanged) {
|
|
4971
|
+
writeJson(hooksPath, settings);
|
|
4972
|
+
console.log("");
|
|
4973
|
+
}
|
|
4974
|
+
const serversToWrap = [];
|
|
4975
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
4976
|
+
if (!server.command || server.command === "node9") continue;
|
|
4977
|
+
const parts = [server.command, ...server.args ?? []];
|
|
4978
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
4979
|
+
}
|
|
4980
|
+
if (serversToWrap.length > 0) {
|
|
4981
|
+
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
4982
|
+
console.log(import_chalk3.default.white(` ${mcpPath}`));
|
|
4983
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
4984
|
+
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
4985
|
+
}
|
|
4986
|
+
console.log("");
|
|
4987
|
+
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
4988
|
+
if (proceed) {
|
|
4989
|
+
for (const { name, parts } of serversToWrap) {
|
|
4990
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
4136
4991
|
}
|
|
4992
|
+
claudeConfig.mcpServers = servers;
|
|
4993
|
+
writeJson(mcpPath, claudeConfig);
|
|
4994
|
+
console.log(import_chalk3.default.green(`
|
|
4995
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
4996
|
+
anythingChanged = true;
|
|
4997
|
+
} else {
|
|
4998
|
+
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
4137
4999
|
}
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
5000
|
+
console.log("");
|
|
5001
|
+
}
|
|
5002
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
5003
|
+
console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
5004
|
+
printDaemonTip();
|
|
5005
|
+
return;
|
|
5006
|
+
}
|
|
5007
|
+
if (anythingChanged) {
|
|
5008
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
5009
|
+
console.log(import_chalk3.default.gray(" Restart Claude Code for changes to take effect."));
|
|
5010
|
+
printDaemonTip();
|
|
5011
|
+
}
|
|
5012
|
+
}
|
|
5013
|
+
async function setupGemini() {
|
|
5014
|
+
const homeDir2 = import_os3.default.homedir();
|
|
5015
|
+
const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
|
|
5016
|
+
const settings = readJson(settingsPath) ?? {};
|
|
5017
|
+
const servers = settings.mcpServers ?? {};
|
|
5018
|
+
let anythingChanged = false;
|
|
5019
|
+
if (!settings.hooks) settings.hooks = {};
|
|
5020
|
+
const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
|
|
5021
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
5022
|
+
);
|
|
5023
|
+
if (!hasBeforeHook) {
|
|
5024
|
+
if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
|
|
5025
|
+
if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
|
|
5026
|
+
settings.hooks.BeforeTool.push({
|
|
5027
|
+
matcher: ".*",
|
|
5028
|
+
hooks: [
|
|
5029
|
+
{
|
|
5030
|
+
name: "node9-check",
|
|
5031
|
+
type: "command",
|
|
5032
|
+
command: fullPathCommand("check"),
|
|
5033
|
+
timeout: 6e5
|
|
5034
|
+
}
|
|
5035
|
+
]
|
|
5036
|
+
});
|
|
5037
|
+
console.log(import_chalk3.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
5038
|
+
anythingChanged = true;
|
|
5039
|
+
}
|
|
5040
|
+
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
5041
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
5042
|
+
);
|
|
5043
|
+
if (!hasAfterHook) {
|
|
5044
|
+
if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
|
|
5045
|
+
if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
|
|
5046
|
+
settings.hooks.AfterTool.push({
|
|
5047
|
+
matcher: ".*",
|
|
5048
|
+
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
5049
|
+
});
|
|
5050
|
+
console.log(import_chalk3.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
5051
|
+
anythingChanged = true;
|
|
5052
|
+
}
|
|
5053
|
+
if (anythingChanged) {
|
|
5054
|
+
writeJson(settingsPath, settings);
|
|
5055
|
+
console.log("");
|
|
5056
|
+
}
|
|
5057
|
+
const serversToWrap = [];
|
|
5058
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
5059
|
+
if (!server.command || server.command === "node9") continue;
|
|
5060
|
+
const parts = [server.command, ...server.args ?? []];
|
|
5061
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
5062
|
+
}
|
|
5063
|
+
if (serversToWrap.length > 0) {
|
|
5064
|
+
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
5065
|
+
console.log(import_chalk3.default.white(` ${settingsPath} (mcpServers)`));
|
|
5066
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
5067
|
+
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
5068
|
+
}
|
|
5069
|
+
console.log("");
|
|
5070
|
+
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
5071
|
+
if (proceed) {
|
|
5072
|
+
for (const { name, parts } of serversToWrap) {
|
|
5073
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
4160
5074
|
}
|
|
5075
|
+
settings.mcpServers = servers;
|
|
5076
|
+
writeJson(settingsPath, settings);
|
|
5077
|
+
console.log(import_chalk3.default.green(`
|
|
5078
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
5079
|
+
anythingChanged = true;
|
|
5080
|
+
} else {
|
|
5081
|
+
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
4161
5082
|
}
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
5083
|
+
console.log("");
|
|
5084
|
+
}
|
|
5085
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
5086
|
+
console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
|
|
5087
|
+
printDaemonTip();
|
|
5088
|
+
return;
|
|
5089
|
+
}
|
|
5090
|
+
if (anythingChanged) {
|
|
5091
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
5092
|
+
console.log(import_chalk3.default.gray(" Restart Gemini CLI for changes to take effect."));
|
|
5093
|
+
printDaemonTip();
|
|
5094
|
+
}
|
|
5095
|
+
}
|
|
5096
|
+
async function setupCursor() {
|
|
5097
|
+
const homeDir2 = import_os3.default.homedir();
|
|
5098
|
+
const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
|
|
5099
|
+
const mcpConfig = readJson(mcpPath) ?? {};
|
|
5100
|
+
const servers = mcpConfig.mcpServers ?? {};
|
|
5101
|
+
let anythingChanged = false;
|
|
5102
|
+
const serversToWrap = [];
|
|
5103
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
5104
|
+
if (!server.command || server.command === "node9") continue;
|
|
5105
|
+
const parts = [server.command, ...server.args ?? []];
|
|
5106
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
5107
|
+
}
|
|
5108
|
+
if (serversToWrap.length > 0) {
|
|
5109
|
+
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
5110
|
+
console.log(import_chalk3.default.white(` ${mcpPath}`));
|
|
5111
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
5112
|
+
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
4165
5113
|
}
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
try {
|
|
4172
|
-
if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
|
|
4173
|
-
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4174
|
-
process.kill(pid, 0);
|
|
4175
|
-
return process.exit(0);
|
|
4176
|
-
}
|
|
4177
|
-
} catch {
|
|
4178
|
-
try {
|
|
4179
|
-
import_fs4.default.unlinkSync(DAEMON_PID_FILE);
|
|
4180
|
-
} catch {
|
|
4181
|
-
}
|
|
4182
|
-
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
4183
|
-
return;
|
|
5114
|
+
console.log("");
|
|
5115
|
+
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
5116
|
+
if (proceed) {
|
|
5117
|
+
for (const { name, parts } of serversToWrap) {
|
|
5118
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
4184
5119
|
}
|
|
5120
|
+
mcpConfig.mcpServers = servers;
|
|
5121
|
+
writeJson(mcpPath, mcpConfig);
|
|
5122
|
+
console.log(import_chalk3.default.green(`
|
|
5123
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
5124
|
+
anythingChanged = true;
|
|
5125
|
+
} else {
|
|
5126
|
+
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
4185
5127
|
}
|
|
4186
|
-
console.
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
5128
|
+
console.log("");
|
|
5129
|
+
}
|
|
5130
|
+
console.log(
|
|
5131
|
+
import_chalk3.default.yellow(
|
|
5132
|
+
" \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
|
|
5133
|
+
)
|
|
5134
|
+
);
|
|
5135
|
+
console.log("");
|
|
5136
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
5137
|
+
console.log(
|
|
5138
|
+
import_chalk3.default.blue(
|
|
5139
|
+
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
|
|
5140
|
+
)
|
|
4194
5141
|
);
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
}
|
|
4198
|
-
function stopDaemon() {
|
|
4199
|
-
if (!import_fs4.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
|
|
4200
|
-
try {
|
|
4201
|
-
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4202
|
-
process.kill(pid, "SIGTERM");
|
|
4203
|
-
console.log(import_chalk4.default.green("\u2705 Stopped."));
|
|
4204
|
-
} catch {
|
|
4205
|
-
console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
|
|
4206
|
-
} finally {
|
|
4207
|
-
try {
|
|
4208
|
-
import_fs4.default.unlinkSync(DAEMON_PID_FILE);
|
|
4209
|
-
} catch {
|
|
4210
|
-
}
|
|
5142
|
+
printDaemonTip();
|
|
5143
|
+
return;
|
|
4211
5144
|
}
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
try {
|
|
4217
|
-
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4218
|
-
process.kill(pid, 0);
|
|
4219
|
-
console.log(import_chalk4.default.green("Node9 daemon: running"));
|
|
4220
|
-
} catch {
|
|
4221
|
-
console.log(import_chalk4.default.yellow("Node9 daemon: not running (stale PID)"));
|
|
5145
|
+
if (anythingChanged) {
|
|
5146
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
|
|
5147
|
+
console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
|
|
5148
|
+
printDaemonTip();
|
|
4222
5149
|
}
|
|
4223
5150
|
}
|
|
4224
5151
|
|
|
4225
5152
|
// src/cli.ts
|
|
4226
|
-
|
|
5153
|
+
init_daemon();
|
|
5154
|
+
var import_child_process6 = require("child_process");
|
|
4227
5155
|
var import_execa = require("execa");
|
|
4228
5156
|
var import_execa2 = require("execa");
|
|
4229
|
-
var
|
|
4230
|
-
var
|
|
4231
|
-
var
|
|
4232
|
-
var
|
|
4233
|
-
var
|
|
5157
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
5158
|
+
var import_readline2 = __toESM(require("readline"));
|
|
5159
|
+
var import_fs7 = __toESM(require("fs"));
|
|
5160
|
+
var import_path9 = __toESM(require("path"));
|
|
5161
|
+
var import_os7 = __toESM(require("os"));
|
|
4234
5162
|
|
|
4235
5163
|
// src/undo.ts
|
|
4236
|
-
var
|
|
5164
|
+
var import_child_process4 = require("child_process");
|
|
4237
5165
|
var import_fs5 = __toESM(require("fs"));
|
|
4238
5166
|
var import_path7 = __toESM(require("path"));
|
|
4239
5167
|
var import_os5 = __toESM(require("os"));
|
|
@@ -4270,12 +5198,12 @@ async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
|
4270
5198
|
if (!import_fs5.default.existsSync(import_path7.default.join(cwd, ".git"))) return null;
|
|
4271
5199
|
const tempIndex = import_path7.default.join(cwd, ".git", `node9_index_${Date.now()}`);
|
|
4272
5200
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
4273
|
-
(0,
|
|
4274
|
-
const treeRes = (0,
|
|
5201
|
+
(0, import_child_process4.spawnSync)("git", ["add", "-A"], { env });
|
|
5202
|
+
const treeRes = (0, import_child_process4.spawnSync)("git", ["write-tree"], { env });
|
|
4275
5203
|
const treeHash = treeRes.stdout.toString().trim();
|
|
4276
5204
|
if (import_fs5.default.existsSync(tempIndex)) import_fs5.default.unlinkSync(tempIndex);
|
|
4277
5205
|
if (!treeHash || treeRes.status !== 0) return null;
|
|
4278
|
-
const commitRes = (0,
|
|
5206
|
+
const commitRes = (0, import_child_process4.spawnSync)("git", [
|
|
4279
5207
|
"commit-tree",
|
|
4280
5208
|
treeHash,
|
|
4281
5209
|
"-m",
|
|
@@ -4306,10 +5234,10 @@ function getSnapshotHistory() {
|
|
|
4306
5234
|
}
|
|
4307
5235
|
function computeUndoDiff(hash, cwd) {
|
|
4308
5236
|
try {
|
|
4309
|
-
const result = (0,
|
|
5237
|
+
const result = (0, import_child_process4.spawnSync)("git", ["diff", hash, "--stat", "--", "."], { cwd });
|
|
4310
5238
|
const stat = result.stdout.toString().trim();
|
|
4311
5239
|
if (!stat) return null;
|
|
4312
|
-
const diff = (0,
|
|
5240
|
+
const diff = (0, import_child_process4.spawnSync)("git", ["diff", hash, "--", "."], { cwd });
|
|
4313
5241
|
const raw = diff.stdout.toString();
|
|
4314
5242
|
if (!raw) return null;
|
|
4315
5243
|
const lines = raw.split("\n").filter(
|
|
@@ -4323,14 +5251,14 @@ function computeUndoDiff(hash, cwd) {
|
|
|
4323
5251
|
function applyUndo(hash, cwd) {
|
|
4324
5252
|
try {
|
|
4325
5253
|
const dir = cwd ?? process.cwd();
|
|
4326
|
-
const restore = (0,
|
|
5254
|
+
const restore = (0, import_child_process4.spawnSync)("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
|
|
4327
5255
|
cwd: dir
|
|
4328
5256
|
});
|
|
4329
5257
|
if (restore.status !== 0) return false;
|
|
4330
|
-
const lsTree = (0,
|
|
5258
|
+
const lsTree = (0, import_child_process4.spawnSync)("git", ["ls-tree", "-r", "--name-only", hash], { cwd: dir });
|
|
4331
5259
|
const snapshotFiles = new Set(lsTree.stdout.toString().trim().split("\n").filter(Boolean));
|
|
4332
|
-
const tracked = (0,
|
|
4333
|
-
const untracked = (0,
|
|
5260
|
+
const tracked = (0, import_child_process4.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
5261
|
+
const untracked = (0, import_child_process4.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
4334
5262
|
for (const file of [...tracked, ...untracked]) {
|
|
4335
5263
|
const fullPath = import_path7.default.join(dir, file);
|
|
4336
5264
|
if (!snapshotFiles.has(file) && import_fs5.default.existsSync(fullPath)) {
|
|
@@ -4344,9 +5272,10 @@ function applyUndo(hash, cwd) {
|
|
|
4344
5272
|
}
|
|
4345
5273
|
|
|
4346
5274
|
// src/cli.ts
|
|
5275
|
+
init_shields();
|
|
4347
5276
|
var import_prompts3 = require("@inquirer/prompts");
|
|
4348
5277
|
var { version } = JSON.parse(
|
|
4349
|
-
|
|
5278
|
+
import_fs7.default.readFileSync(import_path9.default.join(__dirname, "../package.json"), "utf-8")
|
|
4350
5279
|
);
|
|
4351
5280
|
function parseDuration(str) {
|
|
4352
5281
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -4434,15 +5363,15 @@ function openBrowserLocal() {
|
|
|
4434
5363
|
const url = `http://${DAEMON_HOST2}:${DAEMON_PORT2}/`;
|
|
4435
5364
|
try {
|
|
4436
5365
|
const opts = { stdio: "ignore" };
|
|
4437
|
-
if (process.platform === "darwin") (0,
|
|
4438
|
-
else if (process.platform === "win32") (0,
|
|
4439
|
-
else (0,
|
|
5366
|
+
if (process.platform === "darwin") (0, import_child_process6.execSync)(`open "${url}"`, opts);
|
|
5367
|
+
else if (process.platform === "win32") (0, import_child_process6.execSync)(`cmd /c start "" "${url}"`, opts);
|
|
5368
|
+
else (0, import_child_process6.execSync)(`xdg-open "${url}"`, opts);
|
|
4440
5369
|
} catch {
|
|
4441
5370
|
}
|
|
4442
5371
|
}
|
|
4443
5372
|
async function autoStartDaemonAndWait() {
|
|
4444
5373
|
try {
|
|
4445
|
-
const child = (0,
|
|
5374
|
+
const child = (0, import_child_process6.spawn)("node9", ["daemon"], {
|
|
4446
5375
|
detached: true,
|
|
4447
5376
|
stdio: "ignore",
|
|
4448
5377
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -4478,14 +5407,14 @@ async function runProxy(targetCommand) {
|
|
|
4478
5407
|
if (stdout) executable = stdout.trim();
|
|
4479
5408
|
} catch {
|
|
4480
5409
|
}
|
|
4481
|
-
console.log(
|
|
4482
|
-
const child = (0,
|
|
5410
|
+
console.log(import_chalk6.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
5411
|
+
const child = (0, import_child_process6.spawn)(executable, args, {
|
|
4483
5412
|
stdio: ["pipe", "pipe", "inherit"],
|
|
4484
5413
|
// We control STDIN and STDOUT
|
|
4485
5414
|
shell: false,
|
|
4486
5415
|
env: { ...process.env, FORCE_COLOR: "1" }
|
|
4487
5416
|
});
|
|
4488
|
-
const agentIn =
|
|
5417
|
+
const agentIn = import_readline2.default.createInterface({ input: process.stdin, terminal: false });
|
|
4489
5418
|
agentIn.on("line", async (line) => {
|
|
4490
5419
|
let message;
|
|
4491
5420
|
try {
|
|
@@ -4503,10 +5432,10 @@ async function runProxy(targetCommand) {
|
|
|
4503
5432
|
agent: "Proxy/MCP"
|
|
4504
5433
|
});
|
|
4505
5434
|
if (!result.approved) {
|
|
4506
|
-
console.error(
|
|
5435
|
+
console.error(import_chalk6.default.red(`
|
|
4507
5436
|
\u{1F6D1} Node9 Sudo: Action Blocked`));
|
|
4508
|
-
console.error(
|
|
4509
|
-
console.error(
|
|
5437
|
+
console.error(import_chalk6.default.gray(` Tool: ${name}`));
|
|
5438
|
+
console.error(import_chalk6.default.gray(` Reason: ${result.reason || "Security Policy"}
|
|
4510
5439
|
`));
|
|
4511
5440
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
4512
5441
|
const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -4548,14 +5477,14 @@ async function runProxy(targetCommand) {
|
|
|
4548
5477
|
}
|
|
4549
5478
|
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) => {
|
|
4550
5479
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
4551
|
-
const credPath =
|
|
4552
|
-
if (!
|
|
4553
|
-
|
|
5480
|
+
const credPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "credentials.json");
|
|
5481
|
+
if (!import_fs7.default.existsSync(import_path9.default.dirname(credPath)))
|
|
5482
|
+
import_fs7.default.mkdirSync(import_path9.default.dirname(credPath), { recursive: true });
|
|
4554
5483
|
const profileName = options.profile || "default";
|
|
4555
5484
|
let existingCreds = {};
|
|
4556
5485
|
try {
|
|
4557
|
-
if (
|
|
4558
|
-
const raw = JSON.parse(
|
|
5486
|
+
if (import_fs7.default.existsSync(credPath)) {
|
|
5487
|
+
const raw = JSON.parse(import_fs7.default.readFileSync(credPath, "utf-8"));
|
|
4559
5488
|
if (raw.apiKey) {
|
|
4560
5489
|
existingCreds = {
|
|
4561
5490
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -4567,13 +5496,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4567
5496
|
} catch {
|
|
4568
5497
|
}
|
|
4569
5498
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
4570
|
-
|
|
5499
|
+
import_fs7.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
4571
5500
|
if (profileName === "default") {
|
|
4572
|
-
const configPath =
|
|
5501
|
+
const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
|
|
4573
5502
|
let config = {};
|
|
4574
5503
|
try {
|
|
4575
|
-
if (
|
|
4576
|
-
config = JSON.parse(
|
|
5504
|
+
if (import_fs7.default.existsSync(configPath))
|
|
5505
|
+
config = JSON.parse(import_fs7.default.readFileSync(configPath, "utf-8"));
|
|
4577
5506
|
} catch {
|
|
4578
5507
|
}
|
|
4579
5508
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -4588,36 +5517,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4588
5517
|
approvers.cloud = false;
|
|
4589
5518
|
}
|
|
4590
5519
|
s.approvers = approvers;
|
|
4591
|
-
if (!
|
|
4592
|
-
|
|
4593
|
-
|
|
5520
|
+
if (!import_fs7.default.existsSync(import_path9.default.dirname(configPath)))
|
|
5521
|
+
import_fs7.default.mkdirSync(import_path9.default.dirname(configPath), { recursive: true });
|
|
5522
|
+
import_fs7.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
4594
5523
|
}
|
|
4595
5524
|
if (options.profile && profileName !== "default") {
|
|
4596
|
-
console.log(
|
|
4597
|
-
console.log(
|
|
5525
|
+
console.log(import_chalk6.default.green(`\u2705 Profile "${profileName}" saved`));
|
|
5526
|
+
console.log(import_chalk6.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
4598
5527
|
} else if (options.local) {
|
|
4599
|
-
console.log(
|
|
4600
|
-
console.log(
|
|
5528
|
+
console.log(import_chalk6.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
5529
|
+
console.log(import_chalk6.default.gray(` All decisions stay on this machine.`));
|
|
4601
5530
|
} else {
|
|
4602
|
-
console.log(
|
|
4603
|
-
console.log(
|
|
5531
|
+
console.log(import_chalk6.default.green(`\u2705 Logged in \u2014 agent mode`));
|
|
5532
|
+
console.log(import_chalk6.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
4604
5533
|
}
|
|
4605
5534
|
});
|
|
4606
5535
|
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) => {
|
|
4607
5536
|
if (target === "gemini") return await setupGemini();
|
|
4608
5537
|
if (target === "claude") return await setupClaude();
|
|
4609
5538
|
if (target === "cursor") return await setupCursor();
|
|
4610
|
-
console.error(
|
|
5539
|
+
console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
4611
5540
|
process.exit(1);
|
|
4612
5541
|
});
|
|
4613
5542
|
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) => {
|
|
4614
5543
|
if (!target) {
|
|
4615
|
-
console.log(
|
|
4616
|
-
console.log(" Usage: " +
|
|
5544
|
+
console.log(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
5545
|
+
console.log(" Usage: " + import_chalk6.default.white("node9 setup <target>") + "\n");
|
|
4617
5546
|
console.log(" Targets:");
|
|
4618
|
-
console.log(" " +
|
|
4619
|
-
console.log(" " +
|
|
4620
|
-
console.log(" " +
|
|
5547
|
+
console.log(" " + import_chalk6.default.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
5548
|
+
console.log(" " + import_chalk6.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
5549
|
+
console.log(" " + import_chalk6.default.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
4621
5550
|
console.log("");
|
|
4622
5551
|
return;
|
|
4623
5552
|
}
|
|
@@ -4625,33 +5554,33 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
4625
5554
|
if (t === "gemini") return await setupGemini();
|
|
4626
5555
|
if (t === "claude") return await setupClaude();
|
|
4627
5556
|
if (t === "cursor") return await setupCursor();
|
|
4628
|
-
console.error(
|
|
5557
|
+
console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
4629
5558
|
process.exit(1);
|
|
4630
5559
|
});
|
|
4631
5560
|
program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
4632
|
-
const homeDir2 =
|
|
5561
|
+
const homeDir2 = import_os7.default.homedir();
|
|
4633
5562
|
let failures = 0;
|
|
4634
5563
|
function pass(msg) {
|
|
4635
|
-
console.log(
|
|
5564
|
+
console.log(import_chalk6.default.green(" \u2705 ") + msg);
|
|
4636
5565
|
}
|
|
4637
5566
|
function fail(msg, hint) {
|
|
4638
|
-
console.log(
|
|
4639
|
-
if (hint) console.log(
|
|
5567
|
+
console.log(import_chalk6.default.red(" \u274C ") + msg);
|
|
5568
|
+
if (hint) console.log(import_chalk6.default.gray(" " + hint));
|
|
4640
5569
|
failures++;
|
|
4641
5570
|
}
|
|
4642
5571
|
function warn(msg, hint) {
|
|
4643
|
-
console.log(
|
|
4644
|
-
if (hint) console.log(
|
|
5572
|
+
console.log(import_chalk6.default.yellow(" \u26A0\uFE0F ") + msg);
|
|
5573
|
+
if (hint) console.log(import_chalk6.default.gray(" " + hint));
|
|
4645
5574
|
}
|
|
4646
5575
|
function section(title) {
|
|
4647
|
-
console.log("\n" +
|
|
5576
|
+
console.log("\n" + import_chalk6.default.bold(title));
|
|
4648
5577
|
}
|
|
4649
|
-
console.log(
|
|
5578
|
+
console.log(import_chalk6.default.cyan.bold(`
|
|
4650
5579
|
\u{1F6E1}\uFE0F Node9 Doctor v${version}
|
|
4651
5580
|
`));
|
|
4652
5581
|
section("Binary");
|
|
4653
5582
|
try {
|
|
4654
|
-
const which = (0,
|
|
5583
|
+
const which = (0, import_child_process6.execSync)("which node9", { encoding: "utf-8" }).trim();
|
|
4655
5584
|
pass(`node9 found at ${which}`);
|
|
4656
5585
|
} catch {
|
|
4657
5586
|
warn("node9 not found in $PATH \u2014 hooks may not find it", "Run: npm install -g @node9/proxy");
|
|
@@ -4666,7 +5595,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4666
5595
|
);
|
|
4667
5596
|
}
|
|
4668
5597
|
try {
|
|
4669
|
-
const gitVersion = (0,
|
|
5598
|
+
const gitVersion = (0, import_child_process6.execSync)("git --version", { encoding: "utf-8" }).trim();
|
|
4670
5599
|
pass(gitVersion);
|
|
4671
5600
|
} catch {
|
|
4672
5601
|
warn(
|
|
@@ -4675,10 +5604,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4675
5604
|
);
|
|
4676
5605
|
}
|
|
4677
5606
|
section("Configuration");
|
|
4678
|
-
const globalConfigPath =
|
|
4679
|
-
if (
|
|
5607
|
+
const globalConfigPath = import_path9.default.join(homeDir2, ".node9", "config.json");
|
|
5608
|
+
if (import_fs7.default.existsSync(globalConfigPath)) {
|
|
4680
5609
|
try {
|
|
4681
|
-
JSON.parse(
|
|
5610
|
+
JSON.parse(import_fs7.default.readFileSync(globalConfigPath, "utf-8"));
|
|
4682
5611
|
pass("~/.node9/config.json found and valid");
|
|
4683
5612
|
} catch {
|
|
4684
5613
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -4686,17 +5615,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4686
5615
|
} else {
|
|
4687
5616
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
4688
5617
|
}
|
|
4689
|
-
const projectConfigPath =
|
|
4690
|
-
if (
|
|
5618
|
+
const projectConfigPath = import_path9.default.join(process.cwd(), "node9.config.json");
|
|
5619
|
+
if (import_fs7.default.existsSync(projectConfigPath)) {
|
|
4691
5620
|
try {
|
|
4692
|
-
JSON.parse(
|
|
5621
|
+
JSON.parse(import_fs7.default.readFileSync(projectConfigPath, "utf-8"));
|
|
4693
5622
|
pass("node9.config.json found and valid (project)");
|
|
4694
5623
|
} catch {
|
|
4695
5624
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
4696
5625
|
}
|
|
4697
5626
|
}
|
|
4698
|
-
const credsPath =
|
|
4699
|
-
if (
|
|
5627
|
+
const credsPath = import_path9.default.join(homeDir2, ".node9", "credentials.json");
|
|
5628
|
+
if (import_fs7.default.existsSync(credsPath)) {
|
|
4700
5629
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
4701
5630
|
} else {
|
|
4702
5631
|
warn(
|
|
@@ -4705,10 +5634,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4705
5634
|
);
|
|
4706
5635
|
}
|
|
4707
5636
|
section("Agent Hooks");
|
|
4708
|
-
const claudeSettingsPath =
|
|
4709
|
-
if (
|
|
5637
|
+
const claudeSettingsPath = import_path9.default.join(homeDir2, ".claude", "settings.json");
|
|
5638
|
+
if (import_fs7.default.existsSync(claudeSettingsPath)) {
|
|
4710
5639
|
try {
|
|
4711
|
-
const cs = JSON.parse(
|
|
5640
|
+
const cs = JSON.parse(import_fs7.default.readFileSync(claudeSettingsPath, "utf-8"));
|
|
4712
5641
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
4713
5642
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4714
5643
|
);
|
|
@@ -4721,10 +5650,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4721
5650
|
} else {
|
|
4722
5651
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
4723
5652
|
}
|
|
4724
|
-
const geminiSettingsPath =
|
|
4725
|
-
if (
|
|
5653
|
+
const geminiSettingsPath = import_path9.default.join(homeDir2, ".gemini", "settings.json");
|
|
5654
|
+
if (import_fs7.default.existsSync(geminiSettingsPath)) {
|
|
4726
5655
|
try {
|
|
4727
|
-
const gs = JSON.parse(
|
|
5656
|
+
const gs = JSON.parse(import_fs7.default.readFileSync(geminiSettingsPath, "utf-8"));
|
|
4728
5657
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
4729
5658
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4730
5659
|
);
|
|
@@ -4737,10 +5666,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4737
5666
|
} else {
|
|
4738
5667
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
4739
5668
|
}
|
|
4740
|
-
const cursorHooksPath =
|
|
4741
|
-
if (
|
|
5669
|
+
const cursorHooksPath = import_path9.default.join(homeDir2, ".cursor", "hooks.json");
|
|
5670
|
+
if (import_fs7.default.existsSync(cursorHooksPath)) {
|
|
4742
5671
|
try {
|
|
4743
|
-
const cur = JSON.parse(
|
|
5672
|
+
const cur = JSON.parse(import_fs7.default.readFileSync(cursorHooksPath, "utf-8"));
|
|
4744
5673
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
4745
5674
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
4746
5675
|
);
|
|
@@ -4761,9 +5690,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4761
5690
|
}
|
|
4762
5691
|
console.log("");
|
|
4763
5692
|
if (failures === 0) {
|
|
4764
|
-
console.log(
|
|
5693
|
+
console.log(import_chalk6.default.green.bold(" All checks passed. Node9 is ready.\n"));
|
|
4765
5694
|
} else {
|
|
4766
|
-
console.log(
|
|
5695
|
+
console.log(import_chalk6.default.red.bold(` ${failures} check(s) failed. See hints above.
|
|
4767
5696
|
`));
|
|
4768
5697
|
process.exit(1);
|
|
4769
5698
|
}
|
|
@@ -4778,7 +5707,7 @@ program.command("explain").description(
|
|
|
4778
5707
|
try {
|
|
4779
5708
|
args = JSON.parse(trimmed);
|
|
4780
5709
|
} catch {
|
|
4781
|
-
console.error(
|
|
5710
|
+
console.error(import_chalk6.default.red(`
|
|
4782
5711
|
\u274C Invalid JSON: ${trimmed}
|
|
4783
5712
|
`));
|
|
4784
5713
|
process.exit(1);
|
|
@@ -4789,63 +5718,63 @@ program.command("explain").description(
|
|
|
4789
5718
|
}
|
|
4790
5719
|
const result = await explainPolicy(tool, args);
|
|
4791
5720
|
console.log("");
|
|
4792
|
-
console.log(
|
|
5721
|
+
console.log(import_chalk6.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
4793
5722
|
console.log("");
|
|
4794
|
-
console.log(` ${
|
|
5723
|
+
console.log(` ${import_chalk6.default.bold("Tool:")} ${import_chalk6.default.white(result.tool)}`);
|
|
4795
5724
|
if (argsRaw) {
|
|
4796
5725
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
4797
|
-
console.log(` ${
|
|
5726
|
+
console.log(` ${import_chalk6.default.bold("Input:")} ${import_chalk6.default.gray(preview)}`);
|
|
4798
5727
|
}
|
|
4799
5728
|
console.log("");
|
|
4800
|
-
console.log(
|
|
5729
|
+
console.log(import_chalk6.default.bold("Config Sources (Waterfall):"));
|
|
4801
5730
|
for (const tier of result.waterfall) {
|
|
4802
|
-
const num =
|
|
5731
|
+
const num = import_chalk6.default.gray(` ${tier.tier}.`);
|
|
4803
5732
|
const label = tier.label.padEnd(16);
|
|
4804
5733
|
let statusStr;
|
|
4805
5734
|
if (tier.tier === 1) {
|
|
4806
|
-
statusStr =
|
|
5735
|
+
statusStr = import_chalk6.default.gray(tier.note ?? "");
|
|
4807
5736
|
} else if (tier.status === "active") {
|
|
4808
|
-
const loc = tier.path ?
|
|
4809
|
-
const note = tier.note ?
|
|
4810
|
-
statusStr =
|
|
5737
|
+
const loc = tier.path ? import_chalk6.default.gray(tier.path) : "";
|
|
5738
|
+
const note = tier.note ? import_chalk6.default.gray(`(${tier.note})`) : "";
|
|
5739
|
+
statusStr = import_chalk6.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
4811
5740
|
} else {
|
|
4812
|
-
statusStr =
|
|
5741
|
+
statusStr = import_chalk6.default.gray("\u25CB " + (tier.note ?? "not found"));
|
|
4813
5742
|
}
|
|
4814
|
-
console.log(`${num} ${
|
|
5743
|
+
console.log(`${num} ${import_chalk6.default.white(label)} ${statusStr}`);
|
|
4815
5744
|
}
|
|
4816
5745
|
console.log("");
|
|
4817
|
-
console.log(
|
|
5746
|
+
console.log(import_chalk6.default.bold("Policy Evaluation:"));
|
|
4818
5747
|
for (const step of result.steps) {
|
|
4819
5748
|
const isFinal = step.isFinal;
|
|
4820
5749
|
let icon;
|
|
4821
|
-
if (step.outcome === "allow") icon =
|
|
4822
|
-
else if (step.outcome === "review") icon =
|
|
4823
|
-
else if (step.outcome === "skip") icon =
|
|
4824
|
-
else icon =
|
|
5750
|
+
if (step.outcome === "allow") icon = import_chalk6.default.green(" \u2705");
|
|
5751
|
+
else if (step.outcome === "review") icon = import_chalk6.default.red(" \u{1F534}");
|
|
5752
|
+
else if (step.outcome === "skip") icon = import_chalk6.default.gray(" \u2500 ");
|
|
5753
|
+
else icon = import_chalk6.default.gray(" \u25CB ");
|
|
4825
5754
|
const name = step.name.padEnd(18);
|
|
4826
|
-
const nameStr = isFinal ?
|
|
4827
|
-
const detail = isFinal ?
|
|
4828
|
-
const arrow = isFinal ?
|
|
5755
|
+
const nameStr = isFinal ? import_chalk6.default.white.bold(name) : import_chalk6.default.white(name);
|
|
5756
|
+
const detail = isFinal ? import_chalk6.default.white(step.detail) : import_chalk6.default.gray(step.detail);
|
|
5757
|
+
const arrow = isFinal ? import_chalk6.default.yellow(" \u2190 STOP") : "";
|
|
4829
5758
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
4830
5759
|
}
|
|
4831
5760
|
console.log("");
|
|
4832
5761
|
if (result.decision === "allow") {
|
|
4833
|
-
console.log(
|
|
5762
|
+
console.log(import_chalk6.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk6.default.gray(" \u2014 no approval needed"));
|
|
4834
5763
|
} else {
|
|
4835
5764
|
console.log(
|
|
4836
|
-
|
|
5765
|
+
import_chalk6.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk6.default.gray(" \u2014 human approval required")
|
|
4837
5766
|
);
|
|
4838
5767
|
if (result.blockedByLabel) {
|
|
4839
|
-
console.log(
|
|
5768
|
+
console.log(import_chalk6.default.gray(` Reason: ${result.blockedByLabel}`));
|
|
4840
5769
|
}
|
|
4841
5770
|
}
|
|
4842
5771
|
console.log("");
|
|
4843
5772
|
});
|
|
4844
5773
|
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) => {
|
|
4845
|
-
const configPath =
|
|
4846
|
-
if (
|
|
4847
|
-
console.log(
|
|
4848
|
-
console.log(
|
|
5774
|
+
const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
|
|
5775
|
+
if (import_fs7.default.existsSync(configPath) && !options.force) {
|
|
5776
|
+
console.log(import_chalk6.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
5777
|
+
console.log(import_chalk6.default.gray(` Run with --force to overwrite.`));
|
|
4849
5778
|
return;
|
|
4850
5779
|
}
|
|
4851
5780
|
const requestedMode = options.mode.toLowerCase();
|
|
@@ -4857,13 +5786,13 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
4857
5786
|
mode: safeMode
|
|
4858
5787
|
}
|
|
4859
5788
|
};
|
|
4860
|
-
const dir =
|
|
4861
|
-
if (!
|
|
4862
|
-
|
|
4863
|
-
console.log(
|
|
4864
|
-
console.log(
|
|
5789
|
+
const dir = import_path9.default.dirname(configPath);
|
|
5790
|
+
if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
|
|
5791
|
+
import_fs7.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
5792
|
+
console.log(import_chalk6.default.green(`\u2705 Global config created: ${configPath}`));
|
|
5793
|
+
console.log(import_chalk6.default.cyan(` Mode set to: ${safeMode}`));
|
|
4865
5794
|
console.log(
|
|
4866
|
-
|
|
5795
|
+
import_chalk6.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
4867
5796
|
);
|
|
4868
5797
|
});
|
|
4869
5798
|
function formatRelativeTime(timestamp) {
|
|
@@ -4877,14 +5806,14 @@ function formatRelativeTime(timestamp) {
|
|
|
4877
5806
|
return new Date(timestamp).toLocaleDateString();
|
|
4878
5807
|
}
|
|
4879
5808
|
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) => {
|
|
4880
|
-
const logPath =
|
|
4881
|
-
if (!
|
|
5809
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
|
|
5810
|
+
if (!import_fs7.default.existsSync(logPath)) {
|
|
4882
5811
|
console.log(
|
|
4883
|
-
|
|
5812
|
+
import_chalk6.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
4884
5813
|
);
|
|
4885
5814
|
return;
|
|
4886
5815
|
}
|
|
4887
|
-
const raw =
|
|
5816
|
+
const raw = import_fs7.default.readFileSync(logPath, "utf-8");
|
|
4888
5817
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
4889
5818
|
let entries = lines.flatMap((line) => {
|
|
4890
5819
|
try {
|
|
@@ -4906,31 +5835,31 @@ program.command("audit").description("View local execution audit log").option("-
|
|
|
4906
5835
|
return;
|
|
4907
5836
|
}
|
|
4908
5837
|
if (entries.length === 0) {
|
|
4909
|
-
console.log(
|
|
5838
|
+
console.log(import_chalk6.default.yellow("No matching audit entries."));
|
|
4910
5839
|
return;
|
|
4911
5840
|
}
|
|
4912
5841
|
console.log(
|
|
4913
5842
|
`
|
|
4914
|
-
${
|
|
5843
|
+
${import_chalk6.default.bold("Node9 Audit Log")} ${import_chalk6.default.dim(`(${entries.length} entries)`)}`
|
|
4915
5844
|
);
|
|
4916
|
-
console.log(
|
|
5845
|
+
console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
|
|
4917
5846
|
console.log(
|
|
4918
5847
|
` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
|
|
4919
5848
|
);
|
|
4920
|
-
console.log(
|
|
5849
|
+
console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
|
|
4921
5850
|
for (const e of entries) {
|
|
4922
5851
|
const time = formatRelativeTime(String(e.ts)).padEnd(12);
|
|
4923
5852
|
const tool = String(e.tool).slice(0, 17).padEnd(18);
|
|
4924
|
-
const result = e.decision === "allow" ?
|
|
5853
|
+
const result = e.decision === "allow" ? import_chalk6.default.green("ALLOW".padEnd(10)) : import_chalk6.default.red("DENY".padEnd(10));
|
|
4925
5854
|
const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
|
|
4926
5855
|
const agent = String(e.agent || "unknown");
|
|
4927
5856
|
console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
|
|
4928
5857
|
}
|
|
4929
5858
|
const allowed = entries.filter((e) => e.decision === "allow").length;
|
|
4930
5859
|
const denied = entries.filter((e) => e.decision === "deny").length;
|
|
4931
|
-
console.log(
|
|
5860
|
+
console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
|
|
4932
5861
|
console.log(
|
|
4933
|
-
` ${entries.length} entries | ${
|
|
5862
|
+
` ${entries.length} entries | ${import_chalk6.default.green(allowed + " allowed")} | ${import_chalk6.default.red(denied + " denied")}
|
|
4934
5863
|
`
|
|
4935
5864
|
);
|
|
4936
5865
|
});
|
|
@@ -4941,43 +5870,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4941
5870
|
const settings = mergedConfig.settings;
|
|
4942
5871
|
console.log("");
|
|
4943
5872
|
if (creds && settings.approvers.cloud) {
|
|
4944
|
-
console.log(
|
|
5873
|
+
console.log(import_chalk6.default.green(" \u25CF Agent mode") + import_chalk6.default.gray(" \u2014 cloud team policy enforced"));
|
|
4945
5874
|
} else if (creds && !settings.approvers.cloud) {
|
|
4946
5875
|
console.log(
|
|
4947
|
-
|
|
5876
|
+
import_chalk6.default.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + import_chalk6.default.gray(" \u2014 all decisions stay on this machine")
|
|
4948
5877
|
);
|
|
4949
5878
|
} else {
|
|
4950
5879
|
console.log(
|
|
4951
|
-
|
|
5880
|
+
import_chalk6.default.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + import_chalk6.default.gray(" \u2014 no API key (Local rules only)")
|
|
4952
5881
|
);
|
|
4953
5882
|
}
|
|
4954
5883
|
console.log("");
|
|
4955
5884
|
if (daemonRunning) {
|
|
4956
5885
|
console.log(
|
|
4957
|
-
|
|
5886
|
+
import_chalk6.default.green(" \u25CF Daemon running") + import_chalk6.default.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
|
|
4958
5887
|
);
|
|
4959
5888
|
} else {
|
|
4960
|
-
console.log(
|
|
5889
|
+
console.log(import_chalk6.default.gray(" \u25CB Daemon stopped"));
|
|
4961
5890
|
}
|
|
4962
5891
|
if (settings.enableUndo) {
|
|
4963
5892
|
console.log(
|
|
4964
|
-
|
|
5893
|
+
import_chalk6.default.magenta(" \u25CF Undo Engine") + import_chalk6.default.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
|
|
4965
5894
|
);
|
|
4966
5895
|
}
|
|
4967
5896
|
console.log("");
|
|
4968
|
-
const modeLabel = settings.mode === "audit" ?
|
|
5897
|
+
const modeLabel = settings.mode === "audit" ? import_chalk6.default.blue("audit") : settings.mode === "strict" ? import_chalk6.default.red("strict") : import_chalk6.default.white("standard");
|
|
4969
5898
|
console.log(` Mode: ${modeLabel}`);
|
|
4970
|
-
const projectConfig =
|
|
4971
|
-
const globalConfig =
|
|
5899
|
+
const projectConfig = import_path9.default.join(process.cwd(), "node9.config.json");
|
|
5900
|
+
const globalConfig = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
|
|
4972
5901
|
console.log(
|
|
4973
|
-
` Local: ${
|
|
5902
|
+
` Local: ${import_fs7.default.existsSync(projectConfig) ? import_chalk6.default.green("Active (node9.config.json)") : import_chalk6.default.gray("Not present")}`
|
|
4974
5903
|
);
|
|
4975
5904
|
console.log(
|
|
4976
|
-
` Global: ${
|
|
5905
|
+
` Global: ${import_fs7.default.existsSync(globalConfig) ? import_chalk6.default.green("Active (~/.node9/config.json)") : import_chalk6.default.gray("Not present")}`
|
|
4977
5906
|
);
|
|
4978
5907
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
4979
5908
|
console.log(
|
|
4980
|
-
` Sandbox: ${
|
|
5909
|
+
` Sandbox: ${import_chalk6.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
4981
5910
|
);
|
|
4982
5911
|
}
|
|
4983
5912
|
const pauseState = checkPause();
|
|
@@ -4985,47 +5914,63 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4985
5914
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
4986
5915
|
console.log("");
|
|
4987
5916
|
console.log(
|
|
4988
|
-
|
|
5917
|
+
import_chalk6.default.yellow(` \u23F8 PAUSED until ${expiresAt}`) + import_chalk6.default.gray(" \u2014 all tool calls allowed")
|
|
4989
5918
|
);
|
|
4990
5919
|
}
|
|
4991
5920
|
console.log("");
|
|
4992
5921
|
});
|
|
4993
|
-
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").
|
|
5922
|
+
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(
|
|
5923
|
+
"-w, --watch",
|
|
5924
|
+
"Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
|
|
5925
|
+
).action(
|
|
4994
5926
|
async (action, options) => {
|
|
4995
5927
|
const cmd = (action ?? "start").toLowerCase();
|
|
4996
5928
|
if (cmd === "stop") return stopDaemon();
|
|
4997
5929
|
if (cmd === "status") return daemonStatus();
|
|
4998
5930
|
if (cmd !== "start" && action !== void 0) {
|
|
4999
|
-
console.error(
|
|
5931
|
+
console.error(import_chalk6.default.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
|
|
5000
5932
|
process.exit(1);
|
|
5001
5933
|
}
|
|
5934
|
+
if (options.watch) {
|
|
5935
|
+
process.env.NODE9_WATCH_MODE = "1";
|
|
5936
|
+
setTimeout(() => {
|
|
5937
|
+
openBrowserLocal();
|
|
5938
|
+
console.log(import_chalk6.default.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
5939
|
+
}, 600);
|
|
5940
|
+
startDaemon();
|
|
5941
|
+
return;
|
|
5942
|
+
}
|
|
5002
5943
|
if (options.openui) {
|
|
5003
5944
|
if (isDaemonRunning()) {
|
|
5004
5945
|
openBrowserLocal();
|
|
5005
|
-
console.log(
|
|
5946
|
+
console.log(import_chalk6.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
5006
5947
|
process.exit(0);
|
|
5007
5948
|
}
|
|
5008
|
-
const child = (0,
|
|
5949
|
+
const child = (0, import_child_process6.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
5009
5950
|
child.unref();
|
|
5010
5951
|
for (let i = 0; i < 12; i++) {
|
|
5011
5952
|
await new Promise((r) => setTimeout(r, 250));
|
|
5012
5953
|
if (isDaemonRunning()) break;
|
|
5013
5954
|
}
|
|
5014
5955
|
openBrowserLocal();
|
|
5015
|
-
console.log(
|
|
5956
|
+
console.log(import_chalk6.default.green(`
|
|
5016
5957
|
\u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
|
|
5017
5958
|
process.exit(0);
|
|
5018
5959
|
}
|
|
5019
5960
|
if (options.background) {
|
|
5020
|
-
const child = (0,
|
|
5961
|
+
const child = (0, import_child_process6.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
5021
5962
|
child.unref();
|
|
5022
|
-
console.log(
|
|
5963
|
+
console.log(import_chalk6.default.green(`
|
|
5023
5964
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
5024
5965
|
process.exit(0);
|
|
5025
5966
|
}
|
|
5026
5967
|
startDaemon();
|
|
5027
5968
|
}
|
|
5028
5969
|
);
|
|
5970
|
+
program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).option("--clear", "Clear history buffer and stream live events fresh", false).action(async (options) => {
|
|
5971
|
+
const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
|
|
5972
|
+
await startTail2(options);
|
|
5973
|
+
});
|
|
5029
5974
|
program.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
|
|
5030
5975
|
const processPayload = async (raw) => {
|
|
5031
5976
|
try {
|
|
@@ -5036,9 +5981,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
5036
5981
|
} catch (err) {
|
|
5037
5982
|
const tempConfig = getConfig();
|
|
5038
5983
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
5039
|
-
const logPath =
|
|
5984
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
|
|
5040
5985
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
5041
|
-
|
|
5986
|
+
import_fs7.default.appendFileSync(
|
|
5042
5987
|
logPath,
|
|
5043
5988
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
5044
5989
|
RAW: ${raw}
|
|
@@ -5056,10 +6001,10 @@ RAW: ${raw}
|
|
|
5056
6001
|
}
|
|
5057
6002
|
const config = getConfig();
|
|
5058
6003
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
5059
|
-
const logPath =
|
|
5060
|
-
if (!
|
|
5061
|
-
|
|
5062
|
-
|
|
6004
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
|
|
6005
|
+
if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
|
|
6006
|
+
import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
|
|
6007
|
+
import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
5063
6008
|
`);
|
|
5064
6009
|
}
|
|
5065
6010
|
const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
|
|
@@ -5071,18 +6016,18 @@ RAW: ${raw}
|
|
|
5071
6016
|
const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
|
|
5072
6017
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
5073
6018
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
5074
|
-
console.error(
|
|
6019
|
+
console.error(import_chalk6.default.bgRed.white.bold(`
|
|
5075
6020
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
5076
|
-
console.error(
|
|
6021
|
+
console.error(import_chalk6.default.red.bold(` A sensitive secret was found in the tool arguments!`));
|
|
5077
6022
|
} else {
|
|
5078
|
-
console.error(
|
|
6023
|
+
console.error(import_chalk6.default.red(`
|
|
5079
6024
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
5080
6025
|
}
|
|
5081
|
-
console.error(
|
|
5082
|
-
if (result2?.changeHint) console.error(
|
|
6026
|
+
console.error(import_chalk6.default.gray(` Triggered by: ${blockedByContext}`));
|
|
6027
|
+
if (result2?.changeHint) console.error(import_chalk6.default.cyan(` To change: ${result2.changeHint}`));
|
|
5083
6028
|
console.error("");
|
|
5084
6029
|
const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
|
|
5085
|
-
console.error(
|
|
6030
|
+
console.error(import_chalk6.default.dim(` (Detailed instructions sent to AI agent)`));
|
|
5086
6031
|
process.stdout.write(
|
|
5087
6032
|
JSON.stringify({
|
|
5088
6033
|
decision: "block",
|
|
@@ -5113,7 +6058,7 @@ RAW: ${raw}
|
|
|
5113
6058
|
process.exit(0);
|
|
5114
6059
|
}
|
|
5115
6060
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
5116
|
-
console.error(
|
|
6061
|
+
console.error(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
5117
6062
|
const daemonReady = await autoStartDaemonAndWait();
|
|
5118
6063
|
if (daemonReady) {
|
|
5119
6064
|
const retry = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
@@ -5136,9 +6081,9 @@ RAW: ${raw}
|
|
|
5136
6081
|
});
|
|
5137
6082
|
} catch (err) {
|
|
5138
6083
|
if (process.env.NODE9_DEBUG === "1") {
|
|
5139
|
-
const logPath =
|
|
6084
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
|
|
5140
6085
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
5141
|
-
|
|
6086
|
+
import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
5142
6087
|
`);
|
|
5143
6088
|
}
|
|
5144
6089
|
process.exit(0);
|
|
@@ -5183,10 +6128,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
5183
6128
|
decision: "allowed",
|
|
5184
6129
|
source: "post-hook"
|
|
5185
6130
|
};
|
|
5186
|
-
const logPath =
|
|
5187
|
-
if (!
|
|
5188
|
-
|
|
5189
|
-
|
|
6131
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
|
|
6132
|
+
if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
|
|
6133
|
+
import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
|
|
6134
|
+
import_fs7.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
5190
6135
|
const config = getConfig();
|
|
5191
6136
|
if (shouldSnapshot(tool, {}, config)) {
|
|
5192
6137
|
await createShadowSnapshot();
|
|
@@ -5213,7 +6158,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
5213
6158
|
const ms = parseDuration(options.duration);
|
|
5214
6159
|
if (ms === null) {
|
|
5215
6160
|
console.error(
|
|
5216
|
-
|
|
6161
|
+
import_chalk6.default.red(`
|
|
5217
6162
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
5218
6163
|
`)
|
|
5219
6164
|
);
|
|
@@ -5221,20 +6166,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
5221
6166
|
}
|
|
5222
6167
|
pauseNode9(ms, options.duration);
|
|
5223
6168
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
5224
|
-
console.log(
|
|
6169
|
+
console.log(import_chalk6.default.yellow(`
|
|
5225
6170
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
5226
|
-
console.log(
|
|
5227
|
-
console.log(
|
|
6171
|
+
console.log(import_chalk6.default.gray(` All tool calls will be allowed without review.`));
|
|
6172
|
+
console.log(import_chalk6.default.gray(` Run "node9 resume" to re-enable early.
|
|
5228
6173
|
`));
|
|
5229
6174
|
});
|
|
5230
6175
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
5231
6176
|
const { paused } = checkPause();
|
|
5232
6177
|
if (!paused) {
|
|
5233
|
-
console.log(
|
|
6178
|
+
console.log(import_chalk6.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
5234
6179
|
return;
|
|
5235
6180
|
}
|
|
5236
6181
|
resumeNode9();
|
|
5237
|
-
console.log(
|
|
6182
|
+
console.log(import_chalk6.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
5238
6183
|
});
|
|
5239
6184
|
var HOOK_BASED_AGENTS = {
|
|
5240
6185
|
claude: "claude",
|
|
@@ -5247,15 +6192,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
5247
6192
|
if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
|
|
5248
6193
|
const target = HOOK_BASED_AGENTS[firstArg];
|
|
5249
6194
|
console.error(
|
|
5250
|
-
|
|
6195
|
+
import_chalk6.default.yellow(`
|
|
5251
6196
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
5252
6197
|
);
|
|
5253
|
-
console.error(
|
|
6198
|
+
console.error(import_chalk6.default.white(`
|
|
5254
6199
|
"${target}" uses its own hook system. Use:`));
|
|
5255
6200
|
console.error(
|
|
5256
|
-
|
|
6201
|
+
import_chalk6.default.green(` node9 addto ${target} `) + import_chalk6.default.gray("# one-time setup")
|
|
5257
6202
|
);
|
|
5258
|
-
console.error(
|
|
6203
|
+
console.error(import_chalk6.default.green(` ${target} `) + import_chalk6.default.gray("# run normally"));
|
|
5259
6204
|
process.exit(1);
|
|
5260
6205
|
}
|
|
5261
6206
|
const fullCommand = commandArgs.join(" ");
|
|
@@ -5263,7 +6208,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
5263
6208
|
agent: "Terminal"
|
|
5264
6209
|
});
|
|
5265
6210
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
5266
|
-
console.error(
|
|
6211
|
+
console.error(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
5267
6212
|
const daemonReady = await autoStartDaemonAndWait();
|
|
5268
6213
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
5269
6214
|
}
|
|
@@ -5272,12 +6217,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
5272
6217
|
}
|
|
5273
6218
|
if (!result.approved) {
|
|
5274
6219
|
console.error(
|
|
5275
|
-
|
|
6220
|
+
import_chalk6.default.red(`
|
|
5276
6221
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
5277
6222
|
);
|
|
5278
6223
|
process.exit(1);
|
|
5279
6224
|
}
|
|
5280
|
-
console.error(
|
|
6225
|
+
console.error(import_chalk6.default.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
5281
6226
|
await runProxy(fullCommand);
|
|
5282
6227
|
} else {
|
|
5283
6228
|
program.help();
|
|
@@ -5292,22 +6237,22 @@ program.command("undo").description(
|
|
|
5292
6237
|
if (history.length === 0) {
|
|
5293
6238
|
if (!options.all && allHistory.length > 0) {
|
|
5294
6239
|
console.log(
|
|
5295
|
-
|
|
6240
|
+
import_chalk6.default.yellow(
|
|
5296
6241
|
`
|
|
5297
6242
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
5298
|
-
Run ${
|
|
6243
|
+
Run ${import_chalk6.default.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
5299
6244
|
`
|
|
5300
6245
|
)
|
|
5301
6246
|
);
|
|
5302
6247
|
} else {
|
|
5303
|
-
console.log(
|
|
6248
|
+
console.log(import_chalk6.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
5304
6249
|
}
|
|
5305
6250
|
return;
|
|
5306
6251
|
}
|
|
5307
6252
|
const idx = history.length - steps;
|
|
5308
6253
|
if (idx < 0) {
|
|
5309
6254
|
console.log(
|
|
5310
|
-
|
|
6255
|
+
import_chalk6.default.yellow(
|
|
5311
6256
|
`
|
|
5312
6257
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
5313
6258
|
`
|
|
@@ -5318,18 +6263,18 @@ program.command("undo").description(
|
|
|
5318
6263
|
const snapshot = history[idx];
|
|
5319
6264
|
const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
|
|
5320
6265
|
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
|
|
5321
|
-
console.log(
|
|
6266
|
+
console.log(import_chalk6.default.magenta.bold(`
|
|
5322
6267
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
|
|
5323
6268
|
console.log(
|
|
5324
|
-
|
|
5325
|
-
` Tool: ${
|
|
6269
|
+
import_chalk6.default.white(
|
|
6270
|
+
` Tool: ${import_chalk6.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk6.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
5326
6271
|
)
|
|
5327
6272
|
);
|
|
5328
|
-
console.log(
|
|
5329
|
-
console.log(
|
|
6273
|
+
console.log(import_chalk6.default.white(` When: ${import_chalk6.default.gray(ageStr)}`));
|
|
6274
|
+
console.log(import_chalk6.default.white(` Dir: ${import_chalk6.default.gray(snapshot.cwd)}`));
|
|
5330
6275
|
if (steps > 1)
|
|
5331
6276
|
console.log(
|
|
5332
|
-
|
|
6277
|
+
import_chalk6.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
5333
6278
|
);
|
|
5334
6279
|
console.log("");
|
|
5335
6280
|
const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
@@ -5337,21 +6282,21 @@ program.command("undo").description(
|
|
|
5337
6282
|
const lines = diff.split("\n");
|
|
5338
6283
|
for (const line of lines) {
|
|
5339
6284
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
5340
|
-
console.log(
|
|
6285
|
+
console.log(import_chalk6.default.bold(line));
|
|
5341
6286
|
} else if (line.startsWith("+")) {
|
|
5342
|
-
console.log(
|
|
6287
|
+
console.log(import_chalk6.default.green(line));
|
|
5343
6288
|
} else if (line.startsWith("-")) {
|
|
5344
|
-
console.log(
|
|
6289
|
+
console.log(import_chalk6.default.red(line));
|
|
5345
6290
|
} else if (line.startsWith("@@")) {
|
|
5346
|
-
console.log(
|
|
6291
|
+
console.log(import_chalk6.default.cyan(line));
|
|
5347
6292
|
} else {
|
|
5348
|
-
console.log(
|
|
6293
|
+
console.log(import_chalk6.default.gray(line));
|
|
5349
6294
|
}
|
|
5350
6295
|
}
|
|
5351
6296
|
console.log("");
|
|
5352
6297
|
} else {
|
|
5353
6298
|
console.log(
|
|
5354
|
-
|
|
6299
|
+
import_chalk6.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
5355
6300
|
);
|
|
5356
6301
|
}
|
|
5357
6302
|
const proceed = await (0, import_prompts3.confirm)({
|
|
@@ -5360,42 +6305,42 @@ program.command("undo").description(
|
|
|
5360
6305
|
});
|
|
5361
6306
|
if (proceed) {
|
|
5362
6307
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
5363
|
-
console.log(
|
|
6308
|
+
console.log(import_chalk6.default.green("\n\u2705 Reverted successfully.\n"));
|
|
5364
6309
|
} else {
|
|
5365
|
-
console.error(
|
|
6310
|
+
console.error(import_chalk6.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
5366
6311
|
}
|
|
5367
6312
|
} else {
|
|
5368
|
-
console.log(
|
|
6313
|
+
console.log(import_chalk6.default.gray("\nCancelled.\n"));
|
|
5369
6314
|
}
|
|
5370
6315
|
});
|
|
5371
6316
|
var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
|
|
5372
6317
|
shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
|
|
5373
6318
|
const name = resolveShieldName(service);
|
|
5374
6319
|
if (!name) {
|
|
5375
|
-
console.error(
|
|
6320
|
+
console.error(import_chalk6.default.red(`
|
|
5376
6321
|
\u274C Unknown shield: "${service}"
|
|
5377
6322
|
`));
|
|
5378
|
-
console.log(`Run ${
|
|
6323
|
+
console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
|
|
5379
6324
|
`);
|
|
5380
6325
|
process.exit(1);
|
|
5381
6326
|
}
|
|
5382
6327
|
const shield = getShield(name);
|
|
5383
6328
|
const active = readActiveShields();
|
|
5384
6329
|
if (active.includes(name)) {
|
|
5385
|
-
console.log(
|
|
6330
|
+
console.log(import_chalk6.default.yellow(`
|
|
5386
6331
|
\u2139\uFE0F Shield "${name}" is already active.
|
|
5387
6332
|
`));
|
|
5388
6333
|
return;
|
|
5389
6334
|
}
|
|
5390
6335
|
writeActiveShields([...active, name]);
|
|
5391
|
-
console.log(
|
|
6336
|
+
console.log(import_chalk6.default.green(`
|
|
5392
6337
|
\u{1F6E1}\uFE0F Shield "${name}" enabled.`));
|
|
5393
|
-
console.log(
|
|
6338
|
+
console.log(import_chalk6.default.gray(` ${shield.smartRules.length} smart rules now active.`));
|
|
5394
6339
|
if (shield.dangerousWords.length > 0)
|
|
5395
|
-
console.log(
|
|
6340
|
+
console.log(import_chalk6.default.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
|
|
5396
6341
|
if (name === "filesystem") {
|
|
5397
6342
|
console.log(
|
|
5398
|
-
|
|
6343
|
+
import_chalk6.default.yellow(
|
|
5399
6344
|
`
|
|
5400
6345
|
\u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
|
|
5401
6346
|
Tools like unlink, find -delete, or language-level file ops are not intercepted.`
|
|
@@ -5407,51 +6352,51 @@ shieldCmd.command("enable <service>").description("Enable a security shield for
|
|
|
5407
6352
|
shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
|
|
5408
6353
|
const name = resolveShieldName(service);
|
|
5409
6354
|
if (!name) {
|
|
5410
|
-
console.error(
|
|
6355
|
+
console.error(import_chalk6.default.red(`
|
|
5411
6356
|
\u274C Unknown shield: "${service}"
|
|
5412
6357
|
`));
|
|
5413
|
-
console.log(`Run ${
|
|
6358
|
+
console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
|
|
5414
6359
|
`);
|
|
5415
6360
|
process.exit(1);
|
|
5416
6361
|
}
|
|
5417
6362
|
const active = readActiveShields();
|
|
5418
6363
|
if (!active.includes(name)) {
|
|
5419
|
-
console.log(
|
|
6364
|
+
console.log(import_chalk6.default.yellow(`
|
|
5420
6365
|
\u2139\uFE0F Shield "${name}" is not active.
|
|
5421
6366
|
`));
|
|
5422
6367
|
return;
|
|
5423
6368
|
}
|
|
5424
6369
|
writeActiveShields(active.filter((s) => s !== name));
|
|
5425
|
-
console.log(
|
|
6370
|
+
console.log(import_chalk6.default.green(`
|
|
5426
6371
|
\u{1F6E1}\uFE0F Shield "${name}" disabled.
|
|
5427
6372
|
`));
|
|
5428
6373
|
});
|
|
5429
6374
|
shieldCmd.command("list").description("Show all available shields").action(() => {
|
|
5430
6375
|
const active = new Set(readActiveShields());
|
|
5431
|
-
console.log(
|
|
6376
|
+
console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
|
|
5432
6377
|
for (const shield of listShields()) {
|
|
5433
|
-
const status = active.has(shield.name) ?
|
|
5434
|
-
console.log(` ${status} ${
|
|
6378
|
+
const status = active.has(shield.name) ? import_chalk6.default.green("\u25CF enabled") : import_chalk6.default.gray("\u25CB disabled");
|
|
6379
|
+
console.log(` ${status} ${import_chalk6.default.cyan(shield.name.padEnd(12))} ${shield.description}`);
|
|
5435
6380
|
if (shield.aliases.length > 0)
|
|
5436
|
-
console.log(
|
|
6381
|
+
console.log(import_chalk6.default.gray(` aliases: ${shield.aliases.join(", ")}`));
|
|
5437
6382
|
}
|
|
5438
6383
|
console.log("");
|
|
5439
6384
|
});
|
|
5440
6385
|
shieldCmd.command("status").description("Show which shields are currently active").action(() => {
|
|
5441
6386
|
const active = readActiveShields();
|
|
5442
6387
|
if (active.length === 0) {
|
|
5443
|
-
console.log(
|
|
5444
|
-
console.log(`Run ${
|
|
6388
|
+
console.log(import_chalk6.default.yellow("\n\u2139\uFE0F No shields are active.\n"));
|
|
6389
|
+
console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
|
|
5445
6390
|
`);
|
|
5446
6391
|
return;
|
|
5447
6392
|
}
|
|
5448
|
-
console.log(
|
|
6393
|
+
console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
|
|
5449
6394
|
for (const name of active) {
|
|
5450
6395
|
const shield = getShield(name);
|
|
5451
6396
|
if (!shield) continue;
|
|
5452
|
-
console.log(` ${
|
|
6397
|
+
console.log(` ${import_chalk6.default.green("\u25CF")} ${import_chalk6.default.cyan(name)}`);
|
|
5453
6398
|
console.log(
|
|
5454
|
-
|
|
6399
|
+
import_chalk6.default.gray(
|
|
5455
6400
|
` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
|
|
5456
6401
|
)
|
|
5457
6402
|
);
|
|
@@ -5462,9 +6407,9 @@ process.on("unhandledRejection", (reason) => {
|
|
|
5462
6407
|
const isCheckHook = process.argv[2] === "check";
|
|
5463
6408
|
if (isCheckHook) {
|
|
5464
6409
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
5465
|
-
const logPath =
|
|
6410
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
|
|
5466
6411
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
5467
|
-
|
|
6412
|
+
import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
5468
6413
|
`);
|
|
5469
6414
|
}
|
|
5470
6415
|
process.exit(0);
|