@node9/proxy 1.0.14 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -7
- package/dist/cli.js +2037 -1175
- package/dist/cli.mjs +2021 -1157
- package/dist/index.js +60 -3
- package/dist/index.mjs +60 -3
- 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,87 @@ 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" }
|
|
606
|
-
],
|
|
607
|
-
verdict: "review",
|
|
608
|
-
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
609
|
-
},
|
|
610
|
-
{
|
|
611
|
-
name: "shield:filesystem:review-write-etc",
|
|
612
|
-
tool: "bash",
|
|
613
|
-
conditions: [
|
|
614
|
-
{
|
|
615
|
-
field: "command",
|
|
616
|
-
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
617
|
-
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
618
|
-
op: "matches",
|
|
619
|
-
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
620
|
-
}
|
|
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"
|
|
621
412
|
],
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
+
});
|
|
422
|
+
SmartRuleSchema = import_zod.z.object({
|
|
423
|
+
name: import_zod.z.string().optional(),
|
|
424
|
+
tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
|
|
425
|
+
conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
426
|
+
conditionMode: import_zod.z.enum(["all", "any"]).optional(),
|
|
427
|
+
verdict: import_zod.z.enum(["allow", "review", "block"], {
|
|
428
|
+
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
429
|
+
}),
|
|
430
|
+
reason: import_zod.z.string().optional()
|
|
431
|
+
});
|
|
432
|
+
ConfigFileSchema = import_zod.z.object({
|
|
433
|
+
version: import_zod.z.string().optional(),
|
|
434
|
+
settings: import_zod.z.object({
|
|
435
|
+
mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
|
|
436
|
+
autoStartDaemon: import_zod.z.boolean().optional(),
|
|
437
|
+
enableUndo: import_zod.z.boolean().optional(),
|
|
438
|
+
enableHookLogDebug: import_zod.z.boolean().optional(),
|
|
439
|
+
approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
|
|
440
|
+
approvers: import_zod.z.object({
|
|
441
|
+
native: import_zod.z.boolean().optional(),
|
|
442
|
+
browser: import_zod.z.boolean().optional(),
|
|
443
|
+
cloud: import_zod.z.boolean().optional(),
|
|
444
|
+
terminal: import_zod.z.boolean().optional()
|
|
445
|
+
}).optional(),
|
|
446
|
+
environment: import_zod.z.string().optional(),
|
|
447
|
+
slackEnabled: import_zod.z.boolean().optional(),
|
|
448
|
+
enableTrustSessions: import_zod.z.boolean().optional(),
|
|
449
|
+
allowGlobalPause: import_zod.z.boolean().optional()
|
|
450
|
+
}).optional(),
|
|
451
|
+
policy: import_zod.z.object({
|
|
452
|
+
sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
453
|
+
dangerousWords: import_zod.z.array(noNewlines).optional(),
|
|
454
|
+
ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
455
|
+
toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
|
|
456
|
+
smartRules: import_zod.z.array(SmartRuleSchema).optional(),
|
|
457
|
+
snapshot: import_zod.z.object({
|
|
458
|
+
tools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
459
|
+
onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
460
|
+
ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
461
|
+
}).optional(),
|
|
462
|
+
dlp: import_zod.z.object({
|
|
463
|
+
enabled: import_zod.z.boolean().optional(),
|
|
464
|
+
scanIgnoredTools: import_zod.z.boolean().optional()
|
|
465
|
+
}).optional()
|
|
466
|
+
}).optional(),
|
|
467
|
+
environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
468
|
+
}).strict({ message: "Config contains unknown top-level keys" });
|
|
630
469
|
}
|
|
631
|
-
};
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// src/shields.ts
|
|
632
473
|
function resolveShieldName(input) {
|
|
633
474
|
const lower = input.toLowerCase();
|
|
634
475
|
if (SHIELDS[lower]) return lower;
|
|
@@ -644,7 +485,6 @@ function getShield(name) {
|
|
|
644
485
|
function listShields() {
|
|
645
486
|
return Object.values(SHIELDS);
|
|
646
487
|
}
|
|
647
|
-
var SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
|
|
648
488
|
function readActiveShields() {
|
|
649
489
|
try {
|
|
650
490
|
const raw = import_fs.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
@@ -669,21 +509,186 @@ function writeActiveShields(active) {
|
|
|
669
509
|
import_fs.default.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
|
|
670
510
|
import_fs.default.renameSync(tmp, SHIELDS_STATE_FILE);
|
|
671
511
|
}
|
|
512
|
+
var import_fs, import_path3, import_os, import_crypto, SHIELDS, SHIELDS_STATE_FILE;
|
|
513
|
+
var init_shields = __esm({
|
|
514
|
+
"src/shields.ts"() {
|
|
515
|
+
"use strict";
|
|
516
|
+
import_fs = __toESM(require("fs"));
|
|
517
|
+
import_path3 = __toESM(require("path"));
|
|
518
|
+
import_os = __toESM(require("os"));
|
|
519
|
+
import_crypto = __toESM(require("crypto"));
|
|
520
|
+
SHIELDS = {
|
|
521
|
+
postgres: {
|
|
522
|
+
name: "postgres",
|
|
523
|
+
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
524
|
+
aliases: ["pg", "postgresql"],
|
|
525
|
+
smartRules: [
|
|
526
|
+
{
|
|
527
|
+
name: "shield:postgres:block-drop-table",
|
|
528
|
+
tool: "*",
|
|
529
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
530
|
+
verdict: "block",
|
|
531
|
+
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: "shield:postgres:block-truncate",
|
|
535
|
+
tool: "*",
|
|
536
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
537
|
+
verdict: "block",
|
|
538
|
+
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "shield:postgres:block-drop-column",
|
|
542
|
+
tool: "*",
|
|
543
|
+
conditions: [
|
|
544
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
545
|
+
],
|
|
546
|
+
verdict: "block",
|
|
547
|
+
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: "shield:postgres:review-grant-revoke",
|
|
551
|
+
tool: "*",
|
|
552
|
+
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
553
|
+
verdict: "review",
|
|
554
|
+
reason: "Permission changes require human approval (Postgres shield)"
|
|
555
|
+
}
|
|
556
|
+
],
|
|
557
|
+
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
558
|
+
},
|
|
559
|
+
github: {
|
|
560
|
+
name: "github",
|
|
561
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
562
|
+
aliases: ["git"],
|
|
563
|
+
smartRules: [
|
|
564
|
+
{
|
|
565
|
+
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
566
|
+
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
567
|
+
name: "shield:github:review-delete-branch-remote",
|
|
568
|
+
tool: "bash",
|
|
569
|
+
conditions: [
|
|
570
|
+
{
|
|
571
|
+
field: "command",
|
|
572
|
+
op: "matches",
|
|
573
|
+
value: "git\\s+push\\s+.*--delete",
|
|
574
|
+
flags: "i"
|
|
575
|
+
}
|
|
576
|
+
],
|
|
577
|
+
verdict: "review",
|
|
578
|
+
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: "shield:github:block-delete-repo",
|
|
582
|
+
tool: "*",
|
|
583
|
+
conditions: [
|
|
584
|
+
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
585
|
+
],
|
|
586
|
+
verdict: "block",
|
|
587
|
+
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
588
|
+
}
|
|
589
|
+
],
|
|
590
|
+
dangerousWords: []
|
|
591
|
+
},
|
|
592
|
+
aws: {
|
|
593
|
+
name: "aws",
|
|
594
|
+
description: "Protects AWS infrastructure from destructive AI operations",
|
|
595
|
+
aliases: ["amazon"],
|
|
596
|
+
smartRules: [
|
|
597
|
+
{
|
|
598
|
+
name: "shield:aws:block-delete-s3-bucket",
|
|
599
|
+
tool: "*",
|
|
600
|
+
conditions: [
|
|
601
|
+
{
|
|
602
|
+
field: "command",
|
|
603
|
+
op: "matches",
|
|
604
|
+
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
605
|
+
flags: "i"
|
|
606
|
+
}
|
|
607
|
+
],
|
|
608
|
+
verdict: "block",
|
|
609
|
+
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
name: "shield:aws:review-iam-changes",
|
|
613
|
+
tool: "*",
|
|
614
|
+
conditions: [
|
|
615
|
+
{
|
|
616
|
+
field: "command",
|
|
617
|
+
op: "matches",
|
|
618
|
+
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
619
|
+
flags: "i"
|
|
620
|
+
}
|
|
621
|
+
],
|
|
622
|
+
verdict: "review",
|
|
623
|
+
reason: "IAM changes require human approval (AWS shield)"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: "shield:aws:block-ec2-terminate",
|
|
627
|
+
tool: "*",
|
|
628
|
+
conditions: [
|
|
629
|
+
{
|
|
630
|
+
field: "command",
|
|
631
|
+
op: "matches",
|
|
632
|
+
value: "aws\\s+ec2\\s+terminate-instances",
|
|
633
|
+
flags: "i"
|
|
634
|
+
}
|
|
635
|
+
],
|
|
636
|
+
verdict: "block",
|
|
637
|
+
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
name: "shield:aws:review-rds-delete",
|
|
641
|
+
tool: "*",
|
|
642
|
+
conditions: [
|
|
643
|
+
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
644
|
+
],
|
|
645
|
+
verdict: "review",
|
|
646
|
+
reason: "RDS deletion requires human approval (AWS shield)"
|
|
647
|
+
}
|
|
648
|
+
],
|
|
649
|
+
dangerousWords: []
|
|
650
|
+
},
|
|
651
|
+
filesystem: {
|
|
652
|
+
name: "filesystem",
|
|
653
|
+
description: "Protects the local filesystem from dangerous AI operations",
|
|
654
|
+
aliases: ["fs"],
|
|
655
|
+
smartRules: [
|
|
656
|
+
{
|
|
657
|
+
name: "shield:filesystem:review-chmod-777",
|
|
658
|
+
tool: "bash",
|
|
659
|
+
conditions: [
|
|
660
|
+
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
661
|
+
],
|
|
662
|
+
verdict: "review",
|
|
663
|
+
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: "shield:filesystem:review-write-etc",
|
|
667
|
+
tool: "bash",
|
|
668
|
+
conditions: [
|
|
669
|
+
{
|
|
670
|
+
field: "command",
|
|
671
|
+
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
672
|
+
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
673
|
+
op: "matches",
|
|
674
|
+
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
675
|
+
}
|
|
676
|
+
],
|
|
677
|
+
verdict: "review",
|
|
678
|
+
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
679
|
+
}
|
|
680
|
+
],
|
|
681
|
+
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
682
|
+
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
683
|
+
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
684
|
+
dangerousWords: ["wipefs"]
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
|
|
688
|
+
}
|
|
689
|
+
});
|
|
672
690
|
|
|
673
691
|
// 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
692
|
function maskSecret(raw, pattern) {
|
|
688
693
|
const match = raw.match(pattern);
|
|
689
694
|
if (!match) return "****";
|
|
@@ -694,9 +699,6 @@ function maskSecret(raw, pattern) {
|
|
|
694
699
|
const stars = "*".repeat(Math.min(secret.length - 8, 12));
|
|
695
700
|
return `${prefix}${stars}${suffix}`;
|
|
696
701
|
}
|
|
697
|
-
var MAX_DEPTH = 5;
|
|
698
|
-
var MAX_STRING_BYTES = 1e5;
|
|
699
|
-
var MAX_JSON_PARSE_BYTES = 1e4;
|
|
700
702
|
function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
701
703
|
if (depth > MAX_DEPTH || args === null || args === void 0) return null;
|
|
702
704
|
if (Array.isArray(args)) {
|
|
@@ -739,12 +741,30 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
739
741
|
}
|
|
740
742
|
return null;
|
|
741
743
|
}
|
|
744
|
+
var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
|
|
745
|
+
var init_dlp = __esm({
|
|
746
|
+
"src/dlp.ts"() {
|
|
747
|
+
"use strict";
|
|
748
|
+
DLP_PATTERNS = [
|
|
749
|
+
{ name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
|
|
750
|
+
{ name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
|
|
751
|
+
{ name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
|
|
752
|
+
{ name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
|
|
753
|
+
{ name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
|
|
754
|
+
{
|
|
755
|
+
name: "Private Key (PEM)",
|
|
756
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
757
|
+
severity: "block"
|
|
758
|
+
},
|
|
759
|
+
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
|
|
760
|
+
];
|
|
761
|
+
MAX_DEPTH = 5;
|
|
762
|
+
MAX_STRING_BYTES = 1e5;
|
|
763
|
+
MAX_JSON_PARSE_BYTES = 1e4;
|
|
764
|
+
}
|
|
765
|
+
});
|
|
742
766
|
|
|
743
767
|
// 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
768
|
function checkPause() {
|
|
749
769
|
try {
|
|
750
770
|
if (!import_fs2.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -857,9 +877,9 @@ function matchesPattern(text, patterns) {
|
|
|
857
877
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
858
878
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
859
879
|
}
|
|
860
|
-
function getNestedValue(obj,
|
|
880
|
+
function getNestedValue(obj, path10) {
|
|
861
881
|
if (!obj || typeof obj !== "object") return null;
|
|
862
|
-
return
|
|
882
|
+
return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
863
883
|
}
|
|
864
884
|
function shouldSnapshot(toolName, args, config) {
|
|
865
885
|
if (!config.settings.enableUndo) return false;
|
|
@@ -929,7 +949,6 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
929
949
|
const fieldName = toolInspection[matchingPattern];
|
|
930
950
|
return fieldName === "sql" || fieldName === "query";
|
|
931
951
|
}
|
|
932
|
-
var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
933
952
|
async function analyzeShellCommand(command) {
|
|
934
953
|
const actions = [];
|
|
935
954
|
const paths = [];
|
|
@@ -1011,208 +1030,6 @@ function redactSecrets(text) {
|
|
|
1011
1030
|
);
|
|
1012
1031
|
return redacted;
|
|
1013
1032
|
}
|
|
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
1033
|
function _resetConfigCache() {
|
|
1217
1034
|
cachedConfig = null;
|
|
1218
1035
|
}
|
|
@@ -1619,8 +1436,6 @@ function isIgnoredTool(toolName) {
|
|
|
1619
1436
|
const config = getConfig();
|
|
1620
1437
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
1621
1438
|
}
|
|
1622
|
-
var DAEMON_PORT = 7391;
|
|
1623
|
-
var DAEMON_HOST = "127.0.0.1";
|
|
1624
1439
|
function isDaemonRunning() {
|
|
1625
1440
|
try {
|
|
1626
1441
|
const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
|
|
@@ -1644,7 +1459,7 @@ function getPersistentDecision(toolName) {
|
|
|
1644
1459
|
}
|
|
1645
1460
|
return null;
|
|
1646
1461
|
}
|
|
1647
|
-
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
1462
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
|
|
1648
1463
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1649
1464
|
const checkCtrl = new AbortController();
|
|
1650
1465
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -1659,6 +1474,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
|
1659
1474
|
args,
|
|
1660
1475
|
agent: meta?.agent,
|
|
1661
1476
|
mcpServer: meta?.mcpServer,
|
|
1477
|
+
fromCLI: true,
|
|
1478
|
+
// Pass the flight-recorder ID so the daemon uses the same UUID for
|
|
1479
|
+
// activity-result as the CLI used for the pending activity event.
|
|
1480
|
+
// Without this, the two UUIDs never match and tail.ts never resolves
|
|
1481
|
+
// the pending item.
|
|
1482
|
+
activityId,
|
|
1662
1483
|
...riskMetadata && { riskMetadata }
|
|
1663
1484
|
}),
|
|
1664
1485
|
signal: checkCtrl.signal
|
|
@@ -1713,7 +1534,44 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1713
1534
|
signal: AbortSignal.timeout(3e3)
|
|
1714
1535
|
});
|
|
1715
1536
|
}
|
|
1537
|
+
function notifyActivity(data) {
|
|
1538
|
+
return new Promise((resolve) => {
|
|
1539
|
+
try {
|
|
1540
|
+
const payload = JSON.stringify(data);
|
|
1541
|
+
const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
|
|
1542
|
+
sock.on("connect", () => {
|
|
1543
|
+
sock.on("close", resolve);
|
|
1544
|
+
sock.end(payload);
|
|
1545
|
+
});
|
|
1546
|
+
sock.on("error", resolve);
|
|
1547
|
+
} catch {
|
|
1548
|
+
resolve();
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1716
1552
|
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1553
|
+
if (!options?.calledFromDaemon) {
|
|
1554
|
+
const actId = (0, import_crypto2.randomUUID)();
|
|
1555
|
+
const actTs = Date.now();
|
|
1556
|
+
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
|
|
1557
|
+
const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
|
|
1558
|
+
...options,
|
|
1559
|
+
activityId: actId
|
|
1560
|
+
});
|
|
1561
|
+
if (!result.noApprovalMechanism) {
|
|
1562
|
+
await notifyActivity({
|
|
1563
|
+
id: actId,
|
|
1564
|
+
tool: toolName,
|
|
1565
|
+
ts: actTs,
|
|
1566
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
1567
|
+
label: result.blockedByLabel
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
return result;
|
|
1571
|
+
}
|
|
1572
|
+
return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
|
|
1573
|
+
}
|
|
1574
|
+
async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1717
1575
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1718
1576
|
const pauseState = checkPause();
|
|
1719
1577
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
@@ -1749,6 +1607,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1749
1607
|
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
1750
1608
|
};
|
|
1751
1609
|
}
|
|
1610
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
|
|
1752
1611
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
1753
1612
|
}
|
|
1754
1613
|
}
|
|
@@ -1971,9 +1830,16 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1971
1830
|
console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
1972
1831
|
`));
|
|
1973
1832
|
}
|
|
1974
|
-
const daemonDecision = await askDaemon(
|
|
1975
|
-
|
|
1976
|
-
|
|
1833
|
+
const daemonDecision = await askDaemon(
|
|
1834
|
+
toolName,
|
|
1835
|
+
args,
|
|
1836
|
+
meta,
|
|
1837
|
+
signal,
|
|
1838
|
+
riskMetadata,
|
|
1839
|
+
options?.activityId
|
|
1840
|
+
);
|
|
1841
|
+
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1842
|
+
const isApproved = daemonDecision === "allow";
|
|
1977
1843
|
return {
|
|
1978
1844
|
approved: isApproved,
|
|
1979
1845
|
reason: isApproved ? void 0 : "The human user rejected this action via the Node9 Browser Dashboard.",
|
|
@@ -2175,7 +2041,10 @@ function getConfig() {
|
|
|
2175
2041
|
for (const rule of shield.smartRules) {
|
|
2176
2042
|
if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
2177
2043
|
}
|
|
2178
|
-
|
|
2044
|
+
const existingWords = new Set(mergedPolicy.dangerousWords);
|
|
2045
|
+
for (const word of shield.dangerousWords) {
|
|
2046
|
+
if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
|
|
2047
|
+
}
|
|
2179
2048
|
}
|
|
2180
2049
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2181
2050
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
@@ -2376,280 +2245,272 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
2376
2245
|
} catch {
|
|
2377
2246
|
}
|
|
2378
2247
|
}
|
|
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
|
-
|
|
2248
|
+
var import_chalk2, import_prompts, import_fs2, import_path4, import_os2, import_net, import_crypto2, 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;
|
|
2249
|
+
var init_core = __esm({
|
|
2250
|
+
"src/core.ts"() {
|
|
2251
|
+
"use strict";
|
|
2252
|
+
import_chalk2 = __toESM(require("chalk"));
|
|
2253
|
+
import_prompts = require("@inquirer/prompts");
|
|
2254
|
+
import_fs2 = __toESM(require("fs"));
|
|
2255
|
+
import_path4 = __toESM(require("path"));
|
|
2256
|
+
import_os2 = __toESM(require("os"));
|
|
2257
|
+
import_net = __toESM(require("net"));
|
|
2258
|
+
import_crypto2 = require("crypto");
|
|
2259
|
+
import_picomatch = __toESM(require("picomatch"));
|
|
2260
|
+
import_sh_syntax = require("sh-syntax");
|
|
2261
|
+
init_native();
|
|
2262
|
+
init_context_sniper();
|
|
2263
|
+
init_config_schema();
|
|
2264
|
+
init_shields();
|
|
2265
|
+
init_dlp();
|
|
2266
|
+
PAUSED_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
|
|
2267
|
+
TRUST_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "trust.json");
|
|
2268
|
+
LOCAL_AUDIT_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "audit.log");
|
|
2269
|
+
HOOK_DEBUG_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
|
|
2270
|
+
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2271
|
+
DANGEROUS_WORDS = [
|
|
2272
|
+
"mkfs",
|
|
2273
|
+
// formats/wipes a filesystem partition
|
|
2274
|
+
"shred"
|
|
2275
|
+
// permanently overwrites file contents (unrecoverable)
|
|
2276
|
+
];
|
|
2277
|
+
DEFAULT_CONFIG = {
|
|
2278
|
+
settings: {
|
|
2279
|
+
mode: "standard",
|
|
2280
|
+
autoStartDaemon: true,
|
|
2281
|
+
enableUndo: true,
|
|
2282
|
+
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
2283
|
+
enableHookLogDebug: false,
|
|
2284
|
+
approvalTimeoutMs: 0,
|
|
2285
|
+
// 0 = disabled; set e.g. 30000 for 30-second auto-deny
|
|
2286
|
+
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
2287
|
+
},
|
|
2288
|
+
policy: {
|
|
2289
|
+
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
2290
|
+
dangerousWords: DANGEROUS_WORDS,
|
|
2291
|
+
ignoredTools: [
|
|
2292
|
+
"list_*",
|
|
2293
|
+
"get_*",
|
|
2294
|
+
"read_*",
|
|
2295
|
+
"describe_*",
|
|
2296
|
+
"read",
|
|
2297
|
+
"glob",
|
|
2298
|
+
"grep",
|
|
2299
|
+
"ls",
|
|
2300
|
+
"notebookread",
|
|
2301
|
+
"notebookedit",
|
|
2302
|
+
"webfetch",
|
|
2303
|
+
"websearch",
|
|
2304
|
+
"exitplanmode",
|
|
2305
|
+
"askuserquestion",
|
|
2306
|
+
"agent",
|
|
2307
|
+
"task*",
|
|
2308
|
+
"toolsearch",
|
|
2309
|
+
"mcp__ide__*",
|
|
2310
|
+
"getDiagnostics"
|
|
2311
|
+
],
|
|
2312
|
+
toolInspection: {
|
|
2313
|
+
bash: "command",
|
|
2314
|
+
shell: "command",
|
|
2315
|
+
run_shell_command: "command",
|
|
2316
|
+
"terminal.execute": "command",
|
|
2317
|
+
"postgres:query": "sql"
|
|
2318
|
+
},
|
|
2319
|
+
snapshot: {
|
|
2320
|
+
tools: [
|
|
2321
|
+
"str_replace_based_edit_tool",
|
|
2322
|
+
"write_file",
|
|
2323
|
+
"edit_file",
|
|
2324
|
+
"create_file",
|
|
2325
|
+
"edit",
|
|
2326
|
+
"replace"
|
|
2327
|
+
],
|
|
2328
|
+
onlyPaths: [],
|
|
2329
|
+
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
2330
|
+
},
|
|
2331
|
+
smartRules: [
|
|
2332
|
+
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
2333
|
+
{
|
|
2334
|
+
name: "block-rm-rf-home",
|
|
2335
|
+
tool: "bash",
|
|
2336
|
+
conditionMode: "all",
|
|
2337
|
+
conditions: [
|
|
2338
|
+
{
|
|
2339
|
+
field: "command",
|
|
2340
|
+
op: "matches",
|
|
2341
|
+
value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
field: "command",
|
|
2345
|
+
op: "matches",
|
|
2346
|
+
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
2347
|
+
}
|
|
2348
|
+
],
|
|
2349
|
+
verdict: "block",
|
|
2350
|
+
reason: "Recursive delete of home directory is irreversible"
|
|
2351
|
+
},
|
|
2352
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
2353
|
+
{
|
|
2354
|
+
name: "no-delete-without-where",
|
|
2355
|
+
tool: "*",
|
|
2356
|
+
conditions: [
|
|
2357
|
+
{ field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
|
|
2358
|
+
{ field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
|
|
2359
|
+
],
|
|
2360
|
+
conditionMode: "all",
|
|
2361
|
+
verdict: "review",
|
|
2362
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
2363
|
+
},
|
|
2364
|
+
{
|
|
2365
|
+
name: "review-drop-truncate-shell",
|
|
2366
|
+
tool: "bash",
|
|
2367
|
+
conditions: [
|
|
2368
|
+
{
|
|
2369
|
+
field: "command",
|
|
2370
|
+
op: "matches",
|
|
2371
|
+
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
2372
|
+
flags: "i"
|
|
2373
|
+
}
|
|
2374
|
+
],
|
|
2375
|
+
conditionMode: "all",
|
|
2376
|
+
verdict: "review",
|
|
2377
|
+
reason: "SQL DDL destructive statement inside a shell command"
|
|
2378
|
+
},
|
|
2379
|
+
// ── Git safety ────────────────────────────────────────────────────────
|
|
2380
|
+
{
|
|
2381
|
+
name: "block-force-push",
|
|
2382
|
+
tool: "bash",
|
|
2383
|
+
conditions: [
|
|
2384
|
+
{
|
|
2385
|
+
field: "command",
|
|
2386
|
+
op: "matches",
|
|
2387
|
+
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
2388
|
+
flags: "i"
|
|
2389
|
+
}
|
|
2390
|
+
],
|
|
2391
|
+
conditionMode: "all",
|
|
2392
|
+
verdict: "block",
|
|
2393
|
+
reason: "Force push overwrites remote history and cannot be undone"
|
|
2394
|
+
},
|
|
2395
|
+
{
|
|
2396
|
+
name: "review-git-push",
|
|
2397
|
+
tool: "bash",
|
|
2398
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
|
|
2399
|
+
conditionMode: "all",
|
|
2400
|
+
verdict: "review",
|
|
2401
|
+
reason: "git push sends changes to a shared remote"
|
|
2402
|
+
},
|
|
2403
|
+
{
|
|
2404
|
+
name: "review-git-destructive",
|
|
2405
|
+
tool: "bash",
|
|
2406
|
+
conditions: [
|
|
2407
|
+
{
|
|
2408
|
+
field: "command",
|
|
2409
|
+
op: "matches",
|
|
2410
|
+
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
2411
|
+
flags: "i"
|
|
2412
|
+
}
|
|
2413
|
+
],
|
|
2414
|
+
conditionMode: "all",
|
|
2415
|
+
verdict: "review",
|
|
2416
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
2417
|
+
},
|
|
2418
|
+
// ── Shell safety ──────────────────────────────────────────────────────
|
|
2419
|
+
{
|
|
2420
|
+
name: "review-sudo",
|
|
2421
|
+
tool: "bash",
|
|
2422
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
2423
|
+
conditionMode: "all",
|
|
2424
|
+
verdict: "review",
|
|
2425
|
+
reason: "Command requires elevated privileges"
|
|
2426
|
+
},
|
|
2427
|
+
{
|
|
2428
|
+
name: "review-curl-pipe-shell",
|
|
2429
|
+
tool: "bash",
|
|
2430
|
+
conditions: [
|
|
2431
|
+
{
|
|
2432
|
+
field: "command",
|
|
2433
|
+
op: "matches",
|
|
2434
|
+
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
2435
|
+
flags: "i"
|
|
2436
|
+
}
|
|
2437
|
+
],
|
|
2438
|
+
conditionMode: "all",
|
|
2439
|
+
verdict: "block",
|
|
2440
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
2441
|
+
}
|
|
2442
|
+
],
|
|
2443
|
+
dlp: { enabled: true, scanIgnoredTools: true }
|
|
2444
|
+
},
|
|
2445
|
+
environments: {}
|
|
2446
|
+
};
|
|
2447
|
+
ADVISORY_SMART_RULES = [
|
|
2448
|
+
{
|
|
2449
|
+
name: "allow-rm-safe-paths",
|
|
2450
|
+
tool: "*",
|
|
2451
|
+
conditionMode: "all",
|
|
2452
|
+
conditions: [
|
|
2453
|
+
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
2454
|
+
{
|
|
2455
|
+
field: "command",
|
|
2456
|
+
op: "matches",
|
|
2457
|
+
// Matches known-safe build artifact paths in the command.
|
|
2458
|
+
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
2459
|
+
}
|
|
2460
|
+
],
|
|
2461
|
+
verdict: "allow",
|
|
2462
|
+
reason: "Deleting a known-safe build artifact path"
|
|
2463
|
+
},
|
|
2464
|
+
{
|
|
2465
|
+
name: "review-rm",
|
|
2466
|
+
tool: "*",
|
|
2467
|
+
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
2468
|
+
verdict: "review",
|
|
2469
|
+
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
2465
2470
|
}
|
|
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 });
|
|
2471
|
+
];
|
|
2472
|
+
cachedConfig = null;
|
|
2473
|
+
DAEMON_PORT = 7391;
|
|
2474
|
+
DAEMON_HOST = "127.0.0.1";
|
|
2475
|
+
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path4.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
|
|
2536
2476
|
}
|
|
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;
|
|
2477
|
+
});
|
|
2478
|
+
|
|
2479
|
+
// src/daemon/ui.html
|
|
2480
|
+
var ui_default;
|
|
2481
|
+
var init_ui = __esm({
|
|
2482
|
+
"src/daemon/ui.html"() {
|
|
2483
|
+
ui_default = `<!doctype html>
|
|
2484
|
+
<html lang="en">
|
|
2485
|
+
<head>
|
|
2486
|
+
<meta charset="UTF-8" />
|
|
2487
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2488
|
+
<title>Node9 Security Guard</title>
|
|
2489
|
+
<style>
|
|
2490
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Fira+Code:wght@400;500&display=swap');
|
|
2491
|
+
:root {
|
|
2492
|
+
--bg: #0a0c10;
|
|
2493
|
+
--card: #1c2128;
|
|
2494
|
+
--panel: #161b22;
|
|
2495
|
+
--border: #30363d;
|
|
2496
|
+
--text: #adbac7;
|
|
2497
|
+
--text-bright: #cdd9e5;
|
|
2498
|
+
--muted: #768390;
|
|
2499
|
+
--primary: #f0883e;
|
|
2500
|
+
--success: #347d39;
|
|
2501
|
+
--danger: #c93c37;
|
|
2502
|
+
--accent: #539bf5;
|
|
2647
2503
|
}
|
|
2648
2504
|
* {
|
|
2649
2505
|
box-sizing: border-box;
|
|
2650
2506
|
margin: 0;
|
|
2651
2507
|
padding: 0;
|
|
2652
2508
|
}
|
|
2509
|
+
html,
|
|
2510
|
+
body {
|
|
2511
|
+
height: 100%;
|
|
2512
|
+
overflow: hidden;
|
|
2513
|
+
}
|
|
2653
2514
|
body {
|
|
2654
2515
|
background: var(--bg);
|
|
2655
2516
|
color: var(--text);
|
|
@@ -2657,16 +2518,17 @@ var ui_default = `<!doctype html>
|
|
|
2657
2518
|
'Inter',
|
|
2658
2519
|
-apple-system,
|
|
2659
2520
|
sans-serif;
|
|
2660
|
-
min-height: 100vh;
|
|
2661
2521
|
}
|
|
2662
2522
|
|
|
2663
2523
|
.shell {
|
|
2664
|
-
max-width:
|
|
2524
|
+
max-width: 1440px;
|
|
2525
|
+
height: 100vh;
|
|
2665
2526
|
margin: 0 auto;
|
|
2666
|
-
padding:
|
|
2527
|
+
padding: 16px 20px 16px;
|
|
2667
2528
|
display: grid;
|
|
2668
2529
|
grid-template-rows: auto 1fr;
|
|
2669
|
-
gap:
|
|
2530
|
+
gap: 16px;
|
|
2531
|
+
overflow: hidden;
|
|
2670
2532
|
}
|
|
2671
2533
|
header {
|
|
2672
2534
|
display: flex;
|
|
@@ -2703,9 +2565,10 @@ var ui_default = `<!doctype html>
|
|
|
2703
2565
|
|
|
2704
2566
|
.body {
|
|
2705
2567
|
display: grid;
|
|
2706
|
-
grid-template-columns: 1fr
|
|
2707
|
-
gap:
|
|
2708
|
-
|
|
2568
|
+
grid-template-columns: 360px 1fr 270px;
|
|
2569
|
+
gap: 16px;
|
|
2570
|
+
min-height: 0;
|
|
2571
|
+
overflow: hidden;
|
|
2709
2572
|
}
|
|
2710
2573
|
|
|
2711
2574
|
.warning-banner {
|
|
@@ -2725,6 +2588,10 @@ var ui_default = `<!doctype html>
|
|
|
2725
2588
|
|
|
2726
2589
|
.main {
|
|
2727
2590
|
min-width: 0;
|
|
2591
|
+
min-height: 0;
|
|
2592
|
+
overflow-y: auto;
|
|
2593
|
+
scrollbar-width: thin;
|
|
2594
|
+
scrollbar-color: var(--border) transparent;
|
|
2728
2595
|
}
|
|
2729
2596
|
.section-title {
|
|
2730
2597
|
font-size: 11px;
|
|
@@ -2755,14 +2622,64 @@ var ui_default = `<!doctype html>
|
|
|
2755
2622
|
background: var(--card);
|
|
2756
2623
|
border: 1px solid var(--border);
|
|
2757
2624
|
border-radius: 14px;
|
|
2758
|
-
padding:
|
|
2759
|
-
margin-bottom:
|
|
2625
|
+
padding: 20px;
|
|
2626
|
+
margin-bottom: 14px;
|
|
2760
2627
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
2761
2628
|
animation: pop 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
2762
2629
|
}
|
|
2763
2630
|
.card.slack-viewer {
|
|
2764
2631
|
border-color: rgba(83, 155, 245, 0.3);
|
|
2765
2632
|
}
|
|
2633
|
+
.card-header {
|
|
2634
|
+
display: flex;
|
|
2635
|
+
align-items: center;
|
|
2636
|
+
gap: 8px;
|
|
2637
|
+
margin-bottom: 12px;
|
|
2638
|
+
padding-bottom: 12px;
|
|
2639
|
+
border-bottom: 1px solid var(--border);
|
|
2640
|
+
}
|
|
2641
|
+
.card-header-icon {
|
|
2642
|
+
font-size: 16px;
|
|
2643
|
+
}
|
|
2644
|
+
.card-header-title {
|
|
2645
|
+
font-size: 12px;
|
|
2646
|
+
font-weight: 700;
|
|
2647
|
+
color: var(--text-bright);
|
|
2648
|
+
text-transform: uppercase;
|
|
2649
|
+
letter-spacing: 0.5px;
|
|
2650
|
+
}
|
|
2651
|
+
.card-timer {
|
|
2652
|
+
margin-left: auto;
|
|
2653
|
+
font-size: 11px;
|
|
2654
|
+
font-family: 'Fira Code', monospace;
|
|
2655
|
+
color: var(--muted);
|
|
2656
|
+
background: rgba(48, 54, 61, 0.6);
|
|
2657
|
+
padding: 2px 8px;
|
|
2658
|
+
border-radius: 5px;
|
|
2659
|
+
}
|
|
2660
|
+
.card-timer.urgent {
|
|
2661
|
+
color: var(--danger);
|
|
2662
|
+
background: rgba(201, 60, 55, 0.1);
|
|
2663
|
+
}
|
|
2664
|
+
.btn-allow {
|
|
2665
|
+
background: var(--success);
|
|
2666
|
+
color: #fff;
|
|
2667
|
+
grid-column: span 2;
|
|
2668
|
+
font-size: 14px;
|
|
2669
|
+
padding: 13px 14px;
|
|
2670
|
+
}
|
|
2671
|
+
.btn-deny {
|
|
2672
|
+
background: rgba(201, 60, 55, 0.15);
|
|
2673
|
+
color: #e5534b;
|
|
2674
|
+
border: 1px solid rgba(201, 60, 55, 0.3);
|
|
2675
|
+
grid-column: span 2;
|
|
2676
|
+
}
|
|
2677
|
+
.btn-deny:hover:not(:disabled) {
|
|
2678
|
+
background: var(--danger);
|
|
2679
|
+
color: #fff;
|
|
2680
|
+
border-color: transparent;
|
|
2681
|
+
filter: none;
|
|
2682
|
+
}
|
|
2766
2683
|
@keyframes pop {
|
|
2767
2684
|
from {
|
|
2768
2685
|
opacity: 0;
|
|
@@ -2970,24 +2887,178 @@ var ui_default = `<!doctype html>
|
|
|
2970
2887
|
cursor: not-allowed;
|
|
2971
2888
|
}
|
|
2972
2889
|
|
|
2890
|
+
.flight-col {
|
|
2891
|
+
display: flex;
|
|
2892
|
+
flex-direction: column;
|
|
2893
|
+
min-height: 0;
|
|
2894
|
+
overflow: hidden;
|
|
2895
|
+
}
|
|
2896
|
+
.flight-panel {
|
|
2897
|
+
flex: 1;
|
|
2898
|
+
min-height: 0;
|
|
2899
|
+
display: flex;
|
|
2900
|
+
flex-direction: column;
|
|
2901
|
+
overflow: hidden;
|
|
2902
|
+
}
|
|
2973
2903
|
.sidebar {
|
|
2974
2904
|
display: flex;
|
|
2975
2905
|
flex-direction: column;
|
|
2976
2906
|
gap: 12px;
|
|
2977
|
-
|
|
2978
|
-
|
|
2907
|
+
min-height: 0;
|
|
2908
|
+
overflow-y: auto;
|
|
2909
|
+
scrollbar-width: thin;
|
|
2910
|
+
scrollbar-color: var(--border) transparent;
|
|
2979
2911
|
}
|
|
2980
2912
|
.panel {
|
|
2981
2913
|
background: var(--panel);
|
|
2982
2914
|
border: 1px solid var(--border);
|
|
2983
2915
|
border-radius: 12px;
|
|
2984
|
-
padding:
|
|
2916
|
+
padding: 14px;
|
|
2985
2917
|
}
|
|
2918
|
+
/* \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 */
|
|
2919
|
+
#activity-feed {
|
|
2920
|
+
display: flex;
|
|
2921
|
+
flex-direction: column;
|
|
2922
|
+
gap: 4px;
|
|
2923
|
+
margin-top: 4px;
|
|
2924
|
+
flex: 1;
|
|
2925
|
+
min-height: 0;
|
|
2926
|
+
overflow-y: auto;
|
|
2927
|
+
scrollbar-width: thin;
|
|
2928
|
+
scrollbar-color: var(--border) transparent;
|
|
2929
|
+
}
|
|
2930
|
+
.feed-row {
|
|
2931
|
+
display: grid;
|
|
2932
|
+
grid-template-columns: 58px 20px 1fr 48px;
|
|
2933
|
+
align-items: start;
|
|
2934
|
+
gap: 6px;
|
|
2935
|
+
background: rgba(22, 27, 34, 0.6);
|
|
2936
|
+
border: 1px solid var(--border);
|
|
2937
|
+
padding: 7px 10px;
|
|
2938
|
+
border-radius: 7px;
|
|
2939
|
+
font-size: 11px;
|
|
2940
|
+
animation: frSlideIn 0.15s ease-out;
|
|
2941
|
+
transition: background 0.1s;
|
|
2942
|
+
cursor: default;
|
|
2943
|
+
}
|
|
2944
|
+
.feed-row:hover {
|
|
2945
|
+
background: rgba(30, 38, 48, 0.9);
|
|
2946
|
+
border-color: rgba(83, 155, 245, 0.2);
|
|
2947
|
+
}
|
|
2948
|
+
@keyframes frSlideIn {
|
|
2949
|
+
from {
|
|
2950
|
+
opacity: 0;
|
|
2951
|
+
transform: translateX(-4px);
|
|
2952
|
+
}
|
|
2953
|
+
to {
|
|
2954
|
+
opacity: 1;
|
|
2955
|
+
transform: none;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
.feed-ts {
|
|
2959
|
+
color: var(--muted);
|
|
2960
|
+
font-family: monospace;
|
|
2961
|
+
font-size: 9px;
|
|
2962
|
+
}
|
|
2963
|
+
.feed-icon {
|
|
2964
|
+
text-align: center;
|
|
2965
|
+
font-size: 13px;
|
|
2966
|
+
}
|
|
2967
|
+
.feed-content {
|
|
2968
|
+
min-width: 0;
|
|
2969
|
+
color: var(--text-bright);
|
|
2970
|
+
word-break: break-all;
|
|
2971
|
+
}
|
|
2972
|
+
.feed-args {
|
|
2973
|
+
display: block;
|
|
2974
|
+
color: var(--muted);
|
|
2975
|
+
font-family: monospace;
|
|
2976
|
+
margin-top: 2px;
|
|
2977
|
+
font-size: 10px;
|
|
2978
|
+
word-break: break-all;
|
|
2979
|
+
}
|
|
2980
|
+
.feed-badge {
|
|
2981
|
+
text-align: right;
|
|
2982
|
+
font-weight: 700;
|
|
2983
|
+
font-size: 9px;
|
|
2984
|
+
letter-spacing: 0.03em;
|
|
2985
|
+
}
|
|
2986
|
+
.fr-pending {
|
|
2987
|
+
color: var(--muted);
|
|
2988
|
+
}
|
|
2989
|
+
.fr-allow {
|
|
2990
|
+
color: #57ab5a;
|
|
2991
|
+
}
|
|
2992
|
+
.fr-block {
|
|
2993
|
+
color: var(--danger);
|
|
2994
|
+
}
|
|
2995
|
+
.fr-dlp {
|
|
2996
|
+
color: var(--primary);
|
|
2997
|
+
animation: frBlink 1s infinite;
|
|
2998
|
+
}
|
|
2999
|
+
@keyframes frBlink {
|
|
3000
|
+
50% {
|
|
3001
|
+
opacity: 0.4;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
.fr-dlp-row {
|
|
3005
|
+
border-color: var(--primary) !important;
|
|
3006
|
+
}
|
|
3007
|
+
.feed-clear-btn {
|
|
3008
|
+
background: transparent;
|
|
3009
|
+
border: none;
|
|
3010
|
+
color: var(--muted);
|
|
3011
|
+
font-size: 10px;
|
|
3012
|
+
padding: 0;
|
|
3013
|
+
cursor: pointer;
|
|
3014
|
+
margin-left: auto;
|
|
3015
|
+
font-family: inherit;
|
|
3016
|
+
font-weight: 500;
|
|
3017
|
+
transition: color 0.15s;
|
|
3018
|
+
}
|
|
3019
|
+
.feed-clear-btn:hover {
|
|
3020
|
+
color: var(--text);
|
|
3021
|
+
filter: none;
|
|
3022
|
+
transform: none;
|
|
3023
|
+
}
|
|
3024
|
+
/* \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 */
|
|
3025
|
+
.shield-row {
|
|
3026
|
+
display: flex;
|
|
3027
|
+
align-items: flex-start;
|
|
3028
|
+
gap: 10px;
|
|
3029
|
+
padding: 8px 0;
|
|
3030
|
+
border-bottom: 1px solid var(--border);
|
|
3031
|
+
}
|
|
3032
|
+
.shield-row:last-child {
|
|
3033
|
+
border-bottom: none;
|
|
3034
|
+
padding-bottom: 0;
|
|
3035
|
+
}
|
|
3036
|
+
.shield-row:first-child {
|
|
3037
|
+
padding-top: 0;
|
|
3038
|
+
}
|
|
3039
|
+
.shield-info {
|
|
3040
|
+
flex: 1;
|
|
3041
|
+
min-width: 0;
|
|
3042
|
+
}
|
|
3043
|
+
.shield-name {
|
|
3044
|
+
font-size: 12px;
|
|
3045
|
+
color: var(--text-bright);
|
|
3046
|
+
font-weight: 600;
|
|
3047
|
+
font-family: 'Fira Code', monospace;
|
|
3048
|
+
}
|
|
3049
|
+
.shield-desc {
|
|
3050
|
+
font-size: 10px;
|
|
3051
|
+
color: var(--muted);
|
|
3052
|
+
margin-top: 2px;
|
|
3053
|
+
line-height: 1.4;
|
|
3054
|
+
}
|
|
3055
|
+
|
|
2986
3056
|
.panel-title {
|
|
2987
3057
|
font-size: 12px;
|
|
2988
3058
|
font-weight: 700;
|
|
2989
3059
|
color: var(--text-bright);
|
|
2990
3060
|
margin-bottom: 12px;
|
|
3061
|
+
flex-shrink: 0;
|
|
2991
3062
|
display: flex;
|
|
2992
3063
|
align-items: center;
|
|
2993
3064
|
gap: 6px;
|
|
@@ -2995,8 +3066,8 @@ var ui_default = `<!doctype html>
|
|
|
2995
3066
|
.setting-row {
|
|
2996
3067
|
display: flex;
|
|
2997
3068
|
align-items: flex-start;
|
|
2998
|
-
gap:
|
|
2999
|
-
margin-bottom:
|
|
3069
|
+
gap: 10px;
|
|
3070
|
+
margin-bottom: 8px;
|
|
3000
3071
|
}
|
|
3001
3072
|
.setting-row:last-child {
|
|
3002
3073
|
margin-bottom: 0;
|
|
@@ -3005,20 +3076,21 @@ var ui_default = `<!doctype html>
|
|
|
3005
3076
|
flex: 1;
|
|
3006
3077
|
}
|
|
3007
3078
|
.setting-label {
|
|
3008
|
-
font-size:
|
|
3079
|
+
font-size: 11px;
|
|
3009
3080
|
color: var(--text-bright);
|
|
3010
|
-
margin-bottom:
|
|
3081
|
+
margin-bottom: 2px;
|
|
3082
|
+
font-weight: 600;
|
|
3011
3083
|
}
|
|
3012
3084
|
.setting-desc {
|
|
3013
|
-
font-size:
|
|
3085
|
+
font-size: 10px;
|
|
3014
3086
|
color: var(--muted);
|
|
3015
|
-
line-height: 1.
|
|
3087
|
+
line-height: 1.4;
|
|
3016
3088
|
}
|
|
3017
3089
|
.toggle {
|
|
3018
3090
|
position: relative;
|
|
3019
3091
|
display: inline-block;
|
|
3020
|
-
width:
|
|
3021
|
-
height:
|
|
3092
|
+
width: 34px;
|
|
3093
|
+
height: 19px;
|
|
3022
3094
|
flex-shrink: 0;
|
|
3023
3095
|
margin-top: 1px;
|
|
3024
3096
|
}
|
|
@@ -3038,8 +3110,8 @@ var ui_default = `<!doctype html>
|
|
|
3038
3110
|
.slider:before {
|
|
3039
3111
|
content: '';
|
|
3040
3112
|
position: absolute;
|
|
3041
|
-
width:
|
|
3042
|
-
height:
|
|
3113
|
+
width: 13px;
|
|
3114
|
+
height: 13px;
|
|
3043
3115
|
left: 3px;
|
|
3044
3116
|
bottom: 3px;
|
|
3045
3117
|
background: #fff;
|
|
@@ -3050,7 +3122,7 @@ var ui_default = `<!doctype html>
|
|
|
3050
3122
|
background: var(--success);
|
|
3051
3123
|
}
|
|
3052
3124
|
input:checked + .slider:before {
|
|
3053
|
-
transform: translateX(
|
|
3125
|
+
transform: translateX(15px);
|
|
3054
3126
|
}
|
|
3055
3127
|
input:disabled + .slider {
|
|
3056
3128
|
opacity: 0.4;
|
|
@@ -3209,12 +3281,17 @@ var ui_default = `<!doctype html>
|
|
|
3209
3281
|
border: 1px solid var(--border);
|
|
3210
3282
|
}
|
|
3211
3283
|
|
|
3212
|
-
@media (max-width:
|
|
3284
|
+
@media (max-width: 960px) {
|
|
3213
3285
|
.body {
|
|
3214
|
-
grid-template-columns: 1fr;
|
|
3286
|
+
grid-template-columns: 1fr 220px;
|
|
3215
3287
|
}
|
|
3216
|
-
.
|
|
3217
|
-
|
|
3288
|
+
.flight-col {
|
|
3289
|
+
display: none;
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
@media (max-width: 640px) {
|
|
3293
|
+
.body {
|
|
3294
|
+
grid-template-columns: 1fr;
|
|
3218
3295
|
}
|
|
3219
3296
|
}
|
|
3220
3297
|
</style>
|
|
@@ -3228,6 +3305,19 @@ var ui_default = `<!doctype html>
|
|
|
3228
3305
|
</header>
|
|
3229
3306
|
|
|
3230
3307
|
<div class="body">
|
|
3308
|
+
<div class="flight-col">
|
|
3309
|
+
<div class="panel flight-panel">
|
|
3310
|
+
<div class="panel-title">
|
|
3311
|
+
\u{1F6F0}\uFE0F Flight Recorder
|
|
3312
|
+
<span style="font-weight: 400; color: var(--muted); font-size: 11px">live</span>
|
|
3313
|
+
<button class="feed-clear-btn" onclick="clearFeed()">clear</button>
|
|
3314
|
+
</div>
|
|
3315
|
+
<div id="activity-feed">
|
|
3316
|
+
<span class="decisions-empty">Waiting for agent activity\u2026</span>
|
|
3317
|
+
</div>
|
|
3318
|
+
</div>
|
|
3319
|
+
</div>
|
|
3320
|
+
|
|
3231
3321
|
<div class="main">
|
|
3232
3322
|
<div id="warnBanner" class="warning-banner">
|
|
3233
3323
|
\u26A0\uFE0F Auto-start is off \u2014 daemon started manually. Run "node9 daemon stop" to stop it, or
|
|
@@ -3308,6 +3398,11 @@ var ui_default = `<!doctype html>
|
|
|
3308
3398
|
<div id="slackStatusLine" class="slack-status-line">No key saved</div>
|
|
3309
3399
|
</div>
|
|
3310
3400
|
|
|
3401
|
+
<div class="panel">
|
|
3402
|
+
<div class="panel-title">\u{1F6E1}\uFE0F Active Shields</div>
|
|
3403
|
+
<div id="shieldsList"><span class="decisions-empty">Loading\u2026</span></div>
|
|
3404
|
+
</div>
|
|
3405
|
+
|
|
3311
3406
|
<div class="panel">
|
|
3312
3407
|
<div class="panel-title">\u{1F4CB} Persistent Decisions</div>
|
|
3313
3408
|
<div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
|
|
@@ -3353,14 +3448,23 @@ var ui_default = `<!doctype html>
|
|
|
3353
3448
|
|
|
3354
3449
|
function updateDenyButton(id, timestamp) {
|
|
3355
3450
|
const btn = document.querySelector('#c-' + id + ' .btn-deny');
|
|
3451
|
+
const timer = document.querySelector('#timer-' + id);
|
|
3356
3452
|
if (!btn) return;
|
|
3357
3453
|
const elapsed = Date.now() - timestamp;
|
|
3358
3454
|
const remaining = Math.max(0, Math.ceil((autoDenyMs - elapsed) / 1000));
|
|
3359
3455
|
if (remaining <= 0) {
|
|
3360
|
-
btn.textContent = 'Auto-Denying
|
|
3456
|
+
btn.textContent = '\u23F3 Auto-Denying\u2026';
|
|
3361
3457
|
btn.disabled = true;
|
|
3458
|
+
if (timer) {
|
|
3459
|
+
timer.textContent = 'auto-deny';
|
|
3460
|
+
timer.className = 'card-timer urgent';
|
|
3461
|
+
}
|
|
3362
3462
|
} else {
|
|
3363
|
-
btn.textContent = 'Block Action
|
|
3463
|
+
btn.textContent = '\u{1F6AB} Block this Action';
|
|
3464
|
+
if (timer) {
|
|
3465
|
+
timer.textContent = remaining + 's';
|
|
3466
|
+
timer.className = 'card-timer' + (remaining < 15 ? ' urgent' : '');
|
|
3467
|
+
}
|
|
3364
3468
|
setTimeout(() => updateDenyButton(id, timestamp), 1000);
|
|
3365
3469
|
}
|
|
3366
3470
|
}
|
|
@@ -3376,34 +3480,61 @@ var ui_default = `<!doctype html>
|
|
|
3376
3480
|
empty.style.display = requests.size === 0 ? 'block' : 'none';
|
|
3377
3481
|
}
|
|
3378
3482
|
|
|
3379
|
-
function
|
|
3483
|
+
function setCardBusy(card, busy) {
|
|
3484
|
+
if (!card) return;
|
|
3485
|
+
card.querySelectorAll('button').forEach((b) => (b.disabled = busy));
|
|
3486
|
+
card.style.opacity = busy ? '0.5' : '1';
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
function showCardError(card, msg) {
|
|
3490
|
+
if (!card) return;
|
|
3491
|
+
card.style.outline = '2px solid #f87171';
|
|
3492
|
+
let err = card.querySelector('.card-error');
|
|
3493
|
+
if (!err) {
|
|
3494
|
+
err = document.createElement('p');
|
|
3495
|
+
err.className = 'card-error';
|
|
3496
|
+
err.style.cssText = 'color:#f87171;font-size:11px;margin:6px 0 0;';
|
|
3497
|
+
card.appendChild(err);
|
|
3498
|
+
}
|
|
3499
|
+
err.textContent = '\u26A0 ' + msg + ' \u2014 please try again or refresh.';
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
async function sendDecision(id, decision, persist) {
|
|
3380
3503
|
const card = document.getElementById('c-' + id);
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3504
|
+
setCardBusy(card, true);
|
|
3505
|
+
try {
|
|
3506
|
+
const res = await fetch('/decision/' + id, {
|
|
3507
|
+
method: 'POST',
|
|
3508
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3509
|
+
body: JSON.stringify({ decision, persist: !!persist }),
|
|
3510
|
+
});
|
|
3511
|
+
if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
|
|
3388
3512
|
card?.remove();
|
|
3389
3513
|
requests.delete(id);
|
|
3390
3514
|
refresh();
|
|
3391
|
-
}
|
|
3515
|
+
} catch (err) {
|
|
3516
|
+
setCardBusy(card, false);
|
|
3517
|
+
showCardError(card, err.message || 'Network error');
|
|
3518
|
+
}
|
|
3392
3519
|
}
|
|
3393
3520
|
|
|
3394
|
-
function sendTrust(id, duration) {
|
|
3521
|
+
async function sendTrust(id, duration) {
|
|
3395
3522
|
const card = document.getElementById('c-' + id);
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3523
|
+
setCardBusy(card, true);
|
|
3524
|
+
try {
|
|
3525
|
+
const res = await fetch('/decision/' + id, {
|
|
3526
|
+
method: 'POST',
|
|
3527
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3528
|
+
body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
|
|
3529
|
+
});
|
|
3530
|
+
if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
|
|
3403
3531
|
card?.remove();
|
|
3404
3532
|
requests.delete(id);
|
|
3405
3533
|
refresh();
|
|
3406
|
-
}
|
|
3534
|
+
} catch (err) {
|
|
3535
|
+
setCardBusy(card, false);
|
|
3536
|
+
showCardError(card, err.message || 'Network error');
|
|
3537
|
+
}
|
|
3407
3538
|
}
|
|
3408
3539
|
|
|
3409
3540
|
function renderPayload(req) {
|
|
@@ -3454,16 +3585,21 @@ var ui_default = `<!doctype html>
|
|
|
3454
3585
|
const mcpLabel = req.mcpServer ? esc(req.mcpServer) : null;
|
|
3455
3586
|
const dis = isSlack ? 'disabled' : '';
|
|
3456
3587
|
card.innerHTML = \`
|
|
3588
|
+
<div class="card-header">
|
|
3589
|
+
<span class="card-header-icon">\${isSlack ? '\u26A1' : '\u26A0\uFE0F'}</span>
|
|
3590
|
+
<span class="card-header-title">\${isSlack ? 'Awaiting Cloud Approval' : 'Action Required'}</span>
|
|
3591
|
+
<span class="card-timer" id="timer-\${req.id}">\${autoDenyMs > 0 ? Math.ceil(autoDenyMs / 1000) + 's' : ''}</span>
|
|
3592
|
+
</div>
|
|
3457
3593
|
<div class="source-row">
|
|
3458
3594
|
<span class="agent-badge">\${agentLabel}</span>
|
|
3459
3595
|
\${mcpLabel ? \`<span class="source-arrow">\u2192</span><span class="mcp-badge">mcp::\${mcpLabel}</span>\` : ''}
|
|
3460
3596
|
</div>
|
|
3461
3597
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
3462
|
-
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting
|
|
3598
|
+
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
|
|
3463
3599
|
\${renderPayload(req)}
|
|
3464
3600
|
<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}
|
|
3601
|
+
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
|
|
3602
|
+
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>\u{1F6AB} Block this Action</button>
|
|
3467
3603
|
<div class="trust-row\${trustEnabled ? ' show' : ''}" id="tr-\${req.id}">
|
|
3468
3604
|
<button class="btn-trust" onclick="sendTrust('\${req.id}','30m')" \${dis}>\u23F1 Trust 30m</button>
|
|
3469
3605
|
<button class="btn-trust" onclick="sendTrust('\${req.id}','1h')" \${dis}>\u23F1 Trust 1h</button>
|
|
@@ -3523,8 +3659,83 @@ var ui_default = `<!doctype html>
|
|
|
3523
3659
|
ev.addEventListener('slack-status', (e) => {
|
|
3524
3660
|
applySlackStatus(JSON.parse(e.data));
|
|
3525
3661
|
});
|
|
3526
|
-
|
|
3527
|
-
|
|
3662
|
+
ev.addEventListener('shields-status', (e) => {
|
|
3663
|
+
renderShields(JSON.parse(e.data).shields);
|
|
3664
|
+
});
|
|
3665
|
+
|
|
3666
|
+
// \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
|
|
3667
|
+
ev.addEventListener('activity', (e) => {
|
|
3668
|
+
const data = JSON.parse(e.data);
|
|
3669
|
+
const feed = document.getElementById('activity-feed');
|
|
3670
|
+
// Remove placeholder on first item
|
|
3671
|
+
const placeholder = feed.querySelector('.decisions-empty');
|
|
3672
|
+
if (placeholder) placeholder.remove();
|
|
3673
|
+
|
|
3674
|
+
const time = new Date(data.ts).toLocaleTimeString([], {
|
|
3675
|
+
hour12: false,
|
|
3676
|
+
hour: '2-digit',
|
|
3677
|
+
minute: '2-digit',
|
|
3678
|
+
second: '2-digit',
|
|
3679
|
+
});
|
|
3680
|
+
const icon = frIcon(data.tool);
|
|
3681
|
+
const argsStr = JSON.stringify(data.args ?? {});
|
|
3682
|
+
const argsPreview = esc(argsStr.length > 120 ? argsStr.slice(0, 120) + '\u2026' : argsStr);
|
|
3683
|
+
|
|
3684
|
+
const row = document.createElement('div');
|
|
3685
|
+
row.className = 'feed-row';
|
|
3686
|
+
row.id = 'fr-' + data.id;
|
|
3687
|
+
row.innerHTML = \`
|
|
3688
|
+
<span class="feed-ts">\${time}</span>
|
|
3689
|
+
<span class="feed-icon">\${icon}</span>
|
|
3690
|
+
<span class="feed-content"><strong>\${esc(data.tool)}</strong><span class="feed-args">\${argsPreview}</span></span>
|
|
3691
|
+
<span class="feed-badge fr-pending">\u25CF</span>
|
|
3692
|
+
\`;
|
|
3693
|
+
feed.prepend(row);
|
|
3694
|
+
if (feed.children.length > 100) feed.lastChild.remove();
|
|
3695
|
+
});
|
|
3696
|
+
|
|
3697
|
+
ev.addEventListener('activity-result', (e) => {
|
|
3698
|
+
const { id, status, label } = JSON.parse(e.data);
|
|
3699
|
+
const row = document.getElementById('fr-' + id);
|
|
3700
|
+
if (!row) return;
|
|
3701
|
+
const badge = row.querySelector('.feed-badge');
|
|
3702
|
+
if (status === 'allow') {
|
|
3703
|
+
badge.textContent = 'ALLOW';
|
|
3704
|
+
badge.className = 'feed-badge fr-allow';
|
|
3705
|
+
} else if (status === 'dlp') {
|
|
3706
|
+
badge.textContent = '\u{1F6E1}\uFE0F DLP';
|
|
3707
|
+
badge.className = 'feed-badge fr-dlp';
|
|
3708
|
+
row.classList.add('fr-dlp-row');
|
|
3709
|
+
} else {
|
|
3710
|
+
badge.textContent = 'BLOCK';
|
|
3711
|
+
badge.className = 'feed-badge fr-block';
|
|
3712
|
+
}
|
|
3713
|
+
});
|
|
3714
|
+
}
|
|
3715
|
+
connect();
|
|
3716
|
+
|
|
3717
|
+
const FR_ICONS = {
|
|
3718
|
+
bash: '\u{1F4BB}',
|
|
3719
|
+
read: '\u{1F4D6}',
|
|
3720
|
+
edit: '\u270F\uFE0F',
|
|
3721
|
+
write: '\u270F\uFE0F',
|
|
3722
|
+
glob: '\u{1F4C2}',
|
|
3723
|
+
grep: '\u{1F50D}',
|
|
3724
|
+
agent: '\u{1F916}',
|
|
3725
|
+
search: '\u{1F50D}',
|
|
3726
|
+
sql: '\u{1F5C4}\uFE0F',
|
|
3727
|
+
query: '\u{1F5C4}\uFE0F',
|
|
3728
|
+
list: '\u{1F4C2}',
|
|
3729
|
+
delete: '\u{1F5D1}\uFE0F',
|
|
3730
|
+
web: '\u{1F310}',
|
|
3731
|
+
};
|
|
3732
|
+
function frIcon(tool) {
|
|
3733
|
+
const t = (tool || '').toLowerCase();
|
|
3734
|
+
for (const [k, v] of Object.entries(FR_ICONS)) {
|
|
3735
|
+
if (t.includes(k)) return v;
|
|
3736
|
+
}
|
|
3737
|
+
return '\u{1F6E0}\uFE0F';
|
|
3738
|
+
}
|
|
3528
3739
|
|
|
3529
3740
|
function saveSetting(key, value) {
|
|
3530
3741
|
fetch('/settings', {
|
|
@@ -3615,6 +3826,49 @@ var ui_default = `<!doctype html>
|
|
|
3615
3826
|
}
|
|
3616
3827
|
}
|
|
3617
3828
|
|
|
3829
|
+
function clearFeed() {
|
|
3830
|
+
const feed = document.getElementById('activity-feed');
|
|
3831
|
+
feed.innerHTML = '<span class="decisions-empty">Feed cleared.</span>';
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
function renderShields(shields) {
|
|
3835
|
+
const list = document.getElementById('shieldsList');
|
|
3836
|
+
if (!shields || shields.length === 0) {
|
|
3837
|
+
list.innerHTML = '<span class="decisions-empty">No shields available.</span>';
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
list.innerHTML = shields
|
|
3841
|
+
.map(
|
|
3842
|
+
(s) => \`
|
|
3843
|
+
<div class="shield-row">
|
|
3844
|
+
<div class="shield-info">
|
|
3845
|
+
<div class="shield-name">\${esc(s.name)}</div>
|
|
3846
|
+
<div class="shield-desc">\${esc(s.description)}</div>
|
|
3847
|
+
</div>
|
|
3848
|
+
<label class="toggle">
|
|
3849
|
+
<input type="checkbox" \${s.active ? 'checked' : ''}
|
|
3850
|
+
onchange="toggleShield('\${esc(s.name)}', this.checked)" />
|
|
3851
|
+
<span class="slider"></span>
|
|
3852
|
+
</label>
|
|
3853
|
+
</div>
|
|
3854
|
+
\`
|
|
3855
|
+
)
|
|
3856
|
+
.join('');
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
function toggleShield(name, active) {
|
|
3860
|
+
fetch('/shields', {
|
|
3861
|
+
method: 'POST',
|
|
3862
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3863
|
+
body: JSON.stringify({ name, active }),
|
|
3864
|
+
}).catch(() => {});
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
fetch('/shields')
|
|
3868
|
+
.then((r) => r.json())
|
|
3869
|
+
.then(({ shields }) => renderShields(shields))
|
|
3870
|
+
.catch(() => {});
|
|
3871
|
+
|
|
3618
3872
|
function renderDecisions(decisions) {
|
|
3619
3873
|
const dl = document.getElementById('decisionsList');
|
|
3620
3874
|
const entries = Object.entries(decisions);
|
|
@@ -3661,31 +3915,24 @@ var ui_default = `<!doctype html>
|
|
|
3661
3915
|
</body>
|
|
3662
3916
|
</html>
|
|
3663
3917
|
`;
|
|
3918
|
+
}
|
|
3919
|
+
});
|
|
3664
3920
|
|
|
3665
3921
|
// src/daemon/ui.ts
|
|
3666
|
-
var UI_HTML_TEMPLATE
|
|
3922
|
+
var UI_HTML_TEMPLATE;
|
|
3923
|
+
var init_ui2 = __esm({
|
|
3924
|
+
"src/daemon/ui.ts"() {
|
|
3925
|
+
"use strict";
|
|
3926
|
+
init_ui();
|
|
3927
|
+
UI_HTML_TEMPLATE = ui_default;
|
|
3928
|
+
}
|
|
3929
|
+
});
|
|
3667
3930
|
|
|
3668
3931
|
// 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
3932
|
function atomicWriteSync2(filePath, data, options) {
|
|
3686
3933
|
const dir = import_path6.default.dirname(filePath);
|
|
3687
3934
|
if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
3688
|
-
const tmpPath = `${filePath}.${(0,
|
|
3935
|
+
const tmpPath = `${filePath}.${(0, import_crypto3.randomUUID)()}.tmp`;
|
|
3689
3936
|
import_fs4.default.writeFileSync(tmpPath, data, options);
|
|
3690
3937
|
import_fs4.default.renameSync(tmpPath, filePath);
|
|
3691
3938
|
}
|
|
@@ -3703,12 +3950,6 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
3703
3950
|
} catch {
|
|
3704
3951
|
}
|
|
3705
3952
|
}
|
|
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
3953
|
function redactArgs(value) {
|
|
3713
3954
|
if (!value || typeof value !== "object") return value;
|
|
3714
3955
|
if (Array.isArray(value)) return value.map(redactArgs);
|
|
@@ -3743,7 +3984,6 @@ function getAuditHistory(limit = 20) {
|
|
|
3743
3984
|
return [];
|
|
3744
3985
|
}
|
|
3745
3986
|
}
|
|
3746
|
-
var AUTO_DENY_MS = 12e4;
|
|
3747
3987
|
function getOrgName() {
|
|
3748
3988
|
try {
|
|
3749
3989
|
if (import_fs4.default.existsSync(CREDENTIALS_FILE)) {
|
|
@@ -3753,7 +3993,6 @@ function getOrgName() {
|
|
|
3753
3993
|
}
|
|
3754
3994
|
return null;
|
|
3755
3995
|
}
|
|
3756
|
-
var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
3757
3996
|
function hasStoredSlackKey() {
|
|
3758
3997
|
return import_fs4.default.existsSync(CREDENTIALS_FILE);
|
|
3759
3998
|
}
|
|
@@ -3769,11 +4008,6 @@ function writeGlobalSetting(key, value) {
|
|
|
3769
4008
|
config.settings[key] = value;
|
|
3770
4009
|
atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3771
4010
|
}
|
|
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
4011
|
function abandonPending() {
|
|
3778
4012
|
abandonTimer = null;
|
|
3779
4013
|
pending.forEach((entry, id) => {
|
|
@@ -3795,6 +4029,18 @@ function abandonPending() {
|
|
|
3795
4029
|
}
|
|
3796
4030
|
}
|
|
3797
4031
|
function broadcast(event, data) {
|
|
4032
|
+
if (event === "activity") {
|
|
4033
|
+
activityRing.push({ event, data });
|
|
4034
|
+
if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
|
|
4035
|
+
} else if (event === "activity-result") {
|
|
4036
|
+
const { id, status, label } = data;
|
|
4037
|
+
for (let i = activityRing.length - 1; i >= 0; i--) {
|
|
4038
|
+
if (activityRing[i].data.id === id) {
|
|
4039
|
+
Object.assign(activityRing[i].data, { status, label });
|
|
4040
|
+
break;
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
3798
4044
|
const msg = `event: ${event}
|
|
3799
4045
|
data: ${JSON.stringify(data)}
|
|
3800
4046
|
|
|
@@ -3840,13 +4086,15 @@ function writePersistentDecision(toolName, decision) {
|
|
|
3840
4086
|
}
|
|
3841
4087
|
}
|
|
3842
4088
|
function startDaemon() {
|
|
3843
|
-
const csrfToken = (0,
|
|
3844
|
-
const internalToken = (0,
|
|
4089
|
+
const csrfToken = (0, import_crypto3.randomUUID)();
|
|
4090
|
+
const internalToken = (0, import_crypto3.randomUUID)();
|
|
3845
4091
|
const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
|
|
3846
4092
|
const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
|
|
3847
4093
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
4094
|
+
const watchMode = process.env.NODE9_WATCH_MODE === "1";
|
|
3848
4095
|
let idleTimer;
|
|
3849
4096
|
function resetIdleTimer() {
|
|
4097
|
+
if (watchMode) return;
|
|
3850
4098
|
if (idleTimer) clearTimeout(idleTimer);
|
|
3851
4099
|
idleTimer = setTimeout(() => {
|
|
3852
4100
|
if (autoStarted) {
|
|
@@ -3901,6 +4149,12 @@ data: ${JSON.stringify({
|
|
|
3901
4149
|
data: ${JSON.stringify(readPersistentDecisions())}
|
|
3902
4150
|
|
|
3903
4151
|
`);
|
|
4152
|
+
for (const item of activityRing) {
|
|
4153
|
+
res.write(`event: ${item.event}
|
|
4154
|
+
data: ${JSON.stringify(item.data)}
|
|
4155
|
+
|
|
4156
|
+
`);
|
|
4157
|
+
}
|
|
3904
4158
|
return req.on("close", () => {
|
|
3905
4159
|
sseClients.delete(res);
|
|
3906
4160
|
if (sseClients.size === 0 && pending.size > 0) {
|
|
@@ -3920,9 +4174,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3920
4174
|
slackDelegated = false,
|
|
3921
4175
|
agent,
|
|
3922
4176
|
mcpServer,
|
|
3923
|
-
riskMetadata
|
|
4177
|
+
riskMetadata,
|
|
4178
|
+
fromCLI = false,
|
|
4179
|
+
activityId
|
|
3924
4180
|
} = JSON.parse(body);
|
|
3925
|
-
const id = (0,
|
|
4181
|
+
const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto3.randomUUID)();
|
|
3926
4182
|
const entry = {
|
|
3927
4183
|
id,
|
|
3928
4184
|
toolName,
|
|
@@ -3953,6 +4209,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3953
4209
|
}, AUTO_DENY_MS)
|
|
3954
4210
|
};
|
|
3955
4211
|
pending.set(id, entry);
|
|
4212
|
+
if (!fromCLI) {
|
|
4213
|
+
broadcast("activity", {
|
|
4214
|
+
id,
|
|
4215
|
+
ts: entry.timestamp,
|
|
4216
|
+
tool: toolName,
|
|
4217
|
+
args: redactArgs(args),
|
|
4218
|
+
status: "pending"
|
|
4219
|
+
});
|
|
4220
|
+
}
|
|
3956
4221
|
const browserEnabled = getConfig().settings.approvers?.browser !== false;
|
|
3957
4222
|
if (browserEnabled) {
|
|
3958
4223
|
broadcast("add", {
|
|
@@ -3982,6 +4247,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3982
4247
|
const e = pending.get(id);
|
|
3983
4248
|
if (!e) return;
|
|
3984
4249
|
if (result.noApprovalMechanism) return;
|
|
4250
|
+
broadcast("activity-result", {
|
|
4251
|
+
id,
|
|
4252
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
4253
|
+
label: result.blockedByLabel
|
|
4254
|
+
});
|
|
3985
4255
|
clearTimeout(e.timer);
|
|
3986
4256
|
const decision = result.approved ? "allow" : "deny";
|
|
3987
4257
|
appendAuditLog({ toolName: e.toolName, args: e.args, decision });
|
|
@@ -4016,8 +4286,8 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
4016
4286
|
const entry = pending.get(id);
|
|
4017
4287
|
if (!entry) return res.writeHead(404).end();
|
|
4018
4288
|
if (entry.earlyDecision) {
|
|
4289
|
+
clearTimeout(entry.timer);
|
|
4019
4290
|
pending.delete(id);
|
|
4020
|
-
broadcast("remove", { id });
|
|
4021
4291
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4022
4292
|
const body = { decision: entry.earlyDecision };
|
|
4023
4293
|
if (entry.earlyReason) body.reason = entry.earlyReason;
|
|
@@ -4047,10 +4317,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
4047
4317
|
decision: `trust:${trustDuration}`
|
|
4048
4318
|
});
|
|
4049
4319
|
clearTimeout(entry.timer);
|
|
4050
|
-
if (entry.waiter)
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4320
|
+
if (entry.waiter) {
|
|
4321
|
+
entry.waiter("allow");
|
|
4322
|
+
pending.delete(id);
|
|
4323
|
+
broadcast("remove", { id });
|
|
4324
|
+
} else {
|
|
4325
|
+
entry.earlyDecision = "allow";
|
|
4326
|
+
broadcast("remove", { id });
|
|
4327
|
+
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
4328
|
+
}
|
|
4054
4329
|
res.writeHead(200);
|
|
4055
4330
|
return res.end(JSON.stringify({ ok: true }));
|
|
4056
4331
|
}
|
|
@@ -4062,13 +4337,16 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
4062
4337
|
decision: resolvedDecision
|
|
4063
4338
|
});
|
|
4064
4339
|
clearTimeout(entry.timer);
|
|
4065
|
-
if (entry.waiter)
|
|
4066
|
-
|
|
4340
|
+
if (entry.waiter) {
|
|
4341
|
+
entry.waiter(resolvedDecision, reason);
|
|
4342
|
+
pending.delete(id);
|
|
4343
|
+
broadcast("remove", { id });
|
|
4344
|
+
} else {
|
|
4067
4345
|
entry.earlyDecision = resolvedDecision;
|
|
4068
4346
|
entry.earlyReason = reason;
|
|
4347
|
+
broadcast("remove", { id });
|
|
4348
|
+
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
4069
4349
|
}
|
|
4070
|
-
pending.delete(id);
|
|
4071
|
-
broadcast("remove", { id });
|
|
4072
4350
|
res.writeHead(200);
|
|
4073
4351
|
return res.end(JSON.stringify({ ok: true }));
|
|
4074
4352
|
} catch {
|
|
@@ -4121,116 +4399,683 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
4121
4399
|
res.writeHead(400).end();
|
|
4122
4400
|
}
|
|
4123
4401
|
}
|
|
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();
|
|
4402
|
+
if (req.method === "DELETE" && pathname.startsWith("/decisions/")) {
|
|
4403
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4404
|
+
try {
|
|
4405
|
+
const toolName = decodeURIComponent(pathname.split("/").pop());
|
|
4406
|
+
const decisions = readPersistentDecisions();
|
|
4407
|
+
delete decisions[toolName];
|
|
4408
|
+
atomicWriteSync2(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
|
|
4409
|
+
broadcast("decisions", decisions);
|
|
4410
|
+
res.writeHead(200);
|
|
4411
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4412
|
+
} catch {
|
|
4413
|
+
res.writeHead(400).end();
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
if (req.method === "POST" && pathname.startsWith("/resolve/")) {
|
|
4417
|
+
const internalAuth = req.headers["x-node9-internal"];
|
|
4418
|
+
if (internalAuth !== internalToken) return res.writeHead(403).end();
|
|
4419
|
+
try {
|
|
4420
|
+
const id = pathname.split("/").pop();
|
|
4421
|
+
const entry = pending.get(id);
|
|
4422
|
+
if (!entry) return res.writeHead(404).end();
|
|
4423
|
+
const { decision } = JSON.parse(await readBody(req));
|
|
4424
|
+
appendAuditLog({
|
|
4425
|
+
toolName: entry.toolName,
|
|
4426
|
+
args: entry.args,
|
|
4427
|
+
decision
|
|
4428
|
+
});
|
|
4429
|
+
clearTimeout(entry.timer);
|
|
4430
|
+
if (entry.waiter) entry.waiter(decision);
|
|
4431
|
+
else entry.earlyDecision = decision;
|
|
4432
|
+
pending.delete(id);
|
|
4433
|
+
broadcast("remove", { id });
|
|
4434
|
+
res.writeHead(200);
|
|
4435
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4436
|
+
} catch {
|
|
4437
|
+
res.writeHead(400).end();
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
if (req.method === "GET" && pathname === "/audit") {
|
|
4441
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4442
|
+
return res.end(JSON.stringify(getAuditHistory()));
|
|
4443
|
+
}
|
|
4444
|
+
if (req.method === "GET" && pathname === "/shields") {
|
|
4445
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4446
|
+
const active = readActiveShields();
|
|
4447
|
+
const shields = Object.values(SHIELDS).map((s) => ({
|
|
4448
|
+
name: s.name,
|
|
4449
|
+
description: s.description,
|
|
4450
|
+
active: active.includes(s.name)
|
|
4451
|
+
}));
|
|
4452
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4453
|
+
return res.end(JSON.stringify({ shields }));
|
|
4454
|
+
}
|
|
4455
|
+
if (req.method === "POST" && pathname === "/shields") {
|
|
4456
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4457
|
+
try {
|
|
4458
|
+
const { name, active } = JSON.parse(await readBody(req));
|
|
4459
|
+
if (!SHIELDS[name]) return res.writeHead(400).end();
|
|
4460
|
+
const current = readActiveShields();
|
|
4461
|
+
const updated = active ? [.../* @__PURE__ */ new Set([...current, name])] : current.filter((n) => n !== name);
|
|
4462
|
+
writeActiveShields(updated);
|
|
4463
|
+
_resetConfigCache();
|
|
4464
|
+
const shieldsPayload = Object.values(SHIELDS).map((s) => ({
|
|
4465
|
+
name: s.name,
|
|
4466
|
+
description: s.description,
|
|
4467
|
+
active: updated.includes(s.name)
|
|
4468
|
+
}));
|
|
4469
|
+
broadcast("shields-status", { shields: shieldsPayload });
|
|
4470
|
+
res.writeHead(200);
|
|
4471
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4472
|
+
} catch {
|
|
4473
|
+
res.writeHead(400).end();
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4476
|
+
res.writeHead(404).end();
|
|
4477
|
+
});
|
|
4478
|
+
daemonServer = server;
|
|
4479
|
+
server.on("error", (e) => {
|
|
4480
|
+
if (e.code === "EADDRINUSE") {
|
|
4481
|
+
try {
|
|
4482
|
+
if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
|
|
4483
|
+
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4484
|
+
process.kill(pid, 0);
|
|
4485
|
+
return process.exit(0);
|
|
4486
|
+
}
|
|
4487
|
+
} catch {
|
|
4488
|
+
try {
|
|
4489
|
+
import_fs4.default.unlinkSync(DAEMON_PID_FILE);
|
|
4490
|
+
} catch {
|
|
4491
|
+
}
|
|
4492
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
4493
|
+
return;
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
console.error(import_chalk4.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
|
|
4497
|
+
process.exit(1);
|
|
4498
|
+
});
|
|
4499
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
|
|
4500
|
+
atomicWriteSync2(
|
|
4501
|
+
DAEMON_PID_FILE,
|
|
4502
|
+
JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
|
|
4503
|
+
{ mode: 384 }
|
|
4504
|
+
);
|
|
4505
|
+
console.log(import_chalk4.default.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
|
|
4506
|
+
});
|
|
4507
|
+
if (watchMode) {
|
|
4508
|
+
console.log(import_chalk4.default.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
|
|
4509
|
+
}
|
|
4510
|
+
try {
|
|
4511
|
+
import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4512
|
+
} catch {
|
|
4513
|
+
}
|
|
4514
|
+
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
4515
|
+
const unixServer = import_net2.default.createServer((socket) => {
|
|
4516
|
+
const chunks = [];
|
|
4517
|
+
let bytesReceived = 0;
|
|
4518
|
+
socket.on("data", (chunk) => {
|
|
4519
|
+
bytesReceived += chunk.length;
|
|
4520
|
+
if (bytesReceived > ACTIVITY_MAX_BYTES) {
|
|
4521
|
+
socket.destroy();
|
|
4522
|
+
return;
|
|
4523
|
+
}
|
|
4524
|
+
chunks.push(chunk);
|
|
4525
|
+
});
|
|
4526
|
+
socket.on("end", () => {
|
|
4527
|
+
try {
|
|
4528
|
+
const data = JSON.parse(Buffer.concat(chunks).toString());
|
|
4529
|
+
if (data.status === "pending") {
|
|
4530
|
+
broadcast("activity", {
|
|
4531
|
+
id: data.id,
|
|
4532
|
+
ts: data.ts,
|
|
4533
|
+
tool: data.tool,
|
|
4534
|
+
args: redactArgs(data.args),
|
|
4535
|
+
status: "pending"
|
|
4536
|
+
});
|
|
4537
|
+
} else {
|
|
4538
|
+
broadcast("activity-result", {
|
|
4539
|
+
id: data.id,
|
|
4540
|
+
status: data.status,
|
|
4541
|
+
label: data.label
|
|
4542
|
+
});
|
|
4543
|
+
}
|
|
4544
|
+
} catch {
|
|
4545
|
+
}
|
|
4546
|
+
});
|
|
4547
|
+
socket.on("error", () => {
|
|
4548
|
+
});
|
|
4549
|
+
});
|
|
4550
|
+
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
4551
|
+
process.on("exit", () => {
|
|
4552
|
+
try {
|
|
4553
|
+
import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4554
|
+
} catch {
|
|
4555
|
+
}
|
|
4556
|
+
});
|
|
4557
|
+
}
|
|
4558
|
+
function stopDaemon() {
|
|
4559
|
+
if (!import_fs4.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
|
|
4560
|
+
try {
|
|
4561
|
+
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4562
|
+
process.kill(pid, "SIGTERM");
|
|
4563
|
+
console.log(import_chalk4.default.green("\u2705 Stopped."));
|
|
4564
|
+
} catch {
|
|
4565
|
+
console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
|
|
4566
|
+
} finally {
|
|
4567
|
+
try {
|
|
4568
|
+
import_fs4.default.unlinkSync(DAEMON_PID_FILE);
|
|
4569
|
+
} catch {
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
function daemonStatus() {
|
|
4574
|
+
if (!import_fs4.default.existsSync(DAEMON_PID_FILE))
|
|
4575
|
+
return console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
|
|
4576
|
+
try {
|
|
4577
|
+
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4578
|
+
process.kill(pid, 0);
|
|
4579
|
+
console.log(import_chalk4.default.green("Node9 daemon: running"));
|
|
4580
|
+
} catch {
|
|
4581
|
+
console.log(import_chalk4.default.yellow("Node9 daemon: not running (stale PID)"));
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
var import_http, import_net2, import_fs4, import_path6, import_os4, import_child_process2, 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;
|
|
4585
|
+
var init_daemon = __esm({
|
|
4586
|
+
"src/daemon/index.ts"() {
|
|
4587
|
+
"use strict";
|
|
4588
|
+
init_ui2();
|
|
4589
|
+
import_http = __toESM(require("http"));
|
|
4590
|
+
import_net2 = __toESM(require("net"));
|
|
4591
|
+
import_fs4 = __toESM(require("fs"));
|
|
4592
|
+
import_path6 = __toESM(require("path"));
|
|
4593
|
+
import_os4 = __toESM(require("os"));
|
|
4594
|
+
import_child_process2 = require("child_process");
|
|
4595
|
+
import_crypto3 = require("crypto");
|
|
4596
|
+
import_chalk4 = __toESM(require("chalk"));
|
|
4597
|
+
init_core();
|
|
4598
|
+
init_shields();
|
|
4599
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path6.default.join(import_os4.default.tmpdir(), "node9-activity.sock");
|
|
4600
|
+
DAEMON_PORT2 = 7391;
|
|
4601
|
+
DAEMON_HOST2 = "127.0.0.1";
|
|
4602
|
+
homeDir = import_os4.default.homedir();
|
|
4603
|
+
DAEMON_PID_FILE = import_path6.default.join(homeDir, ".node9", "daemon.pid");
|
|
4604
|
+
DECISIONS_FILE = import_path6.default.join(homeDir, ".node9", "decisions.json");
|
|
4605
|
+
GLOBAL_CONFIG_FILE = import_path6.default.join(homeDir, ".node9", "config.json");
|
|
4606
|
+
CREDENTIALS_FILE = import_path6.default.join(homeDir, ".node9", "credentials.json");
|
|
4607
|
+
AUDIT_LOG_FILE = import_path6.default.join(homeDir, ".node9", "audit.log");
|
|
4608
|
+
TRUST_FILE2 = import_path6.default.join(homeDir, ".node9", "trust.json");
|
|
4609
|
+
TRUST_DURATIONS = {
|
|
4610
|
+
"30m": 30 * 6e4,
|
|
4611
|
+
"1h": 60 * 6e4,
|
|
4612
|
+
"2h": 2 * 60 * 6e4
|
|
4613
|
+
};
|
|
4614
|
+
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
4615
|
+
AUTO_DENY_MS = 12e4;
|
|
4616
|
+
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
4617
|
+
pending = /* @__PURE__ */ new Map();
|
|
4618
|
+
sseClients = /* @__PURE__ */ new Set();
|
|
4619
|
+
abandonTimer = null;
|
|
4620
|
+
daemonServer = null;
|
|
4621
|
+
hadBrowserClient = false;
|
|
4622
|
+
ACTIVITY_RING_SIZE = 100;
|
|
4623
|
+
activityRing = [];
|
|
4624
|
+
}
|
|
4625
|
+
});
|
|
4626
|
+
|
|
4627
|
+
// src/tui/tail.ts
|
|
4628
|
+
var tail_exports = {};
|
|
4629
|
+
__export(tail_exports, {
|
|
4630
|
+
startTail: () => startTail
|
|
4631
|
+
});
|
|
4632
|
+
function getIcon(tool) {
|
|
4633
|
+
const t = tool.toLowerCase();
|
|
4634
|
+
for (const [k, v] of Object.entries(ICONS)) {
|
|
4635
|
+
if (t.includes(k)) return v;
|
|
4636
|
+
}
|
|
4637
|
+
return "\u{1F6E0}\uFE0F";
|
|
4638
|
+
}
|
|
4639
|
+
function formatBase(activity) {
|
|
4640
|
+
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
4641
|
+
const icon = getIcon(activity.tool);
|
|
4642
|
+
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
4643
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
4644
|
+
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
4645
|
+
return `${import_chalk5.default.gray(time)} ${icon} ${import_chalk5.default.white.bold(toolName)} ${import_chalk5.default.dim(argsPreview)}`;
|
|
4646
|
+
}
|
|
4647
|
+
function renderResult(activity, result) {
|
|
4648
|
+
const base = formatBase(activity);
|
|
4649
|
+
let status;
|
|
4650
|
+
if (result.status === "allow") {
|
|
4651
|
+
status = import_chalk5.default.green("\u2713 ALLOW");
|
|
4652
|
+
} else if (result.status === "dlp") {
|
|
4653
|
+
status = import_chalk5.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
4654
|
+
} else {
|
|
4655
|
+
status = import_chalk5.default.red("\u2717 BLOCK");
|
|
4656
|
+
}
|
|
4657
|
+
if (process.stdout.isTTY) {
|
|
4658
|
+
import_readline.default.clearLine(process.stdout, 0);
|
|
4659
|
+
import_readline.default.cursorTo(process.stdout, 0);
|
|
4660
|
+
}
|
|
4661
|
+
console.log(`${base} ${status}`);
|
|
4662
|
+
}
|
|
4663
|
+
function renderPending(activity) {
|
|
4664
|
+
if (!process.stdout.isTTY) return;
|
|
4665
|
+
process.stdout.write(`${formatBase(activity)} ${import_chalk5.default.yellow("\u25CF \u2026")}\r`);
|
|
4666
|
+
}
|
|
4667
|
+
async function ensureDaemon() {
|
|
4668
|
+
if (import_fs6.default.existsSync(PID_FILE)) {
|
|
4669
|
+
try {
|
|
4670
|
+
const { port } = JSON.parse(import_fs6.default.readFileSync(PID_FILE, "utf-8"));
|
|
4671
|
+
return port;
|
|
4672
|
+
} catch {
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
console.log(import_chalk5.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
4676
|
+
const child = (0, import_child_process4.spawn)(process.execPath, [process.argv[1], "daemon"], {
|
|
4677
|
+
detached: true,
|
|
4678
|
+
stdio: "ignore",
|
|
4679
|
+
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
4680
|
+
});
|
|
4681
|
+
child.unref();
|
|
4682
|
+
for (let i = 0; i < 20; i++) {
|
|
4683
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
4684
|
+
if (!import_fs6.default.existsSync(PID_FILE)) continue;
|
|
4685
|
+
try {
|
|
4686
|
+
const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
|
|
4687
|
+
signal: AbortSignal.timeout(500)
|
|
4688
|
+
});
|
|
4689
|
+
if (res.ok) {
|
|
4690
|
+
const { port } = JSON.parse(import_fs6.default.readFileSync(PID_FILE, "utf-8"));
|
|
4691
|
+
return port;
|
|
4692
|
+
}
|
|
4693
|
+
} catch {
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
console.error(import_chalk5.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
4697
|
+
process.exit(1);
|
|
4698
|
+
}
|
|
4699
|
+
async function startTail(options = {}) {
|
|
4700
|
+
const port = await ensureDaemon();
|
|
4701
|
+
const connectionTime = Date.now();
|
|
4702
|
+
const pending2 = /* @__PURE__ */ new Map();
|
|
4703
|
+
console.log(import_chalk5.default.cyan.bold(`
|
|
4704
|
+
\u{1F6F0}\uFE0F Node9 tail `) + import_chalk5.default.dim(`\u2192 localhost:${port}`));
|
|
4705
|
+
if (options.history) {
|
|
4706
|
+
console.log(import_chalk5.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
4707
|
+
} else {
|
|
4708
|
+
console.log(
|
|
4709
|
+
import_chalk5.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
4710
|
+
);
|
|
4711
|
+
}
|
|
4712
|
+
process.on("SIGINT", () => {
|
|
4713
|
+
if (process.stdout.isTTY) {
|
|
4714
|
+
import_readline.default.clearLine(process.stdout, 0);
|
|
4715
|
+
import_readline.default.cursorTo(process.stdout, 0);
|
|
4716
|
+
}
|
|
4717
|
+
console.log(import_chalk5.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
4718
|
+
process.exit(0);
|
|
4719
|
+
});
|
|
4720
|
+
const req = import_http2.default.get(`http://127.0.0.1:${port}/events`, (res) => {
|
|
4721
|
+
if (res.statusCode !== 200) {
|
|
4722
|
+
console.error(import_chalk5.default.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
4723
|
+
process.exit(1);
|
|
4724
|
+
}
|
|
4725
|
+
let currentEvent = "";
|
|
4726
|
+
let currentData = "";
|
|
4727
|
+
res.on("error", () => {
|
|
4728
|
+
});
|
|
4729
|
+
const rl = import_readline.default.createInterface({ input: res, crlfDelay: Infinity });
|
|
4730
|
+
rl.on("error", () => {
|
|
4731
|
+
});
|
|
4732
|
+
rl.on("line", (line) => {
|
|
4733
|
+
if (line.startsWith("event:")) {
|
|
4734
|
+
currentEvent = line.slice(6).trim();
|
|
4735
|
+
} else if (line.startsWith("data:")) {
|
|
4736
|
+
currentData = line.slice(5).trim();
|
|
4737
|
+
} else if (line === "") {
|
|
4738
|
+
if (currentEvent && currentData) {
|
|
4739
|
+
handleMessage(currentEvent, currentData);
|
|
4740
|
+
}
|
|
4741
|
+
currentEvent = "";
|
|
4742
|
+
currentData = "";
|
|
4743
|
+
}
|
|
4744
|
+
});
|
|
4745
|
+
rl.on("close", () => {
|
|
4746
|
+
if (process.stdout.isTTY) {
|
|
4747
|
+
import_readline.default.clearLine(process.stdout, 0);
|
|
4748
|
+
import_readline.default.cursorTo(process.stdout, 0);
|
|
4749
|
+
}
|
|
4750
|
+
console.log(import_chalk5.default.red("\n\u274C Daemon disconnected."));
|
|
4751
|
+
process.exit(1);
|
|
4752
|
+
});
|
|
4753
|
+
});
|
|
4754
|
+
function handleMessage(event, rawData) {
|
|
4755
|
+
let data;
|
|
4756
|
+
try {
|
|
4757
|
+
data = JSON.parse(rawData);
|
|
4758
|
+
} catch {
|
|
4759
|
+
return;
|
|
4760
|
+
}
|
|
4761
|
+
if (event === "activity") {
|
|
4762
|
+
if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
|
|
4763
|
+
if (data.status && data.status !== "pending") {
|
|
4764
|
+
renderResult(data, data);
|
|
4765
|
+
return;
|
|
4766
|
+
}
|
|
4767
|
+
pending2.set(data.id, data);
|
|
4768
|
+
const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
|
|
4769
|
+
if (slowTool) renderPending(data);
|
|
4770
|
+
}
|
|
4771
|
+
if (event === "activity-result") {
|
|
4772
|
+
const original = pending2.get(data.id);
|
|
4773
|
+
if (original) {
|
|
4774
|
+
renderResult(original, data);
|
|
4775
|
+
pending2.delete(data.id);
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
req.on("error", (err) => {
|
|
4780
|
+
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
4781
|
+
console.error(import_chalk5.default.red(`
|
|
4782
|
+
\u274C ${msg}`));
|
|
4783
|
+
process.exit(1);
|
|
4784
|
+
});
|
|
4785
|
+
}
|
|
4786
|
+
var import_http2, import_chalk5, import_fs6, import_os6, import_path8, import_readline, import_child_process4, PID_FILE, ICONS;
|
|
4787
|
+
var init_tail = __esm({
|
|
4788
|
+
"src/tui/tail.ts"() {
|
|
4789
|
+
"use strict";
|
|
4790
|
+
import_http2 = __toESM(require("http"));
|
|
4791
|
+
import_chalk5 = __toESM(require("chalk"));
|
|
4792
|
+
import_fs6 = __toESM(require("fs"));
|
|
4793
|
+
import_os6 = __toESM(require("os"));
|
|
4794
|
+
import_path8 = __toESM(require("path"));
|
|
4795
|
+
import_readline = __toESM(require("readline"));
|
|
4796
|
+
import_child_process4 = require("child_process");
|
|
4797
|
+
init_daemon();
|
|
4798
|
+
PID_FILE = import_path8.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
|
|
4799
|
+
ICONS = {
|
|
4800
|
+
bash: "\u{1F4BB}",
|
|
4801
|
+
shell: "\u{1F4BB}",
|
|
4802
|
+
terminal: "\u{1F4BB}",
|
|
4803
|
+
read: "\u{1F4D6}",
|
|
4804
|
+
edit: "\u270F\uFE0F",
|
|
4805
|
+
write: "\u270F\uFE0F",
|
|
4806
|
+
glob: "\u{1F4C2}",
|
|
4807
|
+
grep: "\u{1F50D}",
|
|
4808
|
+
agent: "\u{1F916}",
|
|
4809
|
+
search: "\u{1F50D}",
|
|
4810
|
+
sql: "\u{1F5C4}\uFE0F",
|
|
4811
|
+
query: "\u{1F5C4}\uFE0F",
|
|
4812
|
+
list: "\u{1F4C2}",
|
|
4813
|
+
delete: "\u{1F5D1}\uFE0F",
|
|
4814
|
+
web: "\u{1F310}"
|
|
4815
|
+
};
|
|
4816
|
+
}
|
|
4817
|
+
});
|
|
4818
|
+
|
|
4819
|
+
// src/cli.ts
|
|
4820
|
+
var import_commander = require("commander");
|
|
4821
|
+
init_core();
|
|
4822
|
+
|
|
4823
|
+
// src/setup.ts
|
|
4824
|
+
var import_fs3 = __toESM(require("fs"));
|
|
4825
|
+
var import_path5 = __toESM(require("path"));
|
|
4826
|
+
var import_os3 = __toESM(require("os"));
|
|
4827
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
4828
|
+
var import_prompts2 = require("@inquirer/prompts");
|
|
4829
|
+
function printDaemonTip() {
|
|
4830
|
+
console.log(
|
|
4831
|
+
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")
|
|
4832
|
+
);
|
|
4833
|
+
}
|
|
4834
|
+
function fullPathCommand(subcommand) {
|
|
4835
|
+
if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
|
|
4836
|
+
const nodeExec = process.execPath;
|
|
4837
|
+
const cliScript = process.argv[1];
|
|
4838
|
+
return `${nodeExec} ${cliScript} ${subcommand}`;
|
|
4839
|
+
}
|
|
4840
|
+
function readJson(filePath) {
|
|
4841
|
+
try {
|
|
4842
|
+
if (import_fs3.default.existsSync(filePath)) {
|
|
4843
|
+
return JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
|
|
4844
|
+
}
|
|
4845
|
+
} catch {
|
|
4846
|
+
}
|
|
4847
|
+
return null;
|
|
4848
|
+
}
|
|
4849
|
+
function writeJson(filePath, data) {
|
|
4850
|
+
const dir = import_path5.default.dirname(filePath);
|
|
4851
|
+
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
4852
|
+
import_fs3.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
4853
|
+
}
|
|
4854
|
+
async function setupClaude() {
|
|
4855
|
+
const homeDir2 = import_os3.default.homedir();
|
|
4856
|
+
const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
|
|
4857
|
+
const hooksPath = import_path5.default.join(homeDir2, ".claude", "settings.json");
|
|
4858
|
+
const claudeConfig = readJson(mcpPath) ?? {};
|
|
4859
|
+
const settings = readJson(hooksPath) ?? {};
|
|
4860
|
+
const servers = claudeConfig.mcpServers ?? {};
|
|
4861
|
+
let anythingChanged = false;
|
|
4862
|
+
if (!settings.hooks) settings.hooks = {};
|
|
4863
|
+
const hasPreHook = settings.hooks.PreToolUse?.some(
|
|
4864
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
4865
|
+
);
|
|
4866
|
+
if (!hasPreHook) {
|
|
4867
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
4868
|
+
settings.hooks.PreToolUse.push({
|
|
4869
|
+
matcher: ".*",
|
|
4870
|
+
hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
|
|
4871
|
+
});
|
|
4872
|
+
console.log(import_chalk3.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
|
|
4873
|
+
anythingChanged = true;
|
|
4874
|
+
}
|
|
4875
|
+
const hasPostHook = settings.hooks.PostToolUse?.some(
|
|
4876
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
4877
|
+
);
|
|
4878
|
+
if (!hasPostHook) {
|
|
4879
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
4880
|
+
settings.hooks.PostToolUse.push({
|
|
4881
|
+
matcher: ".*",
|
|
4882
|
+
hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
|
|
4883
|
+
});
|
|
4884
|
+
console.log(import_chalk3.default.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
|
|
4885
|
+
anythingChanged = true;
|
|
4886
|
+
}
|
|
4887
|
+
if (anythingChanged) {
|
|
4888
|
+
writeJson(hooksPath, settings);
|
|
4889
|
+
console.log("");
|
|
4890
|
+
}
|
|
4891
|
+
const serversToWrap = [];
|
|
4892
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
4893
|
+
if (!server.command || server.command === "node9") continue;
|
|
4894
|
+
const parts = [server.command, ...server.args ?? []];
|
|
4895
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
4896
|
+
}
|
|
4897
|
+
if (serversToWrap.length > 0) {
|
|
4898
|
+
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
4899
|
+
console.log(import_chalk3.default.white(` ${mcpPath}`));
|
|
4900
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
4901
|
+
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
4902
|
+
}
|
|
4903
|
+
console.log("");
|
|
4904
|
+
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
4905
|
+
if (proceed) {
|
|
4906
|
+
for (const { name, parts } of serversToWrap) {
|
|
4907
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
4136
4908
|
}
|
|
4909
|
+
claudeConfig.mcpServers = servers;
|
|
4910
|
+
writeJson(mcpPath, claudeConfig);
|
|
4911
|
+
console.log(import_chalk3.default.green(`
|
|
4912
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
4913
|
+
anythingChanged = true;
|
|
4914
|
+
} else {
|
|
4915
|
+
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
4137
4916
|
}
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4917
|
+
console.log("");
|
|
4918
|
+
}
|
|
4919
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
4920
|
+
console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
4921
|
+
printDaemonTip();
|
|
4922
|
+
return;
|
|
4923
|
+
}
|
|
4924
|
+
if (anythingChanged) {
|
|
4925
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
4926
|
+
console.log(import_chalk3.default.gray(" Restart Claude Code for changes to take effect."));
|
|
4927
|
+
printDaemonTip();
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
async function setupGemini() {
|
|
4931
|
+
const homeDir2 = import_os3.default.homedir();
|
|
4932
|
+
const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
|
|
4933
|
+
const settings = readJson(settingsPath) ?? {};
|
|
4934
|
+
const servers = settings.mcpServers ?? {};
|
|
4935
|
+
let anythingChanged = false;
|
|
4936
|
+
if (!settings.hooks) settings.hooks = {};
|
|
4937
|
+
const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
|
|
4938
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
4939
|
+
);
|
|
4940
|
+
if (!hasBeforeHook) {
|
|
4941
|
+
if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
|
|
4942
|
+
if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
|
|
4943
|
+
settings.hooks.BeforeTool.push({
|
|
4944
|
+
matcher: ".*",
|
|
4945
|
+
hooks: [
|
|
4946
|
+
{
|
|
4947
|
+
name: "node9-check",
|
|
4948
|
+
type: "command",
|
|
4949
|
+
command: fullPathCommand("check"),
|
|
4950
|
+
timeout: 6e5
|
|
4951
|
+
}
|
|
4952
|
+
]
|
|
4953
|
+
});
|
|
4954
|
+
console.log(import_chalk3.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
4955
|
+
anythingChanged = true;
|
|
4956
|
+
}
|
|
4957
|
+
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
4958
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
4959
|
+
);
|
|
4960
|
+
if (!hasAfterHook) {
|
|
4961
|
+
if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
|
|
4962
|
+
if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
|
|
4963
|
+
settings.hooks.AfterTool.push({
|
|
4964
|
+
matcher: ".*",
|
|
4965
|
+
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
4966
|
+
});
|
|
4967
|
+
console.log(import_chalk3.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
4968
|
+
anythingChanged = true;
|
|
4969
|
+
}
|
|
4970
|
+
if (anythingChanged) {
|
|
4971
|
+
writeJson(settingsPath, 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(` ${settingsPath} (mcpServers)`));
|
|
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 };
|
|
4160
4991
|
}
|
|
4992
|
+
settings.mcpServers = servers;
|
|
4993
|
+
writeJson(settingsPath, settings);
|
|
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."));
|
|
4161
4999
|
}
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
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 Gemini CLI."));
|
|
5004
|
+
printDaemonTip();
|
|
5005
|
+
return;
|
|
5006
|
+
}
|
|
5007
|
+
if (anythingChanged) {
|
|
5008
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
5009
|
+
console.log(import_chalk3.default.gray(" Restart Gemini CLI for changes to take effect."));
|
|
5010
|
+
printDaemonTip();
|
|
5011
|
+
}
|
|
5012
|
+
}
|
|
5013
|
+
async function setupCursor() {
|
|
5014
|
+
const homeDir2 = import_os3.default.homedir();
|
|
5015
|
+
const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
|
|
5016
|
+
const mcpConfig = readJson(mcpPath) ?? {};
|
|
5017
|
+
const servers = mcpConfig.mcpServers ?? {};
|
|
5018
|
+
let anythingChanged = false;
|
|
5019
|
+
const serversToWrap = [];
|
|
5020
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
5021
|
+
if (!server.command || server.command === "node9") continue;
|
|
5022
|
+
const parts = [server.command, ...server.args ?? []];
|
|
5023
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
5024
|
+
}
|
|
5025
|
+
if (serversToWrap.length > 0) {
|
|
5026
|
+
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
5027
|
+
console.log(import_chalk3.default.white(` ${mcpPath}`));
|
|
5028
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
5029
|
+
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
4165
5030
|
}
|
|
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;
|
|
5031
|
+
console.log("");
|
|
5032
|
+
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
5033
|
+
if (proceed) {
|
|
5034
|
+
for (const { name, parts } of serversToWrap) {
|
|
5035
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
4184
5036
|
}
|
|
5037
|
+
mcpConfig.mcpServers = servers;
|
|
5038
|
+
writeJson(mcpPath, mcpConfig);
|
|
5039
|
+
console.log(import_chalk3.default.green(`
|
|
5040
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
5041
|
+
anythingChanged = true;
|
|
5042
|
+
} else {
|
|
5043
|
+
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
4185
5044
|
}
|
|
4186
|
-
console.
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
5045
|
+
console.log("");
|
|
5046
|
+
}
|
|
5047
|
+
console.log(
|
|
5048
|
+
import_chalk3.default.yellow(
|
|
5049
|
+
" \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
|
|
5050
|
+
)
|
|
5051
|
+
);
|
|
5052
|
+
console.log("");
|
|
5053
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
5054
|
+
console.log(
|
|
5055
|
+
import_chalk3.default.blue(
|
|
5056
|
+
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
|
|
5057
|
+
)
|
|
4194
5058
|
);
|
|
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
|
-
}
|
|
5059
|
+
printDaemonTip();
|
|
5060
|
+
return;
|
|
4211
5061
|
}
|
|
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)"));
|
|
5062
|
+
if (anythingChanged) {
|
|
5063
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
|
|
5064
|
+
console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
|
|
5065
|
+
printDaemonTip();
|
|
4222
5066
|
}
|
|
4223
5067
|
}
|
|
4224
5068
|
|
|
4225
5069
|
// src/cli.ts
|
|
4226
|
-
|
|
5070
|
+
init_daemon();
|
|
5071
|
+
var import_child_process5 = require("child_process");
|
|
4227
5072
|
var import_execa = require("execa");
|
|
4228
5073
|
var import_execa2 = require("execa");
|
|
4229
|
-
var
|
|
4230
|
-
var
|
|
4231
|
-
var
|
|
4232
|
-
var
|
|
4233
|
-
var
|
|
5074
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
5075
|
+
var import_readline2 = __toESM(require("readline"));
|
|
5076
|
+
var import_fs7 = __toESM(require("fs"));
|
|
5077
|
+
var import_path9 = __toESM(require("path"));
|
|
5078
|
+
var import_os7 = __toESM(require("os"));
|
|
4234
5079
|
|
|
4235
5080
|
// src/undo.ts
|
|
4236
5081
|
var import_child_process3 = require("child_process");
|
|
@@ -4344,9 +5189,10 @@ function applyUndo(hash, cwd) {
|
|
|
4344
5189
|
}
|
|
4345
5190
|
|
|
4346
5191
|
// src/cli.ts
|
|
5192
|
+
init_shields();
|
|
4347
5193
|
var import_prompts3 = require("@inquirer/prompts");
|
|
4348
5194
|
var { version } = JSON.parse(
|
|
4349
|
-
|
|
5195
|
+
import_fs7.default.readFileSync(import_path9.default.join(__dirname, "../package.json"), "utf-8")
|
|
4350
5196
|
);
|
|
4351
5197
|
function parseDuration(str) {
|
|
4352
5198
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -4434,15 +5280,15 @@ function openBrowserLocal() {
|
|
|
4434
5280
|
const url = `http://${DAEMON_HOST2}:${DAEMON_PORT2}/`;
|
|
4435
5281
|
try {
|
|
4436
5282
|
const opts = { stdio: "ignore" };
|
|
4437
|
-
if (process.platform === "darwin") (0,
|
|
4438
|
-
else if (process.platform === "win32") (0,
|
|
4439
|
-
else (0,
|
|
5283
|
+
if (process.platform === "darwin") (0, import_child_process5.execSync)(`open "${url}"`, opts);
|
|
5284
|
+
else if (process.platform === "win32") (0, import_child_process5.execSync)(`cmd /c start "" "${url}"`, opts);
|
|
5285
|
+
else (0, import_child_process5.execSync)(`xdg-open "${url}"`, opts);
|
|
4440
5286
|
} catch {
|
|
4441
5287
|
}
|
|
4442
5288
|
}
|
|
4443
5289
|
async function autoStartDaemonAndWait() {
|
|
4444
5290
|
try {
|
|
4445
|
-
const child = (0,
|
|
5291
|
+
const child = (0, import_child_process5.spawn)("node9", ["daemon"], {
|
|
4446
5292
|
detached: true,
|
|
4447
5293
|
stdio: "ignore",
|
|
4448
5294
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -4478,14 +5324,14 @@ async function runProxy(targetCommand) {
|
|
|
4478
5324
|
if (stdout) executable = stdout.trim();
|
|
4479
5325
|
} catch {
|
|
4480
5326
|
}
|
|
4481
|
-
console.log(
|
|
4482
|
-
const child = (0,
|
|
5327
|
+
console.log(import_chalk6.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
5328
|
+
const child = (0, import_child_process5.spawn)(executable, args, {
|
|
4483
5329
|
stdio: ["pipe", "pipe", "inherit"],
|
|
4484
5330
|
// We control STDIN and STDOUT
|
|
4485
5331
|
shell: false,
|
|
4486
5332
|
env: { ...process.env, FORCE_COLOR: "1" }
|
|
4487
5333
|
});
|
|
4488
|
-
const agentIn =
|
|
5334
|
+
const agentIn = import_readline2.default.createInterface({ input: process.stdin, terminal: false });
|
|
4489
5335
|
agentIn.on("line", async (line) => {
|
|
4490
5336
|
let message;
|
|
4491
5337
|
try {
|
|
@@ -4503,10 +5349,10 @@ async function runProxy(targetCommand) {
|
|
|
4503
5349
|
agent: "Proxy/MCP"
|
|
4504
5350
|
});
|
|
4505
5351
|
if (!result.approved) {
|
|
4506
|
-
console.error(
|
|
5352
|
+
console.error(import_chalk6.default.red(`
|
|
4507
5353
|
\u{1F6D1} Node9 Sudo: Action Blocked`));
|
|
4508
|
-
console.error(
|
|
4509
|
-
console.error(
|
|
5354
|
+
console.error(import_chalk6.default.gray(` Tool: ${name}`));
|
|
5355
|
+
console.error(import_chalk6.default.gray(` Reason: ${result.reason || "Security Policy"}
|
|
4510
5356
|
`));
|
|
4511
5357
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
4512
5358
|
const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -4548,14 +5394,14 @@ async function runProxy(targetCommand) {
|
|
|
4548
5394
|
}
|
|
4549
5395
|
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
5396
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
4551
|
-
const credPath =
|
|
4552
|
-
if (!
|
|
4553
|
-
|
|
5397
|
+
const credPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "credentials.json");
|
|
5398
|
+
if (!import_fs7.default.existsSync(import_path9.default.dirname(credPath)))
|
|
5399
|
+
import_fs7.default.mkdirSync(import_path9.default.dirname(credPath), { recursive: true });
|
|
4554
5400
|
const profileName = options.profile || "default";
|
|
4555
5401
|
let existingCreds = {};
|
|
4556
5402
|
try {
|
|
4557
|
-
if (
|
|
4558
|
-
const raw = JSON.parse(
|
|
5403
|
+
if (import_fs7.default.existsSync(credPath)) {
|
|
5404
|
+
const raw = JSON.parse(import_fs7.default.readFileSync(credPath, "utf-8"));
|
|
4559
5405
|
if (raw.apiKey) {
|
|
4560
5406
|
existingCreds = {
|
|
4561
5407
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -4567,13 +5413,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4567
5413
|
} catch {
|
|
4568
5414
|
}
|
|
4569
5415
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
4570
|
-
|
|
5416
|
+
import_fs7.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
4571
5417
|
if (profileName === "default") {
|
|
4572
|
-
const configPath =
|
|
5418
|
+
const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
|
|
4573
5419
|
let config = {};
|
|
4574
5420
|
try {
|
|
4575
|
-
if (
|
|
4576
|
-
config = JSON.parse(
|
|
5421
|
+
if (import_fs7.default.existsSync(configPath))
|
|
5422
|
+
config = JSON.parse(import_fs7.default.readFileSync(configPath, "utf-8"));
|
|
4577
5423
|
} catch {
|
|
4578
5424
|
}
|
|
4579
5425
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -4588,36 +5434,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4588
5434
|
approvers.cloud = false;
|
|
4589
5435
|
}
|
|
4590
5436
|
s.approvers = approvers;
|
|
4591
|
-
if (!
|
|
4592
|
-
|
|
4593
|
-
|
|
5437
|
+
if (!import_fs7.default.existsSync(import_path9.default.dirname(configPath)))
|
|
5438
|
+
import_fs7.default.mkdirSync(import_path9.default.dirname(configPath), { recursive: true });
|
|
5439
|
+
import_fs7.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
4594
5440
|
}
|
|
4595
5441
|
if (options.profile && profileName !== "default") {
|
|
4596
|
-
console.log(
|
|
4597
|
-
console.log(
|
|
5442
|
+
console.log(import_chalk6.default.green(`\u2705 Profile "${profileName}" saved`));
|
|
5443
|
+
console.log(import_chalk6.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
4598
5444
|
} else if (options.local) {
|
|
4599
|
-
console.log(
|
|
4600
|
-
console.log(
|
|
5445
|
+
console.log(import_chalk6.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
5446
|
+
console.log(import_chalk6.default.gray(` All decisions stay on this machine.`));
|
|
4601
5447
|
} else {
|
|
4602
|
-
console.log(
|
|
4603
|
-
console.log(
|
|
5448
|
+
console.log(import_chalk6.default.green(`\u2705 Logged in \u2014 agent mode`));
|
|
5449
|
+
console.log(import_chalk6.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
4604
5450
|
}
|
|
4605
5451
|
});
|
|
4606
5452
|
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
5453
|
if (target === "gemini") return await setupGemini();
|
|
4608
5454
|
if (target === "claude") return await setupClaude();
|
|
4609
5455
|
if (target === "cursor") return await setupCursor();
|
|
4610
|
-
console.error(
|
|
5456
|
+
console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
4611
5457
|
process.exit(1);
|
|
4612
5458
|
});
|
|
4613
5459
|
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
5460
|
if (!target) {
|
|
4615
|
-
console.log(
|
|
4616
|
-
console.log(" Usage: " +
|
|
5461
|
+
console.log(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
5462
|
+
console.log(" Usage: " + import_chalk6.default.white("node9 setup <target>") + "\n");
|
|
4617
5463
|
console.log(" Targets:");
|
|
4618
|
-
console.log(" " +
|
|
4619
|
-
console.log(" " +
|
|
4620
|
-
console.log(" " +
|
|
5464
|
+
console.log(" " + import_chalk6.default.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
5465
|
+
console.log(" " + import_chalk6.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
5466
|
+
console.log(" " + import_chalk6.default.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
4621
5467
|
console.log("");
|
|
4622
5468
|
return;
|
|
4623
5469
|
}
|
|
@@ -4625,33 +5471,33 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
4625
5471
|
if (t === "gemini") return await setupGemini();
|
|
4626
5472
|
if (t === "claude") return await setupClaude();
|
|
4627
5473
|
if (t === "cursor") return await setupCursor();
|
|
4628
|
-
console.error(
|
|
5474
|
+
console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
4629
5475
|
process.exit(1);
|
|
4630
5476
|
});
|
|
4631
5477
|
program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
4632
|
-
const homeDir2 =
|
|
5478
|
+
const homeDir2 = import_os7.default.homedir();
|
|
4633
5479
|
let failures = 0;
|
|
4634
5480
|
function pass(msg) {
|
|
4635
|
-
console.log(
|
|
5481
|
+
console.log(import_chalk6.default.green(" \u2705 ") + msg);
|
|
4636
5482
|
}
|
|
4637
5483
|
function fail(msg, hint) {
|
|
4638
|
-
console.log(
|
|
4639
|
-
if (hint) console.log(
|
|
5484
|
+
console.log(import_chalk6.default.red(" \u274C ") + msg);
|
|
5485
|
+
if (hint) console.log(import_chalk6.default.gray(" " + hint));
|
|
4640
5486
|
failures++;
|
|
4641
5487
|
}
|
|
4642
5488
|
function warn(msg, hint) {
|
|
4643
|
-
console.log(
|
|
4644
|
-
if (hint) console.log(
|
|
5489
|
+
console.log(import_chalk6.default.yellow(" \u26A0\uFE0F ") + msg);
|
|
5490
|
+
if (hint) console.log(import_chalk6.default.gray(" " + hint));
|
|
4645
5491
|
}
|
|
4646
5492
|
function section(title) {
|
|
4647
|
-
console.log("\n" +
|
|
5493
|
+
console.log("\n" + import_chalk6.default.bold(title));
|
|
4648
5494
|
}
|
|
4649
|
-
console.log(
|
|
5495
|
+
console.log(import_chalk6.default.cyan.bold(`
|
|
4650
5496
|
\u{1F6E1}\uFE0F Node9 Doctor v${version}
|
|
4651
5497
|
`));
|
|
4652
5498
|
section("Binary");
|
|
4653
5499
|
try {
|
|
4654
|
-
const which = (0,
|
|
5500
|
+
const which = (0, import_child_process5.execSync)("which node9", { encoding: "utf-8" }).trim();
|
|
4655
5501
|
pass(`node9 found at ${which}`);
|
|
4656
5502
|
} catch {
|
|
4657
5503
|
warn("node9 not found in $PATH \u2014 hooks may not find it", "Run: npm install -g @node9/proxy");
|
|
@@ -4666,7 +5512,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4666
5512
|
);
|
|
4667
5513
|
}
|
|
4668
5514
|
try {
|
|
4669
|
-
const gitVersion = (0,
|
|
5515
|
+
const gitVersion = (0, import_child_process5.execSync)("git --version", { encoding: "utf-8" }).trim();
|
|
4670
5516
|
pass(gitVersion);
|
|
4671
5517
|
} catch {
|
|
4672
5518
|
warn(
|
|
@@ -4675,10 +5521,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4675
5521
|
);
|
|
4676
5522
|
}
|
|
4677
5523
|
section("Configuration");
|
|
4678
|
-
const globalConfigPath =
|
|
4679
|
-
if (
|
|
5524
|
+
const globalConfigPath = import_path9.default.join(homeDir2, ".node9", "config.json");
|
|
5525
|
+
if (import_fs7.default.existsSync(globalConfigPath)) {
|
|
4680
5526
|
try {
|
|
4681
|
-
JSON.parse(
|
|
5527
|
+
JSON.parse(import_fs7.default.readFileSync(globalConfigPath, "utf-8"));
|
|
4682
5528
|
pass("~/.node9/config.json found and valid");
|
|
4683
5529
|
} catch {
|
|
4684
5530
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -4686,17 +5532,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4686
5532
|
} else {
|
|
4687
5533
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
4688
5534
|
}
|
|
4689
|
-
const projectConfigPath =
|
|
4690
|
-
if (
|
|
5535
|
+
const projectConfigPath = import_path9.default.join(process.cwd(), "node9.config.json");
|
|
5536
|
+
if (import_fs7.default.existsSync(projectConfigPath)) {
|
|
4691
5537
|
try {
|
|
4692
|
-
JSON.parse(
|
|
5538
|
+
JSON.parse(import_fs7.default.readFileSync(projectConfigPath, "utf-8"));
|
|
4693
5539
|
pass("node9.config.json found and valid (project)");
|
|
4694
5540
|
} catch {
|
|
4695
5541
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
4696
5542
|
}
|
|
4697
5543
|
}
|
|
4698
|
-
const credsPath =
|
|
4699
|
-
if (
|
|
5544
|
+
const credsPath = import_path9.default.join(homeDir2, ".node9", "credentials.json");
|
|
5545
|
+
if (import_fs7.default.existsSync(credsPath)) {
|
|
4700
5546
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
4701
5547
|
} else {
|
|
4702
5548
|
warn(
|
|
@@ -4705,10 +5551,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4705
5551
|
);
|
|
4706
5552
|
}
|
|
4707
5553
|
section("Agent Hooks");
|
|
4708
|
-
const claudeSettingsPath =
|
|
4709
|
-
if (
|
|
5554
|
+
const claudeSettingsPath = import_path9.default.join(homeDir2, ".claude", "settings.json");
|
|
5555
|
+
if (import_fs7.default.existsSync(claudeSettingsPath)) {
|
|
4710
5556
|
try {
|
|
4711
|
-
const cs = JSON.parse(
|
|
5557
|
+
const cs = JSON.parse(import_fs7.default.readFileSync(claudeSettingsPath, "utf-8"));
|
|
4712
5558
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
4713
5559
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4714
5560
|
);
|
|
@@ -4721,10 +5567,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4721
5567
|
} else {
|
|
4722
5568
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
4723
5569
|
}
|
|
4724
|
-
const geminiSettingsPath =
|
|
4725
|
-
if (
|
|
5570
|
+
const geminiSettingsPath = import_path9.default.join(homeDir2, ".gemini", "settings.json");
|
|
5571
|
+
if (import_fs7.default.existsSync(geminiSettingsPath)) {
|
|
4726
5572
|
try {
|
|
4727
|
-
const gs = JSON.parse(
|
|
5573
|
+
const gs = JSON.parse(import_fs7.default.readFileSync(geminiSettingsPath, "utf-8"));
|
|
4728
5574
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
4729
5575
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4730
5576
|
);
|
|
@@ -4737,10 +5583,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4737
5583
|
} else {
|
|
4738
5584
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
4739
5585
|
}
|
|
4740
|
-
const cursorHooksPath =
|
|
4741
|
-
if (
|
|
5586
|
+
const cursorHooksPath = import_path9.default.join(homeDir2, ".cursor", "hooks.json");
|
|
5587
|
+
if (import_fs7.default.existsSync(cursorHooksPath)) {
|
|
4742
5588
|
try {
|
|
4743
|
-
const cur = JSON.parse(
|
|
5589
|
+
const cur = JSON.parse(import_fs7.default.readFileSync(cursorHooksPath, "utf-8"));
|
|
4744
5590
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
4745
5591
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
4746
5592
|
);
|
|
@@ -4761,9 +5607,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4761
5607
|
}
|
|
4762
5608
|
console.log("");
|
|
4763
5609
|
if (failures === 0) {
|
|
4764
|
-
console.log(
|
|
5610
|
+
console.log(import_chalk6.default.green.bold(" All checks passed. Node9 is ready.\n"));
|
|
4765
5611
|
} else {
|
|
4766
|
-
console.log(
|
|
5612
|
+
console.log(import_chalk6.default.red.bold(` ${failures} check(s) failed. See hints above.
|
|
4767
5613
|
`));
|
|
4768
5614
|
process.exit(1);
|
|
4769
5615
|
}
|
|
@@ -4778,7 +5624,7 @@ program.command("explain").description(
|
|
|
4778
5624
|
try {
|
|
4779
5625
|
args = JSON.parse(trimmed);
|
|
4780
5626
|
} catch {
|
|
4781
|
-
console.error(
|
|
5627
|
+
console.error(import_chalk6.default.red(`
|
|
4782
5628
|
\u274C Invalid JSON: ${trimmed}
|
|
4783
5629
|
`));
|
|
4784
5630
|
process.exit(1);
|
|
@@ -4789,63 +5635,63 @@ program.command("explain").description(
|
|
|
4789
5635
|
}
|
|
4790
5636
|
const result = await explainPolicy(tool, args);
|
|
4791
5637
|
console.log("");
|
|
4792
|
-
console.log(
|
|
5638
|
+
console.log(import_chalk6.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
4793
5639
|
console.log("");
|
|
4794
|
-
console.log(` ${
|
|
5640
|
+
console.log(` ${import_chalk6.default.bold("Tool:")} ${import_chalk6.default.white(result.tool)}`);
|
|
4795
5641
|
if (argsRaw) {
|
|
4796
5642
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
4797
|
-
console.log(` ${
|
|
5643
|
+
console.log(` ${import_chalk6.default.bold("Input:")} ${import_chalk6.default.gray(preview)}`);
|
|
4798
5644
|
}
|
|
4799
5645
|
console.log("");
|
|
4800
|
-
console.log(
|
|
5646
|
+
console.log(import_chalk6.default.bold("Config Sources (Waterfall):"));
|
|
4801
5647
|
for (const tier of result.waterfall) {
|
|
4802
|
-
const num =
|
|
5648
|
+
const num = import_chalk6.default.gray(` ${tier.tier}.`);
|
|
4803
5649
|
const label = tier.label.padEnd(16);
|
|
4804
5650
|
let statusStr;
|
|
4805
5651
|
if (tier.tier === 1) {
|
|
4806
|
-
statusStr =
|
|
5652
|
+
statusStr = import_chalk6.default.gray(tier.note ?? "");
|
|
4807
5653
|
} else if (tier.status === "active") {
|
|
4808
|
-
const loc = tier.path ?
|
|
4809
|
-
const note = tier.note ?
|
|
4810
|
-
statusStr =
|
|
5654
|
+
const loc = tier.path ? import_chalk6.default.gray(tier.path) : "";
|
|
5655
|
+
const note = tier.note ? import_chalk6.default.gray(`(${tier.note})`) : "";
|
|
5656
|
+
statusStr = import_chalk6.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
4811
5657
|
} else {
|
|
4812
|
-
statusStr =
|
|
5658
|
+
statusStr = import_chalk6.default.gray("\u25CB " + (tier.note ?? "not found"));
|
|
4813
5659
|
}
|
|
4814
|
-
console.log(`${num} ${
|
|
5660
|
+
console.log(`${num} ${import_chalk6.default.white(label)} ${statusStr}`);
|
|
4815
5661
|
}
|
|
4816
5662
|
console.log("");
|
|
4817
|
-
console.log(
|
|
5663
|
+
console.log(import_chalk6.default.bold("Policy Evaluation:"));
|
|
4818
5664
|
for (const step of result.steps) {
|
|
4819
5665
|
const isFinal = step.isFinal;
|
|
4820
5666
|
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 =
|
|
5667
|
+
if (step.outcome === "allow") icon = import_chalk6.default.green(" \u2705");
|
|
5668
|
+
else if (step.outcome === "review") icon = import_chalk6.default.red(" \u{1F534}");
|
|
5669
|
+
else if (step.outcome === "skip") icon = import_chalk6.default.gray(" \u2500 ");
|
|
5670
|
+
else icon = import_chalk6.default.gray(" \u25CB ");
|
|
4825
5671
|
const name = step.name.padEnd(18);
|
|
4826
|
-
const nameStr = isFinal ?
|
|
4827
|
-
const detail = isFinal ?
|
|
4828
|
-
const arrow = isFinal ?
|
|
5672
|
+
const nameStr = isFinal ? import_chalk6.default.white.bold(name) : import_chalk6.default.white(name);
|
|
5673
|
+
const detail = isFinal ? import_chalk6.default.white(step.detail) : import_chalk6.default.gray(step.detail);
|
|
5674
|
+
const arrow = isFinal ? import_chalk6.default.yellow(" \u2190 STOP") : "";
|
|
4829
5675
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
4830
5676
|
}
|
|
4831
5677
|
console.log("");
|
|
4832
5678
|
if (result.decision === "allow") {
|
|
4833
|
-
console.log(
|
|
5679
|
+
console.log(import_chalk6.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk6.default.gray(" \u2014 no approval needed"));
|
|
4834
5680
|
} else {
|
|
4835
5681
|
console.log(
|
|
4836
|
-
|
|
5682
|
+
import_chalk6.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk6.default.gray(" \u2014 human approval required")
|
|
4837
5683
|
);
|
|
4838
5684
|
if (result.blockedByLabel) {
|
|
4839
|
-
console.log(
|
|
5685
|
+
console.log(import_chalk6.default.gray(` Reason: ${result.blockedByLabel}`));
|
|
4840
5686
|
}
|
|
4841
5687
|
}
|
|
4842
5688
|
console.log("");
|
|
4843
5689
|
});
|
|
4844
5690
|
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(
|
|
5691
|
+
const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
|
|
5692
|
+
if (import_fs7.default.existsSync(configPath) && !options.force) {
|
|
5693
|
+
console.log(import_chalk6.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
5694
|
+
console.log(import_chalk6.default.gray(` Run with --force to overwrite.`));
|
|
4849
5695
|
return;
|
|
4850
5696
|
}
|
|
4851
5697
|
const requestedMode = options.mode.toLowerCase();
|
|
@@ -4857,13 +5703,13 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
4857
5703
|
mode: safeMode
|
|
4858
5704
|
}
|
|
4859
5705
|
};
|
|
4860
|
-
const dir =
|
|
4861
|
-
if (!
|
|
4862
|
-
|
|
4863
|
-
console.log(
|
|
4864
|
-
console.log(
|
|
5706
|
+
const dir = import_path9.default.dirname(configPath);
|
|
5707
|
+
if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
|
|
5708
|
+
import_fs7.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
5709
|
+
console.log(import_chalk6.default.green(`\u2705 Global config created: ${configPath}`));
|
|
5710
|
+
console.log(import_chalk6.default.cyan(` Mode set to: ${safeMode}`));
|
|
4865
5711
|
console.log(
|
|
4866
|
-
|
|
5712
|
+
import_chalk6.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
4867
5713
|
);
|
|
4868
5714
|
});
|
|
4869
5715
|
function formatRelativeTime(timestamp) {
|
|
@@ -4877,14 +5723,14 @@ function formatRelativeTime(timestamp) {
|
|
|
4877
5723
|
return new Date(timestamp).toLocaleDateString();
|
|
4878
5724
|
}
|
|
4879
5725
|
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 (!
|
|
5726
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
|
|
5727
|
+
if (!import_fs7.default.existsSync(logPath)) {
|
|
4882
5728
|
console.log(
|
|
4883
|
-
|
|
5729
|
+
import_chalk6.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
4884
5730
|
);
|
|
4885
5731
|
return;
|
|
4886
5732
|
}
|
|
4887
|
-
const raw =
|
|
5733
|
+
const raw = import_fs7.default.readFileSync(logPath, "utf-8");
|
|
4888
5734
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
4889
5735
|
let entries = lines.flatMap((line) => {
|
|
4890
5736
|
try {
|
|
@@ -4906,31 +5752,31 @@ program.command("audit").description("View local execution audit log").option("-
|
|
|
4906
5752
|
return;
|
|
4907
5753
|
}
|
|
4908
5754
|
if (entries.length === 0) {
|
|
4909
|
-
console.log(
|
|
5755
|
+
console.log(import_chalk6.default.yellow("No matching audit entries."));
|
|
4910
5756
|
return;
|
|
4911
5757
|
}
|
|
4912
5758
|
console.log(
|
|
4913
5759
|
`
|
|
4914
|
-
${
|
|
5760
|
+
${import_chalk6.default.bold("Node9 Audit Log")} ${import_chalk6.default.dim(`(${entries.length} entries)`)}`
|
|
4915
5761
|
);
|
|
4916
|
-
console.log(
|
|
5762
|
+
console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
|
|
4917
5763
|
console.log(
|
|
4918
5764
|
` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
|
|
4919
5765
|
);
|
|
4920
|
-
console.log(
|
|
5766
|
+
console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
|
|
4921
5767
|
for (const e of entries) {
|
|
4922
5768
|
const time = formatRelativeTime(String(e.ts)).padEnd(12);
|
|
4923
5769
|
const tool = String(e.tool).slice(0, 17).padEnd(18);
|
|
4924
|
-
const result = e.decision === "allow" ?
|
|
5770
|
+
const result = e.decision === "allow" ? import_chalk6.default.green("ALLOW".padEnd(10)) : import_chalk6.default.red("DENY".padEnd(10));
|
|
4925
5771
|
const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
|
|
4926
5772
|
const agent = String(e.agent || "unknown");
|
|
4927
5773
|
console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
|
|
4928
5774
|
}
|
|
4929
5775
|
const allowed = entries.filter((e) => e.decision === "allow").length;
|
|
4930
5776
|
const denied = entries.filter((e) => e.decision === "deny").length;
|
|
4931
|
-
console.log(
|
|
5777
|
+
console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
|
|
4932
5778
|
console.log(
|
|
4933
|
-
` ${entries.length} entries | ${
|
|
5779
|
+
` ${entries.length} entries | ${import_chalk6.default.green(allowed + " allowed")} | ${import_chalk6.default.red(denied + " denied")}
|
|
4934
5780
|
`
|
|
4935
5781
|
);
|
|
4936
5782
|
});
|
|
@@ -4941,43 +5787,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4941
5787
|
const settings = mergedConfig.settings;
|
|
4942
5788
|
console.log("");
|
|
4943
5789
|
if (creds && settings.approvers.cloud) {
|
|
4944
|
-
console.log(
|
|
5790
|
+
console.log(import_chalk6.default.green(" \u25CF Agent mode") + import_chalk6.default.gray(" \u2014 cloud team policy enforced"));
|
|
4945
5791
|
} else if (creds && !settings.approvers.cloud) {
|
|
4946
5792
|
console.log(
|
|
4947
|
-
|
|
5793
|
+
import_chalk6.default.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + import_chalk6.default.gray(" \u2014 all decisions stay on this machine")
|
|
4948
5794
|
);
|
|
4949
5795
|
} else {
|
|
4950
5796
|
console.log(
|
|
4951
|
-
|
|
5797
|
+
import_chalk6.default.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + import_chalk6.default.gray(" \u2014 no API key (Local rules only)")
|
|
4952
5798
|
);
|
|
4953
5799
|
}
|
|
4954
5800
|
console.log("");
|
|
4955
5801
|
if (daemonRunning) {
|
|
4956
5802
|
console.log(
|
|
4957
|
-
|
|
5803
|
+
import_chalk6.default.green(" \u25CF Daemon running") + import_chalk6.default.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
|
|
4958
5804
|
);
|
|
4959
5805
|
} else {
|
|
4960
|
-
console.log(
|
|
5806
|
+
console.log(import_chalk6.default.gray(" \u25CB Daemon stopped"));
|
|
4961
5807
|
}
|
|
4962
5808
|
if (settings.enableUndo) {
|
|
4963
5809
|
console.log(
|
|
4964
|
-
|
|
5810
|
+
import_chalk6.default.magenta(" \u25CF Undo Engine") + import_chalk6.default.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
|
|
4965
5811
|
);
|
|
4966
5812
|
}
|
|
4967
5813
|
console.log("");
|
|
4968
|
-
const modeLabel = settings.mode === "audit" ?
|
|
5814
|
+
const modeLabel = settings.mode === "audit" ? import_chalk6.default.blue("audit") : settings.mode === "strict" ? import_chalk6.default.red("strict") : import_chalk6.default.white("standard");
|
|
4969
5815
|
console.log(` Mode: ${modeLabel}`);
|
|
4970
|
-
const projectConfig =
|
|
4971
|
-
const globalConfig =
|
|
5816
|
+
const projectConfig = import_path9.default.join(process.cwd(), "node9.config.json");
|
|
5817
|
+
const globalConfig = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
|
|
4972
5818
|
console.log(
|
|
4973
|
-
` Local: ${
|
|
5819
|
+
` Local: ${import_fs7.default.existsSync(projectConfig) ? import_chalk6.default.green("Active (node9.config.json)") : import_chalk6.default.gray("Not present")}`
|
|
4974
5820
|
);
|
|
4975
5821
|
console.log(
|
|
4976
|
-
` Global: ${
|
|
5822
|
+
` Global: ${import_fs7.default.existsSync(globalConfig) ? import_chalk6.default.green("Active (~/.node9/config.json)") : import_chalk6.default.gray("Not present")}`
|
|
4977
5823
|
);
|
|
4978
5824
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
4979
5825
|
console.log(
|
|
4980
|
-
` Sandbox: ${
|
|
5826
|
+
` Sandbox: ${import_chalk6.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
4981
5827
|
);
|
|
4982
5828
|
}
|
|
4983
5829
|
const pauseState = checkPause();
|
|
@@ -4985,47 +5831,63 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4985
5831
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
4986
5832
|
console.log("");
|
|
4987
5833
|
console.log(
|
|
4988
|
-
|
|
5834
|
+
import_chalk6.default.yellow(` \u23F8 PAUSED until ${expiresAt}`) + import_chalk6.default.gray(" \u2014 all tool calls allowed")
|
|
4989
5835
|
);
|
|
4990
5836
|
}
|
|
4991
5837
|
console.log("");
|
|
4992
5838
|
});
|
|
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").
|
|
5839
|
+
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(
|
|
5840
|
+
"-w, --watch",
|
|
5841
|
+
"Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
|
|
5842
|
+
).action(
|
|
4994
5843
|
async (action, options) => {
|
|
4995
5844
|
const cmd = (action ?? "start").toLowerCase();
|
|
4996
5845
|
if (cmd === "stop") return stopDaemon();
|
|
4997
5846
|
if (cmd === "status") return daemonStatus();
|
|
4998
5847
|
if (cmd !== "start" && action !== void 0) {
|
|
4999
|
-
console.error(
|
|
5848
|
+
console.error(import_chalk6.default.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
|
|
5000
5849
|
process.exit(1);
|
|
5001
5850
|
}
|
|
5851
|
+
if (options.watch) {
|
|
5852
|
+
process.env.NODE9_WATCH_MODE = "1";
|
|
5853
|
+
setTimeout(() => {
|
|
5854
|
+
openBrowserLocal();
|
|
5855
|
+
console.log(import_chalk6.default.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
5856
|
+
}, 600);
|
|
5857
|
+
startDaemon();
|
|
5858
|
+
return;
|
|
5859
|
+
}
|
|
5002
5860
|
if (options.openui) {
|
|
5003
5861
|
if (isDaemonRunning()) {
|
|
5004
5862
|
openBrowserLocal();
|
|
5005
|
-
console.log(
|
|
5863
|
+
console.log(import_chalk6.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
5006
5864
|
process.exit(0);
|
|
5007
5865
|
}
|
|
5008
|
-
const child = (0,
|
|
5866
|
+
const child = (0, import_child_process5.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
5009
5867
|
child.unref();
|
|
5010
5868
|
for (let i = 0; i < 12; i++) {
|
|
5011
5869
|
await new Promise((r) => setTimeout(r, 250));
|
|
5012
5870
|
if (isDaemonRunning()) break;
|
|
5013
5871
|
}
|
|
5014
5872
|
openBrowserLocal();
|
|
5015
|
-
console.log(
|
|
5873
|
+
console.log(import_chalk6.default.green(`
|
|
5016
5874
|
\u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
|
|
5017
5875
|
process.exit(0);
|
|
5018
5876
|
}
|
|
5019
5877
|
if (options.background) {
|
|
5020
|
-
const child = (0,
|
|
5878
|
+
const child = (0, import_child_process5.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
5021
5879
|
child.unref();
|
|
5022
|
-
console.log(
|
|
5880
|
+
console.log(import_chalk6.default.green(`
|
|
5023
5881
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
5024
5882
|
process.exit(0);
|
|
5025
5883
|
}
|
|
5026
5884
|
startDaemon();
|
|
5027
5885
|
}
|
|
5028
5886
|
);
|
|
5887
|
+
program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).action(async (options) => {
|
|
5888
|
+
const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
|
|
5889
|
+
await startTail2(options);
|
|
5890
|
+
});
|
|
5029
5891
|
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
5892
|
const processPayload = async (raw) => {
|
|
5031
5893
|
try {
|
|
@@ -5036,9 +5898,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
5036
5898
|
} catch (err) {
|
|
5037
5899
|
const tempConfig = getConfig();
|
|
5038
5900
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
5039
|
-
const logPath =
|
|
5901
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
|
|
5040
5902
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
5041
|
-
|
|
5903
|
+
import_fs7.default.appendFileSync(
|
|
5042
5904
|
logPath,
|
|
5043
5905
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
5044
5906
|
RAW: ${raw}
|
|
@@ -5056,10 +5918,10 @@ RAW: ${raw}
|
|
|
5056
5918
|
}
|
|
5057
5919
|
const config = getConfig();
|
|
5058
5920
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
5059
|
-
const logPath =
|
|
5060
|
-
if (!
|
|
5061
|
-
|
|
5062
|
-
|
|
5921
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
|
|
5922
|
+
if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
|
|
5923
|
+
import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
|
|
5924
|
+
import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
5063
5925
|
`);
|
|
5064
5926
|
}
|
|
5065
5927
|
const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
|
|
@@ -5071,18 +5933,18 @@ RAW: ${raw}
|
|
|
5071
5933
|
const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
|
|
5072
5934
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
5073
5935
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
5074
|
-
console.error(
|
|
5936
|
+
console.error(import_chalk6.default.bgRed.white.bold(`
|
|
5075
5937
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
5076
|
-
console.error(
|
|
5938
|
+
console.error(import_chalk6.default.red.bold(` A sensitive secret was found in the tool arguments!`));
|
|
5077
5939
|
} else {
|
|
5078
|
-
console.error(
|
|
5940
|
+
console.error(import_chalk6.default.red(`
|
|
5079
5941
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
5080
5942
|
}
|
|
5081
|
-
console.error(
|
|
5082
|
-
if (result2?.changeHint) console.error(
|
|
5943
|
+
console.error(import_chalk6.default.gray(` Triggered by: ${blockedByContext}`));
|
|
5944
|
+
if (result2?.changeHint) console.error(import_chalk6.default.cyan(` To change: ${result2.changeHint}`));
|
|
5083
5945
|
console.error("");
|
|
5084
5946
|
const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
|
|
5085
|
-
console.error(
|
|
5947
|
+
console.error(import_chalk6.default.dim(` (Detailed instructions sent to AI agent)`));
|
|
5086
5948
|
process.stdout.write(
|
|
5087
5949
|
JSON.stringify({
|
|
5088
5950
|
decision: "block",
|
|
@@ -5113,7 +5975,7 @@ RAW: ${raw}
|
|
|
5113
5975
|
process.exit(0);
|
|
5114
5976
|
}
|
|
5115
5977
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
5116
|
-
console.error(
|
|
5978
|
+
console.error(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
5117
5979
|
const daemonReady = await autoStartDaemonAndWait();
|
|
5118
5980
|
if (daemonReady) {
|
|
5119
5981
|
const retry = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
@@ -5136,9 +5998,9 @@ RAW: ${raw}
|
|
|
5136
5998
|
});
|
|
5137
5999
|
} catch (err) {
|
|
5138
6000
|
if (process.env.NODE9_DEBUG === "1") {
|
|
5139
|
-
const logPath =
|
|
6001
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
|
|
5140
6002
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
5141
|
-
|
|
6003
|
+
import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
5142
6004
|
`);
|
|
5143
6005
|
}
|
|
5144
6006
|
process.exit(0);
|
|
@@ -5183,10 +6045,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
5183
6045
|
decision: "allowed",
|
|
5184
6046
|
source: "post-hook"
|
|
5185
6047
|
};
|
|
5186
|
-
const logPath =
|
|
5187
|
-
if (!
|
|
5188
|
-
|
|
5189
|
-
|
|
6048
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
|
|
6049
|
+
if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
|
|
6050
|
+
import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
|
|
6051
|
+
import_fs7.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
5190
6052
|
const config = getConfig();
|
|
5191
6053
|
if (shouldSnapshot(tool, {}, config)) {
|
|
5192
6054
|
await createShadowSnapshot();
|
|
@@ -5213,7 +6075,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
5213
6075
|
const ms = parseDuration(options.duration);
|
|
5214
6076
|
if (ms === null) {
|
|
5215
6077
|
console.error(
|
|
5216
|
-
|
|
6078
|
+
import_chalk6.default.red(`
|
|
5217
6079
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
5218
6080
|
`)
|
|
5219
6081
|
);
|
|
@@ -5221,20 +6083,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
5221
6083
|
}
|
|
5222
6084
|
pauseNode9(ms, options.duration);
|
|
5223
6085
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
5224
|
-
console.log(
|
|
6086
|
+
console.log(import_chalk6.default.yellow(`
|
|
5225
6087
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
5226
|
-
console.log(
|
|
5227
|
-
console.log(
|
|
6088
|
+
console.log(import_chalk6.default.gray(` All tool calls will be allowed without review.`));
|
|
6089
|
+
console.log(import_chalk6.default.gray(` Run "node9 resume" to re-enable early.
|
|
5228
6090
|
`));
|
|
5229
6091
|
});
|
|
5230
6092
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
5231
6093
|
const { paused } = checkPause();
|
|
5232
6094
|
if (!paused) {
|
|
5233
|
-
console.log(
|
|
6095
|
+
console.log(import_chalk6.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
5234
6096
|
return;
|
|
5235
6097
|
}
|
|
5236
6098
|
resumeNode9();
|
|
5237
|
-
console.log(
|
|
6099
|
+
console.log(import_chalk6.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
5238
6100
|
});
|
|
5239
6101
|
var HOOK_BASED_AGENTS = {
|
|
5240
6102
|
claude: "claude",
|
|
@@ -5247,15 +6109,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
5247
6109
|
if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
|
|
5248
6110
|
const target = HOOK_BASED_AGENTS[firstArg];
|
|
5249
6111
|
console.error(
|
|
5250
|
-
|
|
6112
|
+
import_chalk6.default.yellow(`
|
|
5251
6113
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
5252
6114
|
);
|
|
5253
|
-
console.error(
|
|
6115
|
+
console.error(import_chalk6.default.white(`
|
|
5254
6116
|
"${target}" uses its own hook system. Use:`));
|
|
5255
6117
|
console.error(
|
|
5256
|
-
|
|
6118
|
+
import_chalk6.default.green(` node9 addto ${target} `) + import_chalk6.default.gray("# one-time setup")
|
|
5257
6119
|
);
|
|
5258
|
-
console.error(
|
|
6120
|
+
console.error(import_chalk6.default.green(` ${target} `) + import_chalk6.default.gray("# run normally"));
|
|
5259
6121
|
process.exit(1);
|
|
5260
6122
|
}
|
|
5261
6123
|
const fullCommand = commandArgs.join(" ");
|
|
@@ -5263,7 +6125,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
5263
6125
|
agent: "Terminal"
|
|
5264
6126
|
});
|
|
5265
6127
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
5266
|
-
console.error(
|
|
6128
|
+
console.error(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
5267
6129
|
const daemonReady = await autoStartDaemonAndWait();
|
|
5268
6130
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
5269
6131
|
}
|
|
@@ -5272,12 +6134,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
5272
6134
|
}
|
|
5273
6135
|
if (!result.approved) {
|
|
5274
6136
|
console.error(
|
|
5275
|
-
|
|
6137
|
+
import_chalk6.default.red(`
|
|
5276
6138
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
5277
6139
|
);
|
|
5278
6140
|
process.exit(1);
|
|
5279
6141
|
}
|
|
5280
|
-
console.error(
|
|
6142
|
+
console.error(import_chalk6.default.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
5281
6143
|
await runProxy(fullCommand);
|
|
5282
6144
|
} else {
|
|
5283
6145
|
program.help();
|
|
@@ -5292,22 +6154,22 @@ program.command("undo").description(
|
|
|
5292
6154
|
if (history.length === 0) {
|
|
5293
6155
|
if (!options.all && allHistory.length > 0) {
|
|
5294
6156
|
console.log(
|
|
5295
|
-
|
|
6157
|
+
import_chalk6.default.yellow(
|
|
5296
6158
|
`
|
|
5297
6159
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
5298
|
-
Run ${
|
|
6160
|
+
Run ${import_chalk6.default.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
5299
6161
|
`
|
|
5300
6162
|
)
|
|
5301
6163
|
);
|
|
5302
6164
|
} else {
|
|
5303
|
-
console.log(
|
|
6165
|
+
console.log(import_chalk6.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
5304
6166
|
}
|
|
5305
6167
|
return;
|
|
5306
6168
|
}
|
|
5307
6169
|
const idx = history.length - steps;
|
|
5308
6170
|
if (idx < 0) {
|
|
5309
6171
|
console.log(
|
|
5310
|
-
|
|
6172
|
+
import_chalk6.default.yellow(
|
|
5311
6173
|
`
|
|
5312
6174
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
5313
6175
|
`
|
|
@@ -5318,18 +6180,18 @@ program.command("undo").description(
|
|
|
5318
6180
|
const snapshot = history[idx];
|
|
5319
6181
|
const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
|
|
5320
6182
|
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
|
|
5321
|
-
console.log(
|
|
6183
|
+
console.log(import_chalk6.default.magenta.bold(`
|
|
5322
6184
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
|
|
5323
6185
|
console.log(
|
|
5324
|
-
|
|
5325
|
-
` Tool: ${
|
|
6186
|
+
import_chalk6.default.white(
|
|
6187
|
+
` Tool: ${import_chalk6.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk6.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
5326
6188
|
)
|
|
5327
6189
|
);
|
|
5328
|
-
console.log(
|
|
5329
|
-
console.log(
|
|
6190
|
+
console.log(import_chalk6.default.white(` When: ${import_chalk6.default.gray(ageStr)}`));
|
|
6191
|
+
console.log(import_chalk6.default.white(` Dir: ${import_chalk6.default.gray(snapshot.cwd)}`));
|
|
5330
6192
|
if (steps > 1)
|
|
5331
6193
|
console.log(
|
|
5332
|
-
|
|
6194
|
+
import_chalk6.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
5333
6195
|
);
|
|
5334
6196
|
console.log("");
|
|
5335
6197
|
const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
@@ -5337,21 +6199,21 @@ program.command("undo").description(
|
|
|
5337
6199
|
const lines = diff.split("\n");
|
|
5338
6200
|
for (const line of lines) {
|
|
5339
6201
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
5340
|
-
console.log(
|
|
6202
|
+
console.log(import_chalk6.default.bold(line));
|
|
5341
6203
|
} else if (line.startsWith("+")) {
|
|
5342
|
-
console.log(
|
|
6204
|
+
console.log(import_chalk6.default.green(line));
|
|
5343
6205
|
} else if (line.startsWith("-")) {
|
|
5344
|
-
console.log(
|
|
6206
|
+
console.log(import_chalk6.default.red(line));
|
|
5345
6207
|
} else if (line.startsWith("@@")) {
|
|
5346
|
-
console.log(
|
|
6208
|
+
console.log(import_chalk6.default.cyan(line));
|
|
5347
6209
|
} else {
|
|
5348
|
-
console.log(
|
|
6210
|
+
console.log(import_chalk6.default.gray(line));
|
|
5349
6211
|
}
|
|
5350
6212
|
}
|
|
5351
6213
|
console.log("");
|
|
5352
6214
|
} else {
|
|
5353
6215
|
console.log(
|
|
5354
|
-
|
|
6216
|
+
import_chalk6.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
5355
6217
|
);
|
|
5356
6218
|
}
|
|
5357
6219
|
const proceed = await (0, import_prompts3.confirm)({
|
|
@@ -5360,42 +6222,42 @@ program.command("undo").description(
|
|
|
5360
6222
|
});
|
|
5361
6223
|
if (proceed) {
|
|
5362
6224
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
5363
|
-
console.log(
|
|
6225
|
+
console.log(import_chalk6.default.green("\n\u2705 Reverted successfully.\n"));
|
|
5364
6226
|
} else {
|
|
5365
|
-
console.error(
|
|
6227
|
+
console.error(import_chalk6.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
5366
6228
|
}
|
|
5367
6229
|
} else {
|
|
5368
|
-
console.log(
|
|
6230
|
+
console.log(import_chalk6.default.gray("\nCancelled.\n"));
|
|
5369
6231
|
}
|
|
5370
6232
|
});
|
|
5371
6233
|
var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
|
|
5372
6234
|
shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
|
|
5373
6235
|
const name = resolveShieldName(service);
|
|
5374
6236
|
if (!name) {
|
|
5375
|
-
console.error(
|
|
6237
|
+
console.error(import_chalk6.default.red(`
|
|
5376
6238
|
\u274C Unknown shield: "${service}"
|
|
5377
6239
|
`));
|
|
5378
|
-
console.log(`Run ${
|
|
6240
|
+
console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
|
|
5379
6241
|
`);
|
|
5380
6242
|
process.exit(1);
|
|
5381
6243
|
}
|
|
5382
6244
|
const shield = getShield(name);
|
|
5383
6245
|
const active = readActiveShields();
|
|
5384
6246
|
if (active.includes(name)) {
|
|
5385
|
-
console.log(
|
|
6247
|
+
console.log(import_chalk6.default.yellow(`
|
|
5386
6248
|
\u2139\uFE0F Shield "${name}" is already active.
|
|
5387
6249
|
`));
|
|
5388
6250
|
return;
|
|
5389
6251
|
}
|
|
5390
6252
|
writeActiveShields([...active, name]);
|
|
5391
|
-
console.log(
|
|
6253
|
+
console.log(import_chalk6.default.green(`
|
|
5392
6254
|
\u{1F6E1}\uFE0F Shield "${name}" enabled.`));
|
|
5393
|
-
console.log(
|
|
6255
|
+
console.log(import_chalk6.default.gray(` ${shield.smartRules.length} smart rules now active.`));
|
|
5394
6256
|
if (shield.dangerousWords.length > 0)
|
|
5395
|
-
console.log(
|
|
6257
|
+
console.log(import_chalk6.default.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
|
|
5396
6258
|
if (name === "filesystem") {
|
|
5397
6259
|
console.log(
|
|
5398
|
-
|
|
6260
|
+
import_chalk6.default.yellow(
|
|
5399
6261
|
`
|
|
5400
6262
|
\u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
|
|
5401
6263
|
Tools like unlink, find -delete, or language-level file ops are not intercepted.`
|
|
@@ -5407,51 +6269,51 @@ shieldCmd.command("enable <service>").description("Enable a security shield for
|
|
|
5407
6269
|
shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
|
|
5408
6270
|
const name = resolveShieldName(service);
|
|
5409
6271
|
if (!name) {
|
|
5410
|
-
console.error(
|
|
6272
|
+
console.error(import_chalk6.default.red(`
|
|
5411
6273
|
\u274C Unknown shield: "${service}"
|
|
5412
6274
|
`));
|
|
5413
|
-
console.log(`Run ${
|
|
6275
|
+
console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
|
|
5414
6276
|
`);
|
|
5415
6277
|
process.exit(1);
|
|
5416
6278
|
}
|
|
5417
6279
|
const active = readActiveShields();
|
|
5418
6280
|
if (!active.includes(name)) {
|
|
5419
|
-
console.log(
|
|
6281
|
+
console.log(import_chalk6.default.yellow(`
|
|
5420
6282
|
\u2139\uFE0F Shield "${name}" is not active.
|
|
5421
6283
|
`));
|
|
5422
6284
|
return;
|
|
5423
6285
|
}
|
|
5424
6286
|
writeActiveShields(active.filter((s) => s !== name));
|
|
5425
|
-
console.log(
|
|
6287
|
+
console.log(import_chalk6.default.green(`
|
|
5426
6288
|
\u{1F6E1}\uFE0F Shield "${name}" disabled.
|
|
5427
6289
|
`));
|
|
5428
6290
|
});
|
|
5429
6291
|
shieldCmd.command("list").description("Show all available shields").action(() => {
|
|
5430
6292
|
const active = new Set(readActiveShields());
|
|
5431
|
-
console.log(
|
|
6293
|
+
console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
|
|
5432
6294
|
for (const shield of listShields()) {
|
|
5433
|
-
const status = active.has(shield.name) ?
|
|
5434
|
-
console.log(` ${status} ${
|
|
6295
|
+
const status = active.has(shield.name) ? import_chalk6.default.green("\u25CF enabled") : import_chalk6.default.gray("\u25CB disabled");
|
|
6296
|
+
console.log(` ${status} ${import_chalk6.default.cyan(shield.name.padEnd(12))} ${shield.description}`);
|
|
5435
6297
|
if (shield.aliases.length > 0)
|
|
5436
|
-
console.log(
|
|
6298
|
+
console.log(import_chalk6.default.gray(` aliases: ${shield.aliases.join(", ")}`));
|
|
5437
6299
|
}
|
|
5438
6300
|
console.log("");
|
|
5439
6301
|
});
|
|
5440
6302
|
shieldCmd.command("status").description("Show which shields are currently active").action(() => {
|
|
5441
6303
|
const active = readActiveShields();
|
|
5442
6304
|
if (active.length === 0) {
|
|
5443
|
-
console.log(
|
|
5444
|
-
console.log(`Run ${
|
|
6305
|
+
console.log(import_chalk6.default.yellow("\n\u2139\uFE0F No shields are active.\n"));
|
|
6306
|
+
console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
|
|
5445
6307
|
`);
|
|
5446
6308
|
return;
|
|
5447
6309
|
}
|
|
5448
|
-
console.log(
|
|
6310
|
+
console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
|
|
5449
6311
|
for (const name of active) {
|
|
5450
6312
|
const shield = getShield(name);
|
|
5451
6313
|
if (!shield) continue;
|
|
5452
|
-
console.log(` ${
|
|
6314
|
+
console.log(` ${import_chalk6.default.green("\u25CF")} ${import_chalk6.default.cyan(name)}`);
|
|
5453
6315
|
console.log(
|
|
5454
|
-
|
|
6316
|
+
import_chalk6.default.gray(
|
|
5455
6317
|
` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
|
|
5456
6318
|
)
|
|
5457
6319
|
);
|
|
@@ -5462,9 +6324,9 @@ process.on("unhandledRejection", (reason) => {
|
|
|
5462
6324
|
const isCheckHook = process.argv[2] === "check";
|
|
5463
6325
|
if (isCheckHook) {
|
|
5464
6326
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
5465
|
-
const logPath =
|
|
6327
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
|
|
5466
6328
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
5467
|
-
|
|
6329
|
+
import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
5468
6330
|
`);
|
|
5469
6331
|
}
|
|
5470
6332
|
process.exit(0);
|