@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.mjs
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import os2 from "os";
|
|
12
|
-
import pm from "picomatch";
|
|
13
|
-
import { parse } from "sh-syntax";
|
|
14
|
-
|
|
15
|
-
// src/ui/native.ts
|
|
16
|
-
import { spawn } from "child_process";
|
|
17
|
-
import path2 from "path";
|
|
18
|
-
import chalk from "chalk";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
19
11
|
|
|
20
12
|
// src/context-sniper.ts
|
|
21
13
|
import path from "path";
|
|
@@ -48,22 +40,6 @@ function extractContext(text, matchedWord) {
|
|
|
48
40
|
... [${lines.length - end} lines hidden] ...` : "";
|
|
49
41
|
return { snippet: `${head}${snippet}${tail}`, lineIndex };
|
|
50
42
|
}
|
|
51
|
-
var CODE_KEYS = [
|
|
52
|
-
"command",
|
|
53
|
-
"cmd",
|
|
54
|
-
"shell_command",
|
|
55
|
-
"bash_command",
|
|
56
|
-
"script",
|
|
57
|
-
"code",
|
|
58
|
-
"input",
|
|
59
|
-
"sql",
|
|
60
|
-
"query",
|
|
61
|
-
"arguments",
|
|
62
|
-
"args",
|
|
63
|
-
"param",
|
|
64
|
-
"params",
|
|
65
|
-
"text"
|
|
66
|
-
];
|
|
67
43
|
function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
|
|
68
44
|
let intent = "EXEC";
|
|
69
45
|
let contextSnippet;
|
|
@@ -118,11 +94,33 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
118
94
|
...ruleName && { ruleName }
|
|
119
95
|
};
|
|
120
96
|
}
|
|
97
|
+
var CODE_KEYS;
|
|
98
|
+
var init_context_sniper = __esm({
|
|
99
|
+
"src/context-sniper.ts"() {
|
|
100
|
+
"use strict";
|
|
101
|
+
CODE_KEYS = [
|
|
102
|
+
"command",
|
|
103
|
+
"cmd",
|
|
104
|
+
"shell_command",
|
|
105
|
+
"bash_command",
|
|
106
|
+
"script",
|
|
107
|
+
"code",
|
|
108
|
+
"input",
|
|
109
|
+
"sql",
|
|
110
|
+
"query",
|
|
111
|
+
"arguments",
|
|
112
|
+
"args",
|
|
113
|
+
"param",
|
|
114
|
+
"params",
|
|
115
|
+
"text"
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
});
|
|
121
119
|
|
|
122
120
|
// src/ui/native.ts
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
import { spawn } from "child_process";
|
|
122
|
+
import path2 from "path";
|
|
123
|
+
import chalk from "chalk";
|
|
126
124
|
function formatArgs(args, matchedField, matchedWord) {
|
|
127
125
|
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
128
126
|
let parsed = args;
|
|
@@ -332,81 +330,19 @@ end run`;
|
|
|
332
330
|
}
|
|
333
331
|
});
|
|
334
332
|
}
|
|
333
|
+
var isTestEnv;
|
|
334
|
+
var init_native = __esm({
|
|
335
|
+
"src/ui/native.ts"() {
|
|
336
|
+
"use strict";
|
|
337
|
+
init_context_sniper();
|
|
338
|
+
isTestEnv = () => {
|
|
339
|
+
return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
});
|
|
335
343
|
|
|
336
344
|
// src/config-schema.ts
|
|
337
345
|
import { z } from "zod";
|
|
338
|
-
var noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
339
|
-
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
340
|
-
});
|
|
341
|
-
var SmartConditionSchema = z.object({
|
|
342
|
-
field: z.string().min(1, "Condition field must not be empty"),
|
|
343
|
-
op: z.enum(
|
|
344
|
-
[
|
|
345
|
-
"matches",
|
|
346
|
-
"notMatches",
|
|
347
|
-
"contains",
|
|
348
|
-
"notContains",
|
|
349
|
-
"exists",
|
|
350
|
-
"notExists",
|
|
351
|
-
"matchesGlob",
|
|
352
|
-
"notMatchesGlob"
|
|
353
|
-
],
|
|
354
|
-
{
|
|
355
|
-
errorMap: () => ({
|
|
356
|
-
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
357
|
-
})
|
|
358
|
-
}
|
|
359
|
-
),
|
|
360
|
-
value: z.string().optional(),
|
|
361
|
-
flags: z.string().optional()
|
|
362
|
-
});
|
|
363
|
-
var SmartRuleSchema = z.object({
|
|
364
|
-
name: z.string().optional(),
|
|
365
|
-
tool: z.string().min(1, "Smart rule tool must not be empty"),
|
|
366
|
-
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
367
|
-
conditionMode: z.enum(["all", "any"]).optional(),
|
|
368
|
-
verdict: z.enum(["allow", "review", "block"], {
|
|
369
|
-
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
370
|
-
}),
|
|
371
|
-
reason: z.string().optional()
|
|
372
|
-
});
|
|
373
|
-
var ConfigFileSchema = z.object({
|
|
374
|
-
version: z.string().optional(),
|
|
375
|
-
settings: z.object({
|
|
376
|
-
mode: z.enum(["standard", "strict", "audit"]).optional(),
|
|
377
|
-
autoStartDaemon: z.boolean().optional(),
|
|
378
|
-
enableUndo: z.boolean().optional(),
|
|
379
|
-
enableHookLogDebug: z.boolean().optional(),
|
|
380
|
-
approvalTimeoutMs: z.number().nonnegative().optional(),
|
|
381
|
-
approvers: z.object({
|
|
382
|
-
native: z.boolean().optional(),
|
|
383
|
-
browser: z.boolean().optional(),
|
|
384
|
-
cloud: z.boolean().optional(),
|
|
385
|
-
terminal: z.boolean().optional()
|
|
386
|
-
}).optional(),
|
|
387
|
-
environment: z.string().optional(),
|
|
388
|
-
slackEnabled: z.boolean().optional(),
|
|
389
|
-
enableTrustSessions: z.boolean().optional(),
|
|
390
|
-
allowGlobalPause: z.boolean().optional()
|
|
391
|
-
}).optional(),
|
|
392
|
-
policy: z.object({
|
|
393
|
-
sandboxPaths: z.array(z.string()).optional(),
|
|
394
|
-
dangerousWords: z.array(noNewlines).optional(),
|
|
395
|
-
ignoredTools: z.array(z.string()).optional(),
|
|
396
|
-
toolInspection: z.record(z.string()).optional(),
|
|
397
|
-
smartRules: z.array(SmartRuleSchema).optional(),
|
|
398
|
-
snapshot: z.object({
|
|
399
|
-
tools: z.array(z.string()).optional(),
|
|
400
|
-
onlyPaths: z.array(z.string()).optional(),
|
|
401
|
-
ignorePaths: z.array(z.string()).optional()
|
|
402
|
-
}).optional(),
|
|
403
|
-
dlp: z.object({
|
|
404
|
-
enabled: z.boolean().optional(),
|
|
405
|
-
scanIgnoredTools: z.boolean().optional()
|
|
406
|
-
}).optional()
|
|
407
|
-
}).optional(),
|
|
408
|
-
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
409
|
-
}).strict({ message: "Config contains unknown top-level keys" });
|
|
410
346
|
function sanitizeConfig(raw) {
|
|
411
347
|
const result = ConfigFileSchema.safeParse(raw);
|
|
412
348
|
if (result.success) {
|
|
@@ -424,8 +360,8 @@ function sanitizeConfig(raw) {
|
|
|
424
360
|
}
|
|
425
361
|
}
|
|
426
362
|
const lines = result.error.issues.map((issue) => {
|
|
427
|
-
const
|
|
428
|
-
return ` \u2022 ${
|
|
363
|
+
const path10 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
364
|
+
return ` \u2022 ${path10}: ${issue.message}`;
|
|
429
365
|
});
|
|
430
366
|
return {
|
|
431
367
|
sanitized,
|
|
@@ -433,179 +369,90 @@ function sanitizeConfig(raw) {
|
|
|
433
369
|
${lines.join("\n")}`
|
|
434
370
|
};
|
|
435
371
|
}
|
|
372
|
+
var noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
|
|
373
|
+
var init_config_schema = __esm({
|
|
374
|
+
"src/config-schema.ts"() {
|
|
375
|
+
"use strict";
|
|
376
|
+
noNewlines = z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
377
|
+
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
378
|
+
});
|
|
379
|
+
SmartConditionSchema = z.object({
|
|
380
|
+
field: z.string().min(1, "Condition field must not be empty"),
|
|
381
|
+
op: z.enum(
|
|
382
|
+
[
|
|
383
|
+
"matches",
|
|
384
|
+
"notMatches",
|
|
385
|
+
"contains",
|
|
386
|
+
"notContains",
|
|
387
|
+
"exists",
|
|
388
|
+
"notExists",
|
|
389
|
+
"matchesGlob",
|
|
390
|
+
"notMatchesGlob"
|
|
391
|
+
],
|
|
392
|
+
{
|
|
393
|
+
errorMap: () => ({
|
|
394
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
),
|
|
398
|
+
value: z.string().optional(),
|
|
399
|
+
flags: z.string().optional()
|
|
400
|
+
});
|
|
401
|
+
SmartRuleSchema = z.object({
|
|
402
|
+
name: z.string().optional(),
|
|
403
|
+
tool: z.string().min(1, "Smart rule tool must not be empty"),
|
|
404
|
+
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
405
|
+
conditionMode: z.enum(["all", "any"]).optional(),
|
|
406
|
+
verdict: z.enum(["allow", "review", "block"], {
|
|
407
|
+
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
408
|
+
}),
|
|
409
|
+
reason: z.string().optional()
|
|
410
|
+
});
|
|
411
|
+
ConfigFileSchema = z.object({
|
|
412
|
+
version: z.string().optional(),
|
|
413
|
+
settings: z.object({
|
|
414
|
+
mode: z.enum(["standard", "strict", "audit"]).optional(),
|
|
415
|
+
autoStartDaemon: z.boolean().optional(),
|
|
416
|
+
enableUndo: z.boolean().optional(),
|
|
417
|
+
enableHookLogDebug: z.boolean().optional(),
|
|
418
|
+
approvalTimeoutMs: z.number().nonnegative().optional(),
|
|
419
|
+
approvers: z.object({
|
|
420
|
+
native: z.boolean().optional(),
|
|
421
|
+
browser: z.boolean().optional(),
|
|
422
|
+
cloud: z.boolean().optional(),
|
|
423
|
+
terminal: z.boolean().optional()
|
|
424
|
+
}).optional(),
|
|
425
|
+
environment: z.string().optional(),
|
|
426
|
+
slackEnabled: z.boolean().optional(),
|
|
427
|
+
enableTrustSessions: z.boolean().optional(),
|
|
428
|
+
allowGlobalPause: z.boolean().optional()
|
|
429
|
+
}).optional(),
|
|
430
|
+
policy: z.object({
|
|
431
|
+
sandboxPaths: z.array(z.string()).optional(),
|
|
432
|
+
dangerousWords: z.array(noNewlines).optional(),
|
|
433
|
+
ignoredTools: z.array(z.string()).optional(),
|
|
434
|
+
toolInspection: z.record(z.string()).optional(),
|
|
435
|
+
smartRules: z.array(SmartRuleSchema).optional(),
|
|
436
|
+
snapshot: z.object({
|
|
437
|
+
tools: z.array(z.string()).optional(),
|
|
438
|
+
onlyPaths: z.array(z.string()).optional(),
|
|
439
|
+
ignorePaths: z.array(z.string()).optional()
|
|
440
|
+
}).optional(),
|
|
441
|
+
dlp: z.object({
|
|
442
|
+
enabled: z.boolean().optional(),
|
|
443
|
+
scanIgnoredTools: z.boolean().optional()
|
|
444
|
+
}).optional()
|
|
445
|
+
}).optional(),
|
|
446
|
+
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
447
|
+
}).strict({ message: "Config contains unknown top-level keys" });
|
|
448
|
+
}
|
|
449
|
+
});
|
|
436
450
|
|
|
437
451
|
// src/shields.ts
|
|
438
452
|
import fs from "fs";
|
|
439
453
|
import path3 from "path";
|
|
440
454
|
import os from "os";
|
|
441
455
|
import crypto from "crypto";
|
|
442
|
-
var SHIELDS = {
|
|
443
|
-
postgres: {
|
|
444
|
-
name: "postgres",
|
|
445
|
-
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
446
|
-
aliases: ["pg", "postgresql"],
|
|
447
|
-
smartRules: [
|
|
448
|
-
{
|
|
449
|
-
name: "shield:postgres:block-drop-table",
|
|
450
|
-
tool: "*",
|
|
451
|
-
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
452
|
-
verdict: "block",
|
|
453
|
-
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
454
|
-
},
|
|
455
|
-
{
|
|
456
|
-
name: "shield:postgres:block-truncate",
|
|
457
|
-
tool: "*",
|
|
458
|
-
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
459
|
-
verdict: "block",
|
|
460
|
-
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
461
|
-
},
|
|
462
|
-
{
|
|
463
|
-
name: "shield:postgres:block-drop-column",
|
|
464
|
-
tool: "*",
|
|
465
|
-
conditions: [
|
|
466
|
-
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
467
|
-
],
|
|
468
|
-
verdict: "block",
|
|
469
|
-
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
470
|
-
},
|
|
471
|
-
{
|
|
472
|
-
name: "shield:postgres:review-grant-revoke",
|
|
473
|
-
tool: "*",
|
|
474
|
-
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
475
|
-
verdict: "review",
|
|
476
|
-
reason: "Permission changes require human approval (Postgres shield)"
|
|
477
|
-
}
|
|
478
|
-
],
|
|
479
|
-
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
480
|
-
},
|
|
481
|
-
github: {
|
|
482
|
-
name: "github",
|
|
483
|
-
description: "Protects GitHub repositories from destructive AI operations",
|
|
484
|
-
aliases: ["git"],
|
|
485
|
-
smartRules: [
|
|
486
|
-
{
|
|
487
|
-
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
488
|
-
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
489
|
-
name: "shield:github:review-delete-branch-remote",
|
|
490
|
-
tool: "bash",
|
|
491
|
-
conditions: [
|
|
492
|
-
{
|
|
493
|
-
field: "command",
|
|
494
|
-
op: "matches",
|
|
495
|
-
value: "git\\s+push\\s+.*--delete",
|
|
496
|
-
flags: "i"
|
|
497
|
-
}
|
|
498
|
-
],
|
|
499
|
-
verdict: "review",
|
|
500
|
-
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
501
|
-
},
|
|
502
|
-
{
|
|
503
|
-
name: "shield:github:block-delete-repo",
|
|
504
|
-
tool: "*",
|
|
505
|
-
conditions: [
|
|
506
|
-
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
507
|
-
],
|
|
508
|
-
verdict: "block",
|
|
509
|
-
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
510
|
-
}
|
|
511
|
-
],
|
|
512
|
-
dangerousWords: []
|
|
513
|
-
},
|
|
514
|
-
aws: {
|
|
515
|
-
name: "aws",
|
|
516
|
-
description: "Protects AWS infrastructure from destructive AI operations",
|
|
517
|
-
aliases: ["amazon"],
|
|
518
|
-
smartRules: [
|
|
519
|
-
{
|
|
520
|
-
name: "shield:aws:block-delete-s3-bucket",
|
|
521
|
-
tool: "*",
|
|
522
|
-
conditions: [
|
|
523
|
-
{
|
|
524
|
-
field: "command",
|
|
525
|
-
op: "matches",
|
|
526
|
-
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
527
|
-
flags: "i"
|
|
528
|
-
}
|
|
529
|
-
],
|
|
530
|
-
verdict: "block",
|
|
531
|
-
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
532
|
-
},
|
|
533
|
-
{
|
|
534
|
-
name: "shield:aws:review-iam-changes",
|
|
535
|
-
tool: "*",
|
|
536
|
-
conditions: [
|
|
537
|
-
{
|
|
538
|
-
field: "command",
|
|
539
|
-
op: "matches",
|
|
540
|
-
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
541
|
-
flags: "i"
|
|
542
|
-
}
|
|
543
|
-
],
|
|
544
|
-
verdict: "review",
|
|
545
|
-
reason: "IAM changes require human approval (AWS shield)"
|
|
546
|
-
},
|
|
547
|
-
{
|
|
548
|
-
name: "shield:aws:block-ec2-terminate",
|
|
549
|
-
tool: "*",
|
|
550
|
-
conditions: [
|
|
551
|
-
{
|
|
552
|
-
field: "command",
|
|
553
|
-
op: "matches",
|
|
554
|
-
value: "aws\\s+ec2\\s+terminate-instances",
|
|
555
|
-
flags: "i"
|
|
556
|
-
}
|
|
557
|
-
],
|
|
558
|
-
verdict: "block",
|
|
559
|
-
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
560
|
-
},
|
|
561
|
-
{
|
|
562
|
-
name: "shield:aws:review-rds-delete",
|
|
563
|
-
tool: "*",
|
|
564
|
-
conditions: [
|
|
565
|
-
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
566
|
-
],
|
|
567
|
-
verdict: "review",
|
|
568
|
-
reason: "RDS deletion requires human approval (AWS shield)"
|
|
569
|
-
}
|
|
570
|
-
],
|
|
571
|
-
dangerousWords: []
|
|
572
|
-
},
|
|
573
|
-
filesystem: {
|
|
574
|
-
name: "filesystem",
|
|
575
|
-
description: "Protects the local filesystem from dangerous AI operations",
|
|
576
|
-
aliases: ["fs"],
|
|
577
|
-
smartRules: [
|
|
578
|
-
{
|
|
579
|
-
name: "shield:filesystem:review-chmod-777",
|
|
580
|
-
tool: "bash",
|
|
581
|
-
conditions: [
|
|
582
|
-
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
583
|
-
],
|
|
584
|
-
verdict: "review",
|
|
585
|
-
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
586
|
-
},
|
|
587
|
-
{
|
|
588
|
-
name: "shield:filesystem:review-write-etc",
|
|
589
|
-
tool: "bash",
|
|
590
|
-
conditions: [
|
|
591
|
-
{
|
|
592
|
-
field: "command",
|
|
593
|
-
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
594
|
-
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
595
|
-
op: "matches",
|
|
596
|
-
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
597
|
-
}
|
|
598
|
-
],
|
|
599
|
-
verdict: "review",
|
|
600
|
-
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
601
|
-
}
|
|
602
|
-
],
|
|
603
|
-
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
604
|
-
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
605
|
-
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
606
|
-
dangerousWords: ["wipefs"]
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
456
|
function resolveShieldName(input) {
|
|
610
457
|
const lower = input.toLowerCase();
|
|
611
458
|
if (SHIELDS[lower]) return lower;
|
|
@@ -621,7 +468,6 @@ function getShield(name) {
|
|
|
621
468
|
function listShields() {
|
|
622
469
|
return Object.values(SHIELDS);
|
|
623
470
|
}
|
|
624
|
-
var SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
|
|
625
471
|
function readActiveShields() {
|
|
626
472
|
try {
|
|
627
473
|
const raw = fs.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
@@ -646,21 +492,182 @@ function writeActiveShields(active) {
|
|
|
646
492
|
fs.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
|
|
647
493
|
fs.renameSync(tmp, SHIELDS_STATE_FILE);
|
|
648
494
|
}
|
|
495
|
+
var SHIELDS, SHIELDS_STATE_FILE;
|
|
496
|
+
var init_shields = __esm({
|
|
497
|
+
"src/shields.ts"() {
|
|
498
|
+
"use strict";
|
|
499
|
+
SHIELDS = {
|
|
500
|
+
postgres: {
|
|
501
|
+
name: "postgres",
|
|
502
|
+
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
503
|
+
aliases: ["pg", "postgresql"],
|
|
504
|
+
smartRules: [
|
|
505
|
+
{
|
|
506
|
+
name: "shield:postgres:block-drop-table",
|
|
507
|
+
tool: "*",
|
|
508
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
509
|
+
verdict: "block",
|
|
510
|
+
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
name: "shield:postgres:block-truncate",
|
|
514
|
+
tool: "*",
|
|
515
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
516
|
+
verdict: "block",
|
|
517
|
+
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: "shield:postgres:block-drop-column",
|
|
521
|
+
tool: "*",
|
|
522
|
+
conditions: [
|
|
523
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
524
|
+
],
|
|
525
|
+
verdict: "block",
|
|
526
|
+
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
name: "shield:postgres:review-grant-revoke",
|
|
530
|
+
tool: "*",
|
|
531
|
+
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
532
|
+
verdict: "review",
|
|
533
|
+
reason: "Permission changes require human approval (Postgres shield)"
|
|
534
|
+
}
|
|
535
|
+
],
|
|
536
|
+
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
537
|
+
},
|
|
538
|
+
github: {
|
|
539
|
+
name: "github",
|
|
540
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
541
|
+
aliases: ["git"],
|
|
542
|
+
smartRules: [
|
|
543
|
+
{
|
|
544
|
+
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
545
|
+
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
546
|
+
name: "shield:github:review-delete-branch-remote",
|
|
547
|
+
tool: "bash",
|
|
548
|
+
conditions: [
|
|
549
|
+
{
|
|
550
|
+
field: "command",
|
|
551
|
+
op: "matches",
|
|
552
|
+
value: "git\\s+push\\s+.*--delete",
|
|
553
|
+
flags: "i"
|
|
554
|
+
}
|
|
555
|
+
],
|
|
556
|
+
verdict: "review",
|
|
557
|
+
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
name: "shield:github:block-delete-repo",
|
|
561
|
+
tool: "*",
|
|
562
|
+
conditions: [
|
|
563
|
+
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
564
|
+
],
|
|
565
|
+
verdict: "block",
|
|
566
|
+
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
567
|
+
}
|
|
568
|
+
],
|
|
569
|
+
dangerousWords: []
|
|
570
|
+
},
|
|
571
|
+
aws: {
|
|
572
|
+
name: "aws",
|
|
573
|
+
description: "Protects AWS infrastructure from destructive AI operations",
|
|
574
|
+
aliases: ["amazon"],
|
|
575
|
+
smartRules: [
|
|
576
|
+
{
|
|
577
|
+
name: "shield:aws:block-delete-s3-bucket",
|
|
578
|
+
tool: "*",
|
|
579
|
+
conditions: [
|
|
580
|
+
{
|
|
581
|
+
field: "command",
|
|
582
|
+
op: "matches",
|
|
583
|
+
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
584
|
+
flags: "i"
|
|
585
|
+
}
|
|
586
|
+
],
|
|
587
|
+
verdict: "block",
|
|
588
|
+
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
name: "shield:aws:review-iam-changes",
|
|
592
|
+
tool: "*",
|
|
593
|
+
conditions: [
|
|
594
|
+
{
|
|
595
|
+
field: "command",
|
|
596
|
+
op: "matches",
|
|
597
|
+
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
598
|
+
flags: "i"
|
|
599
|
+
}
|
|
600
|
+
],
|
|
601
|
+
verdict: "review",
|
|
602
|
+
reason: "IAM changes require human approval (AWS shield)"
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
name: "shield:aws:block-ec2-terminate",
|
|
606
|
+
tool: "*",
|
|
607
|
+
conditions: [
|
|
608
|
+
{
|
|
609
|
+
field: "command",
|
|
610
|
+
op: "matches",
|
|
611
|
+
value: "aws\\s+ec2\\s+terminate-instances",
|
|
612
|
+
flags: "i"
|
|
613
|
+
}
|
|
614
|
+
],
|
|
615
|
+
verdict: "block",
|
|
616
|
+
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
name: "shield:aws:review-rds-delete",
|
|
620
|
+
tool: "*",
|
|
621
|
+
conditions: [
|
|
622
|
+
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
623
|
+
],
|
|
624
|
+
verdict: "review",
|
|
625
|
+
reason: "RDS deletion requires human approval (AWS shield)"
|
|
626
|
+
}
|
|
627
|
+
],
|
|
628
|
+
dangerousWords: []
|
|
629
|
+
},
|
|
630
|
+
filesystem: {
|
|
631
|
+
name: "filesystem",
|
|
632
|
+
description: "Protects the local filesystem from dangerous AI operations",
|
|
633
|
+
aliases: ["fs"],
|
|
634
|
+
smartRules: [
|
|
635
|
+
{
|
|
636
|
+
name: "shield:filesystem:review-chmod-777",
|
|
637
|
+
tool: "bash",
|
|
638
|
+
conditions: [
|
|
639
|
+
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
640
|
+
],
|
|
641
|
+
verdict: "review",
|
|
642
|
+
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: "shield:filesystem:review-write-etc",
|
|
646
|
+
tool: "bash",
|
|
647
|
+
conditions: [
|
|
648
|
+
{
|
|
649
|
+
field: "command",
|
|
650
|
+
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
651
|
+
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
652
|
+
op: "matches",
|
|
653
|
+
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
654
|
+
}
|
|
655
|
+
],
|
|
656
|
+
verdict: "review",
|
|
657
|
+
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
658
|
+
}
|
|
659
|
+
],
|
|
660
|
+
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
661
|
+
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
662
|
+
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
663
|
+
dangerousWords: ["wipefs"]
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
|
|
667
|
+
}
|
|
668
|
+
});
|
|
649
669
|
|
|
650
670
|
// src/dlp.ts
|
|
651
|
-
var DLP_PATTERNS = [
|
|
652
|
-
{ name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
|
|
653
|
-
{ name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
|
|
654
|
-
{ name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
|
|
655
|
-
{ name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
|
|
656
|
-
{ name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
|
|
657
|
-
{
|
|
658
|
-
name: "Private Key (PEM)",
|
|
659
|
-
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
660
|
-
severity: "block"
|
|
661
|
-
},
|
|
662
|
-
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
|
|
663
|
-
];
|
|
664
671
|
function maskSecret(raw, pattern) {
|
|
665
672
|
const match = raw.match(pattern);
|
|
666
673
|
if (!match) return "****";
|
|
@@ -671,9 +678,6 @@ function maskSecret(raw, pattern) {
|
|
|
671
678
|
const stars = "*".repeat(Math.min(secret.length - 8, 12));
|
|
672
679
|
return `${prefix}${stars}${suffix}`;
|
|
673
680
|
}
|
|
674
|
-
var MAX_DEPTH = 5;
|
|
675
|
-
var MAX_STRING_BYTES = 1e5;
|
|
676
|
-
var MAX_JSON_PARSE_BYTES = 1e4;
|
|
677
681
|
function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
678
682
|
if (depth > MAX_DEPTH || args === null || args === void 0) return null;
|
|
679
683
|
if (Array.isArray(args)) {
|
|
@@ -716,12 +720,39 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
716
720
|
}
|
|
717
721
|
return null;
|
|
718
722
|
}
|
|
723
|
+
var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
|
|
724
|
+
var init_dlp = __esm({
|
|
725
|
+
"src/dlp.ts"() {
|
|
726
|
+
"use strict";
|
|
727
|
+
DLP_PATTERNS = [
|
|
728
|
+
{ name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
|
|
729
|
+
{ name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
|
|
730
|
+
{ name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
|
|
731
|
+
{ name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
|
|
732
|
+
{ name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
|
|
733
|
+
{
|
|
734
|
+
name: "Private Key (PEM)",
|
|
735
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
736
|
+
severity: "block"
|
|
737
|
+
},
|
|
738
|
+
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
|
|
739
|
+
];
|
|
740
|
+
MAX_DEPTH = 5;
|
|
741
|
+
MAX_STRING_BYTES = 1e5;
|
|
742
|
+
MAX_JSON_PARSE_BYTES = 1e4;
|
|
743
|
+
}
|
|
744
|
+
});
|
|
719
745
|
|
|
720
746
|
// src/core.ts
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
747
|
+
import chalk2 from "chalk";
|
|
748
|
+
import { confirm } from "@inquirer/prompts";
|
|
749
|
+
import fs2 from "fs";
|
|
750
|
+
import path4 from "path";
|
|
751
|
+
import os2 from "os";
|
|
752
|
+
import net from "net";
|
|
753
|
+
import { randomUUID } from "crypto";
|
|
754
|
+
import pm from "picomatch";
|
|
755
|
+
import { parse } from "sh-syntax";
|
|
725
756
|
function checkPause() {
|
|
726
757
|
try {
|
|
727
758
|
if (!fs2.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -834,9 +865,9 @@ function matchesPattern(text, patterns) {
|
|
|
834
865
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
835
866
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
836
867
|
}
|
|
837
|
-
function getNestedValue(obj,
|
|
868
|
+
function getNestedValue(obj, path10) {
|
|
838
869
|
if (!obj || typeof obj !== "object") return null;
|
|
839
|
-
return
|
|
870
|
+
return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
840
871
|
}
|
|
841
872
|
function shouldSnapshot(toolName, args, config) {
|
|
842
873
|
if (!config.settings.enableUndo) return false;
|
|
@@ -906,7 +937,6 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
906
937
|
const fieldName = toolInspection[matchingPattern];
|
|
907
938
|
return fieldName === "sql" || fieldName === "query";
|
|
908
939
|
}
|
|
909
|
-
var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
910
940
|
async function analyzeShellCommand(command) {
|
|
911
941
|
const actions = [];
|
|
912
942
|
const paths = [];
|
|
@@ -988,228 +1018,26 @@ function redactSecrets(text) {
|
|
|
988
1018
|
);
|
|
989
1019
|
return redacted;
|
|
990
1020
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
ignoredTools: [
|
|
1012
|
-
"list_*",
|
|
1013
|
-
"get_*",
|
|
1014
|
-
"read_*",
|
|
1015
|
-
"describe_*",
|
|
1016
|
-
"read",
|
|
1017
|
-
"glob",
|
|
1018
|
-
"grep",
|
|
1019
|
-
"ls",
|
|
1020
|
-
"notebookread",
|
|
1021
|
-
"notebookedit",
|
|
1022
|
-
"webfetch",
|
|
1023
|
-
"websearch",
|
|
1024
|
-
"exitplanmode",
|
|
1025
|
-
"askuserquestion",
|
|
1026
|
-
"agent",
|
|
1027
|
-
"task*",
|
|
1028
|
-
"toolsearch",
|
|
1029
|
-
"mcp__ide__*",
|
|
1030
|
-
"getDiagnostics"
|
|
1031
|
-
],
|
|
1032
|
-
toolInspection: {
|
|
1033
|
-
bash: "command",
|
|
1034
|
-
shell: "command",
|
|
1035
|
-
run_shell_command: "command",
|
|
1036
|
-
"terminal.execute": "command",
|
|
1037
|
-
"postgres:query": "sql"
|
|
1038
|
-
},
|
|
1039
|
-
snapshot: {
|
|
1040
|
-
tools: [
|
|
1041
|
-
"str_replace_based_edit_tool",
|
|
1042
|
-
"write_file",
|
|
1043
|
-
"edit_file",
|
|
1044
|
-
"create_file",
|
|
1045
|
-
"edit",
|
|
1046
|
-
"replace"
|
|
1047
|
-
],
|
|
1048
|
-
onlyPaths: [],
|
|
1049
|
-
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
1050
|
-
},
|
|
1051
|
-
smartRules: [
|
|
1052
|
-
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
1053
|
-
{
|
|
1054
|
-
name: "block-rm-rf-home",
|
|
1055
|
-
tool: "bash",
|
|
1056
|
-
conditionMode: "all",
|
|
1057
|
-
conditions: [
|
|
1058
|
-
{
|
|
1059
|
-
field: "command",
|
|
1060
|
-
op: "matches",
|
|
1061
|
-
value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
|
|
1062
|
-
},
|
|
1063
|
-
{
|
|
1064
|
-
field: "command",
|
|
1065
|
-
op: "matches",
|
|
1066
|
-
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
1067
|
-
}
|
|
1068
|
-
],
|
|
1069
|
-
verdict: "block",
|
|
1070
|
-
reason: "Recursive delete of home directory is irreversible"
|
|
1071
|
-
},
|
|
1072
|
-
// ── SQL safety ────────────────────────────────────────────────────────
|
|
1073
|
-
{
|
|
1074
|
-
name: "no-delete-without-where",
|
|
1075
|
-
tool: "*",
|
|
1076
|
-
conditions: [
|
|
1077
|
-
{ field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
|
|
1078
|
-
{ field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
|
|
1079
|
-
],
|
|
1080
|
-
conditionMode: "all",
|
|
1081
|
-
verdict: "review",
|
|
1082
|
-
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
1083
|
-
},
|
|
1084
|
-
{
|
|
1085
|
-
name: "review-drop-truncate-shell",
|
|
1086
|
-
tool: "bash",
|
|
1087
|
-
conditions: [
|
|
1088
|
-
{
|
|
1089
|
-
field: "command",
|
|
1090
|
-
op: "matches",
|
|
1091
|
-
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
1092
|
-
flags: "i"
|
|
1093
|
-
}
|
|
1094
|
-
],
|
|
1095
|
-
conditionMode: "all",
|
|
1096
|
-
verdict: "review",
|
|
1097
|
-
reason: "SQL DDL destructive statement inside a shell command"
|
|
1098
|
-
},
|
|
1099
|
-
// ── Git safety ────────────────────────────────────────────────────────
|
|
1100
|
-
{
|
|
1101
|
-
name: "block-force-push",
|
|
1102
|
-
tool: "bash",
|
|
1103
|
-
conditions: [
|
|
1104
|
-
{
|
|
1105
|
-
field: "command",
|
|
1106
|
-
op: "matches",
|
|
1107
|
-
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
1108
|
-
flags: "i"
|
|
1109
|
-
}
|
|
1110
|
-
],
|
|
1111
|
-
conditionMode: "all",
|
|
1112
|
-
verdict: "block",
|
|
1113
|
-
reason: "Force push overwrites remote history and cannot be undone"
|
|
1114
|
-
},
|
|
1115
|
-
{
|
|
1116
|
-
name: "review-git-push",
|
|
1117
|
-
tool: "bash",
|
|
1118
|
-
conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
|
|
1119
|
-
conditionMode: "all",
|
|
1120
|
-
verdict: "review",
|
|
1121
|
-
reason: "git push sends changes to a shared remote"
|
|
1122
|
-
},
|
|
1123
|
-
{
|
|
1124
|
-
name: "review-git-destructive",
|
|
1125
|
-
tool: "bash",
|
|
1126
|
-
conditions: [
|
|
1127
|
-
{
|
|
1128
|
-
field: "command",
|
|
1129
|
-
op: "matches",
|
|
1130
|
-
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
1131
|
-
flags: "i"
|
|
1132
|
-
}
|
|
1133
|
-
],
|
|
1134
|
-
conditionMode: "all",
|
|
1135
|
-
verdict: "review",
|
|
1136
|
-
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
1137
|
-
},
|
|
1138
|
-
// ── Shell safety ──────────────────────────────────────────────────────
|
|
1139
|
-
{
|
|
1140
|
-
name: "review-sudo",
|
|
1141
|
-
tool: "bash",
|
|
1142
|
-
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
1143
|
-
conditionMode: "all",
|
|
1144
|
-
verdict: "review",
|
|
1145
|
-
reason: "Command requires elevated privileges"
|
|
1146
|
-
},
|
|
1147
|
-
{
|
|
1148
|
-
name: "review-curl-pipe-shell",
|
|
1149
|
-
tool: "bash",
|
|
1150
|
-
conditions: [
|
|
1151
|
-
{
|
|
1152
|
-
field: "command",
|
|
1153
|
-
op: "matches",
|
|
1154
|
-
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
1155
|
-
flags: "i"
|
|
1156
|
-
}
|
|
1157
|
-
],
|
|
1158
|
-
conditionMode: "all",
|
|
1159
|
-
verdict: "block",
|
|
1160
|
-
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
1161
|
-
}
|
|
1162
|
-
],
|
|
1163
|
-
dlp: { enabled: true, scanIgnoredTools: true }
|
|
1164
|
-
},
|
|
1165
|
-
environments: {}
|
|
1166
|
-
};
|
|
1167
|
-
var ADVISORY_SMART_RULES = [
|
|
1168
|
-
{
|
|
1169
|
-
name: "allow-rm-safe-paths",
|
|
1170
|
-
tool: "*",
|
|
1171
|
-
conditionMode: "all",
|
|
1172
|
-
conditions: [
|
|
1173
|
-
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
1174
|
-
{
|
|
1175
|
-
field: "command",
|
|
1176
|
-
op: "matches",
|
|
1177
|
-
// Matches known-safe build artifact paths in the command.
|
|
1178
|
-
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
1179
|
-
}
|
|
1180
|
-
],
|
|
1181
|
-
verdict: "allow",
|
|
1182
|
-
reason: "Deleting a known-safe build artifact path"
|
|
1183
|
-
},
|
|
1184
|
-
{
|
|
1185
|
-
name: "review-rm",
|
|
1186
|
-
tool: "*",
|
|
1187
|
-
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
1188
|
-
verdict: "review",
|
|
1189
|
-
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
1190
|
-
}
|
|
1191
|
-
];
|
|
1192
|
-
var cachedConfig = null;
|
|
1193
|
-
function _resetConfigCache() {
|
|
1194
|
-
cachedConfig = null;
|
|
1195
|
-
}
|
|
1196
|
-
function getGlobalSettings() {
|
|
1197
|
-
try {
|
|
1198
|
-
const globalConfigPath = path4.join(os2.homedir(), ".node9", "config.json");
|
|
1199
|
-
if (fs2.existsSync(globalConfigPath)) {
|
|
1200
|
-
const parsed = JSON.parse(fs2.readFileSync(globalConfigPath, "utf-8"));
|
|
1201
|
-
const settings = parsed.settings || {};
|
|
1202
|
-
return {
|
|
1203
|
-
mode: settings.mode || "standard",
|
|
1204
|
-
autoStartDaemon: settings.autoStartDaemon !== false,
|
|
1205
|
-
slackEnabled: settings.slackEnabled !== false,
|
|
1206
|
-
enableTrustSessions: settings.enableTrustSessions === true,
|
|
1207
|
-
allowGlobalPause: settings.allowGlobalPause !== false
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1210
|
-
} catch {
|
|
1211
|
-
}
|
|
1212
|
-
return {
|
|
1021
|
+
function _resetConfigCache() {
|
|
1022
|
+
cachedConfig = null;
|
|
1023
|
+
}
|
|
1024
|
+
function getGlobalSettings() {
|
|
1025
|
+
try {
|
|
1026
|
+
const globalConfigPath = path4.join(os2.homedir(), ".node9", "config.json");
|
|
1027
|
+
if (fs2.existsSync(globalConfigPath)) {
|
|
1028
|
+
const parsed = JSON.parse(fs2.readFileSync(globalConfigPath, "utf-8"));
|
|
1029
|
+
const settings = parsed.settings || {};
|
|
1030
|
+
return {
|
|
1031
|
+
mode: settings.mode || "standard",
|
|
1032
|
+
autoStartDaemon: settings.autoStartDaemon !== false,
|
|
1033
|
+
slackEnabled: settings.slackEnabled !== false,
|
|
1034
|
+
enableTrustSessions: settings.enableTrustSessions === true,
|
|
1035
|
+
allowGlobalPause: settings.allowGlobalPause !== false
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
} catch {
|
|
1039
|
+
}
|
|
1040
|
+
return {
|
|
1213
1041
|
mode: "standard",
|
|
1214
1042
|
autoStartDaemon: true,
|
|
1215
1043
|
slackEnabled: true,
|
|
@@ -1596,8 +1424,6 @@ function isIgnoredTool(toolName) {
|
|
|
1596
1424
|
const config = getConfig();
|
|
1597
1425
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
1598
1426
|
}
|
|
1599
|
-
var DAEMON_PORT = 7391;
|
|
1600
|
-
var DAEMON_HOST = "127.0.0.1";
|
|
1601
1427
|
function isDaemonRunning() {
|
|
1602
1428
|
try {
|
|
1603
1429
|
const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
|
|
@@ -1621,7 +1447,7 @@ function getPersistentDecision(toolName) {
|
|
|
1621
1447
|
}
|
|
1622
1448
|
return null;
|
|
1623
1449
|
}
|
|
1624
|
-
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
1450
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
|
|
1625
1451
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1626
1452
|
const checkCtrl = new AbortController();
|
|
1627
1453
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -1636,6 +1462,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
|
1636
1462
|
args,
|
|
1637
1463
|
agent: meta?.agent,
|
|
1638
1464
|
mcpServer: meta?.mcpServer,
|
|
1465
|
+
fromCLI: true,
|
|
1466
|
+
// Pass the flight-recorder ID so the daemon uses the same UUID for
|
|
1467
|
+
// activity-result as the CLI used for the pending activity event.
|
|
1468
|
+
// Without this, the two UUIDs never match and tail.ts never resolves
|
|
1469
|
+
// the pending item.
|
|
1470
|
+
activityId,
|
|
1639
1471
|
...riskMetadata && { riskMetadata }
|
|
1640
1472
|
}),
|
|
1641
1473
|
signal: checkCtrl.signal
|
|
@@ -1690,7 +1522,44 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1690
1522
|
signal: AbortSignal.timeout(3e3)
|
|
1691
1523
|
});
|
|
1692
1524
|
}
|
|
1525
|
+
function notifyActivity(data) {
|
|
1526
|
+
return new Promise((resolve) => {
|
|
1527
|
+
try {
|
|
1528
|
+
const payload = JSON.stringify(data);
|
|
1529
|
+
const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
|
|
1530
|
+
sock.on("connect", () => {
|
|
1531
|
+
sock.on("close", resolve);
|
|
1532
|
+
sock.end(payload);
|
|
1533
|
+
});
|
|
1534
|
+
sock.on("error", resolve);
|
|
1535
|
+
} catch {
|
|
1536
|
+
resolve();
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1693
1540
|
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1541
|
+
if (!options?.calledFromDaemon) {
|
|
1542
|
+
const actId = randomUUID();
|
|
1543
|
+
const actTs = Date.now();
|
|
1544
|
+
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
|
|
1545
|
+
const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
|
|
1546
|
+
...options,
|
|
1547
|
+
activityId: actId
|
|
1548
|
+
});
|
|
1549
|
+
if (!result.noApprovalMechanism) {
|
|
1550
|
+
await notifyActivity({
|
|
1551
|
+
id: actId,
|
|
1552
|
+
tool: toolName,
|
|
1553
|
+
ts: actTs,
|
|
1554
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
1555
|
+
label: result.blockedByLabel
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
return result;
|
|
1559
|
+
}
|
|
1560
|
+
return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
|
|
1561
|
+
}
|
|
1562
|
+
async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1694
1563
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1695
1564
|
const pauseState = checkPause();
|
|
1696
1565
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
@@ -1726,6 +1595,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1726
1595
|
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
1727
1596
|
};
|
|
1728
1597
|
}
|
|
1598
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
|
|
1729
1599
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
1730
1600
|
}
|
|
1731
1601
|
}
|
|
@@ -1948,7 +1818,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1948
1818
|
console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
1949
1819
|
`));
|
|
1950
1820
|
}
|
|
1951
|
-
const daemonDecision = await askDaemon(
|
|
1821
|
+
const daemonDecision = await askDaemon(
|
|
1822
|
+
toolName,
|
|
1823
|
+
args,
|
|
1824
|
+
meta,
|
|
1825
|
+
signal,
|
|
1826
|
+
riskMetadata,
|
|
1827
|
+
options?.activityId
|
|
1828
|
+
);
|
|
1952
1829
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1953
1830
|
const isApproved = daemonDecision === "allow";
|
|
1954
1831
|
return {
|
|
@@ -2152,7 +2029,10 @@ function getConfig() {
|
|
|
2152
2029
|
for (const rule of shield.smartRules) {
|
|
2153
2030
|
if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
2154
2031
|
}
|
|
2155
|
-
|
|
2032
|
+
const existingWords = new Set(mergedPolicy.dangerousWords);
|
|
2033
|
+
for (const word of shield.dangerousWords) {
|
|
2034
|
+
if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
|
|
2035
|
+
}
|
|
2156
2036
|
}
|
|
2157
2037
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2158
2038
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
@@ -2353,255 +2233,233 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
2353
2233
|
} catch {
|
|
2354
2234
|
}
|
|
2355
2235
|
}
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
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
|
-
|
|
2236
|
+
var PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
|
|
2237
|
+
var init_core = __esm({
|
|
2238
|
+
"src/core.ts"() {
|
|
2239
|
+
"use strict";
|
|
2240
|
+
init_native();
|
|
2241
|
+
init_context_sniper();
|
|
2242
|
+
init_config_schema();
|
|
2243
|
+
init_shields();
|
|
2244
|
+
init_dlp();
|
|
2245
|
+
PAUSED_FILE = path4.join(os2.homedir(), ".node9", "PAUSED");
|
|
2246
|
+
TRUST_FILE = path4.join(os2.homedir(), ".node9", "trust.json");
|
|
2247
|
+
LOCAL_AUDIT_LOG = path4.join(os2.homedir(), ".node9", "audit.log");
|
|
2248
|
+
HOOK_DEBUG_LOG = path4.join(os2.homedir(), ".node9", "hook-debug.log");
|
|
2249
|
+
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2250
|
+
DANGEROUS_WORDS = [
|
|
2251
|
+
"mkfs",
|
|
2252
|
+
// formats/wipes a filesystem partition
|
|
2253
|
+
"shred"
|
|
2254
|
+
// permanently overwrites file contents (unrecoverable)
|
|
2255
|
+
];
|
|
2256
|
+
DEFAULT_CONFIG = {
|
|
2257
|
+
settings: {
|
|
2258
|
+
mode: "standard",
|
|
2259
|
+
autoStartDaemon: true,
|
|
2260
|
+
enableUndo: true,
|
|
2261
|
+
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
2262
|
+
enableHookLogDebug: false,
|
|
2263
|
+
approvalTimeoutMs: 0,
|
|
2264
|
+
// 0 = disabled; set e.g. 30000 for 30-second auto-deny
|
|
2265
|
+
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
2266
|
+
},
|
|
2267
|
+
policy: {
|
|
2268
|
+
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
2269
|
+
dangerousWords: DANGEROUS_WORDS,
|
|
2270
|
+
ignoredTools: [
|
|
2271
|
+
"list_*",
|
|
2272
|
+
"get_*",
|
|
2273
|
+
"read_*",
|
|
2274
|
+
"describe_*",
|
|
2275
|
+
"read",
|
|
2276
|
+
"glob",
|
|
2277
|
+
"grep",
|
|
2278
|
+
"ls",
|
|
2279
|
+
"notebookread",
|
|
2280
|
+
"notebookedit",
|
|
2281
|
+
"webfetch",
|
|
2282
|
+
"websearch",
|
|
2283
|
+
"exitplanmode",
|
|
2284
|
+
"askuserquestion",
|
|
2285
|
+
"agent",
|
|
2286
|
+
"task*",
|
|
2287
|
+
"toolsearch",
|
|
2288
|
+
"mcp__ide__*",
|
|
2289
|
+
"getDiagnostics"
|
|
2290
|
+
],
|
|
2291
|
+
toolInspection: {
|
|
2292
|
+
bash: "command",
|
|
2293
|
+
shell: "command",
|
|
2294
|
+
run_shell_command: "command",
|
|
2295
|
+
"terminal.execute": "command",
|
|
2296
|
+
"postgres:query": "sql"
|
|
2297
|
+
},
|
|
2298
|
+
snapshot: {
|
|
2299
|
+
tools: [
|
|
2300
|
+
"str_replace_based_edit_tool",
|
|
2301
|
+
"write_file",
|
|
2302
|
+
"edit_file",
|
|
2303
|
+
"create_file",
|
|
2304
|
+
"edit",
|
|
2305
|
+
"replace"
|
|
2306
|
+
],
|
|
2307
|
+
onlyPaths: [],
|
|
2308
|
+
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
2309
|
+
},
|
|
2310
|
+
smartRules: [
|
|
2311
|
+
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
2312
|
+
{
|
|
2313
|
+
name: "block-rm-rf-home",
|
|
2314
|
+
tool: "bash",
|
|
2315
|
+
conditionMode: "all",
|
|
2316
|
+
conditions: [
|
|
2317
|
+
{
|
|
2318
|
+
field: "command",
|
|
2319
|
+
op: "matches",
|
|
2320
|
+
value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
|
|
2321
|
+
},
|
|
2322
|
+
{
|
|
2323
|
+
field: "command",
|
|
2324
|
+
op: "matches",
|
|
2325
|
+
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
2326
|
+
}
|
|
2327
|
+
],
|
|
2328
|
+
verdict: "block",
|
|
2329
|
+
reason: "Recursive delete of home directory is irreversible"
|
|
2330
|
+
},
|
|
2331
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
2332
|
+
{
|
|
2333
|
+
name: "no-delete-without-where",
|
|
2334
|
+
tool: "*",
|
|
2335
|
+
conditions: [
|
|
2336
|
+
{ field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
|
|
2337
|
+
{ field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
|
|
2338
|
+
],
|
|
2339
|
+
conditionMode: "all",
|
|
2340
|
+
verdict: "review",
|
|
2341
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
name: "review-drop-truncate-shell",
|
|
2345
|
+
tool: "bash",
|
|
2346
|
+
conditions: [
|
|
2347
|
+
{
|
|
2348
|
+
field: "command",
|
|
2349
|
+
op: "matches",
|
|
2350
|
+
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
2351
|
+
flags: "i"
|
|
2352
|
+
}
|
|
2353
|
+
],
|
|
2354
|
+
conditionMode: "all",
|
|
2355
|
+
verdict: "review",
|
|
2356
|
+
reason: "SQL DDL destructive statement inside a shell command"
|
|
2357
|
+
},
|
|
2358
|
+
// ── Git safety ────────────────────────────────────────────────────────
|
|
2359
|
+
{
|
|
2360
|
+
name: "block-force-push",
|
|
2361
|
+
tool: "bash",
|
|
2362
|
+
conditions: [
|
|
2363
|
+
{
|
|
2364
|
+
field: "command",
|
|
2365
|
+
op: "matches",
|
|
2366
|
+
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
2367
|
+
flags: "i"
|
|
2368
|
+
}
|
|
2369
|
+
],
|
|
2370
|
+
conditionMode: "all",
|
|
2371
|
+
verdict: "block",
|
|
2372
|
+
reason: "Force push overwrites remote history and cannot be undone"
|
|
2373
|
+
},
|
|
2374
|
+
{
|
|
2375
|
+
name: "review-git-push",
|
|
2376
|
+
tool: "bash",
|
|
2377
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
|
|
2378
|
+
conditionMode: "all",
|
|
2379
|
+
verdict: "review",
|
|
2380
|
+
reason: "git push sends changes to a shared remote"
|
|
2381
|
+
},
|
|
2382
|
+
{
|
|
2383
|
+
name: "review-git-destructive",
|
|
2384
|
+
tool: "bash",
|
|
2385
|
+
conditions: [
|
|
2386
|
+
{
|
|
2387
|
+
field: "command",
|
|
2388
|
+
op: "matches",
|
|
2389
|
+
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
2390
|
+
flags: "i"
|
|
2391
|
+
}
|
|
2392
|
+
],
|
|
2393
|
+
conditionMode: "all",
|
|
2394
|
+
verdict: "review",
|
|
2395
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
2396
|
+
},
|
|
2397
|
+
// ── Shell safety ──────────────────────────────────────────────────────
|
|
2398
|
+
{
|
|
2399
|
+
name: "review-sudo",
|
|
2400
|
+
tool: "bash",
|
|
2401
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
2402
|
+
conditionMode: "all",
|
|
2403
|
+
verdict: "review",
|
|
2404
|
+
reason: "Command requires elevated privileges"
|
|
2405
|
+
},
|
|
2406
|
+
{
|
|
2407
|
+
name: "review-curl-pipe-shell",
|
|
2408
|
+
tool: "bash",
|
|
2409
|
+
conditions: [
|
|
2410
|
+
{
|
|
2411
|
+
field: "command",
|
|
2412
|
+
op: "matches",
|
|
2413
|
+
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
2414
|
+
flags: "i"
|
|
2415
|
+
}
|
|
2416
|
+
],
|
|
2417
|
+
conditionMode: "all",
|
|
2418
|
+
verdict: "block",
|
|
2419
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
2420
|
+
}
|
|
2421
|
+
],
|
|
2422
|
+
dlp: { enabled: true, scanIgnoredTools: true }
|
|
2423
|
+
},
|
|
2424
|
+
environments: {}
|
|
2425
|
+
};
|
|
2426
|
+
ADVISORY_SMART_RULES = [
|
|
2427
|
+
{
|
|
2428
|
+
name: "allow-rm-safe-paths",
|
|
2429
|
+
tool: "*",
|
|
2430
|
+
conditionMode: "all",
|
|
2431
|
+
conditions: [
|
|
2432
|
+
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
2433
|
+
{
|
|
2434
|
+
field: "command",
|
|
2435
|
+
op: "matches",
|
|
2436
|
+
// Matches known-safe build artifact paths in the command.
|
|
2437
|
+
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
2438
|
+
}
|
|
2439
|
+
],
|
|
2440
|
+
verdict: "allow",
|
|
2441
|
+
reason: "Deleting a known-safe build artifact path"
|
|
2442
|
+
},
|
|
2443
|
+
{
|
|
2444
|
+
name: "review-rm",
|
|
2445
|
+
tool: "*",
|
|
2446
|
+
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
2447
|
+
verdict: "review",
|
|
2448
|
+
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
2442
2449
|
}
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
} else {
|
|
2449
|
-
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
2450
|
-
}
|
|
2451
|
-
console.log("");
|
|
2452
|
-
}
|
|
2453
|
-
if (!anythingChanged && serversToWrap.length === 0) {
|
|
2454
|
-
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
2455
|
-
printDaemonTip();
|
|
2456
|
-
return;
|
|
2457
|
-
}
|
|
2458
|
-
if (anythingChanged) {
|
|
2459
|
-
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
2460
|
-
console.log(chalk3.gray(" Restart Claude Code for changes to take effect."));
|
|
2461
|
-
printDaemonTip();
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2464
|
-
async function setupGemini() {
|
|
2465
|
-
const homeDir2 = os3.homedir();
|
|
2466
|
-
const settingsPath = path5.join(homeDir2, ".gemini", "settings.json");
|
|
2467
|
-
const settings = readJson(settingsPath) ?? {};
|
|
2468
|
-
const servers = settings.mcpServers ?? {};
|
|
2469
|
-
let anythingChanged = false;
|
|
2470
|
-
if (!settings.hooks) settings.hooks = {};
|
|
2471
|
-
const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
|
|
2472
|
-
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
2473
|
-
);
|
|
2474
|
-
if (!hasBeforeHook) {
|
|
2475
|
-
if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
|
|
2476
|
-
if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
|
|
2477
|
-
settings.hooks.BeforeTool.push({
|
|
2478
|
-
matcher: ".*",
|
|
2479
|
-
hooks: [
|
|
2480
|
-
{
|
|
2481
|
-
name: "node9-check",
|
|
2482
|
-
type: "command",
|
|
2483
|
-
command: fullPathCommand("check"),
|
|
2484
|
-
timeout: 6e5
|
|
2485
|
-
}
|
|
2486
|
-
]
|
|
2487
|
-
});
|
|
2488
|
-
console.log(chalk3.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
2489
|
-
anythingChanged = true;
|
|
2490
|
-
}
|
|
2491
|
-
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
2492
|
-
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
2493
|
-
);
|
|
2494
|
-
if (!hasAfterHook) {
|
|
2495
|
-
if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
|
|
2496
|
-
if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
|
|
2497
|
-
settings.hooks.AfterTool.push({
|
|
2498
|
-
matcher: ".*",
|
|
2499
|
-
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
2500
|
-
});
|
|
2501
|
-
console.log(chalk3.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
2502
|
-
anythingChanged = true;
|
|
2503
|
-
}
|
|
2504
|
-
if (anythingChanged) {
|
|
2505
|
-
writeJson(settingsPath, settings);
|
|
2506
|
-
console.log("");
|
|
2450
|
+
];
|
|
2451
|
+
cachedConfig = null;
|
|
2452
|
+
DAEMON_PORT = 7391;
|
|
2453
|
+
DAEMON_HOST = "127.0.0.1";
|
|
2454
|
+
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path4.join(os2.tmpdir(), "node9-activity.sock");
|
|
2507
2455
|
}
|
|
2508
|
-
|
|
2509
|
-
for (const [name, server] of Object.entries(servers)) {
|
|
2510
|
-
if (!server.command || server.command === "node9") continue;
|
|
2511
|
-
const parts = [server.command, ...server.args ?? []];
|
|
2512
|
-
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
2513
|
-
}
|
|
2514
|
-
if (serversToWrap.length > 0) {
|
|
2515
|
-
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
2516
|
-
console.log(chalk3.white(` ${settingsPath} (mcpServers)`));
|
|
2517
|
-
for (const { name, originalCmd } of serversToWrap) {
|
|
2518
|
-
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
2519
|
-
}
|
|
2520
|
-
console.log("");
|
|
2521
|
-
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
2522
|
-
if (proceed) {
|
|
2523
|
-
for (const { name, parts } of serversToWrap) {
|
|
2524
|
-
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
2525
|
-
}
|
|
2526
|
-
settings.mcpServers = servers;
|
|
2527
|
-
writeJson(settingsPath, settings);
|
|
2528
|
-
console.log(chalk3.green(`
|
|
2529
|
-
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
2530
|
-
anythingChanged = true;
|
|
2531
|
-
} else {
|
|
2532
|
-
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
2533
|
-
}
|
|
2534
|
-
console.log("");
|
|
2535
|
-
}
|
|
2536
|
-
if (!anythingChanged && serversToWrap.length === 0) {
|
|
2537
|
-
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
|
|
2538
|
-
printDaemonTip();
|
|
2539
|
-
return;
|
|
2540
|
-
}
|
|
2541
|
-
if (anythingChanged) {
|
|
2542
|
-
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
2543
|
-
console.log(chalk3.gray(" Restart Gemini CLI for changes to take effect."));
|
|
2544
|
-
printDaemonTip();
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
async function setupCursor() {
|
|
2548
|
-
const homeDir2 = os3.homedir();
|
|
2549
|
-
const mcpPath = path5.join(homeDir2, ".cursor", "mcp.json");
|
|
2550
|
-
const mcpConfig = readJson(mcpPath) ?? {};
|
|
2551
|
-
const servers = mcpConfig.mcpServers ?? {};
|
|
2552
|
-
let anythingChanged = false;
|
|
2553
|
-
const serversToWrap = [];
|
|
2554
|
-
for (const [name, server] of Object.entries(servers)) {
|
|
2555
|
-
if (!server.command || server.command === "node9") continue;
|
|
2556
|
-
const parts = [server.command, ...server.args ?? []];
|
|
2557
|
-
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
2558
|
-
}
|
|
2559
|
-
if (serversToWrap.length > 0) {
|
|
2560
|
-
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
2561
|
-
console.log(chalk3.white(` ${mcpPath}`));
|
|
2562
|
-
for (const { name, originalCmd } of serversToWrap) {
|
|
2563
|
-
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
2564
|
-
}
|
|
2565
|
-
console.log("");
|
|
2566
|
-
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
2567
|
-
if (proceed) {
|
|
2568
|
-
for (const { name, parts } of serversToWrap) {
|
|
2569
|
-
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
2570
|
-
}
|
|
2571
|
-
mcpConfig.mcpServers = servers;
|
|
2572
|
-
writeJson(mcpPath, mcpConfig);
|
|
2573
|
-
console.log(chalk3.green(`
|
|
2574
|
-
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
2575
|
-
anythingChanged = true;
|
|
2576
|
-
} else {
|
|
2577
|
-
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
2578
|
-
}
|
|
2579
|
-
console.log("");
|
|
2580
|
-
}
|
|
2581
|
-
console.log(
|
|
2582
|
-
chalk3.yellow(
|
|
2583
|
-
" \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
|
|
2584
|
-
)
|
|
2585
|
-
);
|
|
2586
|
-
console.log("");
|
|
2587
|
-
if (!anythingChanged && serversToWrap.length === 0) {
|
|
2588
|
-
console.log(
|
|
2589
|
-
chalk3.blue(
|
|
2590
|
-
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
|
|
2591
|
-
)
|
|
2592
|
-
);
|
|
2593
|
-
printDaemonTip();
|
|
2594
|
-
return;
|
|
2595
|
-
}
|
|
2596
|
-
if (anythingChanged) {
|
|
2597
|
-
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
|
|
2598
|
-
console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
|
|
2599
|
-
printDaemonTip();
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2456
|
+
});
|
|
2602
2457
|
|
|
2603
2458
|
// src/daemon/ui.html
|
|
2604
|
-
var ui_default
|
|
2459
|
+
var ui_default;
|
|
2460
|
+
var init_ui = __esm({
|
|
2461
|
+
"src/daemon/ui.html"() {
|
|
2462
|
+
ui_default = `<!doctype html>
|
|
2605
2463
|
<html lang="en">
|
|
2606
2464
|
<head>
|
|
2607
2465
|
<meta charset="UTF-8" />
|
|
@@ -2627,6 +2485,11 @@ var ui_default = `<!doctype html>
|
|
|
2627
2485
|
margin: 0;
|
|
2628
2486
|
padding: 0;
|
|
2629
2487
|
}
|
|
2488
|
+
html,
|
|
2489
|
+
body {
|
|
2490
|
+
height: 100%;
|
|
2491
|
+
overflow: hidden;
|
|
2492
|
+
}
|
|
2630
2493
|
body {
|
|
2631
2494
|
background: var(--bg);
|
|
2632
2495
|
color: var(--text);
|
|
@@ -2634,16 +2497,17 @@ var ui_default = `<!doctype html>
|
|
|
2634
2497
|
'Inter',
|
|
2635
2498
|
-apple-system,
|
|
2636
2499
|
sans-serif;
|
|
2637
|
-
min-height: 100vh;
|
|
2638
2500
|
}
|
|
2639
2501
|
|
|
2640
2502
|
.shell {
|
|
2641
|
-
max-width:
|
|
2503
|
+
max-width: 1440px;
|
|
2504
|
+
height: 100vh;
|
|
2642
2505
|
margin: 0 auto;
|
|
2643
|
-
padding:
|
|
2506
|
+
padding: 16px 20px 16px;
|
|
2644
2507
|
display: grid;
|
|
2645
2508
|
grid-template-rows: auto 1fr;
|
|
2646
|
-
gap:
|
|
2509
|
+
gap: 16px;
|
|
2510
|
+
overflow: hidden;
|
|
2647
2511
|
}
|
|
2648
2512
|
header {
|
|
2649
2513
|
display: flex;
|
|
@@ -2680,9 +2544,10 @@ var ui_default = `<!doctype html>
|
|
|
2680
2544
|
|
|
2681
2545
|
.body {
|
|
2682
2546
|
display: grid;
|
|
2683
|
-
grid-template-columns: 1fr
|
|
2684
|
-
gap:
|
|
2685
|
-
|
|
2547
|
+
grid-template-columns: 360px 1fr 270px;
|
|
2548
|
+
gap: 16px;
|
|
2549
|
+
min-height: 0;
|
|
2550
|
+
overflow: hidden;
|
|
2686
2551
|
}
|
|
2687
2552
|
|
|
2688
2553
|
.warning-banner {
|
|
@@ -2702,6 +2567,10 @@ var ui_default = `<!doctype html>
|
|
|
2702
2567
|
|
|
2703
2568
|
.main {
|
|
2704
2569
|
min-width: 0;
|
|
2570
|
+
min-height: 0;
|
|
2571
|
+
overflow-y: auto;
|
|
2572
|
+
scrollbar-width: thin;
|
|
2573
|
+
scrollbar-color: var(--border) transparent;
|
|
2705
2574
|
}
|
|
2706
2575
|
.section-title {
|
|
2707
2576
|
font-size: 11px;
|
|
@@ -2732,14 +2601,64 @@ var ui_default = `<!doctype html>
|
|
|
2732
2601
|
background: var(--card);
|
|
2733
2602
|
border: 1px solid var(--border);
|
|
2734
2603
|
border-radius: 14px;
|
|
2735
|
-
padding:
|
|
2736
|
-
margin-bottom:
|
|
2604
|
+
padding: 20px;
|
|
2605
|
+
margin-bottom: 14px;
|
|
2737
2606
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
2738
2607
|
animation: pop 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
2739
2608
|
}
|
|
2740
2609
|
.card.slack-viewer {
|
|
2741
2610
|
border-color: rgba(83, 155, 245, 0.3);
|
|
2742
2611
|
}
|
|
2612
|
+
.card-header {
|
|
2613
|
+
display: flex;
|
|
2614
|
+
align-items: center;
|
|
2615
|
+
gap: 8px;
|
|
2616
|
+
margin-bottom: 12px;
|
|
2617
|
+
padding-bottom: 12px;
|
|
2618
|
+
border-bottom: 1px solid var(--border);
|
|
2619
|
+
}
|
|
2620
|
+
.card-header-icon {
|
|
2621
|
+
font-size: 16px;
|
|
2622
|
+
}
|
|
2623
|
+
.card-header-title {
|
|
2624
|
+
font-size: 12px;
|
|
2625
|
+
font-weight: 700;
|
|
2626
|
+
color: var(--text-bright);
|
|
2627
|
+
text-transform: uppercase;
|
|
2628
|
+
letter-spacing: 0.5px;
|
|
2629
|
+
}
|
|
2630
|
+
.card-timer {
|
|
2631
|
+
margin-left: auto;
|
|
2632
|
+
font-size: 11px;
|
|
2633
|
+
font-family: 'Fira Code', monospace;
|
|
2634
|
+
color: var(--muted);
|
|
2635
|
+
background: rgba(48, 54, 61, 0.6);
|
|
2636
|
+
padding: 2px 8px;
|
|
2637
|
+
border-radius: 5px;
|
|
2638
|
+
}
|
|
2639
|
+
.card-timer.urgent {
|
|
2640
|
+
color: var(--danger);
|
|
2641
|
+
background: rgba(201, 60, 55, 0.1);
|
|
2642
|
+
}
|
|
2643
|
+
.btn-allow {
|
|
2644
|
+
background: var(--success);
|
|
2645
|
+
color: #fff;
|
|
2646
|
+
grid-column: span 2;
|
|
2647
|
+
font-size: 14px;
|
|
2648
|
+
padding: 13px 14px;
|
|
2649
|
+
}
|
|
2650
|
+
.btn-deny {
|
|
2651
|
+
background: rgba(201, 60, 55, 0.15);
|
|
2652
|
+
color: #e5534b;
|
|
2653
|
+
border: 1px solid rgba(201, 60, 55, 0.3);
|
|
2654
|
+
grid-column: span 2;
|
|
2655
|
+
}
|
|
2656
|
+
.btn-deny:hover:not(:disabled) {
|
|
2657
|
+
background: var(--danger);
|
|
2658
|
+
color: #fff;
|
|
2659
|
+
border-color: transparent;
|
|
2660
|
+
filter: none;
|
|
2661
|
+
}
|
|
2743
2662
|
@keyframes pop {
|
|
2744
2663
|
from {
|
|
2745
2664
|
opacity: 0;
|
|
@@ -2947,24 +2866,178 @@ var ui_default = `<!doctype html>
|
|
|
2947
2866
|
cursor: not-allowed;
|
|
2948
2867
|
}
|
|
2949
2868
|
|
|
2869
|
+
.flight-col {
|
|
2870
|
+
display: flex;
|
|
2871
|
+
flex-direction: column;
|
|
2872
|
+
min-height: 0;
|
|
2873
|
+
overflow: hidden;
|
|
2874
|
+
}
|
|
2875
|
+
.flight-panel {
|
|
2876
|
+
flex: 1;
|
|
2877
|
+
min-height: 0;
|
|
2878
|
+
display: flex;
|
|
2879
|
+
flex-direction: column;
|
|
2880
|
+
overflow: hidden;
|
|
2881
|
+
}
|
|
2950
2882
|
.sidebar {
|
|
2951
2883
|
display: flex;
|
|
2952
2884
|
flex-direction: column;
|
|
2953
2885
|
gap: 12px;
|
|
2954
|
-
|
|
2955
|
-
|
|
2886
|
+
min-height: 0;
|
|
2887
|
+
overflow-y: auto;
|
|
2888
|
+
scrollbar-width: thin;
|
|
2889
|
+
scrollbar-color: var(--border) transparent;
|
|
2956
2890
|
}
|
|
2957
2891
|
.panel {
|
|
2958
2892
|
background: var(--panel);
|
|
2959
2893
|
border: 1px solid var(--border);
|
|
2960
2894
|
border-radius: 12px;
|
|
2961
|
-
padding:
|
|
2895
|
+
padding: 14px;
|
|
2896
|
+
}
|
|
2897
|
+
/* \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2898
|
+
#activity-feed {
|
|
2899
|
+
display: flex;
|
|
2900
|
+
flex-direction: column;
|
|
2901
|
+
gap: 4px;
|
|
2902
|
+
margin-top: 4px;
|
|
2903
|
+
flex: 1;
|
|
2904
|
+
min-height: 0;
|
|
2905
|
+
overflow-y: auto;
|
|
2906
|
+
scrollbar-width: thin;
|
|
2907
|
+
scrollbar-color: var(--border) transparent;
|
|
2908
|
+
}
|
|
2909
|
+
.feed-row {
|
|
2910
|
+
display: grid;
|
|
2911
|
+
grid-template-columns: 58px 20px 1fr 48px;
|
|
2912
|
+
align-items: start;
|
|
2913
|
+
gap: 6px;
|
|
2914
|
+
background: rgba(22, 27, 34, 0.6);
|
|
2915
|
+
border: 1px solid var(--border);
|
|
2916
|
+
padding: 7px 10px;
|
|
2917
|
+
border-radius: 7px;
|
|
2918
|
+
font-size: 11px;
|
|
2919
|
+
animation: frSlideIn 0.15s ease-out;
|
|
2920
|
+
transition: background 0.1s;
|
|
2921
|
+
cursor: default;
|
|
2922
|
+
}
|
|
2923
|
+
.feed-row:hover {
|
|
2924
|
+
background: rgba(30, 38, 48, 0.9);
|
|
2925
|
+
border-color: rgba(83, 155, 245, 0.2);
|
|
2926
|
+
}
|
|
2927
|
+
@keyframes frSlideIn {
|
|
2928
|
+
from {
|
|
2929
|
+
opacity: 0;
|
|
2930
|
+
transform: translateX(-4px);
|
|
2931
|
+
}
|
|
2932
|
+
to {
|
|
2933
|
+
opacity: 1;
|
|
2934
|
+
transform: none;
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
.feed-ts {
|
|
2938
|
+
color: var(--muted);
|
|
2939
|
+
font-family: monospace;
|
|
2940
|
+
font-size: 9px;
|
|
2941
|
+
}
|
|
2942
|
+
.feed-icon {
|
|
2943
|
+
text-align: center;
|
|
2944
|
+
font-size: 13px;
|
|
2945
|
+
}
|
|
2946
|
+
.feed-content {
|
|
2947
|
+
min-width: 0;
|
|
2948
|
+
color: var(--text-bright);
|
|
2949
|
+
word-break: break-all;
|
|
2950
|
+
}
|
|
2951
|
+
.feed-args {
|
|
2952
|
+
display: block;
|
|
2953
|
+
color: var(--muted);
|
|
2954
|
+
font-family: monospace;
|
|
2955
|
+
margin-top: 2px;
|
|
2956
|
+
font-size: 10px;
|
|
2957
|
+
word-break: break-all;
|
|
2958
|
+
}
|
|
2959
|
+
.feed-badge {
|
|
2960
|
+
text-align: right;
|
|
2961
|
+
font-weight: 700;
|
|
2962
|
+
font-size: 9px;
|
|
2963
|
+
letter-spacing: 0.03em;
|
|
2964
|
+
}
|
|
2965
|
+
.fr-pending {
|
|
2966
|
+
color: var(--muted);
|
|
2967
|
+
}
|
|
2968
|
+
.fr-allow {
|
|
2969
|
+
color: #57ab5a;
|
|
2970
|
+
}
|
|
2971
|
+
.fr-block {
|
|
2972
|
+
color: var(--danger);
|
|
2973
|
+
}
|
|
2974
|
+
.fr-dlp {
|
|
2975
|
+
color: var(--primary);
|
|
2976
|
+
animation: frBlink 1s infinite;
|
|
2977
|
+
}
|
|
2978
|
+
@keyframes frBlink {
|
|
2979
|
+
50% {
|
|
2980
|
+
opacity: 0.4;
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
.fr-dlp-row {
|
|
2984
|
+
border-color: var(--primary) !important;
|
|
2985
|
+
}
|
|
2986
|
+
.feed-clear-btn {
|
|
2987
|
+
background: transparent;
|
|
2988
|
+
border: none;
|
|
2989
|
+
color: var(--muted);
|
|
2990
|
+
font-size: 10px;
|
|
2991
|
+
padding: 0;
|
|
2992
|
+
cursor: pointer;
|
|
2993
|
+
margin-left: auto;
|
|
2994
|
+
font-family: inherit;
|
|
2995
|
+
font-weight: 500;
|
|
2996
|
+
transition: color 0.15s;
|
|
2997
|
+
}
|
|
2998
|
+
.feed-clear-btn:hover {
|
|
2999
|
+
color: var(--text);
|
|
3000
|
+
filter: none;
|
|
3001
|
+
transform: none;
|
|
3002
|
+
}
|
|
3003
|
+
/* \u2500\u2500 Shields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
3004
|
+
.shield-row {
|
|
3005
|
+
display: flex;
|
|
3006
|
+
align-items: flex-start;
|
|
3007
|
+
gap: 10px;
|
|
3008
|
+
padding: 8px 0;
|
|
3009
|
+
border-bottom: 1px solid var(--border);
|
|
2962
3010
|
}
|
|
3011
|
+
.shield-row:last-child {
|
|
3012
|
+
border-bottom: none;
|
|
3013
|
+
padding-bottom: 0;
|
|
3014
|
+
}
|
|
3015
|
+
.shield-row:first-child {
|
|
3016
|
+
padding-top: 0;
|
|
3017
|
+
}
|
|
3018
|
+
.shield-info {
|
|
3019
|
+
flex: 1;
|
|
3020
|
+
min-width: 0;
|
|
3021
|
+
}
|
|
3022
|
+
.shield-name {
|
|
3023
|
+
font-size: 12px;
|
|
3024
|
+
color: var(--text-bright);
|
|
3025
|
+
font-weight: 600;
|
|
3026
|
+
font-family: 'Fira Code', monospace;
|
|
3027
|
+
}
|
|
3028
|
+
.shield-desc {
|
|
3029
|
+
font-size: 10px;
|
|
3030
|
+
color: var(--muted);
|
|
3031
|
+
margin-top: 2px;
|
|
3032
|
+
line-height: 1.4;
|
|
3033
|
+
}
|
|
3034
|
+
|
|
2963
3035
|
.panel-title {
|
|
2964
3036
|
font-size: 12px;
|
|
2965
3037
|
font-weight: 700;
|
|
2966
3038
|
color: var(--text-bright);
|
|
2967
3039
|
margin-bottom: 12px;
|
|
3040
|
+
flex-shrink: 0;
|
|
2968
3041
|
display: flex;
|
|
2969
3042
|
align-items: center;
|
|
2970
3043
|
gap: 6px;
|
|
@@ -2972,8 +3045,8 @@ var ui_default = `<!doctype html>
|
|
|
2972
3045
|
.setting-row {
|
|
2973
3046
|
display: flex;
|
|
2974
3047
|
align-items: flex-start;
|
|
2975
|
-
gap:
|
|
2976
|
-
margin-bottom:
|
|
3048
|
+
gap: 10px;
|
|
3049
|
+
margin-bottom: 8px;
|
|
2977
3050
|
}
|
|
2978
3051
|
.setting-row:last-child {
|
|
2979
3052
|
margin-bottom: 0;
|
|
@@ -2982,20 +3055,21 @@ var ui_default = `<!doctype html>
|
|
|
2982
3055
|
flex: 1;
|
|
2983
3056
|
}
|
|
2984
3057
|
.setting-label {
|
|
2985
|
-
font-size:
|
|
3058
|
+
font-size: 11px;
|
|
2986
3059
|
color: var(--text-bright);
|
|
2987
|
-
margin-bottom:
|
|
3060
|
+
margin-bottom: 2px;
|
|
3061
|
+
font-weight: 600;
|
|
2988
3062
|
}
|
|
2989
3063
|
.setting-desc {
|
|
2990
|
-
font-size:
|
|
3064
|
+
font-size: 10px;
|
|
2991
3065
|
color: var(--muted);
|
|
2992
|
-
line-height: 1.
|
|
3066
|
+
line-height: 1.4;
|
|
2993
3067
|
}
|
|
2994
3068
|
.toggle {
|
|
2995
3069
|
position: relative;
|
|
2996
3070
|
display: inline-block;
|
|
2997
|
-
width:
|
|
2998
|
-
height:
|
|
3071
|
+
width: 34px;
|
|
3072
|
+
height: 19px;
|
|
2999
3073
|
flex-shrink: 0;
|
|
3000
3074
|
margin-top: 1px;
|
|
3001
3075
|
}
|
|
@@ -3015,8 +3089,8 @@ var ui_default = `<!doctype html>
|
|
|
3015
3089
|
.slider:before {
|
|
3016
3090
|
content: '';
|
|
3017
3091
|
position: absolute;
|
|
3018
|
-
width:
|
|
3019
|
-
height:
|
|
3092
|
+
width: 13px;
|
|
3093
|
+
height: 13px;
|
|
3020
3094
|
left: 3px;
|
|
3021
3095
|
bottom: 3px;
|
|
3022
3096
|
background: #fff;
|
|
@@ -3027,7 +3101,7 @@ var ui_default = `<!doctype html>
|
|
|
3027
3101
|
background: var(--success);
|
|
3028
3102
|
}
|
|
3029
3103
|
input:checked + .slider:before {
|
|
3030
|
-
transform: translateX(
|
|
3104
|
+
transform: translateX(15px);
|
|
3031
3105
|
}
|
|
3032
3106
|
input:disabled + .slider {
|
|
3033
3107
|
opacity: 0.4;
|
|
@@ -3186,12 +3260,17 @@ var ui_default = `<!doctype html>
|
|
|
3186
3260
|
border: 1px solid var(--border);
|
|
3187
3261
|
}
|
|
3188
3262
|
|
|
3189
|
-
@media (max-width:
|
|
3263
|
+
@media (max-width: 960px) {
|
|
3190
3264
|
.body {
|
|
3191
|
-
grid-template-columns: 1fr;
|
|
3265
|
+
grid-template-columns: 1fr 220px;
|
|
3266
|
+
}
|
|
3267
|
+
.flight-col {
|
|
3268
|
+
display: none;
|
|
3192
3269
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
3270
|
+
}
|
|
3271
|
+
@media (max-width: 640px) {
|
|
3272
|
+
.body {
|
|
3273
|
+
grid-template-columns: 1fr;
|
|
3195
3274
|
}
|
|
3196
3275
|
}
|
|
3197
3276
|
</style>
|
|
@@ -3205,6 +3284,19 @@ var ui_default = `<!doctype html>
|
|
|
3205
3284
|
</header>
|
|
3206
3285
|
|
|
3207
3286
|
<div class="body">
|
|
3287
|
+
<div class="flight-col">
|
|
3288
|
+
<div class="panel flight-panel">
|
|
3289
|
+
<div class="panel-title">
|
|
3290
|
+
\u{1F6F0}\uFE0F Flight Recorder
|
|
3291
|
+
<span style="font-weight: 400; color: var(--muted); font-size: 11px">live</span>
|
|
3292
|
+
<button class="feed-clear-btn" onclick="clearFeed()">clear</button>
|
|
3293
|
+
</div>
|
|
3294
|
+
<div id="activity-feed">
|
|
3295
|
+
<span class="decisions-empty">Waiting for agent activity\u2026</span>
|
|
3296
|
+
</div>
|
|
3297
|
+
</div>
|
|
3298
|
+
</div>
|
|
3299
|
+
|
|
3208
3300
|
<div class="main">
|
|
3209
3301
|
<div id="warnBanner" class="warning-banner">
|
|
3210
3302
|
\u26A0\uFE0F Auto-start is off \u2014 daemon started manually. Run "node9 daemon stop" to stop it, or
|
|
@@ -3285,6 +3377,11 @@ var ui_default = `<!doctype html>
|
|
|
3285
3377
|
<div id="slackStatusLine" class="slack-status-line">No key saved</div>
|
|
3286
3378
|
</div>
|
|
3287
3379
|
|
|
3380
|
+
<div class="panel">
|
|
3381
|
+
<div class="panel-title">\u{1F6E1}\uFE0F Active Shields</div>
|
|
3382
|
+
<div id="shieldsList"><span class="decisions-empty">Loading\u2026</span></div>
|
|
3383
|
+
</div>
|
|
3384
|
+
|
|
3288
3385
|
<div class="panel">
|
|
3289
3386
|
<div class="panel-title">\u{1F4CB} Persistent Decisions</div>
|
|
3290
3387
|
<div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
|
|
@@ -3330,14 +3427,23 @@ var ui_default = `<!doctype html>
|
|
|
3330
3427
|
|
|
3331
3428
|
function updateDenyButton(id, timestamp) {
|
|
3332
3429
|
const btn = document.querySelector('#c-' + id + ' .btn-deny');
|
|
3430
|
+
const timer = document.querySelector('#timer-' + id);
|
|
3333
3431
|
if (!btn) return;
|
|
3334
3432
|
const elapsed = Date.now() - timestamp;
|
|
3335
3433
|
const remaining = Math.max(0, Math.ceil((autoDenyMs - elapsed) / 1000));
|
|
3336
3434
|
if (remaining <= 0) {
|
|
3337
|
-
btn.textContent = 'Auto-Denying
|
|
3435
|
+
btn.textContent = '\u23F3 Auto-Denying\u2026';
|
|
3338
3436
|
btn.disabled = true;
|
|
3437
|
+
if (timer) {
|
|
3438
|
+
timer.textContent = 'auto-deny';
|
|
3439
|
+
timer.className = 'card-timer urgent';
|
|
3440
|
+
}
|
|
3339
3441
|
} else {
|
|
3340
|
-
btn.textContent = 'Block Action
|
|
3442
|
+
btn.textContent = '\u{1F6AB} Block this Action';
|
|
3443
|
+
if (timer) {
|
|
3444
|
+
timer.textContent = remaining + 's';
|
|
3445
|
+
timer.className = 'card-timer' + (remaining < 15 ? ' urgent' : '');
|
|
3446
|
+
}
|
|
3341
3447
|
setTimeout(() => updateDenyButton(id, timestamp), 1000);
|
|
3342
3448
|
}
|
|
3343
3449
|
}
|
|
@@ -3353,34 +3459,61 @@ var ui_default = `<!doctype html>
|
|
|
3353
3459
|
empty.style.display = requests.size === 0 ? 'block' : 'none';
|
|
3354
3460
|
}
|
|
3355
3461
|
|
|
3356
|
-
function
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
method: 'POST',
|
|
3361
|
-
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3362
|
-
body: JSON.stringify({ decision, persist: !!persist }),
|
|
3363
|
-
});
|
|
3364
|
-
setTimeout(() => {
|
|
3365
|
-
card?.remove();
|
|
3366
|
-
requests.delete(id);
|
|
3367
|
-
refresh();
|
|
3368
|
-
}, 200);
|
|
3462
|
+
function setCardBusy(card, busy) {
|
|
3463
|
+
if (!card) return;
|
|
3464
|
+
card.querySelectorAll('button').forEach((b) => (b.disabled = busy));
|
|
3465
|
+
card.style.opacity = busy ? '0.5' : '1';
|
|
3369
3466
|
}
|
|
3370
3467
|
|
|
3371
|
-
function
|
|
3468
|
+
function showCardError(card, msg) {
|
|
3469
|
+
if (!card) return;
|
|
3470
|
+
card.style.outline = '2px solid #f87171';
|
|
3471
|
+
let err = card.querySelector('.card-error');
|
|
3472
|
+
if (!err) {
|
|
3473
|
+
err = document.createElement('p');
|
|
3474
|
+
err.className = 'card-error';
|
|
3475
|
+
err.style.cssText = 'color:#f87171;font-size:11px;margin:6px 0 0;';
|
|
3476
|
+
card.appendChild(err);
|
|
3477
|
+
}
|
|
3478
|
+
err.textContent = '\u26A0 ' + msg + ' \u2014 please try again or refresh.';
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
async function sendDecision(id, decision, persist) {
|
|
3372
3482
|
const card = document.getElementById('c-' + id);
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3483
|
+
setCardBusy(card, true);
|
|
3484
|
+
try {
|
|
3485
|
+
const res = await fetch('/decision/' + id, {
|
|
3486
|
+
method: 'POST',
|
|
3487
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3488
|
+
body: JSON.stringify({ decision, persist: !!persist }),
|
|
3489
|
+
});
|
|
3490
|
+
if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
|
|
3491
|
+
card?.remove();
|
|
3492
|
+
requests.delete(id);
|
|
3493
|
+
refresh();
|
|
3494
|
+
} catch (err) {
|
|
3495
|
+
setCardBusy(card, false);
|
|
3496
|
+
showCardError(card, err.message || 'Network error');
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
async function sendTrust(id, duration) {
|
|
3501
|
+
const card = document.getElementById('c-' + id);
|
|
3502
|
+
setCardBusy(card, true);
|
|
3503
|
+
try {
|
|
3504
|
+
const res = await fetch('/decision/' + id, {
|
|
3505
|
+
method: 'POST',
|
|
3506
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3507
|
+
body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
|
|
3508
|
+
});
|
|
3509
|
+
if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
|
|
3380
3510
|
card?.remove();
|
|
3381
3511
|
requests.delete(id);
|
|
3382
3512
|
refresh();
|
|
3383
|
-
}
|
|
3513
|
+
} catch (err) {
|
|
3514
|
+
setCardBusy(card, false);
|
|
3515
|
+
showCardError(card, err.message || 'Network error');
|
|
3516
|
+
}
|
|
3384
3517
|
}
|
|
3385
3518
|
|
|
3386
3519
|
function renderPayload(req) {
|
|
@@ -3431,16 +3564,21 @@ var ui_default = `<!doctype html>
|
|
|
3431
3564
|
const mcpLabel = req.mcpServer ? esc(req.mcpServer) : null;
|
|
3432
3565
|
const dis = isSlack ? 'disabled' : '';
|
|
3433
3566
|
card.innerHTML = \`
|
|
3567
|
+
<div class="card-header">
|
|
3568
|
+
<span class="card-header-icon">\${isSlack ? '\u26A1' : '\u26A0\uFE0F'}</span>
|
|
3569
|
+
<span class="card-header-title">\${isSlack ? 'Awaiting Cloud Approval' : 'Action Required'}</span>
|
|
3570
|
+
<span class="card-timer" id="timer-\${req.id}">\${autoDenyMs > 0 ? Math.ceil(autoDenyMs / 1000) + 's' : ''}</span>
|
|
3571
|
+
</div>
|
|
3434
3572
|
<div class="source-row">
|
|
3435
3573
|
<span class="agent-badge">\${agentLabel}</span>
|
|
3436
3574
|
\${mcpLabel ? \`<span class="source-arrow">\u2192</span><span class="mcp-badge">mcp::\${mcpLabel}</span>\` : ''}
|
|
3437
3575
|
</div>
|
|
3438
3576
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
3439
|
-
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting
|
|
3577
|
+
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
|
|
3440
3578
|
\${renderPayload(req)}
|
|
3441
3579
|
<div class="actions" id="act-\${req.id}">
|
|
3442
|
-
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}
|
|
3443
|
-
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}
|
|
3580
|
+
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
|
|
3581
|
+
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>\u{1F6AB} Block this Action</button>
|
|
3444
3582
|
<div class="trust-row\${trustEnabled ? ' show' : ''}" id="tr-\${req.id}">
|
|
3445
3583
|
<button class="btn-trust" onclick="sendTrust('\${req.id}','30m')" \${dis}>\u23F1 Trust 30m</button>
|
|
3446
3584
|
<button class="btn-trust" onclick="sendTrust('\${req.id}','1h')" \${dis}>\u23F1 Trust 1h</button>
|
|
@@ -3500,9 +3638,84 @@ var ui_default = `<!doctype html>
|
|
|
3500
3638
|
ev.addEventListener('slack-status', (e) => {
|
|
3501
3639
|
applySlackStatus(JSON.parse(e.data));
|
|
3502
3640
|
});
|
|
3641
|
+
ev.addEventListener('shields-status', (e) => {
|
|
3642
|
+
renderShields(JSON.parse(e.data).shields);
|
|
3643
|
+
});
|
|
3644
|
+
|
|
3645
|
+
// \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3646
|
+
ev.addEventListener('activity', (e) => {
|
|
3647
|
+
const data = JSON.parse(e.data);
|
|
3648
|
+
const feed = document.getElementById('activity-feed');
|
|
3649
|
+
// Remove placeholder on first item
|
|
3650
|
+
const placeholder = feed.querySelector('.decisions-empty');
|
|
3651
|
+
if (placeholder) placeholder.remove();
|
|
3652
|
+
|
|
3653
|
+
const time = new Date(data.ts).toLocaleTimeString([], {
|
|
3654
|
+
hour12: false,
|
|
3655
|
+
hour: '2-digit',
|
|
3656
|
+
minute: '2-digit',
|
|
3657
|
+
second: '2-digit',
|
|
3658
|
+
});
|
|
3659
|
+
const icon = frIcon(data.tool);
|
|
3660
|
+
const argsStr = JSON.stringify(data.args ?? {});
|
|
3661
|
+
const argsPreview = esc(argsStr.length > 120 ? argsStr.slice(0, 120) + '\u2026' : argsStr);
|
|
3662
|
+
|
|
3663
|
+
const row = document.createElement('div');
|
|
3664
|
+
row.className = 'feed-row';
|
|
3665
|
+
row.id = 'fr-' + data.id;
|
|
3666
|
+
row.innerHTML = \`
|
|
3667
|
+
<span class="feed-ts">\${time}</span>
|
|
3668
|
+
<span class="feed-icon">\${icon}</span>
|
|
3669
|
+
<span class="feed-content"><strong>\${esc(data.tool)}</strong><span class="feed-args">\${argsPreview}</span></span>
|
|
3670
|
+
<span class="feed-badge fr-pending">\u25CF</span>
|
|
3671
|
+
\`;
|
|
3672
|
+
feed.prepend(row);
|
|
3673
|
+
if (feed.children.length > 100) feed.lastChild.remove();
|
|
3674
|
+
});
|
|
3675
|
+
|
|
3676
|
+
ev.addEventListener('activity-result', (e) => {
|
|
3677
|
+
const { id, status, label } = JSON.parse(e.data);
|
|
3678
|
+
const row = document.getElementById('fr-' + id);
|
|
3679
|
+
if (!row) return;
|
|
3680
|
+
const badge = row.querySelector('.feed-badge');
|
|
3681
|
+
if (status === 'allow') {
|
|
3682
|
+
badge.textContent = 'ALLOW';
|
|
3683
|
+
badge.className = 'feed-badge fr-allow';
|
|
3684
|
+
} else if (status === 'dlp') {
|
|
3685
|
+
badge.textContent = '\u{1F6E1}\uFE0F DLP';
|
|
3686
|
+
badge.className = 'feed-badge fr-dlp';
|
|
3687
|
+
row.classList.add('fr-dlp-row');
|
|
3688
|
+
} else {
|
|
3689
|
+
badge.textContent = 'BLOCK';
|
|
3690
|
+
badge.className = 'feed-badge fr-block';
|
|
3691
|
+
}
|
|
3692
|
+
});
|
|
3503
3693
|
}
|
|
3504
3694
|
connect();
|
|
3505
3695
|
|
|
3696
|
+
const FR_ICONS = {
|
|
3697
|
+
bash: '\u{1F4BB}',
|
|
3698
|
+
read: '\u{1F4D6}',
|
|
3699
|
+
edit: '\u270F\uFE0F',
|
|
3700
|
+
write: '\u270F\uFE0F',
|
|
3701
|
+
glob: '\u{1F4C2}',
|
|
3702
|
+
grep: '\u{1F50D}',
|
|
3703
|
+
agent: '\u{1F916}',
|
|
3704
|
+
search: '\u{1F50D}',
|
|
3705
|
+
sql: '\u{1F5C4}\uFE0F',
|
|
3706
|
+
query: '\u{1F5C4}\uFE0F',
|
|
3707
|
+
list: '\u{1F4C2}',
|
|
3708
|
+
delete: '\u{1F5D1}\uFE0F',
|
|
3709
|
+
web: '\u{1F310}',
|
|
3710
|
+
};
|
|
3711
|
+
function frIcon(tool) {
|
|
3712
|
+
const t = (tool || '').toLowerCase();
|
|
3713
|
+
for (const [k, v] of Object.entries(FR_ICONS)) {
|
|
3714
|
+
if (t.includes(k)) return v;
|
|
3715
|
+
}
|
|
3716
|
+
return '\u{1F6E0}\uFE0F';
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3506
3719
|
function saveSetting(key, value) {
|
|
3507
3720
|
fetch('/settings', {
|
|
3508
3721
|
method: 'POST',
|
|
@@ -3592,6 +3805,49 @@ var ui_default = `<!doctype html>
|
|
|
3592
3805
|
}
|
|
3593
3806
|
}
|
|
3594
3807
|
|
|
3808
|
+
function clearFeed() {
|
|
3809
|
+
const feed = document.getElementById('activity-feed');
|
|
3810
|
+
feed.innerHTML = '<span class="decisions-empty">Feed cleared.</span>';
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
function renderShields(shields) {
|
|
3814
|
+
const list = document.getElementById('shieldsList');
|
|
3815
|
+
if (!shields || shields.length === 0) {
|
|
3816
|
+
list.innerHTML = '<span class="decisions-empty">No shields available.</span>';
|
|
3817
|
+
return;
|
|
3818
|
+
}
|
|
3819
|
+
list.innerHTML = shields
|
|
3820
|
+
.map(
|
|
3821
|
+
(s) => \`
|
|
3822
|
+
<div class="shield-row">
|
|
3823
|
+
<div class="shield-info">
|
|
3824
|
+
<div class="shield-name">\${esc(s.name)}</div>
|
|
3825
|
+
<div class="shield-desc">\${esc(s.description)}</div>
|
|
3826
|
+
</div>
|
|
3827
|
+
<label class="toggle">
|
|
3828
|
+
<input type="checkbox" \${s.active ? 'checked' : ''}
|
|
3829
|
+
onchange="toggleShield('\${esc(s.name)}', this.checked)" />
|
|
3830
|
+
<span class="slider"></span>
|
|
3831
|
+
</label>
|
|
3832
|
+
</div>
|
|
3833
|
+
\`
|
|
3834
|
+
)
|
|
3835
|
+
.join('');
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
function toggleShield(name, active) {
|
|
3839
|
+
fetch('/shields', {
|
|
3840
|
+
method: 'POST',
|
|
3841
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3842
|
+
body: JSON.stringify({ name, active }),
|
|
3843
|
+
}).catch(() => {});
|
|
3844
|
+
}
|
|
3845
|
+
|
|
3846
|
+
fetch('/shields')
|
|
3847
|
+
.then((r) => r.json())
|
|
3848
|
+
.then(({ shields }) => renderShields(shields))
|
|
3849
|
+
.catch(() => {});
|
|
3850
|
+
|
|
3595
3851
|
function renderDecisions(decisions) {
|
|
3596
3852
|
const dl = document.getElementById('decisionsList');
|
|
3597
3853
|
const entries = Object.entries(decisions);
|
|
@@ -3638,31 +3894,32 @@ var ui_default = `<!doctype html>
|
|
|
3638
3894
|
</body>
|
|
3639
3895
|
</html>
|
|
3640
3896
|
`;
|
|
3897
|
+
}
|
|
3898
|
+
});
|
|
3641
3899
|
|
|
3642
3900
|
// src/daemon/ui.ts
|
|
3643
|
-
var UI_HTML_TEMPLATE
|
|
3901
|
+
var UI_HTML_TEMPLATE;
|
|
3902
|
+
var init_ui2 = __esm({
|
|
3903
|
+
"src/daemon/ui.ts"() {
|
|
3904
|
+
"use strict";
|
|
3905
|
+
init_ui();
|
|
3906
|
+
UI_HTML_TEMPLATE = ui_default;
|
|
3907
|
+
}
|
|
3908
|
+
});
|
|
3644
3909
|
|
|
3645
3910
|
// src/daemon/index.ts
|
|
3646
3911
|
import http from "http";
|
|
3912
|
+
import net2 from "net";
|
|
3647
3913
|
import fs4 from "fs";
|
|
3648
3914
|
import path6 from "path";
|
|
3649
3915
|
import os4 from "os";
|
|
3650
3916
|
import { spawn as spawn2 } from "child_process";
|
|
3651
|
-
import { randomUUID } from "crypto";
|
|
3917
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3652
3918
|
import chalk4 from "chalk";
|
|
3653
|
-
var DAEMON_PORT2 = 7391;
|
|
3654
|
-
var DAEMON_HOST2 = "127.0.0.1";
|
|
3655
|
-
var homeDir = os4.homedir();
|
|
3656
|
-
var DAEMON_PID_FILE = path6.join(homeDir, ".node9", "daemon.pid");
|
|
3657
|
-
var DECISIONS_FILE = path6.join(homeDir, ".node9", "decisions.json");
|
|
3658
|
-
var GLOBAL_CONFIG_FILE = path6.join(homeDir, ".node9", "config.json");
|
|
3659
|
-
var CREDENTIALS_FILE = path6.join(homeDir, ".node9", "credentials.json");
|
|
3660
|
-
var AUDIT_LOG_FILE = path6.join(homeDir, ".node9", "audit.log");
|
|
3661
|
-
var TRUST_FILE2 = path6.join(homeDir, ".node9", "trust.json");
|
|
3662
3919
|
function atomicWriteSync2(filePath, data, options) {
|
|
3663
3920
|
const dir = path6.dirname(filePath);
|
|
3664
3921
|
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
3665
|
-
const tmpPath = `${filePath}.${
|
|
3922
|
+
const tmpPath = `${filePath}.${randomUUID2()}.tmp`;
|
|
3666
3923
|
fs4.writeFileSync(tmpPath, data, options);
|
|
3667
3924
|
fs4.renameSync(tmpPath, filePath);
|
|
3668
3925
|
}
|
|
@@ -3680,12 +3937,6 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
3680
3937
|
} catch {
|
|
3681
3938
|
}
|
|
3682
3939
|
}
|
|
3683
|
-
var TRUST_DURATIONS = {
|
|
3684
|
-
"30m": 30 * 6e4,
|
|
3685
|
-
"1h": 60 * 6e4,
|
|
3686
|
-
"2h": 2 * 60 * 6e4
|
|
3687
|
-
};
|
|
3688
|
-
var SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
3689
3940
|
function redactArgs(value) {
|
|
3690
3941
|
if (!value || typeof value !== "object") return value;
|
|
3691
3942
|
if (Array.isArray(value)) return value.map(redactArgs);
|
|
@@ -3720,7 +3971,6 @@ function getAuditHistory(limit = 20) {
|
|
|
3720
3971
|
return [];
|
|
3721
3972
|
}
|
|
3722
3973
|
}
|
|
3723
|
-
var AUTO_DENY_MS = 12e4;
|
|
3724
3974
|
function getOrgName() {
|
|
3725
3975
|
try {
|
|
3726
3976
|
if (fs4.existsSync(CREDENTIALS_FILE)) {
|
|
@@ -3730,7 +3980,6 @@ function getOrgName() {
|
|
|
3730
3980
|
}
|
|
3731
3981
|
return null;
|
|
3732
3982
|
}
|
|
3733
|
-
var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
3734
3983
|
function hasStoredSlackKey() {
|
|
3735
3984
|
return fs4.existsSync(CREDENTIALS_FILE);
|
|
3736
3985
|
}
|
|
@@ -3746,11 +3995,6 @@ function writeGlobalSetting(key, value) {
|
|
|
3746
3995
|
config.settings[key] = value;
|
|
3747
3996
|
atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3748
3997
|
}
|
|
3749
|
-
var pending = /* @__PURE__ */ new Map();
|
|
3750
|
-
var sseClients = /* @__PURE__ */ new Set();
|
|
3751
|
-
var abandonTimer = null;
|
|
3752
|
-
var daemonServer = null;
|
|
3753
|
-
var hadBrowserClient = false;
|
|
3754
3998
|
function abandonPending() {
|
|
3755
3999
|
abandonTimer = null;
|
|
3756
4000
|
pending.forEach((entry, id) => {
|
|
@@ -3772,6 +4016,18 @@ function abandonPending() {
|
|
|
3772
4016
|
}
|
|
3773
4017
|
}
|
|
3774
4018
|
function broadcast(event, data) {
|
|
4019
|
+
if (event === "activity") {
|
|
4020
|
+
activityRing.push({ event, data });
|
|
4021
|
+
if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
|
|
4022
|
+
} else if (event === "activity-result") {
|
|
4023
|
+
const { id, status, label } = data;
|
|
4024
|
+
for (let i = activityRing.length - 1; i >= 0; i--) {
|
|
4025
|
+
if (activityRing[i].data.id === id) {
|
|
4026
|
+
Object.assign(activityRing[i].data, { status, label });
|
|
4027
|
+
break;
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
3775
4031
|
const msg = `event: ${event}
|
|
3776
4032
|
data: ${JSON.stringify(data)}
|
|
3777
4033
|
|
|
@@ -3817,13 +4073,15 @@ function writePersistentDecision(toolName, decision) {
|
|
|
3817
4073
|
}
|
|
3818
4074
|
}
|
|
3819
4075
|
function startDaemon() {
|
|
3820
|
-
const csrfToken =
|
|
3821
|
-
const internalToken =
|
|
4076
|
+
const csrfToken = randomUUID2();
|
|
4077
|
+
const internalToken = randomUUID2();
|
|
3822
4078
|
const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
|
|
3823
4079
|
const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
|
|
3824
4080
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
4081
|
+
const watchMode = process.env.NODE9_WATCH_MODE === "1";
|
|
3825
4082
|
let idleTimer;
|
|
3826
4083
|
function resetIdleTimer() {
|
|
4084
|
+
if (watchMode) return;
|
|
3827
4085
|
if (idleTimer) clearTimeout(idleTimer);
|
|
3828
4086
|
idleTimer = setTimeout(() => {
|
|
3829
4087
|
if (autoStarted) {
|
|
@@ -3878,6 +4136,12 @@ data: ${JSON.stringify({
|
|
|
3878
4136
|
data: ${JSON.stringify(readPersistentDecisions())}
|
|
3879
4137
|
|
|
3880
4138
|
`);
|
|
4139
|
+
for (const item of activityRing) {
|
|
4140
|
+
res.write(`event: ${item.event}
|
|
4141
|
+
data: ${JSON.stringify(item.data)}
|
|
4142
|
+
|
|
4143
|
+
`);
|
|
4144
|
+
}
|
|
3881
4145
|
return req.on("close", () => {
|
|
3882
4146
|
sseClients.delete(res);
|
|
3883
4147
|
if (sseClients.size === 0 && pending.size > 0) {
|
|
@@ -3897,9 +4161,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3897
4161
|
slackDelegated = false,
|
|
3898
4162
|
agent,
|
|
3899
4163
|
mcpServer,
|
|
3900
|
-
riskMetadata
|
|
4164
|
+
riskMetadata,
|
|
4165
|
+
fromCLI = false,
|
|
4166
|
+
activityId
|
|
3901
4167
|
} = JSON.parse(body);
|
|
3902
|
-
const id =
|
|
4168
|
+
const id = fromCLI && typeof activityId === "string" && activityId || randomUUID2();
|
|
3903
4169
|
const entry = {
|
|
3904
4170
|
id,
|
|
3905
4171
|
toolName,
|
|
@@ -3930,6 +4196,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3930
4196
|
}, AUTO_DENY_MS)
|
|
3931
4197
|
};
|
|
3932
4198
|
pending.set(id, entry);
|
|
4199
|
+
if (!fromCLI) {
|
|
4200
|
+
broadcast("activity", {
|
|
4201
|
+
id,
|
|
4202
|
+
ts: entry.timestamp,
|
|
4203
|
+
tool: toolName,
|
|
4204
|
+
args: redactArgs(args),
|
|
4205
|
+
status: "pending"
|
|
4206
|
+
});
|
|
4207
|
+
}
|
|
3933
4208
|
const browserEnabled = getConfig().settings.approvers?.browser !== false;
|
|
3934
4209
|
if (browserEnabled) {
|
|
3935
4210
|
broadcast("add", {
|
|
@@ -3959,6 +4234,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3959
4234
|
const e = pending.get(id);
|
|
3960
4235
|
if (!e) return;
|
|
3961
4236
|
if (result.noApprovalMechanism) return;
|
|
4237
|
+
broadcast("activity-result", {
|
|
4238
|
+
id,
|
|
4239
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
4240
|
+
label: result.blockedByLabel
|
|
4241
|
+
});
|
|
3962
4242
|
clearTimeout(e.timer);
|
|
3963
4243
|
const decision = result.approved ? "allow" : "deny";
|
|
3964
4244
|
appendAuditLog({ toolName: e.toolName, args: e.args, decision });
|
|
@@ -3993,8 +4273,8 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3993
4273
|
const entry = pending.get(id);
|
|
3994
4274
|
if (!entry) return res.writeHead(404).end();
|
|
3995
4275
|
if (entry.earlyDecision) {
|
|
4276
|
+
clearTimeout(entry.timer);
|
|
3996
4277
|
pending.delete(id);
|
|
3997
|
-
broadcast("remove", { id });
|
|
3998
4278
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3999
4279
|
const body = { decision: entry.earlyDecision };
|
|
4000
4280
|
if (entry.earlyReason) body.reason = entry.earlyReason;
|
|
@@ -4024,10 +4304,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
4024
4304
|
decision: `trust:${trustDuration}`
|
|
4025
4305
|
});
|
|
4026
4306
|
clearTimeout(entry.timer);
|
|
4027
|
-
if (entry.waiter)
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4307
|
+
if (entry.waiter) {
|
|
4308
|
+
entry.waiter("allow");
|
|
4309
|
+
pending.delete(id);
|
|
4310
|
+
broadcast("remove", { id });
|
|
4311
|
+
} else {
|
|
4312
|
+
entry.earlyDecision = "allow";
|
|
4313
|
+
broadcast("remove", { id });
|
|
4314
|
+
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
4315
|
+
}
|
|
4031
4316
|
res.writeHead(200);
|
|
4032
4317
|
return res.end(JSON.stringify({ ok: true }));
|
|
4033
4318
|
}
|
|
@@ -4039,13 +4324,16 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
4039
4324
|
decision: resolvedDecision
|
|
4040
4325
|
});
|
|
4041
4326
|
clearTimeout(entry.timer);
|
|
4042
|
-
if (entry.waiter)
|
|
4043
|
-
|
|
4327
|
+
if (entry.waiter) {
|
|
4328
|
+
entry.waiter(resolvedDecision, reason);
|
|
4329
|
+
pending.delete(id);
|
|
4330
|
+
broadcast("remove", { id });
|
|
4331
|
+
} else {
|
|
4044
4332
|
entry.earlyDecision = resolvedDecision;
|
|
4045
4333
|
entry.earlyReason = reason;
|
|
4334
|
+
broadcast("remove", { id });
|
|
4335
|
+
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
4046
4336
|
}
|
|
4047
|
-
pending.delete(id);
|
|
4048
|
-
broadcast("remove", { id });
|
|
4049
4337
|
res.writeHead(200);
|
|
4050
4338
|
return res.end(JSON.stringify({ ok: true }));
|
|
4051
4339
|
} catch {
|
|
@@ -4098,116 +4386,675 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
4098
4386
|
res.writeHead(400).end();
|
|
4099
4387
|
}
|
|
4100
4388
|
}
|
|
4101
|
-
if (req.method === "DELETE" && pathname.startsWith("/decisions/")) {
|
|
4102
|
-
if (!validToken(req)) return res.writeHead(403).end();
|
|
4103
|
-
try {
|
|
4104
|
-
const toolName = decodeURIComponent(pathname.split("/").pop());
|
|
4105
|
-
const decisions = readPersistentDecisions();
|
|
4106
|
-
delete decisions[toolName];
|
|
4107
|
-
atomicWriteSync2(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
|
|
4108
|
-
broadcast("decisions", decisions);
|
|
4109
|
-
res.writeHead(200);
|
|
4110
|
-
return res.end(JSON.stringify({ ok: true }));
|
|
4111
|
-
} catch {
|
|
4112
|
-
res.writeHead(400).end();
|
|
4389
|
+
if (req.method === "DELETE" && pathname.startsWith("/decisions/")) {
|
|
4390
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4391
|
+
try {
|
|
4392
|
+
const toolName = decodeURIComponent(pathname.split("/").pop());
|
|
4393
|
+
const decisions = readPersistentDecisions();
|
|
4394
|
+
delete decisions[toolName];
|
|
4395
|
+
atomicWriteSync2(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
|
|
4396
|
+
broadcast("decisions", decisions);
|
|
4397
|
+
res.writeHead(200);
|
|
4398
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4399
|
+
} catch {
|
|
4400
|
+
res.writeHead(400).end();
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
if (req.method === "POST" && pathname.startsWith("/resolve/")) {
|
|
4404
|
+
const internalAuth = req.headers["x-node9-internal"];
|
|
4405
|
+
if (internalAuth !== internalToken) return res.writeHead(403).end();
|
|
4406
|
+
try {
|
|
4407
|
+
const id = pathname.split("/").pop();
|
|
4408
|
+
const entry = pending.get(id);
|
|
4409
|
+
if (!entry) return res.writeHead(404).end();
|
|
4410
|
+
const { decision } = JSON.parse(await readBody(req));
|
|
4411
|
+
appendAuditLog({
|
|
4412
|
+
toolName: entry.toolName,
|
|
4413
|
+
args: entry.args,
|
|
4414
|
+
decision
|
|
4415
|
+
});
|
|
4416
|
+
clearTimeout(entry.timer);
|
|
4417
|
+
if (entry.waiter) entry.waiter(decision);
|
|
4418
|
+
else entry.earlyDecision = decision;
|
|
4419
|
+
pending.delete(id);
|
|
4420
|
+
broadcast("remove", { id });
|
|
4421
|
+
res.writeHead(200);
|
|
4422
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4423
|
+
} catch {
|
|
4424
|
+
res.writeHead(400).end();
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
if (req.method === "GET" && pathname === "/audit") {
|
|
4428
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4429
|
+
return res.end(JSON.stringify(getAuditHistory()));
|
|
4430
|
+
}
|
|
4431
|
+
if (req.method === "GET" && pathname === "/shields") {
|
|
4432
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4433
|
+
const active = readActiveShields();
|
|
4434
|
+
const shields = Object.values(SHIELDS).map((s) => ({
|
|
4435
|
+
name: s.name,
|
|
4436
|
+
description: s.description,
|
|
4437
|
+
active: active.includes(s.name)
|
|
4438
|
+
}));
|
|
4439
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4440
|
+
return res.end(JSON.stringify({ shields }));
|
|
4441
|
+
}
|
|
4442
|
+
if (req.method === "POST" && pathname === "/shields") {
|
|
4443
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4444
|
+
try {
|
|
4445
|
+
const { name, active } = JSON.parse(await readBody(req));
|
|
4446
|
+
if (!SHIELDS[name]) return res.writeHead(400).end();
|
|
4447
|
+
const current = readActiveShields();
|
|
4448
|
+
const updated = active ? [.../* @__PURE__ */ new Set([...current, name])] : current.filter((n) => n !== name);
|
|
4449
|
+
writeActiveShields(updated);
|
|
4450
|
+
_resetConfigCache();
|
|
4451
|
+
const shieldsPayload = Object.values(SHIELDS).map((s) => ({
|
|
4452
|
+
name: s.name,
|
|
4453
|
+
description: s.description,
|
|
4454
|
+
active: updated.includes(s.name)
|
|
4455
|
+
}));
|
|
4456
|
+
broadcast("shields-status", { shields: shieldsPayload });
|
|
4457
|
+
res.writeHead(200);
|
|
4458
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4459
|
+
} catch {
|
|
4460
|
+
res.writeHead(400).end();
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
res.writeHead(404).end();
|
|
4464
|
+
});
|
|
4465
|
+
daemonServer = server;
|
|
4466
|
+
server.on("error", (e) => {
|
|
4467
|
+
if (e.code === "EADDRINUSE") {
|
|
4468
|
+
try {
|
|
4469
|
+
if (fs4.existsSync(DAEMON_PID_FILE)) {
|
|
4470
|
+
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4471
|
+
process.kill(pid, 0);
|
|
4472
|
+
return process.exit(0);
|
|
4473
|
+
}
|
|
4474
|
+
} catch {
|
|
4475
|
+
try {
|
|
4476
|
+
fs4.unlinkSync(DAEMON_PID_FILE);
|
|
4477
|
+
} catch {
|
|
4478
|
+
}
|
|
4479
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
4480
|
+
return;
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
console.error(chalk4.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
|
|
4484
|
+
process.exit(1);
|
|
4485
|
+
});
|
|
4486
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
|
|
4487
|
+
atomicWriteSync2(
|
|
4488
|
+
DAEMON_PID_FILE,
|
|
4489
|
+
JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
|
|
4490
|
+
{ mode: 384 }
|
|
4491
|
+
);
|
|
4492
|
+
console.log(chalk4.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
|
|
4493
|
+
});
|
|
4494
|
+
if (watchMode) {
|
|
4495
|
+
console.log(chalk4.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
|
|
4496
|
+
}
|
|
4497
|
+
try {
|
|
4498
|
+
fs4.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4499
|
+
} catch {
|
|
4500
|
+
}
|
|
4501
|
+
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
4502
|
+
const unixServer = net2.createServer((socket) => {
|
|
4503
|
+
const chunks = [];
|
|
4504
|
+
let bytesReceived = 0;
|
|
4505
|
+
socket.on("data", (chunk) => {
|
|
4506
|
+
bytesReceived += chunk.length;
|
|
4507
|
+
if (bytesReceived > ACTIVITY_MAX_BYTES) {
|
|
4508
|
+
socket.destroy();
|
|
4509
|
+
return;
|
|
4510
|
+
}
|
|
4511
|
+
chunks.push(chunk);
|
|
4512
|
+
});
|
|
4513
|
+
socket.on("end", () => {
|
|
4514
|
+
try {
|
|
4515
|
+
const data = JSON.parse(Buffer.concat(chunks).toString());
|
|
4516
|
+
if (data.status === "pending") {
|
|
4517
|
+
broadcast("activity", {
|
|
4518
|
+
id: data.id,
|
|
4519
|
+
ts: data.ts,
|
|
4520
|
+
tool: data.tool,
|
|
4521
|
+
args: redactArgs(data.args),
|
|
4522
|
+
status: "pending"
|
|
4523
|
+
});
|
|
4524
|
+
} else {
|
|
4525
|
+
broadcast("activity-result", {
|
|
4526
|
+
id: data.id,
|
|
4527
|
+
status: data.status,
|
|
4528
|
+
label: data.label
|
|
4529
|
+
});
|
|
4530
|
+
}
|
|
4531
|
+
} catch {
|
|
4532
|
+
}
|
|
4533
|
+
});
|
|
4534
|
+
socket.on("error", () => {
|
|
4535
|
+
});
|
|
4536
|
+
});
|
|
4537
|
+
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
4538
|
+
process.on("exit", () => {
|
|
4539
|
+
try {
|
|
4540
|
+
fs4.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4541
|
+
} catch {
|
|
4542
|
+
}
|
|
4543
|
+
});
|
|
4544
|
+
}
|
|
4545
|
+
function stopDaemon() {
|
|
4546
|
+
if (!fs4.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
|
|
4547
|
+
try {
|
|
4548
|
+
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4549
|
+
process.kill(pid, "SIGTERM");
|
|
4550
|
+
console.log(chalk4.green("\u2705 Stopped."));
|
|
4551
|
+
} catch {
|
|
4552
|
+
console.log(chalk4.gray("Cleaned up stale PID file."));
|
|
4553
|
+
} finally {
|
|
4554
|
+
try {
|
|
4555
|
+
fs4.unlinkSync(DAEMON_PID_FILE);
|
|
4556
|
+
} catch {
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
function daemonStatus() {
|
|
4561
|
+
if (!fs4.existsSync(DAEMON_PID_FILE))
|
|
4562
|
+
return console.log(chalk4.yellow("Node9 daemon: not running"));
|
|
4563
|
+
try {
|
|
4564
|
+
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4565
|
+
process.kill(pid, 0);
|
|
4566
|
+
console.log(chalk4.green("Node9 daemon: running"));
|
|
4567
|
+
} catch {
|
|
4568
|
+
console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
var ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
|
|
4572
|
+
var init_daemon = __esm({
|
|
4573
|
+
"src/daemon/index.ts"() {
|
|
4574
|
+
"use strict";
|
|
4575
|
+
init_ui2();
|
|
4576
|
+
init_core();
|
|
4577
|
+
init_shields();
|
|
4578
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path6.join(os4.tmpdir(), "node9-activity.sock");
|
|
4579
|
+
DAEMON_PORT2 = 7391;
|
|
4580
|
+
DAEMON_HOST2 = "127.0.0.1";
|
|
4581
|
+
homeDir = os4.homedir();
|
|
4582
|
+
DAEMON_PID_FILE = path6.join(homeDir, ".node9", "daemon.pid");
|
|
4583
|
+
DECISIONS_FILE = path6.join(homeDir, ".node9", "decisions.json");
|
|
4584
|
+
GLOBAL_CONFIG_FILE = path6.join(homeDir, ".node9", "config.json");
|
|
4585
|
+
CREDENTIALS_FILE = path6.join(homeDir, ".node9", "credentials.json");
|
|
4586
|
+
AUDIT_LOG_FILE = path6.join(homeDir, ".node9", "audit.log");
|
|
4587
|
+
TRUST_FILE2 = path6.join(homeDir, ".node9", "trust.json");
|
|
4588
|
+
TRUST_DURATIONS = {
|
|
4589
|
+
"30m": 30 * 6e4,
|
|
4590
|
+
"1h": 60 * 6e4,
|
|
4591
|
+
"2h": 2 * 60 * 6e4
|
|
4592
|
+
};
|
|
4593
|
+
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
4594
|
+
AUTO_DENY_MS = 12e4;
|
|
4595
|
+
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
4596
|
+
pending = /* @__PURE__ */ new Map();
|
|
4597
|
+
sseClients = /* @__PURE__ */ new Set();
|
|
4598
|
+
abandonTimer = null;
|
|
4599
|
+
daemonServer = null;
|
|
4600
|
+
hadBrowserClient = false;
|
|
4601
|
+
ACTIVITY_RING_SIZE = 100;
|
|
4602
|
+
activityRing = [];
|
|
4603
|
+
}
|
|
4604
|
+
});
|
|
4605
|
+
|
|
4606
|
+
// src/tui/tail.ts
|
|
4607
|
+
var tail_exports = {};
|
|
4608
|
+
__export(tail_exports, {
|
|
4609
|
+
startTail: () => startTail
|
|
4610
|
+
});
|
|
4611
|
+
import http2 from "http";
|
|
4612
|
+
import chalk5 from "chalk";
|
|
4613
|
+
import fs6 from "fs";
|
|
4614
|
+
import os6 from "os";
|
|
4615
|
+
import path8 from "path";
|
|
4616
|
+
import readline from "readline";
|
|
4617
|
+
import { spawn as spawn3 } from "child_process";
|
|
4618
|
+
function getIcon(tool) {
|
|
4619
|
+
const t = tool.toLowerCase();
|
|
4620
|
+
for (const [k, v] of Object.entries(ICONS)) {
|
|
4621
|
+
if (t.includes(k)) return v;
|
|
4622
|
+
}
|
|
4623
|
+
return "\u{1F6E0}\uFE0F";
|
|
4624
|
+
}
|
|
4625
|
+
function formatBase(activity) {
|
|
4626
|
+
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
4627
|
+
const icon = getIcon(activity.tool);
|
|
4628
|
+
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
4629
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
4630
|
+
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
4631
|
+
return `${chalk5.gray(time)} ${icon} ${chalk5.white.bold(toolName)} ${chalk5.dim(argsPreview)}`;
|
|
4632
|
+
}
|
|
4633
|
+
function renderResult(activity, result) {
|
|
4634
|
+
const base = formatBase(activity);
|
|
4635
|
+
let status;
|
|
4636
|
+
if (result.status === "allow") {
|
|
4637
|
+
status = chalk5.green("\u2713 ALLOW");
|
|
4638
|
+
} else if (result.status === "dlp") {
|
|
4639
|
+
status = chalk5.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
4640
|
+
} else {
|
|
4641
|
+
status = chalk5.red("\u2717 BLOCK");
|
|
4642
|
+
}
|
|
4643
|
+
if (process.stdout.isTTY) {
|
|
4644
|
+
readline.clearLine(process.stdout, 0);
|
|
4645
|
+
readline.cursorTo(process.stdout, 0);
|
|
4646
|
+
}
|
|
4647
|
+
console.log(`${base} ${status}`);
|
|
4648
|
+
}
|
|
4649
|
+
function renderPending(activity) {
|
|
4650
|
+
if (!process.stdout.isTTY) return;
|
|
4651
|
+
process.stdout.write(`${formatBase(activity)} ${chalk5.yellow("\u25CF \u2026")}\r`);
|
|
4652
|
+
}
|
|
4653
|
+
async function ensureDaemon() {
|
|
4654
|
+
if (fs6.existsSync(PID_FILE)) {
|
|
4655
|
+
try {
|
|
4656
|
+
const { port } = JSON.parse(fs6.readFileSync(PID_FILE, "utf-8"));
|
|
4657
|
+
return port;
|
|
4658
|
+
} catch {
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
console.log(chalk5.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
4662
|
+
const child = spawn3(process.execPath, [process.argv[1], "daemon"], {
|
|
4663
|
+
detached: true,
|
|
4664
|
+
stdio: "ignore",
|
|
4665
|
+
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
4666
|
+
});
|
|
4667
|
+
child.unref();
|
|
4668
|
+
for (let i = 0; i < 20; i++) {
|
|
4669
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
4670
|
+
if (!fs6.existsSync(PID_FILE)) continue;
|
|
4671
|
+
try {
|
|
4672
|
+
const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
|
|
4673
|
+
signal: AbortSignal.timeout(500)
|
|
4674
|
+
});
|
|
4675
|
+
if (res.ok) {
|
|
4676
|
+
const { port } = JSON.parse(fs6.readFileSync(PID_FILE, "utf-8"));
|
|
4677
|
+
return port;
|
|
4678
|
+
}
|
|
4679
|
+
} catch {
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4682
|
+
console.error(chalk5.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
4683
|
+
process.exit(1);
|
|
4684
|
+
}
|
|
4685
|
+
async function startTail(options = {}) {
|
|
4686
|
+
const port = await ensureDaemon();
|
|
4687
|
+
const connectionTime = Date.now();
|
|
4688
|
+
const pending2 = /* @__PURE__ */ new Map();
|
|
4689
|
+
console.log(chalk5.cyan.bold(`
|
|
4690
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk5.dim(`\u2192 localhost:${port}`));
|
|
4691
|
+
if (options.history) {
|
|
4692
|
+
console.log(chalk5.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
4693
|
+
} else {
|
|
4694
|
+
console.log(
|
|
4695
|
+
chalk5.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
4696
|
+
);
|
|
4697
|
+
}
|
|
4698
|
+
process.on("SIGINT", () => {
|
|
4699
|
+
if (process.stdout.isTTY) {
|
|
4700
|
+
readline.clearLine(process.stdout, 0);
|
|
4701
|
+
readline.cursorTo(process.stdout, 0);
|
|
4702
|
+
}
|
|
4703
|
+
console.log(chalk5.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
4704
|
+
process.exit(0);
|
|
4705
|
+
});
|
|
4706
|
+
const req = http2.get(`http://127.0.0.1:${port}/events`, (res) => {
|
|
4707
|
+
if (res.statusCode !== 200) {
|
|
4708
|
+
console.error(chalk5.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
4709
|
+
process.exit(1);
|
|
4710
|
+
}
|
|
4711
|
+
let currentEvent = "";
|
|
4712
|
+
let currentData = "";
|
|
4713
|
+
res.on("error", () => {
|
|
4714
|
+
});
|
|
4715
|
+
const rl = readline.createInterface({ input: res, crlfDelay: Infinity });
|
|
4716
|
+
rl.on("error", () => {
|
|
4717
|
+
});
|
|
4718
|
+
rl.on("line", (line) => {
|
|
4719
|
+
if (line.startsWith("event:")) {
|
|
4720
|
+
currentEvent = line.slice(6).trim();
|
|
4721
|
+
} else if (line.startsWith("data:")) {
|
|
4722
|
+
currentData = line.slice(5).trim();
|
|
4723
|
+
} else if (line === "") {
|
|
4724
|
+
if (currentEvent && currentData) {
|
|
4725
|
+
handleMessage(currentEvent, currentData);
|
|
4726
|
+
}
|
|
4727
|
+
currentEvent = "";
|
|
4728
|
+
currentData = "";
|
|
4729
|
+
}
|
|
4730
|
+
});
|
|
4731
|
+
rl.on("close", () => {
|
|
4732
|
+
if (process.stdout.isTTY) {
|
|
4733
|
+
readline.clearLine(process.stdout, 0);
|
|
4734
|
+
readline.cursorTo(process.stdout, 0);
|
|
4735
|
+
}
|
|
4736
|
+
console.log(chalk5.red("\n\u274C Daemon disconnected."));
|
|
4737
|
+
process.exit(1);
|
|
4738
|
+
});
|
|
4739
|
+
});
|
|
4740
|
+
function handleMessage(event, rawData) {
|
|
4741
|
+
let data;
|
|
4742
|
+
try {
|
|
4743
|
+
data = JSON.parse(rawData);
|
|
4744
|
+
} catch {
|
|
4745
|
+
return;
|
|
4746
|
+
}
|
|
4747
|
+
if (event === "activity") {
|
|
4748
|
+
if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
|
|
4749
|
+
if (data.status && data.status !== "pending") {
|
|
4750
|
+
renderResult(data, data);
|
|
4751
|
+
return;
|
|
4752
|
+
}
|
|
4753
|
+
pending2.set(data.id, data);
|
|
4754
|
+
const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
|
|
4755
|
+
if (slowTool) renderPending(data);
|
|
4756
|
+
}
|
|
4757
|
+
if (event === "activity-result") {
|
|
4758
|
+
const original = pending2.get(data.id);
|
|
4759
|
+
if (original) {
|
|
4760
|
+
renderResult(original, data);
|
|
4761
|
+
pending2.delete(data.id);
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
req.on("error", (err) => {
|
|
4766
|
+
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
4767
|
+
console.error(chalk5.red(`
|
|
4768
|
+
\u274C ${msg}`));
|
|
4769
|
+
process.exit(1);
|
|
4770
|
+
});
|
|
4771
|
+
}
|
|
4772
|
+
var PID_FILE, ICONS;
|
|
4773
|
+
var init_tail = __esm({
|
|
4774
|
+
"src/tui/tail.ts"() {
|
|
4775
|
+
"use strict";
|
|
4776
|
+
init_daemon();
|
|
4777
|
+
PID_FILE = path8.join(os6.homedir(), ".node9", "daemon.pid");
|
|
4778
|
+
ICONS = {
|
|
4779
|
+
bash: "\u{1F4BB}",
|
|
4780
|
+
shell: "\u{1F4BB}",
|
|
4781
|
+
terminal: "\u{1F4BB}",
|
|
4782
|
+
read: "\u{1F4D6}",
|
|
4783
|
+
edit: "\u270F\uFE0F",
|
|
4784
|
+
write: "\u270F\uFE0F",
|
|
4785
|
+
glob: "\u{1F4C2}",
|
|
4786
|
+
grep: "\u{1F50D}",
|
|
4787
|
+
agent: "\u{1F916}",
|
|
4788
|
+
search: "\u{1F50D}",
|
|
4789
|
+
sql: "\u{1F5C4}\uFE0F",
|
|
4790
|
+
query: "\u{1F5C4}\uFE0F",
|
|
4791
|
+
list: "\u{1F4C2}",
|
|
4792
|
+
delete: "\u{1F5D1}\uFE0F",
|
|
4793
|
+
web: "\u{1F310}"
|
|
4794
|
+
};
|
|
4795
|
+
}
|
|
4796
|
+
});
|
|
4797
|
+
|
|
4798
|
+
// src/cli.ts
|
|
4799
|
+
init_core();
|
|
4800
|
+
import { Command } from "commander";
|
|
4801
|
+
|
|
4802
|
+
// src/setup.ts
|
|
4803
|
+
import fs3 from "fs";
|
|
4804
|
+
import path5 from "path";
|
|
4805
|
+
import os3 from "os";
|
|
4806
|
+
import chalk3 from "chalk";
|
|
4807
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
4808
|
+
function printDaemonTip() {
|
|
4809
|
+
console.log(
|
|
4810
|
+
chalk3.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + chalk3.white("\n To view your history or manage persistent rules, run:") + chalk3.green("\n node9 daemon --openui")
|
|
4811
|
+
);
|
|
4812
|
+
}
|
|
4813
|
+
function fullPathCommand(subcommand) {
|
|
4814
|
+
if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
|
|
4815
|
+
const nodeExec = process.execPath;
|
|
4816
|
+
const cliScript = process.argv[1];
|
|
4817
|
+
return `${nodeExec} ${cliScript} ${subcommand}`;
|
|
4818
|
+
}
|
|
4819
|
+
function readJson(filePath) {
|
|
4820
|
+
try {
|
|
4821
|
+
if (fs3.existsSync(filePath)) {
|
|
4822
|
+
return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
4823
|
+
}
|
|
4824
|
+
} catch {
|
|
4825
|
+
}
|
|
4826
|
+
return null;
|
|
4827
|
+
}
|
|
4828
|
+
function writeJson(filePath, data) {
|
|
4829
|
+
const dir = path5.dirname(filePath);
|
|
4830
|
+
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
4831
|
+
fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
4832
|
+
}
|
|
4833
|
+
async function setupClaude() {
|
|
4834
|
+
const homeDir2 = os3.homedir();
|
|
4835
|
+
const mcpPath = path5.join(homeDir2, ".claude.json");
|
|
4836
|
+
const hooksPath = path5.join(homeDir2, ".claude", "settings.json");
|
|
4837
|
+
const claudeConfig = readJson(mcpPath) ?? {};
|
|
4838
|
+
const settings = readJson(hooksPath) ?? {};
|
|
4839
|
+
const servers = claudeConfig.mcpServers ?? {};
|
|
4840
|
+
let anythingChanged = false;
|
|
4841
|
+
if (!settings.hooks) settings.hooks = {};
|
|
4842
|
+
const hasPreHook = settings.hooks.PreToolUse?.some(
|
|
4843
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
4844
|
+
);
|
|
4845
|
+
if (!hasPreHook) {
|
|
4846
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
4847
|
+
settings.hooks.PreToolUse.push({
|
|
4848
|
+
matcher: ".*",
|
|
4849
|
+
hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
|
|
4850
|
+
});
|
|
4851
|
+
console.log(chalk3.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
|
|
4852
|
+
anythingChanged = true;
|
|
4853
|
+
}
|
|
4854
|
+
const hasPostHook = settings.hooks.PostToolUse?.some(
|
|
4855
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
4856
|
+
);
|
|
4857
|
+
if (!hasPostHook) {
|
|
4858
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
4859
|
+
settings.hooks.PostToolUse.push({
|
|
4860
|
+
matcher: ".*",
|
|
4861
|
+
hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
|
|
4862
|
+
});
|
|
4863
|
+
console.log(chalk3.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
|
|
4864
|
+
anythingChanged = true;
|
|
4865
|
+
}
|
|
4866
|
+
if (anythingChanged) {
|
|
4867
|
+
writeJson(hooksPath, settings);
|
|
4868
|
+
console.log("");
|
|
4869
|
+
}
|
|
4870
|
+
const serversToWrap = [];
|
|
4871
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
4872
|
+
if (!server.command || server.command === "node9") continue;
|
|
4873
|
+
const parts = [server.command, ...server.args ?? []];
|
|
4874
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
4875
|
+
}
|
|
4876
|
+
if (serversToWrap.length > 0) {
|
|
4877
|
+
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
4878
|
+
console.log(chalk3.white(` ${mcpPath}`));
|
|
4879
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
4880
|
+
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
4881
|
+
}
|
|
4882
|
+
console.log("");
|
|
4883
|
+
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
4884
|
+
if (proceed) {
|
|
4885
|
+
for (const { name, parts } of serversToWrap) {
|
|
4886
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
4113
4887
|
}
|
|
4888
|
+
claudeConfig.mcpServers = servers;
|
|
4889
|
+
writeJson(mcpPath, claudeConfig);
|
|
4890
|
+
console.log(chalk3.green(`
|
|
4891
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
4892
|
+
anythingChanged = true;
|
|
4893
|
+
} else {
|
|
4894
|
+
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
4114
4895
|
}
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4896
|
+
console.log("");
|
|
4897
|
+
}
|
|
4898
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
4899
|
+
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
4900
|
+
printDaemonTip();
|
|
4901
|
+
return;
|
|
4902
|
+
}
|
|
4903
|
+
if (anythingChanged) {
|
|
4904
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
4905
|
+
console.log(chalk3.gray(" Restart Claude Code for changes to take effect."));
|
|
4906
|
+
printDaemonTip();
|
|
4907
|
+
}
|
|
4908
|
+
}
|
|
4909
|
+
async function setupGemini() {
|
|
4910
|
+
const homeDir2 = os3.homedir();
|
|
4911
|
+
const settingsPath = path5.join(homeDir2, ".gemini", "settings.json");
|
|
4912
|
+
const settings = readJson(settingsPath) ?? {};
|
|
4913
|
+
const servers = settings.mcpServers ?? {};
|
|
4914
|
+
let anythingChanged = false;
|
|
4915
|
+
if (!settings.hooks) settings.hooks = {};
|
|
4916
|
+
const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
|
|
4917
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
4918
|
+
);
|
|
4919
|
+
if (!hasBeforeHook) {
|
|
4920
|
+
if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
|
|
4921
|
+
if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
|
|
4922
|
+
settings.hooks.BeforeTool.push({
|
|
4923
|
+
matcher: ".*",
|
|
4924
|
+
hooks: [
|
|
4925
|
+
{
|
|
4926
|
+
name: "node9-check",
|
|
4927
|
+
type: "command",
|
|
4928
|
+
command: fullPathCommand("check"),
|
|
4929
|
+
timeout: 6e5
|
|
4930
|
+
}
|
|
4931
|
+
]
|
|
4932
|
+
});
|
|
4933
|
+
console.log(chalk3.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
4934
|
+
anythingChanged = true;
|
|
4935
|
+
}
|
|
4936
|
+
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
4937
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
4938
|
+
);
|
|
4939
|
+
if (!hasAfterHook) {
|
|
4940
|
+
if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
|
|
4941
|
+
if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
|
|
4942
|
+
settings.hooks.AfterTool.push({
|
|
4943
|
+
matcher: ".*",
|
|
4944
|
+
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
4945
|
+
});
|
|
4946
|
+
console.log(chalk3.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
4947
|
+
anythingChanged = true;
|
|
4948
|
+
}
|
|
4949
|
+
if (anythingChanged) {
|
|
4950
|
+
writeJson(settingsPath, settings);
|
|
4951
|
+
console.log("");
|
|
4952
|
+
}
|
|
4953
|
+
const serversToWrap = [];
|
|
4954
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
4955
|
+
if (!server.command || server.command === "node9") continue;
|
|
4956
|
+
const parts = [server.command, ...server.args ?? []];
|
|
4957
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
4958
|
+
}
|
|
4959
|
+
if (serversToWrap.length > 0) {
|
|
4960
|
+
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
4961
|
+
console.log(chalk3.white(` ${settingsPath} (mcpServers)`));
|
|
4962
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
4963
|
+
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
4964
|
+
}
|
|
4965
|
+
console.log("");
|
|
4966
|
+
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
4967
|
+
if (proceed) {
|
|
4968
|
+
for (const { name, parts } of serversToWrap) {
|
|
4969
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
4137
4970
|
}
|
|
4971
|
+
settings.mcpServers = servers;
|
|
4972
|
+
writeJson(settingsPath, settings);
|
|
4973
|
+
console.log(chalk3.green(`
|
|
4974
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
4975
|
+
anythingChanged = true;
|
|
4976
|
+
} else {
|
|
4977
|
+
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
4138
4978
|
}
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4979
|
+
console.log("");
|
|
4980
|
+
}
|
|
4981
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
4982
|
+
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
|
|
4983
|
+
printDaemonTip();
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
if (anythingChanged) {
|
|
4987
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
4988
|
+
console.log(chalk3.gray(" Restart Gemini CLI for changes to take effect."));
|
|
4989
|
+
printDaemonTip();
|
|
4990
|
+
}
|
|
4991
|
+
}
|
|
4992
|
+
async function setupCursor() {
|
|
4993
|
+
const homeDir2 = os3.homedir();
|
|
4994
|
+
const mcpPath = path5.join(homeDir2, ".cursor", "mcp.json");
|
|
4995
|
+
const mcpConfig = readJson(mcpPath) ?? {};
|
|
4996
|
+
const servers = mcpConfig.mcpServers ?? {};
|
|
4997
|
+
let anythingChanged = false;
|
|
4998
|
+
const serversToWrap = [];
|
|
4999
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
5000
|
+
if (!server.command || server.command === "node9") continue;
|
|
5001
|
+
const parts = [server.command, ...server.args ?? []];
|
|
5002
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
5003
|
+
}
|
|
5004
|
+
if (serversToWrap.length > 0) {
|
|
5005
|
+
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
5006
|
+
console.log(chalk3.white(` ${mcpPath}`));
|
|
5007
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
5008
|
+
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
4142
5009
|
}
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
try {
|
|
4149
|
-
if (fs4.existsSync(DAEMON_PID_FILE)) {
|
|
4150
|
-
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4151
|
-
process.kill(pid, 0);
|
|
4152
|
-
return process.exit(0);
|
|
4153
|
-
}
|
|
4154
|
-
} catch {
|
|
4155
|
-
try {
|
|
4156
|
-
fs4.unlinkSync(DAEMON_PID_FILE);
|
|
4157
|
-
} catch {
|
|
4158
|
-
}
|
|
4159
|
-
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
4160
|
-
return;
|
|
5010
|
+
console.log("");
|
|
5011
|
+
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
5012
|
+
if (proceed) {
|
|
5013
|
+
for (const { name, parts } of serversToWrap) {
|
|
5014
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
4161
5015
|
}
|
|
5016
|
+
mcpConfig.mcpServers = servers;
|
|
5017
|
+
writeJson(mcpPath, mcpConfig);
|
|
5018
|
+
console.log(chalk3.green(`
|
|
5019
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
5020
|
+
anythingChanged = true;
|
|
5021
|
+
} else {
|
|
5022
|
+
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
4162
5023
|
}
|
|
4163
|
-
console.
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
5024
|
+
console.log("");
|
|
5025
|
+
}
|
|
5026
|
+
console.log(
|
|
5027
|
+
chalk3.yellow(
|
|
5028
|
+
" \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
|
|
5029
|
+
)
|
|
5030
|
+
);
|
|
5031
|
+
console.log("");
|
|
5032
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
5033
|
+
console.log(
|
|
5034
|
+
chalk3.blue(
|
|
5035
|
+
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
|
|
5036
|
+
)
|
|
4171
5037
|
);
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
}
|
|
4175
|
-
function stopDaemon() {
|
|
4176
|
-
if (!fs4.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
|
|
4177
|
-
try {
|
|
4178
|
-
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4179
|
-
process.kill(pid, "SIGTERM");
|
|
4180
|
-
console.log(chalk4.green("\u2705 Stopped."));
|
|
4181
|
-
} catch {
|
|
4182
|
-
console.log(chalk4.gray("Cleaned up stale PID file."));
|
|
4183
|
-
} finally {
|
|
4184
|
-
try {
|
|
4185
|
-
fs4.unlinkSync(DAEMON_PID_FILE);
|
|
4186
|
-
} catch {
|
|
4187
|
-
}
|
|
5038
|
+
printDaemonTip();
|
|
5039
|
+
return;
|
|
4188
5040
|
}
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
try {
|
|
4194
|
-
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4195
|
-
process.kill(pid, 0);
|
|
4196
|
-
console.log(chalk4.green("Node9 daemon: running"));
|
|
4197
|
-
} catch {
|
|
4198
|
-
console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
|
|
5041
|
+
if (anythingChanged) {
|
|
5042
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
|
|
5043
|
+
console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
|
|
5044
|
+
printDaemonTip();
|
|
4199
5045
|
}
|
|
4200
5046
|
}
|
|
4201
5047
|
|
|
4202
5048
|
// src/cli.ts
|
|
4203
|
-
|
|
5049
|
+
init_daemon();
|
|
5050
|
+
import { spawn as spawn4, execSync } from "child_process";
|
|
4204
5051
|
import { parseCommandString } from "execa";
|
|
4205
5052
|
import { execa } from "execa";
|
|
4206
|
-
import
|
|
4207
|
-
import
|
|
4208
|
-
import
|
|
4209
|
-
import
|
|
4210
|
-
import
|
|
5053
|
+
import chalk6 from "chalk";
|
|
5054
|
+
import readline2 from "readline";
|
|
5055
|
+
import fs7 from "fs";
|
|
5056
|
+
import path9 from "path";
|
|
5057
|
+
import os7 from "os";
|
|
4211
5058
|
|
|
4212
5059
|
// src/undo.ts
|
|
4213
5060
|
import { spawnSync } from "child_process";
|
|
@@ -4321,9 +5168,10 @@ function applyUndo(hash, cwd) {
|
|
|
4321
5168
|
}
|
|
4322
5169
|
|
|
4323
5170
|
// src/cli.ts
|
|
5171
|
+
init_shields();
|
|
4324
5172
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
4325
5173
|
var { version } = JSON.parse(
|
|
4326
|
-
|
|
5174
|
+
fs7.readFileSync(path9.join(__dirname, "../package.json"), "utf-8")
|
|
4327
5175
|
);
|
|
4328
5176
|
function parseDuration(str) {
|
|
4329
5177
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -4419,7 +5267,7 @@ function openBrowserLocal() {
|
|
|
4419
5267
|
}
|
|
4420
5268
|
async function autoStartDaemonAndWait() {
|
|
4421
5269
|
try {
|
|
4422
|
-
const child =
|
|
5270
|
+
const child = spawn4("node9", ["daemon"], {
|
|
4423
5271
|
detached: true,
|
|
4424
5272
|
stdio: "ignore",
|
|
4425
5273
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -4455,14 +5303,14 @@ async function runProxy(targetCommand) {
|
|
|
4455
5303
|
if (stdout) executable = stdout.trim();
|
|
4456
5304
|
} catch {
|
|
4457
5305
|
}
|
|
4458
|
-
console.log(
|
|
4459
|
-
const child =
|
|
5306
|
+
console.log(chalk6.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
5307
|
+
const child = spawn4(executable, args, {
|
|
4460
5308
|
stdio: ["pipe", "pipe", "inherit"],
|
|
4461
5309
|
// We control STDIN and STDOUT
|
|
4462
5310
|
shell: false,
|
|
4463
5311
|
env: { ...process.env, FORCE_COLOR: "1" }
|
|
4464
5312
|
});
|
|
4465
|
-
const agentIn =
|
|
5313
|
+
const agentIn = readline2.createInterface({ input: process.stdin, terminal: false });
|
|
4466
5314
|
agentIn.on("line", async (line) => {
|
|
4467
5315
|
let message;
|
|
4468
5316
|
try {
|
|
@@ -4480,10 +5328,10 @@ async function runProxy(targetCommand) {
|
|
|
4480
5328
|
agent: "Proxy/MCP"
|
|
4481
5329
|
});
|
|
4482
5330
|
if (!result.approved) {
|
|
4483
|
-
console.error(
|
|
5331
|
+
console.error(chalk6.red(`
|
|
4484
5332
|
\u{1F6D1} Node9 Sudo: Action Blocked`));
|
|
4485
|
-
console.error(
|
|
4486
|
-
console.error(
|
|
5333
|
+
console.error(chalk6.gray(` Tool: ${name}`));
|
|
5334
|
+
console.error(chalk6.gray(` Reason: ${result.reason || "Security Policy"}
|
|
4487
5335
|
`));
|
|
4488
5336
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
4489
5337
|
const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -4525,14 +5373,14 @@ async function runProxy(targetCommand) {
|
|
|
4525
5373
|
}
|
|
4526
5374
|
program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
|
|
4527
5375
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
4528
|
-
const credPath =
|
|
4529
|
-
if (!
|
|
4530
|
-
|
|
5376
|
+
const credPath = path9.join(os7.homedir(), ".node9", "credentials.json");
|
|
5377
|
+
if (!fs7.existsSync(path9.dirname(credPath)))
|
|
5378
|
+
fs7.mkdirSync(path9.dirname(credPath), { recursive: true });
|
|
4531
5379
|
const profileName = options.profile || "default";
|
|
4532
5380
|
let existingCreds = {};
|
|
4533
5381
|
try {
|
|
4534
|
-
if (
|
|
4535
|
-
const raw = JSON.parse(
|
|
5382
|
+
if (fs7.existsSync(credPath)) {
|
|
5383
|
+
const raw = JSON.parse(fs7.readFileSync(credPath, "utf-8"));
|
|
4536
5384
|
if (raw.apiKey) {
|
|
4537
5385
|
existingCreds = {
|
|
4538
5386
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -4544,13 +5392,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4544
5392
|
} catch {
|
|
4545
5393
|
}
|
|
4546
5394
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
4547
|
-
|
|
5395
|
+
fs7.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
4548
5396
|
if (profileName === "default") {
|
|
4549
|
-
const configPath =
|
|
5397
|
+
const configPath = path9.join(os7.homedir(), ".node9", "config.json");
|
|
4550
5398
|
let config = {};
|
|
4551
5399
|
try {
|
|
4552
|
-
if (
|
|
4553
|
-
config = JSON.parse(
|
|
5400
|
+
if (fs7.existsSync(configPath))
|
|
5401
|
+
config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
|
|
4554
5402
|
} catch {
|
|
4555
5403
|
}
|
|
4556
5404
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -4565,36 +5413,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4565
5413
|
approvers.cloud = false;
|
|
4566
5414
|
}
|
|
4567
5415
|
s.approvers = approvers;
|
|
4568
|
-
if (!
|
|
4569
|
-
|
|
4570
|
-
|
|
5416
|
+
if (!fs7.existsSync(path9.dirname(configPath)))
|
|
5417
|
+
fs7.mkdirSync(path9.dirname(configPath), { recursive: true });
|
|
5418
|
+
fs7.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
4571
5419
|
}
|
|
4572
5420
|
if (options.profile && profileName !== "default") {
|
|
4573
|
-
console.log(
|
|
4574
|
-
console.log(
|
|
5421
|
+
console.log(chalk6.green(`\u2705 Profile "${profileName}" saved`));
|
|
5422
|
+
console.log(chalk6.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
4575
5423
|
} else if (options.local) {
|
|
4576
|
-
console.log(
|
|
4577
|
-
console.log(
|
|
5424
|
+
console.log(chalk6.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
5425
|
+
console.log(chalk6.gray(` All decisions stay on this machine.`));
|
|
4578
5426
|
} else {
|
|
4579
|
-
console.log(
|
|
4580
|
-
console.log(
|
|
5427
|
+
console.log(chalk6.green(`\u2705 Logged in \u2014 agent mode`));
|
|
5428
|
+
console.log(chalk6.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
4581
5429
|
}
|
|
4582
5430
|
});
|
|
4583
5431
|
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
4584
5432
|
if (target === "gemini") return await setupGemini();
|
|
4585
5433
|
if (target === "claude") return await setupClaude();
|
|
4586
5434
|
if (target === "cursor") return await setupCursor();
|
|
4587
|
-
console.error(
|
|
5435
|
+
console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
4588
5436
|
process.exit(1);
|
|
4589
5437
|
});
|
|
4590
5438
|
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
4591
5439
|
if (!target) {
|
|
4592
|
-
console.log(
|
|
4593
|
-
console.log(" Usage: " +
|
|
5440
|
+
console.log(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
5441
|
+
console.log(" Usage: " + chalk6.white("node9 setup <target>") + "\n");
|
|
4594
5442
|
console.log(" Targets:");
|
|
4595
|
-
console.log(" " +
|
|
4596
|
-
console.log(" " +
|
|
4597
|
-
console.log(" " +
|
|
5443
|
+
console.log(" " + chalk6.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
5444
|
+
console.log(" " + chalk6.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
5445
|
+
console.log(" " + chalk6.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
4598
5446
|
console.log("");
|
|
4599
5447
|
return;
|
|
4600
5448
|
}
|
|
@@ -4602,28 +5450,28 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
4602
5450
|
if (t === "gemini") return await setupGemini();
|
|
4603
5451
|
if (t === "claude") return await setupClaude();
|
|
4604
5452
|
if (t === "cursor") return await setupCursor();
|
|
4605
|
-
console.error(
|
|
5453
|
+
console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
4606
5454
|
process.exit(1);
|
|
4607
5455
|
});
|
|
4608
5456
|
program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
4609
|
-
const homeDir2 =
|
|
5457
|
+
const homeDir2 = os7.homedir();
|
|
4610
5458
|
let failures = 0;
|
|
4611
5459
|
function pass(msg) {
|
|
4612
|
-
console.log(
|
|
5460
|
+
console.log(chalk6.green(" \u2705 ") + msg);
|
|
4613
5461
|
}
|
|
4614
5462
|
function fail(msg, hint) {
|
|
4615
|
-
console.log(
|
|
4616
|
-
if (hint) console.log(
|
|
5463
|
+
console.log(chalk6.red(" \u274C ") + msg);
|
|
5464
|
+
if (hint) console.log(chalk6.gray(" " + hint));
|
|
4617
5465
|
failures++;
|
|
4618
5466
|
}
|
|
4619
5467
|
function warn(msg, hint) {
|
|
4620
|
-
console.log(
|
|
4621
|
-
if (hint) console.log(
|
|
5468
|
+
console.log(chalk6.yellow(" \u26A0\uFE0F ") + msg);
|
|
5469
|
+
if (hint) console.log(chalk6.gray(" " + hint));
|
|
4622
5470
|
}
|
|
4623
5471
|
function section(title) {
|
|
4624
|
-
console.log("\n" +
|
|
5472
|
+
console.log("\n" + chalk6.bold(title));
|
|
4625
5473
|
}
|
|
4626
|
-
console.log(
|
|
5474
|
+
console.log(chalk6.cyan.bold(`
|
|
4627
5475
|
\u{1F6E1}\uFE0F Node9 Doctor v${version}
|
|
4628
5476
|
`));
|
|
4629
5477
|
section("Binary");
|
|
@@ -4652,10 +5500,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4652
5500
|
);
|
|
4653
5501
|
}
|
|
4654
5502
|
section("Configuration");
|
|
4655
|
-
const globalConfigPath =
|
|
4656
|
-
if (
|
|
5503
|
+
const globalConfigPath = path9.join(homeDir2, ".node9", "config.json");
|
|
5504
|
+
if (fs7.existsSync(globalConfigPath)) {
|
|
4657
5505
|
try {
|
|
4658
|
-
JSON.parse(
|
|
5506
|
+
JSON.parse(fs7.readFileSync(globalConfigPath, "utf-8"));
|
|
4659
5507
|
pass("~/.node9/config.json found and valid");
|
|
4660
5508
|
} catch {
|
|
4661
5509
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -4663,17 +5511,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4663
5511
|
} else {
|
|
4664
5512
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
4665
5513
|
}
|
|
4666
|
-
const projectConfigPath =
|
|
4667
|
-
if (
|
|
5514
|
+
const projectConfigPath = path9.join(process.cwd(), "node9.config.json");
|
|
5515
|
+
if (fs7.existsSync(projectConfigPath)) {
|
|
4668
5516
|
try {
|
|
4669
|
-
JSON.parse(
|
|
5517
|
+
JSON.parse(fs7.readFileSync(projectConfigPath, "utf-8"));
|
|
4670
5518
|
pass("node9.config.json found and valid (project)");
|
|
4671
5519
|
} catch {
|
|
4672
5520
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
4673
5521
|
}
|
|
4674
5522
|
}
|
|
4675
|
-
const credsPath =
|
|
4676
|
-
if (
|
|
5523
|
+
const credsPath = path9.join(homeDir2, ".node9", "credentials.json");
|
|
5524
|
+
if (fs7.existsSync(credsPath)) {
|
|
4677
5525
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
4678
5526
|
} else {
|
|
4679
5527
|
warn(
|
|
@@ -4682,10 +5530,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4682
5530
|
);
|
|
4683
5531
|
}
|
|
4684
5532
|
section("Agent Hooks");
|
|
4685
|
-
const claudeSettingsPath =
|
|
4686
|
-
if (
|
|
5533
|
+
const claudeSettingsPath = path9.join(homeDir2, ".claude", "settings.json");
|
|
5534
|
+
if (fs7.existsSync(claudeSettingsPath)) {
|
|
4687
5535
|
try {
|
|
4688
|
-
const cs = JSON.parse(
|
|
5536
|
+
const cs = JSON.parse(fs7.readFileSync(claudeSettingsPath, "utf-8"));
|
|
4689
5537
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
4690
5538
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4691
5539
|
);
|
|
@@ -4698,10 +5546,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4698
5546
|
} else {
|
|
4699
5547
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
4700
5548
|
}
|
|
4701
|
-
const geminiSettingsPath =
|
|
4702
|
-
if (
|
|
5549
|
+
const geminiSettingsPath = path9.join(homeDir2, ".gemini", "settings.json");
|
|
5550
|
+
if (fs7.existsSync(geminiSettingsPath)) {
|
|
4703
5551
|
try {
|
|
4704
|
-
const gs = JSON.parse(
|
|
5552
|
+
const gs = JSON.parse(fs7.readFileSync(geminiSettingsPath, "utf-8"));
|
|
4705
5553
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
4706
5554
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4707
5555
|
);
|
|
@@ -4714,10 +5562,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4714
5562
|
} else {
|
|
4715
5563
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
4716
5564
|
}
|
|
4717
|
-
const cursorHooksPath =
|
|
4718
|
-
if (
|
|
5565
|
+
const cursorHooksPath = path9.join(homeDir2, ".cursor", "hooks.json");
|
|
5566
|
+
if (fs7.existsSync(cursorHooksPath)) {
|
|
4719
5567
|
try {
|
|
4720
|
-
const cur = JSON.parse(
|
|
5568
|
+
const cur = JSON.parse(fs7.readFileSync(cursorHooksPath, "utf-8"));
|
|
4721
5569
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
4722
5570
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
4723
5571
|
);
|
|
@@ -4738,9 +5586,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4738
5586
|
}
|
|
4739
5587
|
console.log("");
|
|
4740
5588
|
if (failures === 0) {
|
|
4741
|
-
console.log(
|
|
5589
|
+
console.log(chalk6.green.bold(" All checks passed. Node9 is ready.\n"));
|
|
4742
5590
|
} else {
|
|
4743
|
-
console.log(
|
|
5591
|
+
console.log(chalk6.red.bold(` ${failures} check(s) failed. See hints above.
|
|
4744
5592
|
`));
|
|
4745
5593
|
process.exit(1);
|
|
4746
5594
|
}
|
|
@@ -4755,7 +5603,7 @@ program.command("explain").description(
|
|
|
4755
5603
|
try {
|
|
4756
5604
|
args = JSON.parse(trimmed);
|
|
4757
5605
|
} catch {
|
|
4758
|
-
console.error(
|
|
5606
|
+
console.error(chalk6.red(`
|
|
4759
5607
|
\u274C Invalid JSON: ${trimmed}
|
|
4760
5608
|
`));
|
|
4761
5609
|
process.exit(1);
|
|
@@ -4766,63 +5614,63 @@ program.command("explain").description(
|
|
|
4766
5614
|
}
|
|
4767
5615
|
const result = await explainPolicy(tool, args);
|
|
4768
5616
|
console.log("");
|
|
4769
|
-
console.log(
|
|
5617
|
+
console.log(chalk6.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
4770
5618
|
console.log("");
|
|
4771
|
-
console.log(` ${
|
|
5619
|
+
console.log(` ${chalk6.bold("Tool:")} ${chalk6.white(result.tool)}`);
|
|
4772
5620
|
if (argsRaw) {
|
|
4773
5621
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
4774
|
-
console.log(` ${
|
|
5622
|
+
console.log(` ${chalk6.bold("Input:")} ${chalk6.gray(preview)}`);
|
|
4775
5623
|
}
|
|
4776
5624
|
console.log("");
|
|
4777
|
-
console.log(
|
|
5625
|
+
console.log(chalk6.bold("Config Sources (Waterfall):"));
|
|
4778
5626
|
for (const tier of result.waterfall) {
|
|
4779
|
-
const num =
|
|
5627
|
+
const num = chalk6.gray(` ${tier.tier}.`);
|
|
4780
5628
|
const label = tier.label.padEnd(16);
|
|
4781
5629
|
let statusStr;
|
|
4782
5630
|
if (tier.tier === 1) {
|
|
4783
|
-
statusStr =
|
|
5631
|
+
statusStr = chalk6.gray(tier.note ?? "");
|
|
4784
5632
|
} else if (tier.status === "active") {
|
|
4785
|
-
const loc = tier.path ?
|
|
4786
|
-
const note = tier.note ?
|
|
4787
|
-
statusStr =
|
|
5633
|
+
const loc = tier.path ? chalk6.gray(tier.path) : "";
|
|
5634
|
+
const note = tier.note ? chalk6.gray(`(${tier.note})`) : "";
|
|
5635
|
+
statusStr = chalk6.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
4788
5636
|
} else {
|
|
4789
|
-
statusStr =
|
|
5637
|
+
statusStr = chalk6.gray("\u25CB " + (tier.note ?? "not found"));
|
|
4790
5638
|
}
|
|
4791
|
-
console.log(`${num} ${
|
|
5639
|
+
console.log(`${num} ${chalk6.white(label)} ${statusStr}`);
|
|
4792
5640
|
}
|
|
4793
5641
|
console.log("");
|
|
4794
|
-
console.log(
|
|
5642
|
+
console.log(chalk6.bold("Policy Evaluation:"));
|
|
4795
5643
|
for (const step of result.steps) {
|
|
4796
5644
|
const isFinal = step.isFinal;
|
|
4797
5645
|
let icon;
|
|
4798
|
-
if (step.outcome === "allow") icon =
|
|
4799
|
-
else if (step.outcome === "review") icon =
|
|
4800
|
-
else if (step.outcome === "skip") icon =
|
|
4801
|
-
else icon =
|
|
5646
|
+
if (step.outcome === "allow") icon = chalk6.green(" \u2705");
|
|
5647
|
+
else if (step.outcome === "review") icon = chalk6.red(" \u{1F534}");
|
|
5648
|
+
else if (step.outcome === "skip") icon = chalk6.gray(" \u2500 ");
|
|
5649
|
+
else icon = chalk6.gray(" \u25CB ");
|
|
4802
5650
|
const name = step.name.padEnd(18);
|
|
4803
|
-
const nameStr = isFinal ?
|
|
4804
|
-
const detail = isFinal ?
|
|
4805
|
-
const arrow = isFinal ?
|
|
5651
|
+
const nameStr = isFinal ? chalk6.white.bold(name) : chalk6.white(name);
|
|
5652
|
+
const detail = isFinal ? chalk6.white(step.detail) : chalk6.gray(step.detail);
|
|
5653
|
+
const arrow = isFinal ? chalk6.yellow(" \u2190 STOP") : "";
|
|
4806
5654
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
4807
5655
|
}
|
|
4808
5656
|
console.log("");
|
|
4809
5657
|
if (result.decision === "allow") {
|
|
4810
|
-
console.log(
|
|
5658
|
+
console.log(chalk6.green.bold(" Decision: \u2705 ALLOW") + chalk6.gray(" \u2014 no approval needed"));
|
|
4811
5659
|
} else {
|
|
4812
5660
|
console.log(
|
|
4813
|
-
|
|
5661
|
+
chalk6.red.bold(" Decision: \u{1F534} REVIEW") + chalk6.gray(" \u2014 human approval required")
|
|
4814
5662
|
);
|
|
4815
5663
|
if (result.blockedByLabel) {
|
|
4816
|
-
console.log(
|
|
5664
|
+
console.log(chalk6.gray(` Reason: ${result.blockedByLabel}`));
|
|
4817
5665
|
}
|
|
4818
5666
|
}
|
|
4819
5667
|
console.log("");
|
|
4820
5668
|
});
|
|
4821
5669
|
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
|
|
4822
|
-
const configPath =
|
|
4823
|
-
if (
|
|
4824
|
-
console.log(
|
|
4825
|
-
console.log(
|
|
5670
|
+
const configPath = path9.join(os7.homedir(), ".node9", "config.json");
|
|
5671
|
+
if (fs7.existsSync(configPath) && !options.force) {
|
|
5672
|
+
console.log(chalk6.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
5673
|
+
console.log(chalk6.gray(` Run with --force to overwrite.`));
|
|
4826
5674
|
return;
|
|
4827
5675
|
}
|
|
4828
5676
|
const requestedMode = options.mode.toLowerCase();
|
|
@@ -4834,13 +5682,13 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
4834
5682
|
mode: safeMode
|
|
4835
5683
|
}
|
|
4836
5684
|
};
|
|
4837
|
-
const dir =
|
|
4838
|
-
if (!
|
|
4839
|
-
|
|
4840
|
-
console.log(
|
|
4841
|
-
console.log(
|
|
5685
|
+
const dir = path9.dirname(configPath);
|
|
5686
|
+
if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
|
|
5687
|
+
fs7.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
5688
|
+
console.log(chalk6.green(`\u2705 Global config created: ${configPath}`));
|
|
5689
|
+
console.log(chalk6.cyan(` Mode set to: ${safeMode}`));
|
|
4842
5690
|
console.log(
|
|
4843
|
-
|
|
5691
|
+
chalk6.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
4844
5692
|
);
|
|
4845
5693
|
});
|
|
4846
5694
|
function formatRelativeTime(timestamp) {
|
|
@@ -4854,14 +5702,14 @@ function formatRelativeTime(timestamp) {
|
|
|
4854
5702
|
return new Date(timestamp).toLocaleDateString();
|
|
4855
5703
|
}
|
|
4856
5704
|
program.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
|
|
4857
|
-
const logPath =
|
|
4858
|
-
if (!
|
|
5705
|
+
const logPath = path9.join(os7.homedir(), ".node9", "audit.log");
|
|
5706
|
+
if (!fs7.existsSync(logPath)) {
|
|
4859
5707
|
console.log(
|
|
4860
|
-
|
|
5708
|
+
chalk6.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
4861
5709
|
);
|
|
4862
5710
|
return;
|
|
4863
5711
|
}
|
|
4864
|
-
const raw =
|
|
5712
|
+
const raw = fs7.readFileSync(logPath, "utf-8");
|
|
4865
5713
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
4866
5714
|
let entries = lines.flatMap((line) => {
|
|
4867
5715
|
try {
|
|
@@ -4883,31 +5731,31 @@ program.command("audit").description("View local execution audit log").option("-
|
|
|
4883
5731
|
return;
|
|
4884
5732
|
}
|
|
4885
5733
|
if (entries.length === 0) {
|
|
4886
|
-
console.log(
|
|
5734
|
+
console.log(chalk6.yellow("No matching audit entries."));
|
|
4887
5735
|
return;
|
|
4888
5736
|
}
|
|
4889
5737
|
console.log(
|
|
4890
5738
|
`
|
|
4891
|
-
${
|
|
5739
|
+
${chalk6.bold("Node9 Audit Log")} ${chalk6.dim(`(${entries.length} entries)`)}`
|
|
4892
5740
|
);
|
|
4893
|
-
console.log(
|
|
5741
|
+
console.log(chalk6.dim(" " + "\u2500".repeat(65)));
|
|
4894
5742
|
console.log(
|
|
4895
5743
|
` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
|
|
4896
5744
|
);
|
|
4897
|
-
console.log(
|
|
5745
|
+
console.log(chalk6.dim(" " + "\u2500".repeat(65)));
|
|
4898
5746
|
for (const e of entries) {
|
|
4899
5747
|
const time = formatRelativeTime(String(e.ts)).padEnd(12);
|
|
4900
5748
|
const tool = String(e.tool).slice(0, 17).padEnd(18);
|
|
4901
|
-
const result = e.decision === "allow" ?
|
|
5749
|
+
const result = e.decision === "allow" ? chalk6.green("ALLOW".padEnd(10)) : chalk6.red("DENY".padEnd(10));
|
|
4902
5750
|
const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
|
|
4903
5751
|
const agent = String(e.agent || "unknown");
|
|
4904
5752
|
console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
|
|
4905
5753
|
}
|
|
4906
5754
|
const allowed = entries.filter((e) => e.decision === "allow").length;
|
|
4907
5755
|
const denied = entries.filter((e) => e.decision === "deny").length;
|
|
4908
|
-
console.log(
|
|
5756
|
+
console.log(chalk6.dim(" " + "\u2500".repeat(65)));
|
|
4909
5757
|
console.log(
|
|
4910
|
-
` ${entries.length} entries | ${
|
|
5758
|
+
` ${entries.length} entries | ${chalk6.green(allowed + " allowed")} | ${chalk6.red(denied + " denied")}
|
|
4911
5759
|
`
|
|
4912
5760
|
);
|
|
4913
5761
|
});
|
|
@@ -4918,43 +5766,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4918
5766
|
const settings = mergedConfig.settings;
|
|
4919
5767
|
console.log("");
|
|
4920
5768
|
if (creds && settings.approvers.cloud) {
|
|
4921
|
-
console.log(
|
|
5769
|
+
console.log(chalk6.green(" \u25CF Agent mode") + chalk6.gray(" \u2014 cloud team policy enforced"));
|
|
4922
5770
|
} else if (creds && !settings.approvers.cloud) {
|
|
4923
5771
|
console.log(
|
|
4924
|
-
|
|
5772
|
+
chalk6.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk6.gray(" \u2014 all decisions stay on this machine")
|
|
4925
5773
|
);
|
|
4926
5774
|
} else {
|
|
4927
5775
|
console.log(
|
|
4928
|
-
|
|
5776
|
+
chalk6.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk6.gray(" \u2014 no API key (Local rules only)")
|
|
4929
5777
|
);
|
|
4930
5778
|
}
|
|
4931
5779
|
console.log("");
|
|
4932
5780
|
if (daemonRunning) {
|
|
4933
5781
|
console.log(
|
|
4934
|
-
|
|
5782
|
+
chalk6.green(" \u25CF Daemon running") + chalk6.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
|
|
4935
5783
|
);
|
|
4936
5784
|
} else {
|
|
4937
|
-
console.log(
|
|
5785
|
+
console.log(chalk6.gray(" \u25CB Daemon stopped"));
|
|
4938
5786
|
}
|
|
4939
5787
|
if (settings.enableUndo) {
|
|
4940
5788
|
console.log(
|
|
4941
|
-
|
|
5789
|
+
chalk6.magenta(" \u25CF Undo Engine") + chalk6.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
|
|
4942
5790
|
);
|
|
4943
5791
|
}
|
|
4944
5792
|
console.log("");
|
|
4945
|
-
const modeLabel = settings.mode === "audit" ?
|
|
5793
|
+
const modeLabel = settings.mode === "audit" ? chalk6.blue("audit") : settings.mode === "strict" ? chalk6.red("strict") : chalk6.white("standard");
|
|
4946
5794
|
console.log(` Mode: ${modeLabel}`);
|
|
4947
|
-
const projectConfig =
|
|
4948
|
-
const globalConfig =
|
|
5795
|
+
const projectConfig = path9.join(process.cwd(), "node9.config.json");
|
|
5796
|
+
const globalConfig = path9.join(os7.homedir(), ".node9", "config.json");
|
|
4949
5797
|
console.log(
|
|
4950
|
-
` Local: ${
|
|
5798
|
+
` Local: ${fs7.existsSync(projectConfig) ? chalk6.green("Active (node9.config.json)") : chalk6.gray("Not present")}`
|
|
4951
5799
|
);
|
|
4952
5800
|
console.log(
|
|
4953
|
-
` Global: ${
|
|
5801
|
+
` Global: ${fs7.existsSync(globalConfig) ? chalk6.green("Active (~/.node9/config.json)") : chalk6.gray("Not present")}`
|
|
4954
5802
|
);
|
|
4955
5803
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
4956
5804
|
console.log(
|
|
4957
|
-
` Sandbox: ${
|
|
5805
|
+
` Sandbox: ${chalk6.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
4958
5806
|
);
|
|
4959
5807
|
}
|
|
4960
5808
|
const pauseState = checkPause();
|
|
@@ -4962,47 +5810,63 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4962
5810
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
4963
5811
|
console.log("");
|
|
4964
5812
|
console.log(
|
|
4965
|
-
|
|
5813
|
+
chalk6.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk6.gray(" \u2014 all tool calls allowed")
|
|
4966
5814
|
);
|
|
4967
5815
|
}
|
|
4968
5816
|
console.log("");
|
|
4969
5817
|
});
|
|
4970
|
-
program.command("daemon").description("Run the local approval server").argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").
|
|
5818
|
+
program.command("daemon").description("Run the local approval server").argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").option(
|
|
5819
|
+
"-w, --watch",
|
|
5820
|
+
"Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
|
|
5821
|
+
).action(
|
|
4971
5822
|
async (action, options) => {
|
|
4972
5823
|
const cmd = (action ?? "start").toLowerCase();
|
|
4973
5824
|
if (cmd === "stop") return stopDaemon();
|
|
4974
5825
|
if (cmd === "status") return daemonStatus();
|
|
4975
5826
|
if (cmd !== "start" && action !== void 0) {
|
|
4976
|
-
console.error(
|
|
5827
|
+
console.error(chalk6.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
|
|
4977
5828
|
process.exit(1);
|
|
4978
5829
|
}
|
|
5830
|
+
if (options.watch) {
|
|
5831
|
+
process.env.NODE9_WATCH_MODE = "1";
|
|
5832
|
+
setTimeout(() => {
|
|
5833
|
+
openBrowserLocal();
|
|
5834
|
+
console.log(chalk6.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
5835
|
+
}, 600);
|
|
5836
|
+
startDaemon();
|
|
5837
|
+
return;
|
|
5838
|
+
}
|
|
4979
5839
|
if (options.openui) {
|
|
4980
5840
|
if (isDaemonRunning()) {
|
|
4981
5841
|
openBrowserLocal();
|
|
4982
|
-
console.log(
|
|
5842
|
+
console.log(chalk6.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
4983
5843
|
process.exit(0);
|
|
4984
5844
|
}
|
|
4985
|
-
const child =
|
|
5845
|
+
const child = spawn4("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
4986
5846
|
child.unref();
|
|
4987
5847
|
for (let i = 0; i < 12; i++) {
|
|
4988
5848
|
await new Promise((r) => setTimeout(r, 250));
|
|
4989
5849
|
if (isDaemonRunning()) break;
|
|
4990
5850
|
}
|
|
4991
5851
|
openBrowserLocal();
|
|
4992
|
-
console.log(
|
|
5852
|
+
console.log(chalk6.green(`
|
|
4993
5853
|
\u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
|
|
4994
5854
|
process.exit(0);
|
|
4995
5855
|
}
|
|
4996
5856
|
if (options.background) {
|
|
4997
|
-
const child =
|
|
5857
|
+
const child = spawn4("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
4998
5858
|
child.unref();
|
|
4999
|
-
console.log(
|
|
5859
|
+
console.log(chalk6.green(`
|
|
5000
5860
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
5001
5861
|
process.exit(0);
|
|
5002
5862
|
}
|
|
5003
5863
|
startDaemon();
|
|
5004
5864
|
}
|
|
5005
5865
|
);
|
|
5866
|
+
program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).action(async (options) => {
|
|
5867
|
+
const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
|
|
5868
|
+
await startTail2(options);
|
|
5869
|
+
});
|
|
5006
5870
|
program.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
|
|
5007
5871
|
const processPayload = async (raw) => {
|
|
5008
5872
|
try {
|
|
@@ -5013,9 +5877,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
5013
5877
|
} catch (err) {
|
|
5014
5878
|
const tempConfig = getConfig();
|
|
5015
5879
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
5016
|
-
const logPath =
|
|
5880
|
+
const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
5017
5881
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
5018
|
-
|
|
5882
|
+
fs7.appendFileSync(
|
|
5019
5883
|
logPath,
|
|
5020
5884
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
5021
5885
|
RAW: ${raw}
|
|
@@ -5033,10 +5897,10 @@ RAW: ${raw}
|
|
|
5033
5897
|
}
|
|
5034
5898
|
const config = getConfig();
|
|
5035
5899
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
5036
|
-
const logPath =
|
|
5037
|
-
if (!
|
|
5038
|
-
|
|
5039
|
-
|
|
5900
|
+
const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
5901
|
+
if (!fs7.existsSync(path9.dirname(logPath)))
|
|
5902
|
+
fs7.mkdirSync(path9.dirname(logPath), { recursive: true });
|
|
5903
|
+
fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
5040
5904
|
`);
|
|
5041
5905
|
}
|
|
5042
5906
|
const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
|
|
@@ -5048,18 +5912,18 @@ RAW: ${raw}
|
|
|
5048
5912
|
const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
|
|
5049
5913
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
5050
5914
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
5051
|
-
console.error(
|
|
5915
|
+
console.error(chalk6.bgRed.white.bold(`
|
|
5052
5916
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
5053
|
-
console.error(
|
|
5917
|
+
console.error(chalk6.red.bold(` A sensitive secret was found in the tool arguments!`));
|
|
5054
5918
|
} else {
|
|
5055
|
-
console.error(
|
|
5919
|
+
console.error(chalk6.red(`
|
|
5056
5920
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
5057
5921
|
}
|
|
5058
|
-
console.error(
|
|
5059
|
-
if (result2?.changeHint) console.error(
|
|
5922
|
+
console.error(chalk6.gray(` Triggered by: ${blockedByContext}`));
|
|
5923
|
+
if (result2?.changeHint) console.error(chalk6.cyan(` To change: ${result2.changeHint}`));
|
|
5060
5924
|
console.error("");
|
|
5061
5925
|
const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
|
|
5062
|
-
console.error(
|
|
5926
|
+
console.error(chalk6.dim(` (Detailed instructions sent to AI agent)`));
|
|
5063
5927
|
process.stdout.write(
|
|
5064
5928
|
JSON.stringify({
|
|
5065
5929
|
decision: "block",
|
|
@@ -5090,7 +5954,7 @@ RAW: ${raw}
|
|
|
5090
5954
|
process.exit(0);
|
|
5091
5955
|
}
|
|
5092
5956
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
5093
|
-
console.error(
|
|
5957
|
+
console.error(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
5094
5958
|
const daemonReady = await autoStartDaemonAndWait();
|
|
5095
5959
|
if (daemonReady) {
|
|
5096
5960
|
const retry = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
@@ -5113,9 +5977,9 @@ RAW: ${raw}
|
|
|
5113
5977
|
});
|
|
5114
5978
|
} catch (err) {
|
|
5115
5979
|
if (process.env.NODE9_DEBUG === "1") {
|
|
5116
|
-
const logPath =
|
|
5980
|
+
const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
5117
5981
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
5118
|
-
|
|
5982
|
+
fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
5119
5983
|
`);
|
|
5120
5984
|
}
|
|
5121
5985
|
process.exit(0);
|
|
@@ -5160,10 +6024,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
5160
6024
|
decision: "allowed",
|
|
5161
6025
|
source: "post-hook"
|
|
5162
6026
|
};
|
|
5163
|
-
const logPath =
|
|
5164
|
-
if (!
|
|
5165
|
-
|
|
5166
|
-
|
|
6027
|
+
const logPath = path9.join(os7.homedir(), ".node9", "audit.log");
|
|
6028
|
+
if (!fs7.existsSync(path9.dirname(logPath)))
|
|
6029
|
+
fs7.mkdirSync(path9.dirname(logPath), { recursive: true });
|
|
6030
|
+
fs7.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
5167
6031
|
const config = getConfig();
|
|
5168
6032
|
if (shouldSnapshot(tool, {}, config)) {
|
|
5169
6033
|
await createShadowSnapshot();
|
|
@@ -5190,7 +6054,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
5190
6054
|
const ms = parseDuration(options.duration);
|
|
5191
6055
|
if (ms === null) {
|
|
5192
6056
|
console.error(
|
|
5193
|
-
|
|
6057
|
+
chalk6.red(`
|
|
5194
6058
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
5195
6059
|
`)
|
|
5196
6060
|
);
|
|
@@ -5198,20 +6062,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
5198
6062
|
}
|
|
5199
6063
|
pauseNode9(ms, options.duration);
|
|
5200
6064
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
5201
|
-
console.log(
|
|
6065
|
+
console.log(chalk6.yellow(`
|
|
5202
6066
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
5203
|
-
console.log(
|
|
5204
|
-
console.log(
|
|
6067
|
+
console.log(chalk6.gray(` All tool calls will be allowed without review.`));
|
|
6068
|
+
console.log(chalk6.gray(` Run "node9 resume" to re-enable early.
|
|
5205
6069
|
`));
|
|
5206
6070
|
});
|
|
5207
6071
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
5208
6072
|
const { paused } = checkPause();
|
|
5209
6073
|
if (!paused) {
|
|
5210
|
-
console.log(
|
|
6074
|
+
console.log(chalk6.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
5211
6075
|
return;
|
|
5212
6076
|
}
|
|
5213
6077
|
resumeNode9();
|
|
5214
|
-
console.log(
|
|
6078
|
+
console.log(chalk6.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
5215
6079
|
});
|
|
5216
6080
|
var HOOK_BASED_AGENTS = {
|
|
5217
6081
|
claude: "claude",
|
|
@@ -5224,15 +6088,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
5224
6088
|
if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
|
|
5225
6089
|
const target = HOOK_BASED_AGENTS[firstArg];
|
|
5226
6090
|
console.error(
|
|
5227
|
-
|
|
6091
|
+
chalk6.yellow(`
|
|
5228
6092
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
5229
6093
|
);
|
|
5230
|
-
console.error(
|
|
6094
|
+
console.error(chalk6.white(`
|
|
5231
6095
|
"${target}" uses its own hook system. Use:`));
|
|
5232
6096
|
console.error(
|
|
5233
|
-
|
|
6097
|
+
chalk6.green(` node9 addto ${target} `) + chalk6.gray("# one-time setup")
|
|
5234
6098
|
);
|
|
5235
|
-
console.error(
|
|
6099
|
+
console.error(chalk6.green(` ${target} `) + chalk6.gray("# run normally"));
|
|
5236
6100
|
process.exit(1);
|
|
5237
6101
|
}
|
|
5238
6102
|
const fullCommand = commandArgs.join(" ");
|
|
@@ -5240,7 +6104,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
5240
6104
|
agent: "Terminal"
|
|
5241
6105
|
});
|
|
5242
6106
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
5243
|
-
console.error(
|
|
6107
|
+
console.error(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
5244
6108
|
const daemonReady = await autoStartDaemonAndWait();
|
|
5245
6109
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
5246
6110
|
}
|
|
@@ -5249,12 +6113,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
5249
6113
|
}
|
|
5250
6114
|
if (!result.approved) {
|
|
5251
6115
|
console.error(
|
|
5252
|
-
|
|
6116
|
+
chalk6.red(`
|
|
5253
6117
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
5254
6118
|
);
|
|
5255
6119
|
process.exit(1);
|
|
5256
6120
|
}
|
|
5257
|
-
console.error(
|
|
6121
|
+
console.error(chalk6.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
5258
6122
|
await runProxy(fullCommand);
|
|
5259
6123
|
} else {
|
|
5260
6124
|
program.help();
|
|
@@ -5269,22 +6133,22 @@ program.command("undo").description(
|
|
|
5269
6133
|
if (history.length === 0) {
|
|
5270
6134
|
if (!options.all && allHistory.length > 0) {
|
|
5271
6135
|
console.log(
|
|
5272
|
-
|
|
6136
|
+
chalk6.yellow(
|
|
5273
6137
|
`
|
|
5274
6138
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
5275
|
-
Run ${
|
|
6139
|
+
Run ${chalk6.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
5276
6140
|
`
|
|
5277
6141
|
)
|
|
5278
6142
|
);
|
|
5279
6143
|
} else {
|
|
5280
|
-
console.log(
|
|
6144
|
+
console.log(chalk6.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
5281
6145
|
}
|
|
5282
6146
|
return;
|
|
5283
6147
|
}
|
|
5284
6148
|
const idx = history.length - steps;
|
|
5285
6149
|
if (idx < 0) {
|
|
5286
6150
|
console.log(
|
|
5287
|
-
|
|
6151
|
+
chalk6.yellow(
|
|
5288
6152
|
`
|
|
5289
6153
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
5290
6154
|
`
|
|
@@ -5295,18 +6159,18 @@ program.command("undo").description(
|
|
|
5295
6159
|
const snapshot = history[idx];
|
|
5296
6160
|
const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
|
|
5297
6161
|
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
|
|
5298
|
-
console.log(
|
|
6162
|
+
console.log(chalk6.magenta.bold(`
|
|
5299
6163
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
|
|
5300
6164
|
console.log(
|
|
5301
|
-
|
|
5302
|
-
` Tool: ${
|
|
6165
|
+
chalk6.white(
|
|
6166
|
+
` Tool: ${chalk6.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk6.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
5303
6167
|
)
|
|
5304
6168
|
);
|
|
5305
|
-
console.log(
|
|
5306
|
-
console.log(
|
|
6169
|
+
console.log(chalk6.white(` When: ${chalk6.gray(ageStr)}`));
|
|
6170
|
+
console.log(chalk6.white(` Dir: ${chalk6.gray(snapshot.cwd)}`));
|
|
5307
6171
|
if (steps > 1)
|
|
5308
6172
|
console.log(
|
|
5309
|
-
|
|
6173
|
+
chalk6.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
5310
6174
|
);
|
|
5311
6175
|
console.log("");
|
|
5312
6176
|
const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
@@ -5314,21 +6178,21 @@ program.command("undo").description(
|
|
|
5314
6178
|
const lines = diff.split("\n");
|
|
5315
6179
|
for (const line of lines) {
|
|
5316
6180
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
5317
|
-
console.log(
|
|
6181
|
+
console.log(chalk6.bold(line));
|
|
5318
6182
|
} else if (line.startsWith("+")) {
|
|
5319
|
-
console.log(
|
|
6183
|
+
console.log(chalk6.green(line));
|
|
5320
6184
|
} else if (line.startsWith("-")) {
|
|
5321
|
-
console.log(
|
|
6185
|
+
console.log(chalk6.red(line));
|
|
5322
6186
|
} else if (line.startsWith("@@")) {
|
|
5323
|
-
console.log(
|
|
6187
|
+
console.log(chalk6.cyan(line));
|
|
5324
6188
|
} else {
|
|
5325
|
-
console.log(
|
|
6189
|
+
console.log(chalk6.gray(line));
|
|
5326
6190
|
}
|
|
5327
6191
|
}
|
|
5328
6192
|
console.log("");
|
|
5329
6193
|
} else {
|
|
5330
6194
|
console.log(
|
|
5331
|
-
|
|
6195
|
+
chalk6.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
5332
6196
|
);
|
|
5333
6197
|
}
|
|
5334
6198
|
const proceed = await confirm3({
|
|
@@ -5337,42 +6201,42 @@ program.command("undo").description(
|
|
|
5337
6201
|
});
|
|
5338
6202
|
if (proceed) {
|
|
5339
6203
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
5340
|
-
console.log(
|
|
6204
|
+
console.log(chalk6.green("\n\u2705 Reverted successfully.\n"));
|
|
5341
6205
|
} else {
|
|
5342
|
-
console.error(
|
|
6206
|
+
console.error(chalk6.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
5343
6207
|
}
|
|
5344
6208
|
} else {
|
|
5345
|
-
console.log(
|
|
6209
|
+
console.log(chalk6.gray("\nCancelled.\n"));
|
|
5346
6210
|
}
|
|
5347
6211
|
});
|
|
5348
6212
|
var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
|
|
5349
6213
|
shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
|
|
5350
6214
|
const name = resolveShieldName(service);
|
|
5351
6215
|
if (!name) {
|
|
5352
|
-
console.error(
|
|
6216
|
+
console.error(chalk6.red(`
|
|
5353
6217
|
\u274C Unknown shield: "${service}"
|
|
5354
6218
|
`));
|
|
5355
|
-
console.log(`Run ${
|
|
6219
|
+
console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
|
|
5356
6220
|
`);
|
|
5357
6221
|
process.exit(1);
|
|
5358
6222
|
}
|
|
5359
6223
|
const shield = getShield(name);
|
|
5360
6224
|
const active = readActiveShields();
|
|
5361
6225
|
if (active.includes(name)) {
|
|
5362
|
-
console.log(
|
|
6226
|
+
console.log(chalk6.yellow(`
|
|
5363
6227
|
\u2139\uFE0F Shield "${name}" is already active.
|
|
5364
6228
|
`));
|
|
5365
6229
|
return;
|
|
5366
6230
|
}
|
|
5367
6231
|
writeActiveShields([...active, name]);
|
|
5368
|
-
console.log(
|
|
6232
|
+
console.log(chalk6.green(`
|
|
5369
6233
|
\u{1F6E1}\uFE0F Shield "${name}" enabled.`));
|
|
5370
|
-
console.log(
|
|
6234
|
+
console.log(chalk6.gray(` ${shield.smartRules.length} smart rules now active.`));
|
|
5371
6235
|
if (shield.dangerousWords.length > 0)
|
|
5372
|
-
console.log(
|
|
6236
|
+
console.log(chalk6.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
|
|
5373
6237
|
if (name === "filesystem") {
|
|
5374
6238
|
console.log(
|
|
5375
|
-
|
|
6239
|
+
chalk6.yellow(
|
|
5376
6240
|
`
|
|
5377
6241
|
\u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
|
|
5378
6242
|
Tools like unlink, find -delete, or language-level file ops are not intercepted.`
|
|
@@ -5384,51 +6248,51 @@ shieldCmd.command("enable <service>").description("Enable a security shield for
|
|
|
5384
6248
|
shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
|
|
5385
6249
|
const name = resolveShieldName(service);
|
|
5386
6250
|
if (!name) {
|
|
5387
|
-
console.error(
|
|
6251
|
+
console.error(chalk6.red(`
|
|
5388
6252
|
\u274C Unknown shield: "${service}"
|
|
5389
6253
|
`));
|
|
5390
|
-
console.log(`Run ${
|
|
6254
|
+
console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
|
|
5391
6255
|
`);
|
|
5392
6256
|
process.exit(1);
|
|
5393
6257
|
}
|
|
5394
6258
|
const active = readActiveShields();
|
|
5395
6259
|
if (!active.includes(name)) {
|
|
5396
|
-
console.log(
|
|
6260
|
+
console.log(chalk6.yellow(`
|
|
5397
6261
|
\u2139\uFE0F Shield "${name}" is not active.
|
|
5398
6262
|
`));
|
|
5399
6263
|
return;
|
|
5400
6264
|
}
|
|
5401
6265
|
writeActiveShields(active.filter((s) => s !== name));
|
|
5402
|
-
console.log(
|
|
6266
|
+
console.log(chalk6.green(`
|
|
5403
6267
|
\u{1F6E1}\uFE0F Shield "${name}" disabled.
|
|
5404
6268
|
`));
|
|
5405
6269
|
});
|
|
5406
6270
|
shieldCmd.command("list").description("Show all available shields").action(() => {
|
|
5407
6271
|
const active = new Set(readActiveShields());
|
|
5408
|
-
console.log(
|
|
6272
|
+
console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
|
|
5409
6273
|
for (const shield of listShields()) {
|
|
5410
|
-
const status = active.has(shield.name) ?
|
|
5411
|
-
console.log(` ${status} ${
|
|
6274
|
+
const status = active.has(shield.name) ? chalk6.green("\u25CF enabled") : chalk6.gray("\u25CB disabled");
|
|
6275
|
+
console.log(` ${status} ${chalk6.cyan(shield.name.padEnd(12))} ${shield.description}`);
|
|
5412
6276
|
if (shield.aliases.length > 0)
|
|
5413
|
-
console.log(
|
|
6277
|
+
console.log(chalk6.gray(` aliases: ${shield.aliases.join(", ")}`));
|
|
5414
6278
|
}
|
|
5415
6279
|
console.log("");
|
|
5416
6280
|
});
|
|
5417
6281
|
shieldCmd.command("status").description("Show which shields are currently active").action(() => {
|
|
5418
6282
|
const active = readActiveShields();
|
|
5419
6283
|
if (active.length === 0) {
|
|
5420
|
-
console.log(
|
|
5421
|
-
console.log(`Run ${
|
|
6284
|
+
console.log(chalk6.yellow("\n\u2139\uFE0F No shields are active.\n"));
|
|
6285
|
+
console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
|
|
5422
6286
|
`);
|
|
5423
6287
|
return;
|
|
5424
6288
|
}
|
|
5425
|
-
console.log(
|
|
6289
|
+
console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
|
|
5426
6290
|
for (const name of active) {
|
|
5427
6291
|
const shield = getShield(name);
|
|
5428
6292
|
if (!shield) continue;
|
|
5429
|
-
console.log(` ${
|
|
6293
|
+
console.log(` ${chalk6.green("\u25CF")} ${chalk6.cyan(name)}`);
|
|
5430
6294
|
console.log(
|
|
5431
|
-
|
|
6295
|
+
chalk6.gray(
|
|
5432
6296
|
` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
|
|
5433
6297
|
)
|
|
5434
6298
|
);
|
|
@@ -5439,9 +6303,9 @@ process.on("unhandledRejection", (reason) => {
|
|
|
5439
6303
|
const isCheckHook = process.argv[2] === "check";
|
|
5440
6304
|
if (isCheckHook) {
|
|
5441
6305
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
5442
|
-
const logPath =
|
|
6306
|
+
const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
5443
6307
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
5444
|
-
|
|
6308
|
+
fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
5445
6309
|
`);
|
|
5446
6310
|
}
|
|
5447
6311
|
process.exit(0);
|