@node9/proxy 1.0.14 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -19
- package/dist/cli.js +2139 -1194
- package/dist/cli.mjs +2120 -1173
- package/dist/index.js +93 -16
- package/dist/index.mjs +93 -16
- package/package.json +1 -1
package/dist/cli.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,97 @@ 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
|
+
}).refine(
|
|
401
|
+
(c) => {
|
|
402
|
+
if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
|
|
403
|
+
return true;
|
|
404
|
+
},
|
|
405
|
+
{ message: "matchesGlob and notMatchesGlob conditions require a value field" }
|
|
406
|
+
);
|
|
407
|
+
SmartRuleSchema = z.object({
|
|
408
|
+
name: z.string().optional(),
|
|
409
|
+
tool: z.string().min(1, "Smart rule tool must not be empty"),
|
|
410
|
+
conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
411
|
+
conditionMode: z.enum(["all", "any"]).optional(),
|
|
412
|
+
verdict: z.enum(["allow", "review", "block"], {
|
|
413
|
+
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
414
|
+
}),
|
|
415
|
+
reason: z.string().optional()
|
|
416
|
+
});
|
|
417
|
+
ConfigFileSchema = z.object({
|
|
418
|
+
version: z.string().optional(),
|
|
419
|
+
settings: z.object({
|
|
420
|
+
mode: z.enum(["standard", "strict", "audit"]).optional(),
|
|
421
|
+
autoStartDaemon: z.boolean().optional(),
|
|
422
|
+
enableUndo: z.boolean().optional(),
|
|
423
|
+
enableHookLogDebug: z.boolean().optional(),
|
|
424
|
+
approvalTimeoutMs: z.number().nonnegative().optional(),
|
|
425
|
+
flightRecorder: z.boolean().optional(),
|
|
426
|
+
approvers: z.object({
|
|
427
|
+
native: z.boolean().optional(),
|
|
428
|
+
browser: z.boolean().optional(),
|
|
429
|
+
cloud: z.boolean().optional(),
|
|
430
|
+
terminal: z.boolean().optional()
|
|
431
|
+
}).optional(),
|
|
432
|
+
environment: z.string().optional(),
|
|
433
|
+
slackEnabled: z.boolean().optional(),
|
|
434
|
+
enableTrustSessions: z.boolean().optional(),
|
|
435
|
+
allowGlobalPause: z.boolean().optional()
|
|
436
|
+
}).optional(),
|
|
437
|
+
policy: z.object({
|
|
438
|
+
sandboxPaths: z.array(z.string()).optional(),
|
|
439
|
+
dangerousWords: z.array(noNewlines).optional(),
|
|
440
|
+
ignoredTools: z.array(z.string()).optional(),
|
|
441
|
+
toolInspection: z.record(z.string()).optional(),
|
|
442
|
+
smartRules: z.array(SmartRuleSchema).optional(),
|
|
443
|
+
snapshot: z.object({
|
|
444
|
+
tools: z.array(z.string()).optional(),
|
|
445
|
+
onlyPaths: z.array(z.string()).optional(),
|
|
446
|
+
ignorePaths: z.array(z.string()).optional()
|
|
447
|
+
}).optional(),
|
|
448
|
+
dlp: z.object({
|
|
449
|
+
enabled: z.boolean().optional(),
|
|
450
|
+
scanIgnoredTools: z.boolean().optional()
|
|
451
|
+
}).optional()
|
|
452
|
+
}).optional(),
|
|
453
|
+
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
454
|
+
}).strict({ message: "Config contains unknown top-level keys" });
|
|
455
|
+
}
|
|
456
|
+
});
|
|
436
457
|
|
|
437
458
|
// src/shields.ts
|
|
438
459
|
import fs from "fs";
|
|
439
460
|
import path3 from "path";
|
|
440
461
|
import os from "os";
|
|
441
462
|
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
463
|
function resolveShieldName(input) {
|
|
610
464
|
const lower = input.toLowerCase();
|
|
611
465
|
if (SHIELDS[lower]) return lower;
|
|
@@ -621,7 +475,6 @@ function getShield(name) {
|
|
|
621
475
|
function listShields() {
|
|
622
476
|
return Object.values(SHIELDS);
|
|
623
477
|
}
|
|
624
|
-
var SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
|
|
625
478
|
function readActiveShields() {
|
|
626
479
|
try {
|
|
627
480
|
const raw = fs.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
@@ -646,21 +499,182 @@ function writeActiveShields(active) {
|
|
|
646
499
|
fs.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
|
|
647
500
|
fs.renameSync(tmp, SHIELDS_STATE_FILE);
|
|
648
501
|
}
|
|
502
|
+
var SHIELDS, SHIELDS_STATE_FILE;
|
|
503
|
+
var init_shields = __esm({
|
|
504
|
+
"src/shields.ts"() {
|
|
505
|
+
"use strict";
|
|
506
|
+
SHIELDS = {
|
|
507
|
+
postgres: {
|
|
508
|
+
name: "postgres",
|
|
509
|
+
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
510
|
+
aliases: ["pg", "postgresql"],
|
|
511
|
+
smartRules: [
|
|
512
|
+
{
|
|
513
|
+
name: "shield:postgres:block-drop-table",
|
|
514
|
+
tool: "*",
|
|
515
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
516
|
+
verdict: "block",
|
|
517
|
+
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: "shield:postgres:block-truncate",
|
|
521
|
+
tool: "*",
|
|
522
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
523
|
+
verdict: "block",
|
|
524
|
+
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: "shield:postgres:block-drop-column",
|
|
528
|
+
tool: "*",
|
|
529
|
+
conditions: [
|
|
530
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
531
|
+
],
|
|
532
|
+
verdict: "block",
|
|
533
|
+
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: "shield:postgres:review-grant-revoke",
|
|
537
|
+
tool: "*",
|
|
538
|
+
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
539
|
+
verdict: "review",
|
|
540
|
+
reason: "Permission changes require human approval (Postgres shield)"
|
|
541
|
+
}
|
|
542
|
+
],
|
|
543
|
+
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
544
|
+
},
|
|
545
|
+
github: {
|
|
546
|
+
name: "github",
|
|
547
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
548
|
+
aliases: ["git"],
|
|
549
|
+
smartRules: [
|
|
550
|
+
{
|
|
551
|
+
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
552
|
+
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
553
|
+
name: "shield:github:review-delete-branch-remote",
|
|
554
|
+
tool: "bash",
|
|
555
|
+
conditions: [
|
|
556
|
+
{
|
|
557
|
+
field: "command",
|
|
558
|
+
op: "matches",
|
|
559
|
+
value: "git\\s+push\\s+.*--delete",
|
|
560
|
+
flags: "i"
|
|
561
|
+
}
|
|
562
|
+
],
|
|
563
|
+
verdict: "review",
|
|
564
|
+
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: "shield:github:block-delete-repo",
|
|
568
|
+
tool: "*",
|
|
569
|
+
conditions: [
|
|
570
|
+
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
571
|
+
],
|
|
572
|
+
verdict: "block",
|
|
573
|
+
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
574
|
+
}
|
|
575
|
+
],
|
|
576
|
+
dangerousWords: []
|
|
577
|
+
},
|
|
578
|
+
aws: {
|
|
579
|
+
name: "aws",
|
|
580
|
+
description: "Protects AWS infrastructure from destructive AI operations",
|
|
581
|
+
aliases: ["amazon"],
|
|
582
|
+
smartRules: [
|
|
583
|
+
{
|
|
584
|
+
name: "shield:aws:block-delete-s3-bucket",
|
|
585
|
+
tool: "*",
|
|
586
|
+
conditions: [
|
|
587
|
+
{
|
|
588
|
+
field: "command",
|
|
589
|
+
op: "matches",
|
|
590
|
+
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
591
|
+
flags: "i"
|
|
592
|
+
}
|
|
593
|
+
],
|
|
594
|
+
verdict: "block",
|
|
595
|
+
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: "shield:aws:review-iam-changes",
|
|
599
|
+
tool: "*",
|
|
600
|
+
conditions: [
|
|
601
|
+
{
|
|
602
|
+
field: "command",
|
|
603
|
+
op: "matches",
|
|
604
|
+
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
605
|
+
flags: "i"
|
|
606
|
+
}
|
|
607
|
+
],
|
|
608
|
+
verdict: "review",
|
|
609
|
+
reason: "IAM changes require human approval (AWS shield)"
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
name: "shield:aws:block-ec2-terminate",
|
|
613
|
+
tool: "*",
|
|
614
|
+
conditions: [
|
|
615
|
+
{
|
|
616
|
+
field: "command",
|
|
617
|
+
op: "matches",
|
|
618
|
+
value: "aws\\s+ec2\\s+terminate-instances",
|
|
619
|
+
flags: "i"
|
|
620
|
+
}
|
|
621
|
+
],
|
|
622
|
+
verdict: "block",
|
|
623
|
+
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: "shield:aws:review-rds-delete",
|
|
627
|
+
tool: "*",
|
|
628
|
+
conditions: [
|
|
629
|
+
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
630
|
+
],
|
|
631
|
+
verdict: "review",
|
|
632
|
+
reason: "RDS deletion requires human approval (AWS shield)"
|
|
633
|
+
}
|
|
634
|
+
],
|
|
635
|
+
dangerousWords: []
|
|
636
|
+
},
|
|
637
|
+
filesystem: {
|
|
638
|
+
name: "filesystem",
|
|
639
|
+
description: "Protects the local filesystem from dangerous AI operations",
|
|
640
|
+
aliases: ["fs"],
|
|
641
|
+
smartRules: [
|
|
642
|
+
{
|
|
643
|
+
name: "shield:filesystem:review-chmod-777",
|
|
644
|
+
tool: "bash",
|
|
645
|
+
conditions: [
|
|
646
|
+
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
647
|
+
],
|
|
648
|
+
verdict: "review",
|
|
649
|
+
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: "shield:filesystem:review-write-etc",
|
|
653
|
+
tool: "bash",
|
|
654
|
+
conditions: [
|
|
655
|
+
{
|
|
656
|
+
field: "command",
|
|
657
|
+
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
658
|
+
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
659
|
+
op: "matches",
|
|
660
|
+
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
661
|
+
}
|
|
662
|
+
],
|
|
663
|
+
verdict: "review",
|
|
664
|
+
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
665
|
+
}
|
|
666
|
+
],
|
|
667
|
+
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
668
|
+
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
669
|
+
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
670
|
+
dangerousWords: ["wipefs"]
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
SHIELDS_STATE_FILE = path3.join(os.homedir(), ".node9", "shields.json");
|
|
674
|
+
}
|
|
675
|
+
});
|
|
649
676
|
|
|
650
677
|
// 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
678
|
function maskSecret(raw, pattern) {
|
|
665
679
|
const match = raw.match(pattern);
|
|
666
680
|
if (!match) return "****";
|
|
@@ -671,9 +685,6 @@ function maskSecret(raw, pattern) {
|
|
|
671
685
|
const stars = "*".repeat(Math.min(secret.length - 8, 12));
|
|
672
686
|
return `${prefix}${stars}${suffix}`;
|
|
673
687
|
}
|
|
674
|
-
var MAX_DEPTH = 5;
|
|
675
|
-
var MAX_STRING_BYTES = 1e5;
|
|
676
|
-
var MAX_JSON_PARSE_BYTES = 1e4;
|
|
677
688
|
function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
678
689
|
if (depth > MAX_DEPTH || args === null || args === void 0) return null;
|
|
679
690
|
if (Array.isArray(args)) {
|
|
@@ -716,12 +727,40 @@ function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
|
716
727
|
}
|
|
717
728
|
return null;
|
|
718
729
|
}
|
|
730
|
+
var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
|
|
731
|
+
var init_dlp = __esm({
|
|
732
|
+
"src/dlp.ts"() {
|
|
733
|
+
"use strict";
|
|
734
|
+
DLP_PATTERNS = [
|
|
735
|
+
{ name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
|
|
736
|
+
{ name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
|
|
737
|
+
{ name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
|
|
738
|
+
{ name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
|
|
739
|
+
{ name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
|
|
740
|
+
{
|
|
741
|
+
name: "Private Key (PEM)",
|
|
742
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
743
|
+
severity: "block"
|
|
744
|
+
},
|
|
745
|
+
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
|
|
746
|
+
];
|
|
747
|
+
MAX_DEPTH = 5;
|
|
748
|
+
MAX_STRING_BYTES = 1e5;
|
|
749
|
+
MAX_JSON_PARSE_BYTES = 1e4;
|
|
750
|
+
}
|
|
751
|
+
});
|
|
719
752
|
|
|
720
753
|
// src/core.ts
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
754
|
+
import chalk2 from "chalk";
|
|
755
|
+
import { confirm } from "@inquirer/prompts";
|
|
756
|
+
import fs2 from "fs";
|
|
757
|
+
import path4 from "path";
|
|
758
|
+
import os2 from "os";
|
|
759
|
+
import net from "net";
|
|
760
|
+
import { randomUUID } from "crypto";
|
|
761
|
+
import { spawnSync } from "child_process";
|
|
762
|
+
import pm from "picomatch";
|
|
763
|
+
import { parse } from "sh-syntax";
|
|
725
764
|
function checkPause() {
|
|
726
765
|
try {
|
|
727
766
|
if (!fs2.existsSync(PAUSED_FILE)) return { paused: false };
|
|
@@ -834,9 +873,9 @@ function matchesPattern(text, patterns) {
|
|
|
834
873
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
835
874
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
836
875
|
}
|
|
837
|
-
function getNestedValue(obj,
|
|
876
|
+
function getNestedValue(obj, path10) {
|
|
838
877
|
if (!obj || typeof obj !== "object") return null;
|
|
839
|
-
return
|
|
878
|
+
return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
840
879
|
}
|
|
841
880
|
function shouldSnapshot(toolName, args, config) {
|
|
842
881
|
if (!config.settings.enableUndo) return false;
|
|
@@ -884,7 +923,7 @@ function evaluateSmartConditions(args, rule) {
|
|
|
884
923
|
case "matchesGlob":
|
|
885
924
|
return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
|
|
886
925
|
case "notMatchesGlob":
|
|
887
|
-
return val !== null && cond.value ? !pm.isMatch(val, cond.value) :
|
|
926
|
+
return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
|
|
888
927
|
default:
|
|
889
928
|
return false;
|
|
890
929
|
}
|
|
@@ -906,7 +945,6 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
906
945
|
const fieldName = toolInspection[matchingPattern];
|
|
907
946
|
return fieldName === "sql" || fieldName === "query";
|
|
908
947
|
}
|
|
909
|
-
var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
910
948
|
async function analyzeShellCommand(command) {
|
|
911
949
|
const actions = [];
|
|
912
950
|
const paths = [];
|
|
@@ -988,229 +1026,27 @@ function redactSecrets(text) {
|
|
|
988
1026
|
);
|
|
989
1027
|
return redacted;
|
|
990
1028
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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 {
|
|
1213
|
-
mode: "standard",
|
|
1029
|
+
function _resetConfigCache() {
|
|
1030
|
+
cachedConfig = null;
|
|
1031
|
+
}
|
|
1032
|
+
function getGlobalSettings() {
|
|
1033
|
+
try {
|
|
1034
|
+
const globalConfigPath = path4.join(os2.homedir(), ".node9", "config.json");
|
|
1035
|
+
if (fs2.existsSync(globalConfigPath)) {
|
|
1036
|
+
const parsed = JSON.parse(fs2.readFileSync(globalConfigPath, "utf-8"));
|
|
1037
|
+
const settings = parsed.settings || {};
|
|
1038
|
+
return {
|
|
1039
|
+
mode: settings.mode || "audit",
|
|
1040
|
+
autoStartDaemon: settings.autoStartDaemon !== false,
|
|
1041
|
+
slackEnabled: settings.slackEnabled !== false,
|
|
1042
|
+
enableTrustSessions: settings.enableTrustSessions === true,
|
|
1043
|
+
allowGlobalPause: settings.allowGlobalPause !== false
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
} catch {
|
|
1047
|
+
}
|
|
1048
|
+
return {
|
|
1049
|
+
mode: "audit",
|
|
1214
1050
|
autoStartDaemon: true,
|
|
1215
1051
|
slackEnabled: true,
|
|
1216
1052
|
enableTrustSessions: false,
|
|
@@ -1596,16 +1432,24 @@ function isIgnoredTool(toolName) {
|
|
|
1596
1432
|
const config = getConfig();
|
|
1597
1433
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
1598
1434
|
}
|
|
1599
|
-
var DAEMON_PORT = 7391;
|
|
1600
|
-
var DAEMON_HOST = "127.0.0.1";
|
|
1601
1435
|
function isDaemonRunning() {
|
|
1436
|
+
const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
|
|
1437
|
+
if (fs2.existsSync(pidFile)) {
|
|
1438
|
+
try {
|
|
1439
|
+
const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
|
|
1440
|
+
if (port !== DAEMON_PORT) return false;
|
|
1441
|
+
process.kill(pid, 0);
|
|
1442
|
+
return true;
|
|
1443
|
+
} catch {
|
|
1444
|
+
return false;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1602
1447
|
try {
|
|
1603
|
-
const
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
return true;
|
|
1448
|
+
const r = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
|
|
1449
|
+
encoding: "utf8",
|
|
1450
|
+
timeout: 500
|
|
1451
|
+
});
|
|
1452
|
+
return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
|
|
1609
1453
|
} catch {
|
|
1610
1454
|
return false;
|
|
1611
1455
|
}
|
|
@@ -1621,7 +1465,7 @@ function getPersistentDecision(toolName) {
|
|
|
1621
1465
|
}
|
|
1622
1466
|
return null;
|
|
1623
1467
|
}
|
|
1624
|
-
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
1468
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
|
|
1625
1469
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1626
1470
|
const checkCtrl = new AbortController();
|
|
1627
1471
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -1636,6 +1480,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
|
1636
1480
|
args,
|
|
1637
1481
|
agent: meta?.agent,
|
|
1638
1482
|
mcpServer: meta?.mcpServer,
|
|
1483
|
+
fromCLI: true,
|
|
1484
|
+
// Pass the flight-recorder ID so the daemon uses the same UUID for
|
|
1485
|
+
// activity-result as the CLI used for the pending activity event.
|
|
1486
|
+
// Without this, the two UUIDs never match and tail.ts never resolves
|
|
1487
|
+
// the pending item.
|
|
1488
|
+
activityId,
|
|
1639
1489
|
...riskMetadata && { riskMetadata }
|
|
1640
1490
|
}),
|
|
1641
1491
|
signal: checkCtrl.signal
|
|
@@ -1690,7 +1540,44 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1690
1540
|
signal: AbortSignal.timeout(3e3)
|
|
1691
1541
|
});
|
|
1692
1542
|
}
|
|
1543
|
+
function notifyActivity(data) {
|
|
1544
|
+
return new Promise((resolve) => {
|
|
1545
|
+
try {
|
|
1546
|
+
const payload = JSON.stringify(data);
|
|
1547
|
+
const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
|
|
1548
|
+
sock.on("connect", () => {
|
|
1549
|
+
sock.on("close", resolve);
|
|
1550
|
+
sock.end(payload);
|
|
1551
|
+
});
|
|
1552
|
+
sock.on("error", resolve);
|
|
1553
|
+
} catch {
|
|
1554
|
+
resolve();
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1693
1558
|
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1559
|
+
if (!options?.calledFromDaemon) {
|
|
1560
|
+
const actId = randomUUID();
|
|
1561
|
+
const actTs = Date.now();
|
|
1562
|
+
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
|
|
1563
|
+
const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
|
|
1564
|
+
...options,
|
|
1565
|
+
activityId: actId
|
|
1566
|
+
});
|
|
1567
|
+
if (!result.noApprovalMechanism) {
|
|
1568
|
+
await notifyActivity({
|
|
1569
|
+
id: actId,
|
|
1570
|
+
tool: toolName,
|
|
1571
|
+
ts: actTs,
|
|
1572
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
1573
|
+
label: result.blockedByLabel
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
return result;
|
|
1577
|
+
}
|
|
1578
|
+
return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
|
|
1579
|
+
}
|
|
1580
|
+
async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1694
1581
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1695
1582
|
const pauseState = checkPause();
|
|
1696
1583
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
@@ -1726,6 +1613,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1726
1613
|
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
1727
1614
|
};
|
|
1728
1615
|
}
|
|
1616
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
|
|
1729
1617
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
1730
1618
|
}
|
|
1731
1619
|
}
|
|
@@ -1948,7 +1836,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1948
1836
|
console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
1949
1837
|
`));
|
|
1950
1838
|
}
|
|
1951
|
-
const daemonDecision = await askDaemon(
|
|
1839
|
+
const daemonDecision = await askDaemon(
|
|
1840
|
+
toolName,
|
|
1841
|
+
args,
|
|
1842
|
+
meta,
|
|
1843
|
+
signal,
|
|
1844
|
+
riskMetadata,
|
|
1845
|
+
options?.activityId
|
|
1846
|
+
);
|
|
1952
1847
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1953
1848
|
const isApproved = daemonDecision === "allow";
|
|
1954
1849
|
return {
|
|
@@ -2152,7 +2047,10 @@ function getConfig() {
|
|
|
2152
2047
|
for (const rule of shield.smartRules) {
|
|
2153
2048
|
if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
2154
2049
|
}
|
|
2155
|
-
|
|
2050
|
+
const existingWords = new Set(mergedPolicy.dangerousWords);
|
|
2051
|
+
for (const word of shield.dangerousWords) {
|
|
2052
|
+
if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
|
|
2053
|
+
}
|
|
2156
2054
|
}
|
|
2157
2055
|
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2158
2056
|
for (const rule of ADVISORY_SMART_RULES) {
|
|
@@ -2353,255 +2251,235 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
2353
2251
|
} catch {
|
|
2354
2252
|
}
|
|
2355
2253
|
}
|
|
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
|
-
|
|
2254
|
+
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;
|
|
2255
|
+
var init_core = __esm({
|
|
2256
|
+
"src/core.ts"() {
|
|
2257
|
+
"use strict";
|
|
2258
|
+
init_native();
|
|
2259
|
+
init_context_sniper();
|
|
2260
|
+
init_config_schema();
|
|
2261
|
+
init_shields();
|
|
2262
|
+
init_dlp();
|
|
2263
|
+
PAUSED_FILE = path4.join(os2.homedir(), ".node9", "PAUSED");
|
|
2264
|
+
TRUST_FILE = path4.join(os2.homedir(), ".node9", "trust.json");
|
|
2265
|
+
LOCAL_AUDIT_LOG = path4.join(os2.homedir(), ".node9", "audit.log");
|
|
2266
|
+
HOOK_DEBUG_LOG = path4.join(os2.homedir(), ".node9", "hook-debug.log");
|
|
2267
|
+
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2268
|
+
DANGEROUS_WORDS = [
|
|
2269
|
+
"mkfs",
|
|
2270
|
+
// formats/wipes a filesystem partition
|
|
2271
|
+
"shred"
|
|
2272
|
+
// permanently overwrites file contents (unrecoverable)
|
|
2273
|
+
];
|
|
2274
|
+
DEFAULT_CONFIG = {
|
|
2275
|
+
version: "1.0",
|
|
2276
|
+
settings: {
|
|
2277
|
+
mode: "audit",
|
|
2278
|
+
autoStartDaemon: true,
|
|
2279
|
+
enableUndo: true,
|
|
2280
|
+
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
2281
|
+
enableHookLogDebug: true,
|
|
2282
|
+
approvalTimeoutMs: 3e4,
|
|
2283
|
+
// 30-second auto-deny timeout
|
|
2284
|
+
flightRecorder: true,
|
|
2285
|
+
approvers: { native: true, browser: true, cloud: false, terminal: true }
|
|
2286
|
+
},
|
|
2287
|
+
policy: {
|
|
2288
|
+
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
2289
|
+
dangerousWords: DANGEROUS_WORDS,
|
|
2290
|
+
ignoredTools: [
|
|
2291
|
+
"list_*",
|
|
2292
|
+
"get_*",
|
|
2293
|
+
"read_*",
|
|
2294
|
+
"describe_*",
|
|
2295
|
+
"read",
|
|
2296
|
+
"glob",
|
|
2297
|
+
"grep",
|
|
2298
|
+
"ls",
|
|
2299
|
+
"notebookread",
|
|
2300
|
+
"notebookedit",
|
|
2301
|
+
"webfetch",
|
|
2302
|
+
"websearch",
|
|
2303
|
+
"exitplanmode",
|
|
2304
|
+
"askuserquestion",
|
|
2305
|
+
"agent",
|
|
2306
|
+
"task*",
|
|
2307
|
+
"toolsearch",
|
|
2308
|
+
"mcp__ide__*",
|
|
2309
|
+
"getDiagnostics"
|
|
2310
|
+
],
|
|
2311
|
+
toolInspection: {
|
|
2312
|
+
bash: "command",
|
|
2313
|
+
shell: "command",
|
|
2314
|
+
run_shell_command: "command",
|
|
2315
|
+
"terminal.execute": "command",
|
|
2316
|
+
"postgres:query": "sql"
|
|
2317
|
+
},
|
|
2318
|
+
snapshot: {
|
|
2319
|
+
tools: [
|
|
2320
|
+
"str_replace_based_edit_tool",
|
|
2321
|
+
"write_file",
|
|
2322
|
+
"edit_file",
|
|
2323
|
+
"create_file",
|
|
2324
|
+
"edit",
|
|
2325
|
+
"replace"
|
|
2326
|
+
],
|
|
2327
|
+
onlyPaths: [],
|
|
2328
|
+
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
2329
|
+
},
|
|
2330
|
+
smartRules: [
|
|
2331
|
+
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
2332
|
+
{
|
|
2333
|
+
name: "block-rm-rf-home",
|
|
2334
|
+
tool: "bash",
|
|
2335
|
+
conditionMode: "all",
|
|
2336
|
+
conditions: [
|
|
2337
|
+
{
|
|
2338
|
+
field: "command",
|
|
2339
|
+
op: "matches",
|
|
2340
|
+
value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
|
|
2341
|
+
},
|
|
2342
|
+
{
|
|
2343
|
+
field: "command",
|
|
2344
|
+
op: "matches",
|
|
2345
|
+
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
2346
|
+
}
|
|
2347
|
+
],
|
|
2348
|
+
verdict: "block",
|
|
2349
|
+
reason: "Recursive delete of home directory is irreversible"
|
|
2350
|
+
},
|
|
2351
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
2352
|
+
{
|
|
2353
|
+
name: "no-delete-without-where",
|
|
2354
|
+
tool: "*",
|
|
2355
|
+
conditions: [
|
|
2356
|
+
{ field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
|
|
2357
|
+
{ field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
|
|
2358
|
+
],
|
|
2359
|
+
conditionMode: "all",
|
|
2360
|
+
verdict: "review",
|
|
2361
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
2362
|
+
},
|
|
2363
|
+
{
|
|
2364
|
+
name: "review-drop-truncate-shell",
|
|
2365
|
+
tool: "bash",
|
|
2366
|
+
conditions: [
|
|
2367
|
+
{
|
|
2368
|
+
field: "command",
|
|
2369
|
+
op: "matches",
|
|
2370
|
+
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
2371
|
+
flags: "i"
|
|
2372
|
+
}
|
|
2373
|
+
],
|
|
2374
|
+
conditionMode: "all",
|
|
2375
|
+
verdict: "review",
|
|
2376
|
+
reason: "SQL DDL destructive statement inside a shell command"
|
|
2377
|
+
},
|
|
2378
|
+
// ── Git safety ────────────────────────────────────────────────────────
|
|
2379
|
+
{
|
|
2380
|
+
name: "block-force-push",
|
|
2381
|
+
tool: "bash",
|
|
2382
|
+
conditions: [
|
|
2383
|
+
{
|
|
2384
|
+
field: "command",
|
|
2385
|
+
op: "matches",
|
|
2386
|
+
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
2387
|
+
flags: "i"
|
|
2388
|
+
}
|
|
2389
|
+
],
|
|
2390
|
+
conditionMode: "all",
|
|
2391
|
+
verdict: "block",
|
|
2392
|
+
reason: "Force push overwrites remote history and cannot be undone"
|
|
2393
|
+
},
|
|
2394
|
+
{
|
|
2395
|
+
name: "review-git-push",
|
|
2396
|
+
tool: "bash",
|
|
2397
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
|
|
2398
|
+
conditionMode: "all",
|
|
2399
|
+
verdict: "review",
|
|
2400
|
+
reason: "git push sends changes to a shared remote"
|
|
2401
|
+
},
|
|
2402
|
+
{
|
|
2403
|
+
name: "review-git-destructive",
|
|
2404
|
+
tool: "bash",
|
|
2405
|
+
conditions: [
|
|
2406
|
+
{
|
|
2407
|
+
field: "command",
|
|
2408
|
+
op: "matches",
|
|
2409
|
+
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
2410
|
+
flags: "i"
|
|
2411
|
+
}
|
|
2412
|
+
],
|
|
2413
|
+
conditionMode: "all",
|
|
2414
|
+
verdict: "review",
|
|
2415
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
2416
|
+
},
|
|
2417
|
+
// ── Shell safety ──────────────────────────────────────────────────────
|
|
2418
|
+
{
|
|
2419
|
+
name: "review-sudo",
|
|
2420
|
+
tool: "bash",
|
|
2421
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
2422
|
+
conditionMode: "all",
|
|
2423
|
+
verdict: "review",
|
|
2424
|
+
reason: "Command requires elevated privileges"
|
|
2425
|
+
},
|
|
2426
|
+
{
|
|
2427
|
+
name: "review-curl-pipe-shell",
|
|
2428
|
+
tool: "bash",
|
|
2429
|
+
conditions: [
|
|
2430
|
+
{
|
|
2431
|
+
field: "command",
|
|
2432
|
+
op: "matches",
|
|
2433
|
+
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
2434
|
+
flags: "i"
|
|
2435
|
+
}
|
|
2436
|
+
],
|
|
2437
|
+
conditionMode: "all",
|
|
2438
|
+
verdict: "block",
|
|
2439
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
2440
|
+
}
|
|
2441
|
+
],
|
|
2442
|
+
dlp: { enabled: true, scanIgnoredTools: true }
|
|
2443
|
+
},
|
|
2444
|
+
environments: {}
|
|
2445
|
+
};
|
|
2446
|
+
ADVISORY_SMART_RULES = [
|
|
2447
|
+
{
|
|
2448
|
+
name: "allow-rm-safe-paths",
|
|
2449
|
+
tool: "*",
|
|
2450
|
+
conditionMode: "all",
|
|
2451
|
+
conditions: [
|
|
2452
|
+
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
2453
|
+
{
|
|
2454
|
+
field: "command",
|
|
2455
|
+
op: "matches",
|
|
2456
|
+
// Matches known-safe build artifact paths in the command.
|
|
2457
|
+
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
2458
|
+
}
|
|
2459
|
+
],
|
|
2460
|
+
verdict: "allow",
|
|
2461
|
+
reason: "Deleting a known-safe build artifact path"
|
|
2462
|
+
},
|
|
2463
|
+
{
|
|
2464
|
+
name: "review-rm",
|
|
2465
|
+
tool: "*",
|
|
2466
|
+
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
2467
|
+
verdict: "review",
|
|
2468
|
+
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
2442
2469
|
}
|
|
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;
|
|
2470
|
+
];
|
|
2471
|
+
cachedConfig = null;
|
|
2472
|
+
DAEMON_PORT = 7391;
|
|
2473
|
+
DAEMON_HOST = "127.0.0.1";
|
|
2474
|
+
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path4.join(os2.tmpdir(), "node9-activity.sock");
|
|
2490
2475
|
}
|
|
2491
|
-
|
|
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("");
|
|
2507
|
-
}
|
|
2508
|
-
const serversToWrap = [];
|
|
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
|
-
}
|
|
2476
|
+
});
|
|
2602
2477
|
|
|
2603
2478
|
// src/daemon/ui.html
|
|
2604
|
-
var ui_default
|
|
2479
|
+
var ui_default;
|
|
2480
|
+
var init_ui = __esm({
|
|
2481
|
+
"src/daemon/ui.html"() {
|
|
2482
|
+
ui_default = `<!doctype html>
|
|
2605
2483
|
<html lang="en">
|
|
2606
2484
|
<head>
|
|
2607
2485
|
<meta charset="UTF-8" />
|
|
@@ -2627,6 +2505,11 @@ var ui_default = `<!doctype html>
|
|
|
2627
2505
|
margin: 0;
|
|
2628
2506
|
padding: 0;
|
|
2629
2507
|
}
|
|
2508
|
+
html,
|
|
2509
|
+
body {
|
|
2510
|
+
height: 100%;
|
|
2511
|
+
overflow: hidden;
|
|
2512
|
+
}
|
|
2630
2513
|
body {
|
|
2631
2514
|
background: var(--bg);
|
|
2632
2515
|
color: var(--text);
|
|
@@ -2634,16 +2517,17 @@ var ui_default = `<!doctype html>
|
|
|
2634
2517
|
'Inter',
|
|
2635
2518
|
-apple-system,
|
|
2636
2519
|
sans-serif;
|
|
2637
|
-
min-height: 100vh;
|
|
2638
2520
|
}
|
|
2639
2521
|
|
|
2640
2522
|
.shell {
|
|
2641
|
-
max-width:
|
|
2523
|
+
max-width: 1440px;
|
|
2524
|
+
height: 100vh;
|
|
2642
2525
|
margin: 0 auto;
|
|
2643
|
-
padding:
|
|
2526
|
+
padding: 16px 20px 16px;
|
|
2644
2527
|
display: grid;
|
|
2645
2528
|
grid-template-rows: auto 1fr;
|
|
2646
|
-
gap:
|
|
2529
|
+
gap: 16px;
|
|
2530
|
+
overflow: hidden;
|
|
2647
2531
|
}
|
|
2648
2532
|
header {
|
|
2649
2533
|
display: flex;
|
|
@@ -2680,9 +2564,10 @@ var ui_default = `<!doctype html>
|
|
|
2680
2564
|
|
|
2681
2565
|
.body {
|
|
2682
2566
|
display: grid;
|
|
2683
|
-
grid-template-columns: 1fr
|
|
2684
|
-
gap:
|
|
2685
|
-
|
|
2567
|
+
grid-template-columns: 360px 1fr 270px;
|
|
2568
|
+
gap: 16px;
|
|
2569
|
+
min-height: 0;
|
|
2570
|
+
overflow: hidden;
|
|
2686
2571
|
}
|
|
2687
2572
|
|
|
2688
2573
|
.warning-banner {
|
|
@@ -2702,6 +2587,10 @@ var ui_default = `<!doctype html>
|
|
|
2702
2587
|
|
|
2703
2588
|
.main {
|
|
2704
2589
|
min-width: 0;
|
|
2590
|
+
min-height: 0;
|
|
2591
|
+
overflow-y: auto;
|
|
2592
|
+
scrollbar-width: thin;
|
|
2593
|
+
scrollbar-color: var(--border) transparent;
|
|
2705
2594
|
}
|
|
2706
2595
|
.section-title {
|
|
2707
2596
|
font-size: 11px;
|
|
@@ -2732,14 +2621,64 @@ var ui_default = `<!doctype html>
|
|
|
2732
2621
|
background: var(--card);
|
|
2733
2622
|
border: 1px solid var(--border);
|
|
2734
2623
|
border-radius: 14px;
|
|
2735
|
-
padding:
|
|
2736
|
-
margin-bottom:
|
|
2624
|
+
padding: 20px;
|
|
2625
|
+
margin-bottom: 14px;
|
|
2737
2626
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
2738
2627
|
animation: pop 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
2739
2628
|
}
|
|
2740
2629
|
.card.slack-viewer {
|
|
2741
2630
|
border-color: rgba(83, 155, 245, 0.3);
|
|
2742
2631
|
}
|
|
2632
|
+
.card-header {
|
|
2633
|
+
display: flex;
|
|
2634
|
+
align-items: center;
|
|
2635
|
+
gap: 8px;
|
|
2636
|
+
margin-bottom: 12px;
|
|
2637
|
+
padding-bottom: 12px;
|
|
2638
|
+
border-bottom: 1px solid var(--border);
|
|
2639
|
+
}
|
|
2640
|
+
.card-header-icon {
|
|
2641
|
+
font-size: 16px;
|
|
2642
|
+
}
|
|
2643
|
+
.card-header-title {
|
|
2644
|
+
font-size: 12px;
|
|
2645
|
+
font-weight: 700;
|
|
2646
|
+
color: var(--text-bright);
|
|
2647
|
+
text-transform: uppercase;
|
|
2648
|
+
letter-spacing: 0.5px;
|
|
2649
|
+
}
|
|
2650
|
+
.card-timer {
|
|
2651
|
+
margin-left: auto;
|
|
2652
|
+
font-size: 11px;
|
|
2653
|
+
font-family: 'Fira Code', monospace;
|
|
2654
|
+
color: var(--muted);
|
|
2655
|
+
background: rgba(48, 54, 61, 0.6);
|
|
2656
|
+
padding: 2px 8px;
|
|
2657
|
+
border-radius: 5px;
|
|
2658
|
+
}
|
|
2659
|
+
.card-timer.urgent {
|
|
2660
|
+
color: var(--danger);
|
|
2661
|
+
background: rgba(201, 60, 55, 0.1);
|
|
2662
|
+
}
|
|
2663
|
+
.btn-allow {
|
|
2664
|
+
background: var(--success);
|
|
2665
|
+
color: #fff;
|
|
2666
|
+
grid-column: span 2;
|
|
2667
|
+
font-size: 14px;
|
|
2668
|
+
padding: 13px 14px;
|
|
2669
|
+
}
|
|
2670
|
+
.btn-deny {
|
|
2671
|
+
background: rgba(201, 60, 55, 0.15);
|
|
2672
|
+
color: #e5534b;
|
|
2673
|
+
border: 1px solid rgba(201, 60, 55, 0.3);
|
|
2674
|
+
grid-column: span 2;
|
|
2675
|
+
}
|
|
2676
|
+
.btn-deny:hover:not(:disabled) {
|
|
2677
|
+
background: var(--danger);
|
|
2678
|
+
color: #fff;
|
|
2679
|
+
border-color: transparent;
|
|
2680
|
+
filter: none;
|
|
2681
|
+
}
|
|
2743
2682
|
@keyframes pop {
|
|
2744
2683
|
from {
|
|
2745
2684
|
opacity: 0;
|
|
@@ -2947,24 +2886,178 @@ var ui_default = `<!doctype html>
|
|
|
2947
2886
|
cursor: not-allowed;
|
|
2948
2887
|
}
|
|
2949
2888
|
|
|
2889
|
+
.flight-col {
|
|
2890
|
+
display: flex;
|
|
2891
|
+
flex-direction: column;
|
|
2892
|
+
min-height: 0;
|
|
2893
|
+
overflow: hidden;
|
|
2894
|
+
}
|
|
2895
|
+
.flight-panel {
|
|
2896
|
+
flex: 1;
|
|
2897
|
+
min-height: 0;
|
|
2898
|
+
display: flex;
|
|
2899
|
+
flex-direction: column;
|
|
2900
|
+
overflow: hidden;
|
|
2901
|
+
}
|
|
2950
2902
|
.sidebar {
|
|
2951
2903
|
display: flex;
|
|
2952
2904
|
flex-direction: column;
|
|
2953
2905
|
gap: 12px;
|
|
2954
|
-
|
|
2955
|
-
|
|
2906
|
+
min-height: 0;
|
|
2907
|
+
overflow-y: auto;
|
|
2908
|
+
scrollbar-width: thin;
|
|
2909
|
+
scrollbar-color: var(--border) transparent;
|
|
2956
2910
|
}
|
|
2957
2911
|
.panel {
|
|
2958
2912
|
background: var(--panel);
|
|
2959
2913
|
border: 1px solid var(--border);
|
|
2960
2914
|
border-radius: 12px;
|
|
2961
|
-
padding:
|
|
2915
|
+
padding: 14px;
|
|
2916
|
+
}
|
|
2917
|
+
/* \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 */
|
|
2918
|
+
#activity-feed {
|
|
2919
|
+
display: flex;
|
|
2920
|
+
flex-direction: column;
|
|
2921
|
+
gap: 4px;
|
|
2922
|
+
margin-top: 4px;
|
|
2923
|
+
flex: 1;
|
|
2924
|
+
min-height: 0;
|
|
2925
|
+
overflow-y: auto;
|
|
2926
|
+
scrollbar-width: thin;
|
|
2927
|
+
scrollbar-color: var(--border) transparent;
|
|
2928
|
+
}
|
|
2929
|
+
.feed-row {
|
|
2930
|
+
display: grid;
|
|
2931
|
+
grid-template-columns: 58px 20px 1fr 48px;
|
|
2932
|
+
align-items: start;
|
|
2933
|
+
gap: 6px;
|
|
2934
|
+
background: rgba(22, 27, 34, 0.6);
|
|
2935
|
+
border: 1px solid var(--border);
|
|
2936
|
+
padding: 7px 10px;
|
|
2937
|
+
border-radius: 7px;
|
|
2938
|
+
font-size: 11px;
|
|
2939
|
+
animation: frSlideIn 0.15s ease-out;
|
|
2940
|
+
transition: background 0.1s;
|
|
2941
|
+
cursor: default;
|
|
2942
|
+
}
|
|
2943
|
+
.feed-row:hover {
|
|
2944
|
+
background: rgba(30, 38, 48, 0.9);
|
|
2945
|
+
border-color: rgba(83, 155, 245, 0.2);
|
|
2946
|
+
}
|
|
2947
|
+
@keyframes frSlideIn {
|
|
2948
|
+
from {
|
|
2949
|
+
opacity: 0;
|
|
2950
|
+
transform: translateX(-4px);
|
|
2951
|
+
}
|
|
2952
|
+
to {
|
|
2953
|
+
opacity: 1;
|
|
2954
|
+
transform: none;
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
.feed-ts {
|
|
2958
|
+
color: var(--muted);
|
|
2959
|
+
font-family: monospace;
|
|
2960
|
+
font-size: 9px;
|
|
2961
|
+
}
|
|
2962
|
+
.feed-icon {
|
|
2963
|
+
text-align: center;
|
|
2964
|
+
font-size: 13px;
|
|
2965
|
+
}
|
|
2966
|
+
.feed-content {
|
|
2967
|
+
min-width: 0;
|
|
2968
|
+
color: var(--text-bright);
|
|
2969
|
+
word-break: break-all;
|
|
2970
|
+
}
|
|
2971
|
+
.feed-args {
|
|
2972
|
+
display: block;
|
|
2973
|
+
color: var(--muted);
|
|
2974
|
+
font-family: monospace;
|
|
2975
|
+
margin-top: 2px;
|
|
2976
|
+
font-size: 10px;
|
|
2977
|
+
word-break: break-all;
|
|
2978
|
+
}
|
|
2979
|
+
.feed-badge {
|
|
2980
|
+
text-align: right;
|
|
2981
|
+
font-weight: 700;
|
|
2982
|
+
font-size: 9px;
|
|
2983
|
+
letter-spacing: 0.03em;
|
|
2984
|
+
}
|
|
2985
|
+
.fr-pending {
|
|
2986
|
+
color: var(--muted);
|
|
2987
|
+
}
|
|
2988
|
+
.fr-allow {
|
|
2989
|
+
color: #57ab5a;
|
|
2990
|
+
}
|
|
2991
|
+
.fr-block {
|
|
2992
|
+
color: var(--danger);
|
|
2993
|
+
}
|
|
2994
|
+
.fr-dlp {
|
|
2995
|
+
color: var(--primary);
|
|
2996
|
+
animation: frBlink 1s infinite;
|
|
2962
2997
|
}
|
|
2998
|
+
@keyframes frBlink {
|
|
2999
|
+
50% {
|
|
3000
|
+
opacity: 0.4;
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
.fr-dlp-row {
|
|
3004
|
+
border-color: var(--primary) !important;
|
|
3005
|
+
}
|
|
3006
|
+
.feed-clear-btn {
|
|
3007
|
+
background: transparent;
|
|
3008
|
+
border: none;
|
|
3009
|
+
color: var(--muted);
|
|
3010
|
+
font-size: 10px;
|
|
3011
|
+
padding: 0;
|
|
3012
|
+
cursor: pointer;
|
|
3013
|
+
margin-left: auto;
|
|
3014
|
+
font-family: inherit;
|
|
3015
|
+
font-weight: 500;
|
|
3016
|
+
transition: color 0.15s;
|
|
3017
|
+
}
|
|
3018
|
+
.feed-clear-btn:hover {
|
|
3019
|
+
color: var(--text);
|
|
3020
|
+
filter: none;
|
|
3021
|
+
transform: none;
|
|
3022
|
+
}
|
|
3023
|
+
/* \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 */
|
|
3024
|
+
.shield-row {
|
|
3025
|
+
display: flex;
|
|
3026
|
+
align-items: flex-start;
|
|
3027
|
+
gap: 10px;
|
|
3028
|
+
padding: 8px 0;
|
|
3029
|
+
border-bottom: 1px solid var(--border);
|
|
3030
|
+
}
|
|
3031
|
+
.shield-row:last-child {
|
|
3032
|
+
border-bottom: none;
|
|
3033
|
+
padding-bottom: 0;
|
|
3034
|
+
}
|
|
3035
|
+
.shield-row:first-child {
|
|
3036
|
+
padding-top: 0;
|
|
3037
|
+
}
|
|
3038
|
+
.shield-info {
|
|
3039
|
+
flex: 1;
|
|
3040
|
+
min-width: 0;
|
|
3041
|
+
}
|
|
3042
|
+
.shield-name {
|
|
3043
|
+
font-size: 12px;
|
|
3044
|
+
color: var(--text-bright);
|
|
3045
|
+
font-weight: 600;
|
|
3046
|
+
font-family: 'Fira Code', monospace;
|
|
3047
|
+
}
|
|
3048
|
+
.shield-desc {
|
|
3049
|
+
font-size: 10px;
|
|
3050
|
+
color: var(--muted);
|
|
3051
|
+
margin-top: 2px;
|
|
3052
|
+
line-height: 1.4;
|
|
3053
|
+
}
|
|
3054
|
+
|
|
2963
3055
|
.panel-title {
|
|
2964
3056
|
font-size: 12px;
|
|
2965
3057
|
font-weight: 700;
|
|
2966
3058
|
color: var(--text-bright);
|
|
2967
3059
|
margin-bottom: 12px;
|
|
3060
|
+
flex-shrink: 0;
|
|
2968
3061
|
display: flex;
|
|
2969
3062
|
align-items: center;
|
|
2970
3063
|
gap: 6px;
|
|
@@ -2972,8 +3065,8 @@ var ui_default = `<!doctype html>
|
|
|
2972
3065
|
.setting-row {
|
|
2973
3066
|
display: flex;
|
|
2974
3067
|
align-items: flex-start;
|
|
2975
|
-
gap:
|
|
2976
|
-
margin-bottom:
|
|
3068
|
+
gap: 10px;
|
|
3069
|
+
margin-bottom: 8px;
|
|
2977
3070
|
}
|
|
2978
3071
|
.setting-row:last-child {
|
|
2979
3072
|
margin-bottom: 0;
|
|
@@ -2982,20 +3075,21 @@ var ui_default = `<!doctype html>
|
|
|
2982
3075
|
flex: 1;
|
|
2983
3076
|
}
|
|
2984
3077
|
.setting-label {
|
|
2985
|
-
font-size:
|
|
3078
|
+
font-size: 11px;
|
|
2986
3079
|
color: var(--text-bright);
|
|
2987
|
-
margin-bottom:
|
|
3080
|
+
margin-bottom: 2px;
|
|
3081
|
+
font-weight: 600;
|
|
2988
3082
|
}
|
|
2989
3083
|
.setting-desc {
|
|
2990
|
-
font-size:
|
|
3084
|
+
font-size: 10px;
|
|
2991
3085
|
color: var(--muted);
|
|
2992
|
-
line-height: 1.
|
|
3086
|
+
line-height: 1.4;
|
|
2993
3087
|
}
|
|
2994
3088
|
.toggle {
|
|
2995
3089
|
position: relative;
|
|
2996
3090
|
display: inline-block;
|
|
2997
|
-
width:
|
|
2998
|
-
height:
|
|
3091
|
+
width: 34px;
|
|
3092
|
+
height: 19px;
|
|
2999
3093
|
flex-shrink: 0;
|
|
3000
3094
|
margin-top: 1px;
|
|
3001
3095
|
}
|
|
@@ -3015,8 +3109,8 @@ var ui_default = `<!doctype html>
|
|
|
3015
3109
|
.slider:before {
|
|
3016
3110
|
content: '';
|
|
3017
3111
|
position: absolute;
|
|
3018
|
-
width:
|
|
3019
|
-
height:
|
|
3112
|
+
width: 13px;
|
|
3113
|
+
height: 13px;
|
|
3020
3114
|
left: 3px;
|
|
3021
3115
|
bottom: 3px;
|
|
3022
3116
|
background: #fff;
|
|
@@ -3027,7 +3121,7 @@ var ui_default = `<!doctype html>
|
|
|
3027
3121
|
background: var(--success);
|
|
3028
3122
|
}
|
|
3029
3123
|
input:checked + .slider:before {
|
|
3030
|
-
transform: translateX(
|
|
3124
|
+
transform: translateX(15px);
|
|
3031
3125
|
}
|
|
3032
3126
|
input:disabled + .slider {
|
|
3033
3127
|
opacity: 0.4;
|
|
@@ -3186,12 +3280,17 @@ var ui_default = `<!doctype html>
|
|
|
3186
3280
|
border: 1px solid var(--border);
|
|
3187
3281
|
}
|
|
3188
3282
|
|
|
3189
|
-
@media (max-width:
|
|
3283
|
+
@media (max-width: 960px) {
|
|
3190
3284
|
.body {
|
|
3191
|
-
grid-template-columns: 1fr;
|
|
3285
|
+
grid-template-columns: 1fr 220px;
|
|
3286
|
+
}
|
|
3287
|
+
.flight-col {
|
|
3288
|
+
display: none;
|
|
3192
3289
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
3290
|
+
}
|
|
3291
|
+
@media (max-width: 640px) {
|
|
3292
|
+
.body {
|
|
3293
|
+
grid-template-columns: 1fr;
|
|
3195
3294
|
}
|
|
3196
3295
|
}
|
|
3197
3296
|
</style>
|
|
@@ -3205,6 +3304,19 @@ var ui_default = `<!doctype html>
|
|
|
3205
3304
|
</header>
|
|
3206
3305
|
|
|
3207
3306
|
<div class="body">
|
|
3307
|
+
<div class="flight-col">
|
|
3308
|
+
<div class="panel flight-panel">
|
|
3309
|
+
<div class="panel-title">
|
|
3310
|
+
\u{1F6F0}\uFE0F Flight Recorder
|
|
3311
|
+
<span style="font-weight: 400; color: var(--muted); font-size: 11px">live</span>
|
|
3312
|
+
<button class="feed-clear-btn" onclick="clearFeed()">clear</button>
|
|
3313
|
+
</div>
|
|
3314
|
+
<div id="activity-feed">
|
|
3315
|
+
<span class="decisions-empty">Waiting for agent activity\u2026</span>
|
|
3316
|
+
</div>
|
|
3317
|
+
</div>
|
|
3318
|
+
</div>
|
|
3319
|
+
|
|
3208
3320
|
<div class="main">
|
|
3209
3321
|
<div id="warnBanner" class="warning-banner">
|
|
3210
3322
|
\u26A0\uFE0F Auto-start is off \u2014 daemon started manually. Run "node9 daemon stop" to stop it, or
|
|
@@ -3285,6 +3397,11 @@ var ui_default = `<!doctype html>
|
|
|
3285
3397
|
<div id="slackStatusLine" class="slack-status-line">No key saved</div>
|
|
3286
3398
|
</div>
|
|
3287
3399
|
|
|
3400
|
+
<div class="panel">
|
|
3401
|
+
<div class="panel-title">\u{1F6E1}\uFE0F Active Shields</div>
|
|
3402
|
+
<div id="shieldsList"><span class="decisions-empty">Loading\u2026</span></div>
|
|
3403
|
+
</div>
|
|
3404
|
+
|
|
3288
3405
|
<div class="panel">
|
|
3289
3406
|
<div class="panel-title">\u{1F4CB} Persistent Decisions</div>
|
|
3290
3407
|
<div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
|
|
@@ -3330,14 +3447,23 @@ var ui_default = `<!doctype html>
|
|
|
3330
3447
|
|
|
3331
3448
|
function updateDenyButton(id, timestamp) {
|
|
3332
3449
|
const btn = document.querySelector('#c-' + id + ' .btn-deny');
|
|
3450
|
+
const timer = document.querySelector('#timer-' + id);
|
|
3333
3451
|
if (!btn) return;
|
|
3334
3452
|
const elapsed = Date.now() - timestamp;
|
|
3335
3453
|
const remaining = Math.max(0, Math.ceil((autoDenyMs - elapsed) / 1000));
|
|
3336
3454
|
if (remaining <= 0) {
|
|
3337
|
-
btn.textContent = 'Auto-Denying
|
|
3455
|
+
btn.textContent = '\u23F3 Auto-Denying\u2026';
|
|
3338
3456
|
btn.disabled = true;
|
|
3457
|
+
if (timer) {
|
|
3458
|
+
timer.textContent = 'auto-deny';
|
|
3459
|
+
timer.className = 'card-timer urgent';
|
|
3460
|
+
}
|
|
3339
3461
|
} else {
|
|
3340
|
-
btn.textContent = 'Block Action
|
|
3462
|
+
btn.textContent = '\u{1F6AB} Block this Action';
|
|
3463
|
+
if (timer) {
|
|
3464
|
+
timer.textContent = remaining + 's';
|
|
3465
|
+
timer.className = 'card-timer' + (remaining < 15 ? ' urgent' : '');
|
|
3466
|
+
}
|
|
3341
3467
|
setTimeout(() => updateDenyButton(id, timestamp), 1000);
|
|
3342
3468
|
}
|
|
3343
3469
|
}
|
|
@@ -3353,34 +3479,61 @@ var ui_default = `<!doctype html>
|
|
|
3353
3479
|
empty.style.display = requests.size === 0 ? 'block' : 'none';
|
|
3354
3480
|
}
|
|
3355
3481
|
|
|
3356
|
-
function
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3482
|
+
function setCardBusy(card, busy) {
|
|
3483
|
+
if (!card) return;
|
|
3484
|
+
card.querySelectorAll('button').forEach((b) => (b.disabled = busy));
|
|
3485
|
+
card.style.opacity = busy ? '0.5' : '1';
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
function showCardError(card, msg) {
|
|
3489
|
+
if (!card) return;
|
|
3490
|
+
card.style.outline = '2px solid #f87171';
|
|
3491
|
+
let err = card.querySelector('.card-error');
|
|
3492
|
+
if (!err) {
|
|
3493
|
+
err = document.createElement('p');
|
|
3494
|
+
err.className = 'card-error';
|
|
3495
|
+
err.style.cssText = 'color:#f87171;font-size:11px;margin:6px 0 0;';
|
|
3496
|
+
card.appendChild(err);
|
|
3497
|
+
}
|
|
3498
|
+
err.textContent = '\u26A0 ' + msg + ' \u2014 please try again or refresh.';
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
async function sendDecision(id, decision, persist) {
|
|
3502
|
+
const card = document.getElementById('c-' + id);
|
|
3503
|
+
setCardBusy(card, true);
|
|
3504
|
+
try {
|
|
3505
|
+
const res = await fetch('/decision/' + id, {
|
|
3506
|
+
method: 'POST',
|
|
3507
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3508
|
+
body: JSON.stringify({ decision, persist: !!persist }),
|
|
3509
|
+
});
|
|
3510
|
+
if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
|
|
3365
3511
|
card?.remove();
|
|
3366
3512
|
requests.delete(id);
|
|
3367
3513
|
refresh();
|
|
3368
|
-
}
|
|
3514
|
+
} catch (err) {
|
|
3515
|
+
setCardBusy(card, false);
|
|
3516
|
+
showCardError(card, err.message || 'Network error');
|
|
3517
|
+
}
|
|
3369
3518
|
}
|
|
3370
3519
|
|
|
3371
|
-
function sendTrust(id, duration) {
|
|
3520
|
+
async function sendTrust(id, duration) {
|
|
3372
3521
|
const card = document.getElementById('c-' + id);
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3522
|
+
setCardBusy(card, true);
|
|
3523
|
+
try {
|
|
3524
|
+
const res = await fetch('/decision/' + id, {
|
|
3525
|
+
method: 'POST',
|
|
3526
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3527
|
+
body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
|
|
3528
|
+
});
|
|
3529
|
+
if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
|
|
3380
3530
|
card?.remove();
|
|
3381
3531
|
requests.delete(id);
|
|
3382
3532
|
refresh();
|
|
3383
|
-
}
|
|
3533
|
+
} catch (err) {
|
|
3534
|
+
setCardBusy(card, false);
|
|
3535
|
+
showCardError(card, err.message || 'Network error');
|
|
3536
|
+
}
|
|
3384
3537
|
}
|
|
3385
3538
|
|
|
3386
3539
|
function renderPayload(req) {
|
|
@@ -3431,16 +3584,21 @@ var ui_default = `<!doctype html>
|
|
|
3431
3584
|
const mcpLabel = req.mcpServer ? esc(req.mcpServer) : null;
|
|
3432
3585
|
const dis = isSlack ? 'disabled' : '';
|
|
3433
3586
|
card.innerHTML = \`
|
|
3587
|
+
<div class="card-header">
|
|
3588
|
+
<span class="card-header-icon">\${isSlack ? '\u26A1' : '\u26A0\uFE0F'}</span>
|
|
3589
|
+
<span class="card-header-title">\${isSlack ? 'Awaiting Cloud Approval' : 'Action Required'}</span>
|
|
3590
|
+
<span class="card-timer" id="timer-\${req.id}">\${autoDenyMs > 0 ? Math.ceil(autoDenyMs / 1000) + 's' : ''}</span>
|
|
3591
|
+
</div>
|
|
3434
3592
|
<div class="source-row">
|
|
3435
3593
|
<span class="agent-badge">\${agentLabel}</span>
|
|
3436
3594
|
\${mcpLabel ? \`<span class="source-arrow">\u2192</span><span class="mcp-badge">mcp::\${mcpLabel}</span>\` : ''}
|
|
3437
3595
|
</div>
|
|
3438
3596
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
3439
|
-
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting
|
|
3597
|
+
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
|
|
3440
3598
|
\${renderPayload(req)}
|
|
3441
3599
|
<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}
|
|
3600
|
+
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
|
|
3601
|
+
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>\u{1F6AB} Block this Action</button>
|
|
3444
3602
|
<div class="trust-row\${trustEnabled ? ' show' : ''}" id="tr-\${req.id}">
|
|
3445
3603
|
<button class="btn-trust" onclick="sendTrust('\${req.id}','30m')" \${dis}>\u23F1 Trust 30m</button>
|
|
3446
3604
|
<button class="btn-trust" onclick="sendTrust('\${req.id}','1h')" \${dis}>\u23F1 Trust 1h</button>
|
|
@@ -3500,9 +3658,84 @@ var ui_default = `<!doctype html>
|
|
|
3500
3658
|
ev.addEventListener('slack-status', (e) => {
|
|
3501
3659
|
applySlackStatus(JSON.parse(e.data));
|
|
3502
3660
|
});
|
|
3661
|
+
ev.addEventListener('shields-status', (e) => {
|
|
3662
|
+
renderShields(JSON.parse(e.data).shields);
|
|
3663
|
+
});
|
|
3664
|
+
|
|
3665
|
+
// \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
|
|
3666
|
+
ev.addEventListener('activity', (e) => {
|
|
3667
|
+
const data = JSON.parse(e.data);
|
|
3668
|
+
const feed = document.getElementById('activity-feed');
|
|
3669
|
+
// Remove placeholder on first item
|
|
3670
|
+
const placeholder = feed.querySelector('.decisions-empty');
|
|
3671
|
+
if (placeholder) placeholder.remove();
|
|
3672
|
+
|
|
3673
|
+
const time = new Date(data.ts).toLocaleTimeString([], {
|
|
3674
|
+
hour12: false,
|
|
3675
|
+
hour: '2-digit',
|
|
3676
|
+
minute: '2-digit',
|
|
3677
|
+
second: '2-digit',
|
|
3678
|
+
});
|
|
3679
|
+
const icon = frIcon(data.tool);
|
|
3680
|
+
const argsStr = JSON.stringify(data.args ?? {});
|
|
3681
|
+
const argsPreview = esc(argsStr.length > 120 ? argsStr.slice(0, 120) + '\u2026' : argsStr);
|
|
3682
|
+
|
|
3683
|
+
const row = document.createElement('div');
|
|
3684
|
+
row.className = 'feed-row';
|
|
3685
|
+
row.id = 'fr-' + data.id;
|
|
3686
|
+
row.innerHTML = \`
|
|
3687
|
+
<span class="feed-ts">\${time}</span>
|
|
3688
|
+
<span class="feed-icon">\${icon}</span>
|
|
3689
|
+
<span class="feed-content"><strong>\${esc(data.tool)}</strong><span class="feed-args">\${argsPreview}</span></span>
|
|
3690
|
+
<span class="feed-badge fr-pending">\u25CF</span>
|
|
3691
|
+
\`;
|
|
3692
|
+
feed.prepend(row);
|
|
3693
|
+
if (feed.children.length > 100) feed.lastChild.remove();
|
|
3694
|
+
});
|
|
3695
|
+
|
|
3696
|
+
ev.addEventListener('activity-result', (e) => {
|
|
3697
|
+
const { id, status, label } = JSON.parse(e.data);
|
|
3698
|
+
const row = document.getElementById('fr-' + id);
|
|
3699
|
+
if (!row) return;
|
|
3700
|
+
const badge = row.querySelector('.feed-badge');
|
|
3701
|
+
if (status === 'allow') {
|
|
3702
|
+
badge.textContent = 'ALLOW';
|
|
3703
|
+
badge.className = 'feed-badge fr-allow';
|
|
3704
|
+
} else if (status === 'dlp') {
|
|
3705
|
+
badge.textContent = '\u{1F6E1}\uFE0F DLP';
|
|
3706
|
+
badge.className = 'feed-badge fr-dlp';
|
|
3707
|
+
row.classList.add('fr-dlp-row');
|
|
3708
|
+
} else {
|
|
3709
|
+
badge.textContent = 'BLOCK';
|
|
3710
|
+
badge.className = 'feed-badge fr-block';
|
|
3711
|
+
}
|
|
3712
|
+
});
|
|
3503
3713
|
}
|
|
3504
3714
|
connect();
|
|
3505
3715
|
|
|
3716
|
+
const FR_ICONS = {
|
|
3717
|
+
bash: '\u{1F4BB}',
|
|
3718
|
+
read: '\u{1F4D6}',
|
|
3719
|
+
edit: '\u270F\uFE0F',
|
|
3720
|
+
write: '\u270F\uFE0F',
|
|
3721
|
+
glob: '\u{1F4C2}',
|
|
3722
|
+
grep: '\u{1F50D}',
|
|
3723
|
+
agent: '\u{1F916}',
|
|
3724
|
+
search: '\u{1F50D}',
|
|
3725
|
+
sql: '\u{1F5C4}\uFE0F',
|
|
3726
|
+
query: '\u{1F5C4}\uFE0F',
|
|
3727
|
+
list: '\u{1F4C2}',
|
|
3728
|
+
delete: '\u{1F5D1}\uFE0F',
|
|
3729
|
+
web: '\u{1F310}',
|
|
3730
|
+
};
|
|
3731
|
+
function frIcon(tool) {
|
|
3732
|
+
const t = (tool || '').toLowerCase();
|
|
3733
|
+
for (const [k, v] of Object.entries(FR_ICONS)) {
|
|
3734
|
+
if (t.includes(k)) return v;
|
|
3735
|
+
}
|
|
3736
|
+
return '\u{1F6E0}\uFE0F';
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3506
3739
|
function saveSetting(key, value) {
|
|
3507
3740
|
fetch('/settings', {
|
|
3508
3741
|
method: 'POST',
|
|
@@ -3592,6 +3825,49 @@ var ui_default = `<!doctype html>
|
|
|
3592
3825
|
}
|
|
3593
3826
|
}
|
|
3594
3827
|
|
|
3828
|
+
function clearFeed() {
|
|
3829
|
+
const feed = document.getElementById('activity-feed');
|
|
3830
|
+
feed.innerHTML = '<span class="decisions-empty">Feed cleared.</span>';
|
|
3831
|
+
}
|
|
3832
|
+
|
|
3833
|
+
function renderShields(shields) {
|
|
3834
|
+
const list = document.getElementById('shieldsList');
|
|
3835
|
+
if (!shields || shields.length === 0) {
|
|
3836
|
+
list.innerHTML = '<span class="decisions-empty">No shields available.</span>';
|
|
3837
|
+
return;
|
|
3838
|
+
}
|
|
3839
|
+
list.innerHTML = shields
|
|
3840
|
+
.map(
|
|
3841
|
+
(s) => \`
|
|
3842
|
+
<div class="shield-row">
|
|
3843
|
+
<div class="shield-info">
|
|
3844
|
+
<div class="shield-name">\${esc(s.name)}</div>
|
|
3845
|
+
<div class="shield-desc">\${esc(s.description)}</div>
|
|
3846
|
+
</div>
|
|
3847
|
+
<label class="toggle">
|
|
3848
|
+
<input type="checkbox" \${s.active ? 'checked' : ''}
|
|
3849
|
+
onchange="toggleShield('\${esc(s.name)}', this.checked)" />
|
|
3850
|
+
<span class="slider"></span>
|
|
3851
|
+
</label>
|
|
3852
|
+
</div>
|
|
3853
|
+
\`
|
|
3854
|
+
)
|
|
3855
|
+
.join('');
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
function toggleShield(name, active) {
|
|
3859
|
+
fetch('/shields', {
|
|
3860
|
+
method: 'POST',
|
|
3861
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3862
|
+
body: JSON.stringify({ name, active }),
|
|
3863
|
+
}).catch(() => {});
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
fetch('/shields')
|
|
3867
|
+
.then((r) => r.json())
|
|
3868
|
+
.then(({ shields }) => renderShields(shields))
|
|
3869
|
+
.catch(() => {});
|
|
3870
|
+
|
|
3595
3871
|
function renderDecisions(decisions) {
|
|
3596
3872
|
const dl = document.getElementById('decisionsList');
|
|
3597
3873
|
const entries = Object.entries(decisions);
|
|
@@ -3638,31 +3914,32 @@ var ui_default = `<!doctype html>
|
|
|
3638
3914
|
</body>
|
|
3639
3915
|
</html>
|
|
3640
3916
|
`;
|
|
3917
|
+
}
|
|
3918
|
+
});
|
|
3641
3919
|
|
|
3642
3920
|
// src/daemon/ui.ts
|
|
3643
|
-
var UI_HTML_TEMPLATE
|
|
3921
|
+
var UI_HTML_TEMPLATE;
|
|
3922
|
+
var init_ui2 = __esm({
|
|
3923
|
+
"src/daemon/ui.ts"() {
|
|
3924
|
+
"use strict";
|
|
3925
|
+
init_ui();
|
|
3926
|
+
UI_HTML_TEMPLATE = ui_default;
|
|
3927
|
+
}
|
|
3928
|
+
});
|
|
3644
3929
|
|
|
3645
3930
|
// src/daemon/index.ts
|
|
3646
3931
|
import http from "http";
|
|
3932
|
+
import net2 from "net";
|
|
3647
3933
|
import fs4 from "fs";
|
|
3648
3934
|
import path6 from "path";
|
|
3649
3935
|
import os4 from "os";
|
|
3650
|
-
import { spawn as spawn2 } from "child_process";
|
|
3651
|
-
import { randomUUID } from "crypto";
|
|
3936
|
+
import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
|
|
3937
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3652
3938
|
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
3939
|
function atomicWriteSync2(filePath, data, options) {
|
|
3663
3940
|
const dir = path6.dirname(filePath);
|
|
3664
3941
|
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
3665
|
-
const tmpPath = `${filePath}.${
|
|
3942
|
+
const tmpPath = `${filePath}.${randomUUID2()}.tmp`;
|
|
3666
3943
|
fs4.writeFileSync(tmpPath, data, options);
|
|
3667
3944
|
fs4.renameSync(tmpPath, filePath);
|
|
3668
3945
|
}
|
|
@@ -3680,12 +3957,6 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
3680
3957
|
} catch {
|
|
3681
3958
|
}
|
|
3682
3959
|
}
|
|
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
3960
|
function redactArgs(value) {
|
|
3690
3961
|
if (!value || typeof value !== "object") return value;
|
|
3691
3962
|
if (Array.isArray(value)) return value.map(redactArgs);
|
|
@@ -3720,7 +3991,6 @@ function getAuditHistory(limit = 20) {
|
|
|
3720
3991
|
return [];
|
|
3721
3992
|
}
|
|
3722
3993
|
}
|
|
3723
|
-
var AUTO_DENY_MS = 12e4;
|
|
3724
3994
|
function getOrgName() {
|
|
3725
3995
|
try {
|
|
3726
3996
|
if (fs4.existsSync(CREDENTIALS_FILE)) {
|
|
@@ -3730,7 +4000,6 @@ function getOrgName() {
|
|
|
3730
4000
|
}
|
|
3731
4001
|
return null;
|
|
3732
4002
|
}
|
|
3733
|
-
var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
3734
4003
|
function hasStoredSlackKey() {
|
|
3735
4004
|
return fs4.existsSync(CREDENTIALS_FILE);
|
|
3736
4005
|
}
|
|
@@ -3746,11 +4015,6 @@ function writeGlobalSetting(key, value) {
|
|
|
3746
4015
|
config.settings[key] = value;
|
|
3747
4016
|
atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3748
4017
|
}
|
|
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
4018
|
function abandonPending() {
|
|
3755
4019
|
abandonTimer = null;
|
|
3756
4020
|
pending.forEach((entry, id) => {
|
|
@@ -3772,6 +4036,18 @@ function abandonPending() {
|
|
|
3772
4036
|
}
|
|
3773
4037
|
}
|
|
3774
4038
|
function broadcast(event, data) {
|
|
4039
|
+
if (event === "activity") {
|
|
4040
|
+
activityRing.push({ event, data });
|
|
4041
|
+
if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
|
|
4042
|
+
} else if (event === "activity-result") {
|
|
4043
|
+
const { id, status, label } = data;
|
|
4044
|
+
for (let i = activityRing.length - 1; i >= 0; i--) {
|
|
4045
|
+
if (activityRing[i].data.id === id) {
|
|
4046
|
+
Object.assign(activityRing[i].data, { status, label });
|
|
4047
|
+
break;
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
3775
4051
|
const msg = `event: ${event}
|
|
3776
4052
|
data: ${JSON.stringify(data)}
|
|
3777
4053
|
|
|
@@ -3817,13 +4093,15 @@ function writePersistentDecision(toolName, decision) {
|
|
|
3817
4093
|
}
|
|
3818
4094
|
}
|
|
3819
4095
|
function startDaemon() {
|
|
3820
|
-
const csrfToken =
|
|
3821
|
-
const internalToken =
|
|
4096
|
+
const csrfToken = randomUUID2();
|
|
4097
|
+
const internalToken = randomUUID2();
|
|
3822
4098
|
const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
|
|
3823
4099
|
const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
|
|
3824
4100
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
4101
|
+
const watchMode = process.env.NODE9_WATCH_MODE === "1";
|
|
3825
4102
|
let idleTimer;
|
|
3826
4103
|
function resetIdleTimer() {
|
|
4104
|
+
if (watchMode) return;
|
|
3827
4105
|
if (idleTimer) clearTimeout(idleTimer);
|
|
3828
4106
|
idleTimer = setTimeout(() => {
|
|
3829
4107
|
if (autoStarted) {
|
|
@@ -3878,6 +4156,12 @@ data: ${JSON.stringify({
|
|
|
3878
4156
|
data: ${JSON.stringify(readPersistentDecisions())}
|
|
3879
4157
|
|
|
3880
4158
|
`);
|
|
4159
|
+
for (const item of activityRing) {
|
|
4160
|
+
res.write(`event: ${item.event}
|
|
4161
|
+
data: ${JSON.stringify(item.data)}
|
|
4162
|
+
|
|
4163
|
+
`);
|
|
4164
|
+
}
|
|
3881
4165
|
return req.on("close", () => {
|
|
3882
4166
|
sseClients.delete(res);
|
|
3883
4167
|
if (sseClients.size === 0 && pending.size > 0) {
|
|
@@ -3897,9 +4181,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3897
4181
|
slackDelegated = false,
|
|
3898
4182
|
agent,
|
|
3899
4183
|
mcpServer,
|
|
3900
|
-
riskMetadata
|
|
4184
|
+
riskMetadata,
|
|
4185
|
+
fromCLI = false,
|
|
4186
|
+
activityId
|
|
3901
4187
|
} = JSON.parse(body);
|
|
3902
|
-
const id =
|
|
4188
|
+
const id = fromCLI && typeof activityId === "string" && activityId || randomUUID2();
|
|
3903
4189
|
const entry = {
|
|
3904
4190
|
id,
|
|
3905
4191
|
toolName,
|
|
@@ -3930,6 +4216,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3930
4216
|
}, AUTO_DENY_MS)
|
|
3931
4217
|
};
|
|
3932
4218
|
pending.set(id, entry);
|
|
4219
|
+
if (!fromCLI) {
|
|
4220
|
+
broadcast("activity", {
|
|
4221
|
+
id,
|
|
4222
|
+
ts: entry.timestamp,
|
|
4223
|
+
tool: toolName,
|
|
4224
|
+
args: redactArgs(args),
|
|
4225
|
+
status: "pending"
|
|
4226
|
+
});
|
|
4227
|
+
}
|
|
3933
4228
|
const browserEnabled = getConfig().settings.approvers?.browser !== false;
|
|
3934
4229
|
if (browserEnabled) {
|
|
3935
4230
|
broadcast("add", {
|
|
@@ -3959,6 +4254,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3959
4254
|
const e = pending.get(id);
|
|
3960
4255
|
if (!e) return;
|
|
3961
4256
|
if (result.noApprovalMechanism) return;
|
|
4257
|
+
broadcast("activity-result", {
|
|
4258
|
+
id,
|
|
4259
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
4260
|
+
label: result.blockedByLabel
|
|
4261
|
+
});
|
|
3962
4262
|
clearTimeout(e.timer);
|
|
3963
4263
|
const decision = result.approved ? "allow" : "deny";
|
|
3964
4264
|
appendAuditLog({ toolName: e.toolName, args: e.args, decision });
|
|
@@ -3993,8 +4293,8 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3993
4293
|
const entry = pending.get(id);
|
|
3994
4294
|
if (!entry) return res.writeHead(404).end();
|
|
3995
4295
|
if (entry.earlyDecision) {
|
|
4296
|
+
clearTimeout(entry.timer);
|
|
3996
4297
|
pending.delete(id);
|
|
3997
|
-
broadcast("remove", { id });
|
|
3998
4298
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3999
4299
|
const body = { decision: entry.earlyDecision };
|
|
4000
4300
|
if (entry.earlyReason) body.reason = entry.earlyReason;
|
|
@@ -4024,10 +4324,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
4024
4324
|
decision: `trust:${trustDuration}`
|
|
4025
4325
|
});
|
|
4026
4326
|
clearTimeout(entry.timer);
|
|
4027
|
-
if (entry.waiter)
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4327
|
+
if (entry.waiter) {
|
|
4328
|
+
entry.waiter("allow");
|
|
4329
|
+
pending.delete(id);
|
|
4330
|
+
broadcast("remove", { id });
|
|
4331
|
+
} else {
|
|
4332
|
+
entry.earlyDecision = "allow";
|
|
4333
|
+
broadcast("remove", { id });
|
|
4334
|
+
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
4335
|
+
}
|
|
4031
4336
|
res.writeHead(200);
|
|
4032
4337
|
return res.end(JSON.stringify({ ok: true }));
|
|
4033
4338
|
}
|
|
@@ -4039,13 +4344,16 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
4039
4344
|
decision: resolvedDecision
|
|
4040
4345
|
});
|
|
4041
4346
|
clearTimeout(entry.timer);
|
|
4042
|
-
if (entry.waiter)
|
|
4043
|
-
|
|
4347
|
+
if (entry.waiter) {
|
|
4348
|
+
entry.waiter(resolvedDecision, reason);
|
|
4349
|
+
pending.delete(id);
|
|
4350
|
+
broadcast("remove", { id });
|
|
4351
|
+
} else {
|
|
4044
4352
|
entry.earlyDecision = resolvedDecision;
|
|
4045
4353
|
entry.earlyReason = reason;
|
|
4354
|
+
broadcast("remove", { id });
|
|
4355
|
+
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
4046
4356
|
}
|
|
4047
|
-
pending.delete(id);
|
|
4048
|
-
broadcast("remove", { id });
|
|
4049
4357
|
res.writeHead(200);
|
|
4050
4358
|
return res.end(JSON.stringify({ ok: true }));
|
|
4051
4359
|
} catch {
|
|
@@ -4098,119 +4406,741 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
4098
4406
|
res.writeHead(400).end();
|
|
4099
4407
|
}
|
|
4100
4408
|
}
|
|
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();
|
|
4409
|
+
if (req.method === "DELETE" && pathname.startsWith("/decisions/")) {
|
|
4410
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4411
|
+
try {
|
|
4412
|
+
const toolName = decodeURIComponent(pathname.split("/").pop());
|
|
4413
|
+
const decisions = readPersistentDecisions();
|
|
4414
|
+
delete decisions[toolName];
|
|
4415
|
+
atomicWriteSync2(DECISIONS_FILE, JSON.stringify(decisions, null, 2));
|
|
4416
|
+
broadcast("decisions", decisions);
|
|
4417
|
+
res.writeHead(200);
|
|
4418
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4419
|
+
} catch {
|
|
4420
|
+
res.writeHead(400).end();
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
if (req.method === "POST" && pathname.startsWith("/resolve/")) {
|
|
4424
|
+
const internalAuth = req.headers["x-node9-internal"];
|
|
4425
|
+
if (internalAuth !== internalToken) return res.writeHead(403).end();
|
|
4426
|
+
try {
|
|
4427
|
+
const id = pathname.split("/").pop();
|
|
4428
|
+
const entry = pending.get(id);
|
|
4429
|
+
if (!entry) return res.writeHead(404).end();
|
|
4430
|
+
const { decision } = JSON.parse(await readBody(req));
|
|
4431
|
+
appendAuditLog({
|
|
4432
|
+
toolName: entry.toolName,
|
|
4433
|
+
args: entry.args,
|
|
4434
|
+
decision
|
|
4435
|
+
});
|
|
4436
|
+
clearTimeout(entry.timer);
|
|
4437
|
+
if (entry.waiter) entry.waiter(decision);
|
|
4438
|
+
else entry.earlyDecision = decision;
|
|
4439
|
+
pending.delete(id);
|
|
4440
|
+
broadcast("remove", { id });
|
|
4441
|
+
res.writeHead(200);
|
|
4442
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4443
|
+
} catch {
|
|
4444
|
+
res.writeHead(400).end();
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
if (req.method === "POST" && pathname === "/events/clear") {
|
|
4448
|
+
activityRing.length = 0;
|
|
4449
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4450
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4451
|
+
}
|
|
4452
|
+
if (req.method === "GET" && pathname === "/audit") {
|
|
4453
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4454
|
+
return res.end(JSON.stringify(getAuditHistory()));
|
|
4455
|
+
}
|
|
4456
|
+
if (req.method === "GET" && pathname === "/shields") {
|
|
4457
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4458
|
+
const active = readActiveShields();
|
|
4459
|
+
const shields = Object.values(SHIELDS).map((s) => ({
|
|
4460
|
+
name: s.name,
|
|
4461
|
+
description: s.description,
|
|
4462
|
+
active: active.includes(s.name)
|
|
4463
|
+
}));
|
|
4464
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4465
|
+
return res.end(JSON.stringify({ shields }));
|
|
4466
|
+
}
|
|
4467
|
+
if (req.method === "POST" && pathname === "/shields") {
|
|
4468
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4469
|
+
try {
|
|
4470
|
+
const { name, active } = JSON.parse(await readBody(req));
|
|
4471
|
+
if (!SHIELDS[name]) return res.writeHead(400).end();
|
|
4472
|
+
const current = readActiveShields();
|
|
4473
|
+
const updated = active ? [.../* @__PURE__ */ new Set([...current, name])] : current.filter((n) => n !== name);
|
|
4474
|
+
writeActiveShields(updated);
|
|
4475
|
+
_resetConfigCache();
|
|
4476
|
+
const shieldsPayload = Object.values(SHIELDS).map((s) => ({
|
|
4477
|
+
name: s.name,
|
|
4478
|
+
description: s.description,
|
|
4479
|
+
active: updated.includes(s.name)
|
|
4480
|
+
}));
|
|
4481
|
+
broadcast("shields-status", { shields: shieldsPayload });
|
|
4482
|
+
res.writeHead(200);
|
|
4483
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4484
|
+
} catch {
|
|
4485
|
+
res.writeHead(400).end();
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
res.writeHead(404).end();
|
|
4489
|
+
});
|
|
4490
|
+
daemonServer = server;
|
|
4491
|
+
server.on("error", (e) => {
|
|
4492
|
+
if (e.code === "EADDRINUSE") {
|
|
4493
|
+
try {
|
|
4494
|
+
if (fs4.existsSync(DAEMON_PID_FILE)) {
|
|
4495
|
+
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4496
|
+
process.kill(pid, 0);
|
|
4497
|
+
return process.exit(0);
|
|
4498
|
+
}
|
|
4499
|
+
} catch {
|
|
4500
|
+
try {
|
|
4501
|
+
fs4.unlinkSync(DAEMON_PID_FILE);
|
|
4502
|
+
} catch {
|
|
4503
|
+
}
|
|
4504
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
4505
|
+
return;
|
|
4506
|
+
}
|
|
4507
|
+
fetch(`http://${DAEMON_HOST2}:${DAEMON_PORT2}/settings`, {
|
|
4508
|
+
signal: AbortSignal.timeout(1e3)
|
|
4509
|
+
}).then((res) => {
|
|
4510
|
+
if (res.ok) {
|
|
4511
|
+
try {
|
|
4512
|
+
const r = spawnSync2("ss", ["-Htnp", `sport = :${DAEMON_PORT2}`], {
|
|
4513
|
+
encoding: "utf8",
|
|
4514
|
+
timeout: 1e3
|
|
4515
|
+
});
|
|
4516
|
+
const match = r.stdout?.match(/pid=(\d+)/);
|
|
4517
|
+
if (match) {
|
|
4518
|
+
const orphanPid = parseInt(match[1], 10);
|
|
4519
|
+
process.kill(orphanPid, 0);
|
|
4520
|
+
atomicWriteSync2(
|
|
4521
|
+
DAEMON_PID_FILE,
|
|
4522
|
+
JSON.stringify({ pid: orphanPid, port: DAEMON_PORT2, internalToken, autoStarted }),
|
|
4523
|
+
{ mode: 384 }
|
|
4524
|
+
);
|
|
4525
|
+
}
|
|
4526
|
+
} catch {
|
|
4527
|
+
}
|
|
4528
|
+
process.exit(0);
|
|
4529
|
+
} else {
|
|
4530
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
4531
|
+
}
|
|
4532
|
+
}).catch(() => {
|
|
4533
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
4534
|
+
});
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
console.error(chalk4.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
|
|
4538
|
+
process.exit(1);
|
|
4539
|
+
});
|
|
4540
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
|
|
4541
|
+
atomicWriteSync2(
|
|
4542
|
+
DAEMON_PID_FILE,
|
|
4543
|
+
JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
|
|
4544
|
+
{ mode: 384 }
|
|
4545
|
+
);
|
|
4546
|
+
console.log(chalk4.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
|
|
4547
|
+
});
|
|
4548
|
+
if (watchMode) {
|
|
4549
|
+
console.log(chalk4.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
|
|
4550
|
+
}
|
|
4551
|
+
try {
|
|
4552
|
+
fs4.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4553
|
+
} catch {
|
|
4554
|
+
}
|
|
4555
|
+
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
4556
|
+
const unixServer = net2.createServer((socket) => {
|
|
4557
|
+
const chunks = [];
|
|
4558
|
+
let bytesReceived = 0;
|
|
4559
|
+
socket.on("data", (chunk) => {
|
|
4560
|
+
bytesReceived += chunk.length;
|
|
4561
|
+
if (bytesReceived > ACTIVITY_MAX_BYTES) {
|
|
4562
|
+
socket.destroy();
|
|
4563
|
+
return;
|
|
4564
|
+
}
|
|
4565
|
+
chunks.push(chunk);
|
|
4566
|
+
});
|
|
4567
|
+
socket.on("end", () => {
|
|
4568
|
+
try {
|
|
4569
|
+
const data = JSON.parse(Buffer.concat(chunks).toString());
|
|
4570
|
+
if (data.status === "pending") {
|
|
4571
|
+
broadcast("activity", {
|
|
4572
|
+
id: data.id,
|
|
4573
|
+
ts: data.ts,
|
|
4574
|
+
tool: data.tool,
|
|
4575
|
+
args: redactArgs(data.args),
|
|
4576
|
+
status: "pending"
|
|
4577
|
+
});
|
|
4578
|
+
} else {
|
|
4579
|
+
broadcast("activity-result", {
|
|
4580
|
+
id: data.id,
|
|
4581
|
+
status: data.status,
|
|
4582
|
+
label: data.label
|
|
4583
|
+
});
|
|
4584
|
+
}
|
|
4585
|
+
} catch {
|
|
4586
|
+
}
|
|
4587
|
+
});
|
|
4588
|
+
socket.on("error", () => {
|
|
4589
|
+
});
|
|
4590
|
+
});
|
|
4591
|
+
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
4592
|
+
process.on("exit", () => {
|
|
4593
|
+
try {
|
|
4594
|
+
fs4.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4595
|
+
} catch {
|
|
4596
|
+
}
|
|
4597
|
+
});
|
|
4598
|
+
}
|
|
4599
|
+
function stopDaemon() {
|
|
4600
|
+
if (!fs4.existsSync(DAEMON_PID_FILE)) return console.log(chalk4.yellow("Not running."));
|
|
4601
|
+
try {
|
|
4602
|
+
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4603
|
+
process.kill(pid, "SIGTERM");
|
|
4604
|
+
console.log(chalk4.green("\u2705 Stopped."));
|
|
4605
|
+
} catch {
|
|
4606
|
+
console.log(chalk4.gray("Cleaned up stale PID file."));
|
|
4607
|
+
} finally {
|
|
4608
|
+
try {
|
|
4609
|
+
fs4.unlinkSync(DAEMON_PID_FILE);
|
|
4610
|
+
} catch {
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
function daemonStatus() {
|
|
4615
|
+
if (fs4.existsSync(DAEMON_PID_FILE)) {
|
|
4616
|
+
try {
|
|
4617
|
+
const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4618
|
+
process.kill(pid, 0);
|
|
4619
|
+
console.log(chalk4.green("Node9 daemon: running"));
|
|
4620
|
+
return;
|
|
4621
|
+
} catch {
|
|
4622
|
+
console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
|
|
4623
|
+
return;
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
const r = spawnSync2("ss", ["-Htnp", `sport = :${DAEMON_PORT2}`], {
|
|
4627
|
+
encoding: "utf8",
|
|
4628
|
+
timeout: 500
|
|
4629
|
+
});
|
|
4630
|
+
if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT2}`)) {
|
|
4631
|
+
console.log(chalk4.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
|
|
4632
|
+
} else {
|
|
4633
|
+
console.log(chalk4.yellow("Node9 daemon: not running"));
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
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;
|
|
4637
|
+
var init_daemon = __esm({
|
|
4638
|
+
"src/daemon/index.ts"() {
|
|
4639
|
+
"use strict";
|
|
4640
|
+
init_ui2();
|
|
4641
|
+
init_core();
|
|
4642
|
+
init_shields();
|
|
4643
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path6.join(os4.tmpdir(), "node9-activity.sock");
|
|
4644
|
+
DAEMON_PORT2 = 7391;
|
|
4645
|
+
DAEMON_HOST2 = "127.0.0.1";
|
|
4646
|
+
homeDir = os4.homedir();
|
|
4647
|
+
DAEMON_PID_FILE = path6.join(homeDir, ".node9", "daemon.pid");
|
|
4648
|
+
DECISIONS_FILE = path6.join(homeDir, ".node9", "decisions.json");
|
|
4649
|
+
GLOBAL_CONFIG_FILE = path6.join(homeDir, ".node9", "config.json");
|
|
4650
|
+
CREDENTIALS_FILE = path6.join(homeDir, ".node9", "credentials.json");
|
|
4651
|
+
AUDIT_LOG_FILE = path6.join(homeDir, ".node9", "audit.log");
|
|
4652
|
+
TRUST_FILE2 = path6.join(homeDir, ".node9", "trust.json");
|
|
4653
|
+
TRUST_DURATIONS = {
|
|
4654
|
+
"30m": 30 * 6e4,
|
|
4655
|
+
"1h": 60 * 6e4,
|
|
4656
|
+
"2h": 2 * 60 * 6e4
|
|
4657
|
+
};
|
|
4658
|
+
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
4659
|
+
AUTO_DENY_MS = 12e4;
|
|
4660
|
+
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
4661
|
+
pending = /* @__PURE__ */ new Map();
|
|
4662
|
+
sseClients = /* @__PURE__ */ new Set();
|
|
4663
|
+
abandonTimer = null;
|
|
4664
|
+
daemonServer = null;
|
|
4665
|
+
hadBrowserClient = false;
|
|
4666
|
+
ACTIVITY_RING_SIZE = 100;
|
|
4667
|
+
activityRing = [];
|
|
4668
|
+
}
|
|
4669
|
+
});
|
|
4670
|
+
|
|
4671
|
+
// src/tui/tail.ts
|
|
4672
|
+
var tail_exports = {};
|
|
4673
|
+
__export(tail_exports, {
|
|
4674
|
+
startTail: () => startTail
|
|
4675
|
+
});
|
|
4676
|
+
import http2 from "http";
|
|
4677
|
+
import chalk5 from "chalk";
|
|
4678
|
+
import fs6 from "fs";
|
|
4679
|
+
import os6 from "os";
|
|
4680
|
+
import path8 from "path";
|
|
4681
|
+
import readline from "readline";
|
|
4682
|
+
import { spawn as spawn3 } from "child_process";
|
|
4683
|
+
function getIcon(tool) {
|
|
4684
|
+
const t = tool.toLowerCase();
|
|
4685
|
+
for (const [k, v] of Object.entries(ICONS)) {
|
|
4686
|
+
if (t.includes(k)) return v;
|
|
4687
|
+
}
|
|
4688
|
+
return "\u{1F6E0}\uFE0F";
|
|
4689
|
+
}
|
|
4690
|
+
function formatBase(activity) {
|
|
4691
|
+
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
4692
|
+
const icon = getIcon(activity.tool);
|
|
4693
|
+
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
4694
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
4695
|
+
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
4696
|
+
return `${chalk5.gray(time)} ${icon} ${chalk5.white.bold(toolName)} ${chalk5.dim(argsPreview)}`;
|
|
4697
|
+
}
|
|
4698
|
+
function renderResult(activity, result) {
|
|
4699
|
+
const base = formatBase(activity);
|
|
4700
|
+
let status;
|
|
4701
|
+
if (result.status === "allow") {
|
|
4702
|
+
status = chalk5.green("\u2713 ALLOW");
|
|
4703
|
+
} else if (result.status === "dlp") {
|
|
4704
|
+
status = chalk5.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
4705
|
+
} else {
|
|
4706
|
+
status = chalk5.red("\u2717 BLOCK");
|
|
4707
|
+
}
|
|
4708
|
+
if (process.stdout.isTTY) {
|
|
4709
|
+
readline.clearLine(process.stdout, 0);
|
|
4710
|
+
readline.cursorTo(process.stdout, 0);
|
|
4711
|
+
}
|
|
4712
|
+
console.log(`${base} ${status}`);
|
|
4713
|
+
}
|
|
4714
|
+
function renderPending(activity) {
|
|
4715
|
+
if (!process.stdout.isTTY) return;
|
|
4716
|
+
process.stdout.write(`${formatBase(activity)} ${chalk5.yellow("\u25CF \u2026")}\r`);
|
|
4717
|
+
}
|
|
4718
|
+
async function ensureDaemon() {
|
|
4719
|
+
if (fs6.existsSync(PID_FILE)) {
|
|
4720
|
+
try {
|
|
4721
|
+
const { port } = JSON.parse(fs6.readFileSync(PID_FILE, "utf-8"));
|
|
4722
|
+
return port;
|
|
4723
|
+
} catch {
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
try {
|
|
4727
|
+
const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
|
|
4728
|
+
signal: AbortSignal.timeout(500)
|
|
4729
|
+
});
|
|
4730
|
+
if (res.ok) return DAEMON_PORT2;
|
|
4731
|
+
} catch {
|
|
4732
|
+
}
|
|
4733
|
+
console.log(chalk5.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
4734
|
+
const child = spawn3(process.execPath, [process.argv[1], "daemon"], {
|
|
4735
|
+
detached: true,
|
|
4736
|
+
stdio: "ignore",
|
|
4737
|
+
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
4738
|
+
});
|
|
4739
|
+
child.unref();
|
|
4740
|
+
for (let i = 0; i < 20; i++) {
|
|
4741
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
4742
|
+
try {
|
|
4743
|
+
const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
|
|
4744
|
+
signal: AbortSignal.timeout(500)
|
|
4745
|
+
});
|
|
4746
|
+
if (res.ok) return DAEMON_PORT2;
|
|
4747
|
+
} catch {
|
|
4748
|
+
}
|
|
4749
|
+
}
|
|
4750
|
+
console.error(chalk5.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
4751
|
+
process.exit(1);
|
|
4752
|
+
}
|
|
4753
|
+
async function startTail(options = {}) {
|
|
4754
|
+
const port = await ensureDaemon();
|
|
4755
|
+
if (options.clear) {
|
|
4756
|
+
await new Promise((resolve) => {
|
|
4757
|
+
const req2 = http2.request(
|
|
4758
|
+
{ method: "POST", hostname: "127.0.0.1", port, path: "/events/clear" },
|
|
4759
|
+
(res) => {
|
|
4760
|
+
res.resume();
|
|
4761
|
+
res.on("end", resolve);
|
|
4762
|
+
}
|
|
4763
|
+
);
|
|
4764
|
+
req2.on("error", resolve);
|
|
4765
|
+
req2.end();
|
|
4766
|
+
});
|
|
4767
|
+
}
|
|
4768
|
+
const connectionTime = Date.now();
|
|
4769
|
+
const pending2 = /* @__PURE__ */ new Map();
|
|
4770
|
+
console.log(chalk5.cyan.bold(`
|
|
4771
|
+
\u{1F6F0}\uFE0F Node9 tail `) + chalk5.dim(`\u2192 localhost:${port}`));
|
|
4772
|
+
if (options.clear) {
|
|
4773
|
+
console.log(chalk5.dim("History cleared. Showing live events. Press Ctrl+C to exit.\n"));
|
|
4774
|
+
} else if (options.history) {
|
|
4775
|
+
console.log(chalk5.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
4776
|
+
} else {
|
|
4777
|
+
console.log(
|
|
4778
|
+
chalk5.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
4779
|
+
);
|
|
4780
|
+
}
|
|
4781
|
+
process.on("SIGINT", () => {
|
|
4782
|
+
if (process.stdout.isTTY) {
|
|
4783
|
+
readline.clearLine(process.stdout, 0);
|
|
4784
|
+
readline.cursorTo(process.stdout, 0);
|
|
4785
|
+
}
|
|
4786
|
+
console.log(chalk5.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
4787
|
+
process.exit(0);
|
|
4788
|
+
});
|
|
4789
|
+
const req = http2.get(`http://127.0.0.1:${port}/events`, (res) => {
|
|
4790
|
+
if (res.statusCode !== 200) {
|
|
4791
|
+
console.error(chalk5.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
4792
|
+
process.exit(1);
|
|
4793
|
+
}
|
|
4794
|
+
let currentEvent = "";
|
|
4795
|
+
let currentData = "";
|
|
4796
|
+
res.on("error", () => {
|
|
4797
|
+
});
|
|
4798
|
+
const rl = readline.createInterface({ input: res, crlfDelay: Infinity });
|
|
4799
|
+
rl.on("error", () => {
|
|
4800
|
+
});
|
|
4801
|
+
rl.on("line", (line) => {
|
|
4802
|
+
if (line.startsWith("event:")) {
|
|
4803
|
+
currentEvent = line.slice(6).trim();
|
|
4804
|
+
} else if (line.startsWith("data:")) {
|
|
4805
|
+
currentData = line.slice(5).trim();
|
|
4806
|
+
} else if (line === "") {
|
|
4807
|
+
if (currentEvent && currentData) {
|
|
4808
|
+
handleMessage(currentEvent, currentData);
|
|
4809
|
+
}
|
|
4810
|
+
currentEvent = "";
|
|
4811
|
+
currentData = "";
|
|
4812
|
+
}
|
|
4813
|
+
});
|
|
4814
|
+
rl.on("close", () => {
|
|
4815
|
+
if (process.stdout.isTTY) {
|
|
4816
|
+
readline.clearLine(process.stdout, 0);
|
|
4817
|
+
readline.cursorTo(process.stdout, 0);
|
|
4818
|
+
}
|
|
4819
|
+
console.log(chalk5.red("\n\u274C Daemon disconnected."));
|
|
4820
|
+
process.exit(1);
|
|
4821
|
+
});
|
|
4822
|
+
});
|
|
4823
|
+
function handleMessage(event, rawData) {
|
|
4824
|
+
let data;
|
|
4825
|
+
try {
|
|
4826
|
+
data = JSON.parse(rawData);
|
|
4827
|
+
} catch {
|
|
4828
|
+
return;
|
|
4829
|
+
}
|
|
4830
|
+
if (event === "activity") {
|
|
4831
|
+
if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
|
|
4832
|
+
if (data.status && data.status !== "pending") {
|
|
4833
|
+
renderResult(data, data);
|
|
4834
|
+
return;
|
|
4835
|
+
}
|
|
4836
|
+
pending2.set(data.id, data);
|
|
4837
|
+
const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
|
|
4838
|
+
if (slowTool) renderPending(data);
|
|
4839
|
+
}
|
|
4840
|
+
if (event === "activity-result") {
|
|
4841
|
+
const original = pending2.get(data.id);
|
|
4842
|
+
if (original) {
|
|
4843
|
+
renderResult(original, data);
|
|
4844
|
+
pending2.delete(data.id);
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
}
|
|
4848
|
+
req.on("error", (err) => {
|
|
4849
|
+
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
4850
|
+
console.error(chalk5.red(`
|
|
4851
|
+
\u274C ${msg}`));
|
|
4852
|
+
process.exit(1);
|
|
4853
|
+
});
|
|
4854
|
+
}
|
|
4855
|
+
var PID_FILE, ICONS;
|
|
4856
|
+
var init_tail = __esm({
|
|
4857
|
+
"src/tui/tail.ts"() {
|
|
4858
|
+
"use strict";
|
|
4859
|
+
init_daemon();
|
|
4860
|
+
PID_FILE = path8.join(os6.homedir(), ".node9", "daemon.pid");
|
|
4861
|
+
ICONS = {
|
|
4862
|
+
bash: "\u{1F4BB}",
|
|
4863
|
+
shell: "\u{1F4BB}",
|
|
4864
|
+
terminal: "\u{1F4BB}",
|
|
4865
|
+
read: "\u{1F4D6}",
|
|
4866
|
+
edit: "\u270F\uFE0F",
|
|
4867
|
+
write: "\u270F\uFE0F",
|
|
4868
|
+
glob: "\u{1F4C2}",
|
|
4869
|
+
grep: "\u{1F50D}",
|
|
4870
|
+
agent: "\u{1F916}",
|
|
4871
|
+
search: "\u{1F50D}",
|
|
4872
|
+
sql: "\u{1F5C4}\uFE0F",
|
|
4873
|
+
query: "\u{1F5C4}\uFE0F",
|
|
4874
|
+
list: "\u{1F4C2}",
|
|
4875
|
+
delete: "\u{1F5D1}\uFE0F",
|
|
4876
|
+
web: "\u{1F310}"
|
|
4877
|
+
};
|
|
4878
|
+
}
|
|
4879
|
+
});
|
|
4880
|
+
|
|
4881
|
+
// src/cli.ts
|
|
4882
|
+
init_core();
|
|
4883
|
+
import { Command } from "commander";
|
|
4884
|
+
|
|
4885
|
+
// src/setup.ts
|
|
4886
|
+
import fs3 from "fs";
|
|
4887
|
+
import path5 from "path";
|
|
4888
|
+
import os3 from "os";
|
|
4889
|
+
import chalk3 from "chalk";
|
|
4890
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
4891
|
+
function printDaemonTip() {
|
|
4892
|
+
console.log(
|
|
4893
|
+
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")
|
|
4894
|
+
);
|
|
4895
|
+
}
|
|
4896
|
+
function fullPathCommand(subcommand) {
|
|
4897
|
+
if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
|
|
4898
|
+
const nodeExec = process.execPath;
|
|
4899
|
+
const cliScript = process.argv[1];
|
|
4900
|
+
return `${nodeExec} ${cliScript} ${subcommand}`;
|
|
4901
|
+
}
|
|
4902
|
+
function readJson(filePath) {
|
|
4903
|
+
try {
|
|
4904
|
+
if (fs3.existsSync(filePath)) {
|
|
4905
|
+
return JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
4906
|
+
}
|
|
4907
|
+
} catch {
|
|
4908
|
+
}
|
|
4909
|
+
return null;
|
|
4910
|
+
}
|
|
4911
|
+
function writeJson(filePath, data) {
|
|
4912
|
+
const dir = path5.dirname(filePath);
|
|
4913
|
+
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
4914
|
+
fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
4915
|
+
}
|
|
4916
|
+
async function setupClaude() {
|
|
4917
|
+
const homeDir2 = os3.homedir();
|
|
4918
|
+
const mcpPath = path5.join(homeDir2, ".claude.json");
|
|
4919
|
+
const hooksPath = path5.join(homeDir2, ".claude", "settings.json");
|
|
4920
|
+
const claudeConfig = readJson(mcpPath) ?? {};
|
|
4921
|
+
const settings = readJson(hooksPath) ?? {};
|
|
4922
|
+
const servers = claudeConfig.mcpServers ?? {};
|
|
4923
|
+
let anythingChanged = false;
|
|
4924
|
+
if (!settings.hooks) settings.hooks = {};
|
|
4925
|
+
const hasPreHook = settings.hooks.PreToolUse?.some(
|
|
4926
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
4927
|
+
);
|
|
4928
|
+
if (!hasPreHook) {
|
|
4929
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
4930
|
+
settings.hooks.PreToolUse.push({
|
|
4931
|
+
matcher: ".*",
|
|
4932
|
+
hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
|
|
4933
|
+
});
|
|
4934
|
+
console.log(chalk3.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
|
|
4935
|
+
anythingChanged = true;
|
|
4936
|
+
}
|
|
4937
|
+
const hasPostHook = settings.hooks.PostToolUse?.some(
|
|
4938
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
4939
|
+
);
|
|
4940
|
+
if (!hasPostHook) {
|
|
4941
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
4942
|
+
settings.hooks.PostToolUse.push({
|
|
4943
|
+
matcher: ".*",
|
|
4944
|
+
hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
|
|
4945
|
+
});
|
|
4946
|
+
console.log(chalk3.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
|
|
4947
|
+
anythingChanged = true;
|
|
4948
|
+
}
|
|
4949
|
+
if (anythingChanged) {
|
|
4950
|
+
writeJson(hooksPath, 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(` ${mcpPath}`));
|
|
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 };
|
|
4113
4970
|
}
|
|
4971
|
+
claudeConfig.mcpServers = servers;
|
|
4972
|
+
writeJson(mcpPath, claudeConfig);
|
|
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."));
|
|
4114
4978
|
}
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4979
|
+
console.log("");
|
|
4980
|
+
}
|
|
4981
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
4982
|
+
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
4983
|
+
printDaemonTip();
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
if (anythingChanged) {
|
|
4987
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
4988
|
+
console.log(chalk3.gray(" Restart Claude Code for changes to take effect."));
|
|
4989
|
+
printDaemonTip();
|
|
4990
|
+
}
|
|
4991
|
+
}
|
|
4992
|
+
async function setupGemini() {
|
|
4993
|
+
const homeDir2 = os3.homedir();
|
|
4994
|
+
const settingsPath = path5.join(homeDir2, ".gemini", "settings.json");
|
|
4995
|
+
const settings = readJson(settingsPath) ?? {};
|
|
4996
|
+
const servers = settings.mcpServers ?? {};
|
|
4997
|
+
let anythingChanged = false;
|
|
4998
|
+
if (!settings.hooks) settings.hooks = {};
|
|
4999
|
+
const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
|
|
5000
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
5001
|
+
);
|
|
5002
|
+
if (!hasBeforeHook) {
|
|
5003
|
+
if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
|
|
5004
|
+
if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
|
|
5005
|
+
settings.hooks.BeforeTool.push({
|
|
5006
|
+
matcher: ".*",
|
|
5007
|
+
hooks: [
|
|
5008
|
+
{
|
|
5009
|
+
name: "node9-check",
|
|
5010
|
+
type: "command",
|
|
5011
|
+
command: fullPathCommand("check"),
|
|
5012
|
+
timeout: 6e5
|
|
5013
|
+
}
|
|
5014
|
+
]
|
|
5015
|
+
});
|
|
5016
|
+
console.log(chalk3.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
5017
|
+
anythingChanged = true;
|
|
5018
|
+
}
|
|
5019
|
+
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
5020
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
5021
|
+
);
|
|
5022
|
+
if (!hasAfterHook) {
|
|
5023
|
+
if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
|
|
5024
|
+
if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
|
|
5025
|
+
settings.hooks.AfterTool.push({
|
|
5026
|
+
matcher: ".*",
|
|
5027
|
+
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
5028
|
+
});
|
|
5029
|
+
console.log(chalk3.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
5030
|
+
anythingChanged = true;
|
|
5031
|
+
}
|
|
5032
|
+
if (anythingChanged) {
|
|
5033
|
+
writeJson(settingsPath, settings);
|
|
5034
|
+
console.log("");
|
|
5035
|
+
}
|
|
5036
|
+
const serversToWrap = [];
|
|
5037
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
5038
|
+
if (!server.command || server.command === "node9") continue;
|
|
5039
|
+
const parts = [server.command, ...server.args ?? []];
|
|
5040
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
5041
|
+
}
|
|
5042
|
+
if (serversToWrap.length > 0) {
|
|
5043
|
+
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
5044
|
+
console.log(chalk3.white(` ${settingsPath} (mcpServers)`));
|
|
5045
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
5046
|
+
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
5047
|
+
}
|
|
5048
|
+
console.log("");
|
|
5049
|
+
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
5050
|
+
if (proceed) {
|
|
5051
|
+
for (const { name, parts } of serversToWrap) {
|
|
5052
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
4137
5053
|
}
|
|
5054
|
+
settings.mcpServers = servers;
|
|
5055
|
+
writeJson(settingsPath, settings);
|
|
5056
|
+
console.log(chalk3.green(`
|
|
5057
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
5058
|
+
anythingChanged = true;
|
|
5059
|
+
} else {
|
|
5060
|
+
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
4138
5061
|
}
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
5062
|
+
console.log("");
|
|
5063
|
+
}
|
|
5064
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
5065
|
+
console.log(chalk3.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
|
|
5066
|
+
printDaemonTip();
|
|
5067
|
+
return;
|
|
5068
|
+
}
|
|
5069
|
+
if (anythingChanged) {
|
|
5070
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
5071
|
+
console.log(chalk3.gray(" Restart Gemini CLI for changes to take effect."));
|
|
5072
|
+
printDaemonTip();
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
async function setupCursor() {
|
|
5076
|
+
const homeDir2 = os3.homedir();
|
|
5077
|
+
const mcpPath = path5.join(homeDir2, ".cursor", "mcp.json");
|
|
5078
|
+
const mcpConfig = readJson(mcpPath) ?? {};
|
|
5079
|
+
const servers = mcpConfig.mcpServers ?? {};
|
|
5080
|
+
let anythingChanged = false;
|
|
5081
|
+
const serversToWrap = [];
|
|
5082
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
5083
|
+
if (!server.command || server.command === "node9") continue;
|
|
5084
|
+
const parts = [server.command, ...server.args ?? []];
|
|
5085
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
5086
|
+
}
|
|
5087
|
+
if (serversToWrap.length > 0) {
|
|
5088
|
+
console.log(chalk3.bold("The following existing entries will be modified:\n"));
|
|
5089
|
+
console.log(chalk3.white(` ${mcpPath}`));
|
|
5090
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
5091
|
+
console.log(chalk3.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
4142
5092
|
}
|
|
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;
|
|
5093
|
+
console.log("");
|
|
5094
|
+
const proceed = await confirm2({ message: "Wrap these MCP servers?", default: true });
|
|
5095
|
+
if (proceed) {
|
|
5096
|
+
for (const { name, parts } of serversToWrap) {
|
|
5097
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
4161
5098
|
}
|
|
5099
|
+
mcpConfig.mcpServers = servers;
|
|
5100
|
+
writeJson(mcpPath, mcpConfig);
|
|
5101
|
+
console.log(chalk3.green(`
|
|
5102
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
5103
|
+
anythingChanged = true;
|
|
5104
|
+
} else {
|
|
5105
|
+
console.log(chalk3.yellow(" Skipped MCP server wrapping."));
|
|
4162
5106
|
}
|
|
4163
|
-
console.
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
5107
|
+
console.log("");
|
|
5108
|
+
}
|
|
5109
|
+
console.log(
|
|
5110
|
+
chalk3.yellow(
|
|
5111
|
+
" \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
|
|
5112
|
+
)
|
|
5113
|
+
);
|
|
5114
|
+
console.log("");
|
|
5115
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
5116
|
+
console.log(
|
|
5117
|
+
chalk3.blue(
|
|
5118
|
+
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
|
|
5119
|
+
)
|
|
4171
5120
|
);
|
|
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
|
-
}
|
|
5121
|
+
printDaemonTip();
|
|
5122
|
+
return;
|
|
4188
5123
|
}
|
|
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)"));
|
|
5124
|
+
if (anythingChanged) {
|
|
5125
|
+
console.log(chalk3.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
|
|
5126
|
+
console.log(chalk3.gray(" Restart Cursor for changes to take effect."));
|
|
5127
|
+
printDaemonTip();
|
|
4199
5128
|
}
|
|
4200
5129
|
}
|
|
4201
5130
|
|
|
4202
5131
|
// src/cli.ts
|
|
4203
|
-
|
|
5132
|
+
init_daemon();
|
|
5133
|
+
import { spawn as spawn4, execSync } from "child_process";
|
|
4204
5134
|
import { parseCommandString } from "execa";
|
|
4205
5135
|
import { execa } from "execa";
|
|
4206
|
-
import
|
|
4207
|
-
import
|
|
4208
|
-
import
|
|
4209
|
-
import
|
|
4210
|
-
import
|
|
5136
|
+
import chalk6 from "chalk";
|
|
5137
|
+
import readline2 from "readline";
|
|
5138
|
+
import fs7 from "fs";
|
|
5139
|
+
import path9 from "path";
|
|
5140
|
+
import os7 from "os";
|
|
4211
5141
|
|
|
4212
5142
|
// src/undo.ts
|
|
4213
|
-
import { spawnSync } from "child_process";
|
|
5143
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
4214
5144
|
import fs5 from "fs";
|
|
4215
5145
|
import path7 from "path";
|
|
4216
5146
|
import os5 from "os";
|
|
@@ -4247,12 +5177,12 @@ async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
|
4247
5177
|
if (!fs5.existsSync(path7.join(cwd, ".git"))) return null;
|
|
4248
5178
|
const tempIndex = path7.join(cwd, ".git", `node9_index_${Date.now()}`);
|
|
4249
5179
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
4250
|
-
|
|
4251
|
-
const treeRes =
|
|
5180
|
+
spawnSync3("git", ["add", "-A"], { env });
|
|
5181
|
+
const treeRes = spawnSync3("git", ["write-tree"], { env });
|
|
4252
5182
|
const treeHash = treeRes.stdout.toString().trim();
|
|
4253
5183
|
if (fs5.existsSync(tempIndex)) fs5.unlinkSync(tempIndex);
|
|
4254
5184
|
if (!treeHash || treeRes.status !== 0) return null;
|
|
4255
|
-
const commitRes =
|
|
5185
|
+
const commitRes = spawnSync3("git", [
|
|
4256
5186
|
"commit-tree",
|
|
4257
5187
|
treeHash,
|
|
4258
5188
|
"-m",
|
|
@@ -4283,10 +5213,10 @@ function getSnapshotHistory() {
|
|
|
4283
5213
|
}
|
|
4284
5214
|
function computeUndoDiff(hash, cwd) {
|
|
4285
5215
|
try {
|
|
4286
|
-
const result =
|
|
5216
|
+
const result = spawnSync3("git", ["diff", hash, "--stat", "--", "."], { cwd });
|
|
4287
5217
|
const stat = result.stdout.toString().trim();
|
|
4288
5218
|
if (!stat) return null;
|
|
4289
|
-
const diff =
|
|
5219
|
+
const diff = spawnSync3("git", ["diff", hash, "--", "."], { cwd });
|
|
4290
5220
|
const raw = diff.stdout.toString();
|
|
4291
5221
|
if (!raw) return null;
|
|
4292
5222
|
const lines = raw.split("\n").filter(
|
|
@@ -4300,14 +5230,14 @@ function computeUndoDiff(hash, cwd) {
|
|
|
4300
5230
|
function applyUndo(hash, cwd) {
|
|
4301
5231
|
try {
|
|
4302
5232
|
const dir = cwd ?? process.cwd();
|
|
4303
|
-
const restore =
|
|
5233
|
+
const restore = spawnSync3("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
|
|
4304
5234
|
cwd: dir
|
|
4305
5235
|
});
|
|
4306
5236
|
if (restore.status !== 0) return false;
|
|
4307
|
-
const lsTree =
|
|
5237
|
+
const lsTree = spawnSync3("git", ["ls-tree", "-r", "--name-only", hash], { cwd: dir });
|
|
4308
5238
|
const snapshotFiles = new Set(lsTree.stdout.toString().trim().split("\n").filter(Boolean));
|
|
4309
|
-
const tracked =
|
|
4310
|
-
const untracked =
|
|
5239
|
+
const tracked = spawnSync3("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
5240
|
+
const untracked = spawnSync3("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
4311
5241
|
for (const file of [...tracked, ...untracked]) {
|
|
4312
5242
|
const fullPath = path7.join(dir, file);
|
|
4313
5243
|
if (!snapshotFiles.has(file) && fs5.existsSync(fullPath)) {
|
|
@@ -4321,9 +5251,10 @@ function applyUndo(hash, cwd) {
|
|
|
4321
5251
|
}
|
|
4322
5252
|
|
|
4323
5253
|
// src/cli.ts
|
|
5254
|
+
init_shields();
|
|
4324
5255
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
4325
5256
|
var { version } = JSON.parse(
|
|
4326
|
-
|
|
5257
|
+
fs7.readFileSync(path9.join(__dirname, "../package.json"), "utf-8")
|
|
4327
5258
|
);
|
|
4328
5259
|
function parseDuration(str) {
|
|
4329
5260
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -4419,7 +5350,7 @@ function openBrowserLocal() {
|
|
|
4419
5350
|
}
|
|
4420
5351
|
async function autoStartDaemonAndWait() {
|
|
4421
5352
|
try {
|
|
4422
|
-
const child =
|
|
5353
|
+
const child = spawn4("node9", ["daemon"], {
|
|
4423
5354
|
detached: true,
|
|
4424
5355
|
stdio: "ignore",
|
|
4425
5356
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -4455,14 +5386,14 @@ async function runProxy(targetCommand) {
|
|
|
4455
5386
|
if (stdout) executable = stdout.trim();
|
|
4456
5387
|
} catch {
|
|
4457
5388
|
}
|
|
4458
|
-
console.log(
|
|
4459
|
-
const child =
|
|
5389
|
+
console.log(chalk6.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
5390
|
+
const child = spawn4(executable, args, {
|
|
4460
5391
|
stdio: ["pipe", "pipe", "inherit"],
|
|
4461
5392
|
// We control STDIN and STDOUT
|
|
4462
5393
|
shell: false,
|
|
4463
5394
|
env: { ...process.env, FORCE_COLOR: "1" }
|
|
4464
5395
|
});
|
|
4465
|
-
const agentIn =
|
|
5396
|
+
const agentIn = readline2.createInterface({ input: process.stdin, terminal: false });
|
|
4466
5397
|
agentIn.on("line", async (line) => {
|
|
4467
5398
|
let message;
|
|
4468
5399
|
try {
|
|
@@ -4480,10 +5411,10 @@ async function runProxy(targetCommand) {
|
|
|
4480
5411
|
agent: "Proxy/MCP"
|
|
4481
5412
|
});
|
|
4482
5413
|
if (!result.approved) {
|
|
4483
|
-
console.error(
|
|
5414
|
+
console.error(chalk6.red(`
|
|
4484
5415
|
\u{1F6D1} Node9 Sudo: Action Blocked`));
|
|
4485
|
-
console.error(
|
|
4486
|
-
console.error(
|
|
5416
|
+
console.error(chalk6.gray(` Tool: ${name}`));
|
|
5417
|
+
console.error(chalk6.gray(` Reason: ${result.reason || "Security Policy"}
|
|
4487
5418
|
`));
|
|
4488
5419
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
4489
5420
|
const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -4525,14 +5456,14 @@ async function runProxy(targetCommand) {
|
|
|
4525
5456
|
}
|
|
4526
5457
|
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
5458
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
4528
|
-
const credPath =
|
|
4529
|
-
if (!
|
|
4530
|
-
|
|
5459
|
+
const credPath = path9.join(os7.homedir(), ".node9", "credentials.json");
|
|
5460
|
+
if (!fs7.existsSync(path9.dirname(credPath)))
|
|
5461
|
+
fs7.mkdirSync(path9.dirname(credPath), { recursive: true });
|
|
4531
5462
|
const profileName = options.profile || "default";
|
|
4532
5463
|
let existingCreds = {};
|
|
4533
5464
|
try {
|
|
4534
|
-
if (
|
|
4535
|
-
const raw = JSON.parse(
|
|
5465
|
+
if (fs7.existsSync(credPath)) {
|
|
5466
|
+
const raw = JSON.parse(fs7.readFileSync(credPath, "utf-8"));
|
|
4536
5467
|
if (raw.apiKey) {
|
|
4537
5468
|
existingCreds = {
|
|
4538
5469
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -4544,13 +5475,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4544
5475
|
} catch {
|
|
4545
5476
|
}
|
|
4546
5477
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
4547
|
-
|
|
5478
|
+
fs7.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
4548
5479
|
if (profileName === "default") {
|
|
4549
|
-
const configPath =
|
|
5480
|
+
const configPath = path9.join(os7.homedir(), ".node9", "config.json");
|
|
4550
5481
|
let config = {};
|
|
4551
5482
|
try {
|
|
4552
|
-
if (
|
|
4553
|
-
config = JSON.parse(
|
|
5483
|
+
if (fs7.existsSync(configPath))
|
|
5484
|
+
config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
|
|
4554
5485
|
} catch {
|
|
4555
5486
|
}
|
|
4556
5487
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -4565,36 +5496,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4565
5496
|
approvers.cloud = false;
|
|
4566
5497
|
}
|
|
4567
5498
|
s.approvers = approvers;
|
|
4568
|
-
if (!
|
|
4569
|
-
|
|
4570
|
-
|
|
5499
|
+
if (!fs7.existsSync(path9.dirname(configPath)))
|
|
5500
|
+
fs7.mkdirSync(path9.dirname(configPath), { recursive: true });
|
|
5501
|
+
fs7.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
4571
5502
|
}
|
|
4572
5503
|
if (options.profile && profileName !== "default") {
|
|
4573
|
-
console.log(
|
|
4574
|
-
console.log(
|
|
5504
|
+
console.log(chalk6.green(`\u2705 Profile "${profileName}" saved`));
|
|
5505
|
+
console.log(chalk6.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
4575
5506
|
} else if (options.local) {
|
|
4576
|
-
console.log(
|
|
4577
|
-
console.log(
|
|
5507
|
+
console.log(chalk6.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
5508
|
+
console.log(chalk6.gray(` All decisions stay on this machine.`));
|
|
4578
5509
|
} else {
|
|
4579
|
-
console.log(
|
|
4580
|
-
console.log(
|
|
5510
|
+
console.log(chalk6.green(`\u2705 Logged in \u2014 agent mode`));
|
|
5511
|
+
console.log(chalk6.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
4581
5512
|
}
|
|
4582
5513
|
});
|
|
4583
5514
|
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
5515
|
if (target === "gemini") return await setupGemini();
|
|
4585
5516
|
if (target === "claude") return await setupClaude();
|
|
4586
5517
|
if (target === "cursor") return await setupCursor();
|
|
4587
|
-
console.error(
|
|
5518
|
+
console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
4588
5519
|
process.exit(1);
|
|
4589
5520
|
});
|
|
4590
5521
|
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
5522
|
if (!target) {
|
|
4592
|
-
console.log(
|
|
4593
|
-
console.log(" Usage: " +
|
|
5523
|
+
console.log(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
5524
|
+
console.log(" Usage: " + chalk6.white("node9 setup <target>") + "\n");
|
|
4594
5525
|
console.log(" Targets:");
|
|
4595
|
-
console.log(" " +
|
|
4596
|
-
console.log(" " +
|
|
4597
|
-
console.log(" " +
|
|
5526
|
+
console.log(" " + chalk6.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
5527
|
+
console.log(" " + chalk6.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
5528
|
+
console.log(" " + chalk6.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
4598
5529
|
console.log("");
|
|
4599
5530
|
return;
|
|
4600
5531
|
}
|
|
@@ -4602,28 +5533,28 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
4602
5533
|
if (t === "gemini") return await setupGemini();
|
|
4603
5534
|
if (t === "claude") return await setupClaude();
|
|
4604
5535
|
if (t === "cursor") return await setupCursor();
|
|
4605
|
-
console.error(
|
|
5536
|
+
console.error(chalk6.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
4606
5537
|
process.exit(1);
|
|
4607
5538
|
});
|
|
4608
5539
|
program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
4609
|
-
const homeDir2 =
|
|
5540
|
+
const homeDir2 = os7.homedir();
|
|
4610
5541
|
let failures = 0;
|
|
4611
5542
|
function pass(msg) {
|
|
4612
|
-
console.log(
|
|
5543
|
+
console.log(chalk6.green(" \u2705 ") + msg);
|
|
4613
5544
|
}
|
|
4614
5545
|
function fail(msg, hint) {
|
|
4615
|
-
console.log(
|
|
4616
|
-
if (hint) console.log(
|
|
5546
|
+
console.log(chalk6.red(" \u274C ") + msg);
|
|
5547
|
+
if (hint) console.log(chalk6.gray(" " + hint));
|
|
4617
5548
|
failures++;
|
|
4618
5549
|
}
|
|
4619
5550
|
function warn(msg, hint) {
|
|
4620
|
-
console.log(
|
|
4621
|
-
if (hint) console.log(
|
|
5551
|
+
console.log(chalk6.yellow(" \u26A0\uFE0F ") + msg);
|
|
5552
|
+
if (hint) console.log(chalk6.gray(" " + hint));
|
|
4622
5553
|
}
|
|
4623
5554
|
function section(title) {
|
|
4624
|
-
console.log("\n" +
|
|
5555
|
+
console.log("\n" + chalk6.bold(title));
|
|
4625
5556
|
}
|
|
4626
|
-
console.log(
|
|
5557
|
+
console.log(chalk6.cyan.bold(`
|
|
4627
5558
|
\u{1F6E1}\uFE0F Node9 Doctor v${version}
|
|
4628
5559
|
`));
|
|
4629
5560
|
section("Binary");
|
|
@@ -4652,10 +5583,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4652
5583
|
);
|
|
4653
5584
|
}
|
|
4654
5585
|
section("Configuration");
|
|
4655
|
-
const globalConfigPath =
|
|
4656
|
-
if (
|
|
5586
|
+
const globalConfigPath = path9.join(homeDir2, ".node9", "config.json");
|
|
5587
|
+
if (fs7.existsSync(globalConfigPath)) {
|
|
4657
5588
|
try {
|
|
4658
|
-
JSON.parse(
|
|
5589
|
+
JSON.parse(fs7.readFileSync(globalConfigPath, "utf-8"));
|
|
4659
5590
|
pass("~/.node9/config.json found and valid");
|
|
4660
5591
|
} catch {
|
|
4661
5592
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -4663,17 +5594,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4663
5594
|
} else {
|
|
4664
5595
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
4665
5596
|
}
|
|
4666
|
-
const projectConfigPath =
|
|
4667
|
-
if (
|
|
5597
|
+
const projectConfigPath = path9.join(process.cwd(), "node9.config.json");
|
|
5598
|
+
if (fs7.existsSync(projectConfigPath)) {
|
|
4668
5599
|
try {
|
|
4669
|
-
JSON.parse(
|
|
5600
|
+
JSON.parse(fs7.readFileSync(projectConfigPath, "utf-8"));
|
|
4670
5601
|
pass("node9.config.json found and valid (project)");
|
|
4671
5602
|
} catch {
|
|
4672
5603
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
4673
5604
|
}
|
|
4674
5605
|
}
|
|
4675
|
-
const credsPath =
|
|
4676
|
-
if (
|
|
5606
|
+
const credsPath = path9.join(homeDir2, ".node9", "credentials.json");
|
|
5607
|
+
if (fs7.existsSync(credsPath)) {
|
|
4677
5608
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
4678
5609
|
} else {
|
|
4679
5610
|
warn(
|
|
@@ -4682,10 +5613,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4682
5613
|
);
|
|
4683
5614
|
}
|
|
4684
5615
|
section("Agent Hooks");
|
|
4685
|
-
const claudeSettingsPath =
|
|
4686
|
-
if (
|
|
5616
|
+
const claudeSettingsPath = path9.join(homeDir2, ".claude", "settings.json");
|
|
5617
|
+
if (fs7.existsSync(claudeSettingsPath)) {
|
|
4687
5618
|
try {
|
|
4688
|
-
const cs = JSON.parse(
|
|
5619
|
+
const cs = JSON.parse(fs7.readFileSync(claudeSettingsPath, "utf-8"));
|
|
4689
5620
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
4690
5621
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4691
5622
|
);
|
|
@@ -4698,10 +5629,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4698
5629
|
} else {
|
|
4699
5630
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
4700
5631
|
}
|
|
4701
|
-
const geminiSettingsPath =
|
|
4702
|
-
if (
|
|
5632
|
+
const geminiSettingsPath = path9.join(homeDir2, ".gemini", "settings.json");
|
|
5633
|
+
if (fs7.existsSync(geminiSettingsPath)) {
|
|
4703
5634
|
try {
|
|
4704
|
-
const gs = JSON.parse(
|
|
5635
|
+
const gs = JSON.parse(fs7.readFileSync(geminiSettingsPath, "utf-8"));
|
|
4705
5636
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
4706
5637
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4707
5638
|
);
|
|
@@ -4714,10 +5645,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4714
5645
|
} else {
|
|
4715
5646
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
4716
5647
|
}
|
|
4717
|
-
const cursorHooksPath =
|
|
4718
|
-
if (
|
|
5648
|
+
const cursorHooksPath = path9.join(homeDir2, ".cursor", "hooks.json");
|
|
5649
|
+
if (fs7.existsSync(cursorHooksPath)) {
|
|
4719
5650
|
try {
|
|
4720
|
-
const cur = JSON.parse(
|
|
5651
|
+
const cur = JSON.parse(fs7.readFileSync(cursorHooksPath, "utf-8"));
|
|
4721
5652
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
4722
5653
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
4723
5654
|
);
|
|
@@ -4738,9 +5669,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4738
5669
|
}
|
|
4739
5670
|
console.log("");
|
|
4740
5671
|
if (failures === 0) {
|
|
4741
|
-
console.log(
|
|
5672
|
+
console.log(chalk6.green.bold(" All checks passed. Node9 is ready.\n"));
|
|
4742
5673
|
} else {
|
|
4743
|
-
console.log(
|
|
5674
|
+
console.log(chalk6.red.bold(` ${failures} check(s) failed. See hints above.
|
|
4744
5675
|
`));
|
|
4745
5676
|
process.exit(1);
|
|
4746
5677
|
}
|
|
@@ -4755,7 +5686,7 @@ program.command("explain").description(
|
|
|
4755
5686
|
try {
|
|
4756
5687
|
args = JSON.parse(trimmed);
|
|
4757
5688
|
} catch {
|
|
4758
|
-
console.error(
|
|
5689
|
+
console.error(chalk6.red(`
|
|
4759
5690
|
\u274C Invalid JSON: ${trimmed}
|
|
4760
5691
|
`));
|
|
4761
5692
|
process.exit(1);
|
|
@@ -4766,63 +5697,63 @@ program.command("explain").description(
|
|
|
4766
5697
|
}
|
|
4767
5698
|
const result = await explainPolicy(tool, args);
|
|
4768
5699
|
console.log("");
|
|
4769
|
-
console.log(
|
|
5700
|
+
console.log(chalk6.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
4770
5701
|
console.log("");
|
|
4771
|
-
console.log(` ${
|
|
5702
|
+
console.log(` ${chalk6.bold("Tool:")} ${chalk6.white(result.tool)}`);
|
|
4772
5703
|
if (argsRaw) {
|
|
4773
5704
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
4774
|
-
console.log(` ${
|
|
5705
|
+
console.log(` ${chalk6.bold("Input:")} ${chalk6.gray(preview)}`);
|
|
4775
5706
|
}
|
|
4776
5707
|
console.log("");
|
|
4777
|
-
console.log(
|
|
5708
|
+
console.log(chalk6.bold("Config Sources (Waterfall):"));
|
|
4778
5709
|
for (const tier of result.waterfall) {
|
|
4779
|
-
const num =
|
|
5710
|
+
const num = chalk6.gray(` ${tier.tier}.`);
|
|
4780
5711
|
const label = tier.label.padEnd(16);
|
|
4781
5712
|
let statusStr;
|
|
4782
5713
|
if (tier.tier === 1) {
|
|
4783
|
-
statusStr =
|
|
5714
|
+
statusStr = chalk6.gray(tier.note ?? "");
|
|
4784
5715
|
} else if (tier.status === "active") {
|
|
4785
|
-
const loc = tier.path ?
|
|
4786
|
-
const note = tier.note ?
|
|
4787
|
-
statusStr =
|
|
5716
|
+
const loc = tier.path ? chalk6.gray(tier.path) : "";
|
|
5717
|
+
const note = tier.note ? chalk6.gray(`(${tier.note})`) : "";
|
|
5718
|
+
statusStr = chalk6.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
4788
5719
|
} else {
|
|
4789
|
-
statusStr =
|
|
5720
|
+
statusStr = chalk6.gray("\u25CB " + (tier.note ?? "not found"));
|
|
4790
5721
|
}
|
|
4791
|
-
console.log(`${num} ${
|
|
5722
|
+
console.log(`${num} ${chalk6.white(label)} ${statusStr}`);
|
|
4792
5723
|
}
|
|
4793
5724
|
console.log("");
|
|
4794
|
-
console.log(
|
|
5725
|
+
console.log(chalk6.bold("Policy Evaluation:"));
|
|
4795
5726
|
for (const step of result.steps) {
|
|
4796
5727
|
const isFinal = step.isFinal;
|
|
4797
5728
|
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 =
|
|
5729
|
+
if (step.outcome === "allow") icon = chalk6.green(" \u2705");
|
|
5730
|
+
else if (step.outcome === "review") icon = chalk6.red(" \u{1F534}");
|
|
5731
|
+
else if (step.outcome === "skip") icon = chalk6.gray(" \u2500 ");
|
|
5732
|
+
else icon = chalk6.gray(" \u25CB ");
|
|
4802
5733
|
const name = step.name.padEnd(18);
|
|
4803
|
-
const nameStr = isFinal ?
|
|
4804
|
-
const detail = isFinal ?
|
|
4805
|
-
const arrow = isFinal ?
|
|
5734
|
+
const nameStr = isFinal ? chalk6.white.bold(name) : chalk6.white(name);
|
|
5735
|
+
const detail = isFinal ? chalk6.white(step.detail) : chalk6.gray(step.detail);
|
|
5736
|
+
const arrow = isFinal ? chalk6.yellow(" \u2190 STOP") : "";
|
|
4806
5737
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
4807
5738
|
}
|
|
4808
5739
|
console.log("");
|
|
4809
5740
|
if (result.decision === "allow") {
|
|
4810
|
-
console.log(
|
|
5741
|
+
console.log(chalk6.green.bold(" Decision: \u2705 ALLOW") + chalk6.gray(" \u2014 no approval needed"));
|
|
4811
5742
|
} else {
|
|
4812
5743
|
console.log(
|
|
4813
|
-
|
|
5744
|
+
chalk6.red.bold(" Decision: \u{1F534} REVIEW") + chalk6.gray(" \u2014 human approval required")
|
|
4814
5745
|
);
|
|
4815
5746
|
if (result.blockedByLabel) {
|
|
4816
|
-
console.log(
|
|
5747
|
+
console.log(chalk6.gray(` Reason: ${result.blockedByLabel}`));
|
|
4817
5748
|
}
|
|
4818
5749
|
}
|
|
4819
5750
|
console.log("");
|
|
4820
5751
|
});
|
|
4821
5752
|
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(
|
|
5753
|
+
const configPath = path9.join(os7.homedir(), ".node9", "config.json");
|
|
5754
|
+
if (fs7.existsSync(configPath) && !options.force) {
|
|
5755
|
+
console.log(chalk6.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
5756
|
+
console.log(chalk6.gray(` Run with --force to overwrite.`));
|
|
4826
5757
|
return;
|
|
4827
5758
|
}
|
|
4828
5759
|
const requestedMode = options.mode.toLowerCase();
|
|
@@ -4834,13 +5765,13 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
4834
5765
|
mode: safeMode
|
|
4835
5766
|
}
|
|
4836
5767
|
};
|
|
4837
|
-
const dir =
|
|
4838
|
-
if (!
|
|
4839
|
-
|
|
4840
|
-
console.log(
|
|
4841
|
-
console.log(
|
|
5768
|
+
const dir = path9.dirname(configPath);
|
|
5769
|
+
if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
|
|
5770
|
+
fs7.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
5771
|
+
console.log(chalk6.green(`\u2705 Global config created: ${configPath}`));
|
|
5772
|
+
console.log(chalk6.cyan(` Mode set to: ${safeMode}`));
|
|
4842
5773
|
console.log(
|
|
4843
|
-
|
|
5774
|
+
chalk6.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
4844
5775
|
);
|
|
4845
5776
|
});
|
|
4846
5777
|
function formatRelativeTime(timestamp) {
|
|
@@ -4854,14 +5785,14 @@ function formatRelativeTime(timestamp) {
|
|
|
4854
5785
|
return new Date(timestamp).toLocaleDateString();
|
|
4855
5786
|
}
|
|
4856
5787
|
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 (!
|
|
5788
|
+
const logPath = path9.join(os7.homedir(), ".node9", "audit.log");
|
|
5789
|
+
if (!fs7.existsSync(logPath)) {
|
|
4859
5790
|
console.log(
|
|
4860
|
-
|
|
5791
|
+
chalk6.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
4861
5792
|
);
|
|
4862
5793
|
return;
|
|
4863
5794
|
}
|
|
4864
|
-
const raw =
|
|
5795
|
+
const raw = fs7.readFileSync(logPath, "utf-8");
|
|
4865
5796
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
4866
5797
|
let entries = lines.flatMap((line) => {
|
|
4867
5798
|
try {
|
|
@@ -4883,31 +5814,31 @@ program.command("audit").description("View local execution audit log").option("-
|
|
|
4883
5814
|
return;
|
|
4884
5815
|
}
|
|
4885
5816
|
if (entries.length === 0) {
|
|
4886
|
-
console.log(
|
|
5817
|
+
console.log(chalk6.yellow("No matching audit entries."));
|
|
4887
5818
|
return;
|
|
4888
5819
|
}
|
|
4889
5820
|
console.log(
|
|
4890
5821
|
`
|
|
4891
|
-
${
|
|
5822
|
+
${chalk6.bold("Node9 Audit Log")} ${chalk6.dim(`(${entries.length} entries)`)}`
|
|
4892
5823
|
);
|
|
4893
|
-
console.log(
|
|
5824
|
+
console.log(chalk6.dim(" " + "\u2500".repeat(65)));
|
|
4894
5825
|
console.log(
|
|
4895
5826
|
` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
|
|
4896
5827
|
);
|
|
4897
|
-
console.log(
|
|
5828
|
+
console.log(chalk6.dim(" " + "\u2500".repeat(65)));
|
|
4898
5829
|
for (const e of entries) {
|
|
4899
5830
|
const time = formatRelativeTime(String(e.ts)).padEnd(12);
|
|
4900
5831
|
const tool = String(e.tool).slice(0, 17).padEnd(18);
|
|
4901
|
-
const result = e.decision === "allow" ?
|
|
5832
|
+
const result = e.decision === "allow" ? chalk6.green("ALLOW".padEnd(10)) : chalk6.red("DENY".padEnd(10));
|
|
4902
5833
|
const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
|
|
4903
5834
|
const agent = String(e.agent || "unknown");
|
|
4904
5835
|
console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
|
|
4905
5836
|
}
|
|
4906
5837
|
const allowed = entries.filter((e) => e.decision === "allow").length;
|
|
4907
5838
|
const denied = entries.filter((e) => e.decision === "deny").length;
|
|
4908
|
-
console.log(
|
|
5839
|
+
console.log(chalk6.dim(" " + "\u2500".repeat(65)));
|
|
4909
5840
|
console.log(
|
|
4910
|
-
` ${entries.length} entries | ${
|
|
5841
|
+
` ${entries.length} entries | ${chalk6.green(allowed + " allowed")} | ${chalk6.red(denied + " denied")}
|
|
4911
5842
|
`
|
|
4912
5843
|
);
|
|
4913
5844
|
});
|
|
@@ -4918,43 +5849,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4918
5849
|
const settings = mergedConfig.settings;
|
|
4919
5850
|
console.log("");
|
|
4920
5851
|
if (creds && settings.approvers.cloud) {
|
|
4921
|
-
console.log(
|
|
5852
|
+
console.log(chalk6.green(" \u25CF Agent mode") + chalk6.gray(" \u2014 cloud team policy enforced"));
|
|
4922
5853
|
} else if (creds && !settings.approvers.cloud) {
|
|
4923
5854
|
console.log(
|
|
4924
|
-
|
|
5855
|
+
chalk6.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + chalk6.gray(" \u2014 all decisions stay on this machine")
|
|
4925
5856
|
);
|
|
4926
5857
|
} else {
|
|
4927
5858
|
console.log(
|
|
4928
|
-
|
|
5859
|
+
chalk6.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + chalk6.gray(" \u2014 no API key (Local rules only)")
|
|
4929
5860
|
);
|
|
4930
5861
|
}
|
|
4931
5862
|
console.log("");
|
|
4932
5863
|
if (daemonRunning) {
|
|
4933
5864
|
console.log(
|
|
4934
|
-
|
|
5865
|
+
chalk6.green(" \u25CF Daemon running") + chalk6.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
|
|
4935
5866
|
);
|
|
4936
5867
|
} else {
|
|
4937
|
-
console.log(
|
|
5868
|
+
console.log(chalk6.gray(" \u25CB Daemon stopped"));
|
|
4938
5869
|
}
|
|
4939
5870
|
if (settings.enableUndo) {
|
|
4940
5871
|
console.log(
|
|
4941
|
-
|
|
5872
|
+
chalk6.magenta(" \u25CF Undo Engine") + chalk6.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
|
|
4942
5873
|
);
|
|
4943
5874
|
}
|
|
4944
5875
|
console.log("");
|
|
4945
|
-
const modeLabel = settings.mode === "audit" ?
|
|
5876
|
+
const modeLabel = settings.mode === "audit" ? chalk6.blue("audit") : settings.mode === "strict" ? chalk6.red("strict") : chalk6.white("standard");
|
|
4946
5877
|
console.log(` Mode: ${modeLabel}`);
|
|
4947
|
-
const projectConfig =
|
|
4948
|
-
const globalConfig =
|
|
5878
|
+
const projectConfig = path9.join(process.cwd(), "node9.config.json");
|
|
5879
|
+
const globalConfig = path9.join(os7.homedir(), ".node9", "config.json");
|
|
4949
5880
|
console.log(
|
|
4950
|
-
` Local: ${
|
|
5881
|
+
` Local: ${fs7.existsSync(projectConfig) ? chalk6.green("Active (node9.config.json)") : chalk6.gray("Not present")}`
|
|
4951
5882
|
);
|
|
4952
5883
|
console.log(
|
|
4953
|
-
` Global: ${
|
|
5884
|
+
` Global: ${fs7.existsSync(globalConfig) ? chalk6.green("Active (~/.node9/config.json)") : chalk6.gray("Not present")}`
|
|
4954
5885
|
);
|
|
4955
5886
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
4956
5887
|
console.log(
|
|
4957
|
-
` Sandbox: ${
|
|
5888
|
+
` Sandbox: ${chalk6.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
4958
5889
|
);
|
|
4959
5890
|
}
|
|
4960
5891
|
const pauseState = checkPause();
|
|
@@ -4962,47 +5893,63 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4962
5893
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
4963
5894
|
console.log("");
|
|
4964
5895
|
console.log(
|
|
4965
|
-
|
|
5896
|
+
chalk6.yellow(` \u23F8 PAUSED until ${expiresAt}`) + chalk6.gray(" \u2014 all tool calls allowed")
|
|
4966
5897
|
);
|
|
4967
5898
|
}
|
|
4968
5899
|
console.log("");
|
|
4969
5900
|
});
|
|
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").
|
|
5901
|
+
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(
|
|
5902
|
+
"-w, --watch",
|
|
5903
|
+
"Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
|
|
5904
|
+
).action(
|
|
4971
5905
|
async (action, options) => {
|
|
4972
5906
|
const cmd = (action ?? "start").toLowerCase();
|
|
4973
5907
|
if (cmd === "stop") return stopDaemon();
|
|
4974
5908
|
if (cmd === "status") return daemonStatus();
|
|
4975
5909
|
if (cmd !== "start" && action !== void 0) {
|
|
4976
|
-
console.error(
|
|
5910
|
+
console.error(chalk6.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
|
|
4977
5911
|
process.exit(1);
|
|
4978
5912
|
}
|
|
5913
|
+
if (options.watch) {
|
|
5914
|
+
process.env.NODE9_WATCH_MODE = "1";
|
|
5915
|
+
setTimeout(() => {
|
|
5916
|
+
openBrowserLocal();
|
|
5917
|
+
console.log(chalk6.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
5918
|
+
}, 600);
|
|
5919
|
+
startDaemon();
|
|
5920
|
+
return;
|
|
5921
|
+
}
|
|
4979
5922
|
if (options.openui) {
|
|
4980
5923
|
if (isDaemonRunning()) {
|
|
4981
5924
|
openBrowserLocal();
|
|
4982
|
-
console.log(
|
|
5925
|
+
console.log(chalk6.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
4983
5926
|
process.exit(0);
|
|
4984
5927
|
}
|
|
4985
|
-
const child =
|
|
5928
|
+
const child = spawn4("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
4986
5929
|
child.unref();
|
|
4987
5930
|
for (let i = 0; i < 12; i++) {
|
|
4988
5931
|
await new Promise((r) => setTimeout(r, 250));
|
|
4989
5932
|
if (isDaemonRunning()) break;
|
|
4990
5933
|
}
|
|
4991
5934
|
openBrowserLocal();
|
|
4992
|
-
console.log(
|
|
5935
|
+
console.log(chalk6.green(`
|
|
4993
5936
|
\u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
|
|
4994
5937
|
process.exit(0);
|
|
4995
5938
|
}
|
|
4996
5939
|
if (options.background) {
|
|
4997
|
-
const child =
|
|
5940
|
+
const child = spawn4("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
4998
5941
|
child.unref();
|
|
4999
|
-
console.log(
|
|
5942
|
+
console.log(chalk6.green(`
|
|
5000
5943
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
5001
5944
|
process.exit(0);
|
|
5002
5945
|
}
|
|
5003
5946
|
startDaemon();
|
|
5004
5947
|
}
|
|
5005
5948
|
);
|
|
5949
|
+
program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).option("--clear", "Clear history buffer and stream live events fresh", false).action(async (options) => {
|
|
5950
|
+
const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
|
|
5951
|
+
await startTail2(options);
|
|
5952
|
+
});
|
|
5006
5953
|
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
5954
|
const processPayload = async (raw) => {
|
|
5008
5955
|
try {
|
|
@@ -5013,9 +5960,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
5013
5960
|
} catch (err) {
|
|
5014
5961
|
const tempConfig = getConfig();
|
|
5015
5962
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
5016
|
-
const logPath =
|
|
5963
|
+
const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
5017
5964
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
5018
|
-
|
|
5965
|
+
fs7.appendFileSync(
|
|
5019
5966
|
logPath,
|
|
5020
5967
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
5021
5968
|
RAW: ${raw}
|
|
@@ -5033,10 +5980,10 @@ RAW: ${raw}
|
|
|
5033
5980
|
}
|
|
5034
5981
|
const config = getConfig();
|
|
5035
5982
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
5036
|
-
const logPath =
|
|
5037
|
-
if (!
|
|
5038
|
-
|
|
5039
|
-
|
|
5983
|
+
const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
5984
|
+
if (!fs7.existsSync(path9.dirname(logPath)))
|
|
5985
|
+
fs7.mkdirSync(path9.dirname(logPath), { recursive: true });
|
|
5986
|
+
fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
5040
5987
|
`);
|
|
5041
5988
|
}
|
|
5042
5989
|
const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
|
|
@@ -5048,18 +5995,18 @@ RAW: ${raw}
|
|
|
5048
5995
|
const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
|
|
5049
5996
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
5050
5997
|
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
5051
|
-
console.error(
|
|
5998
|
+
console.error(chalk6.bgRed.white.bold(`
|
|
5052
5999
|
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
5053
|
-
console.error(
|
|
6000
|
+
console.error(chalk6.red.bold(` A sensitive secret was found in the tool arguments!`));
|
|
5054
6001
|
} else {
|
|
5055
|
-
console.error(
|
|
6002
|
+
console.error(chalk6.red(`
|
|
5056
6003
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
5057
6004
|
}
|
|
5058
|
-
console.error(
|
|
5059
|
-
if (result2?.changeHint) console.error(
|
|
6005
|
+
console.error(chalk6.gray(` Triggered by: ${blockedByContext}`));
|
|
6006
|
+
if (result2?.changeHint) console.error(chalk6.cyan(` To change: ${result2.changeHint}`));
|
|
5060
6007
|
console.error("");
|
|
5061
6008
|
const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
|
|
5062
|
-
console.error(
|
|
6009
|
+
console.error(chalk6.dim(` (Detailed instructions sent to AI agent)`));
|
|
5063
6010
|
process.stdout.write(
|
|
5064
6011
|
JSON.stringify({
|
|
5065
6012
|
decision: "block",
|
|
@@ -5090,7 +6037,7 @@ RAW: ${raw}
|
|
|
5090
6037
|
process.exit(0);
|
|
5091
6038
|
}
|
|
5092
6039
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
5093
|
-
console.error(
|
|
6040
|
+
console.error(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
5094
6041
|
const daemonReady = await autoStartDaemonAndWait();
|
|
5095
6042
|
if (daemonReady) {
|
|
5096
6043
|
const retry = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
@@ -5113,9 +6060,9 @@ RAW: ${raw}
|
|
|
5113
6060
|
});
|
|
5114
6061
|
} catch (err) {
|
|
5115
6062
|
if (process.env.NODE9_DEBUG === "1") {
|
|
5116
|
-
const logPath =
|
|
6063
|
+
const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
5117
6064
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
5118
|
-
|
|
6065
|
+
fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
5119
6066
|
`);
|
|
5120
6067
|
}
|
|
5121
6068
|
process.exit(0);
|
|
@@ -5160,10 +6107,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
5160
6107
|
decision: "allowed",
|
|
5161
6108
|
source: "post-hook"
|
|
5162
6109
|
};
|
|
5163
|
-
const logPath =
|
|
5164
|
-
if (!
|
|
5165
|
-
|
|
5166
|
-
|
|
6110
|
+
const logPath = path9.join(os7.homedir(), ".node9", "audit.log");
|
|
6111
|
+
if (!fs7.existsSync(path9.dirname(logPath)))
|
|
6112
|
+
fs7.mkdirSync(path9.dirname(logPath), { recursive: true });
|
|
6113
|
+
fs7.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
5167
6114
|
const config = getConfig();
|
|
5168
6115
|
if (shouldSnapshot(tool, {}, config)) {
|
|
5169
6116
|
await createShadowSnapshot();
|
|
@@ -5190,7 +6137,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
5190
6137
|
const ms = parseDuration(options.duration);
|
|
5191
6138
|
if (ms === null) {
|
|
5192
6139
|
console.error(
|
|
5193
|
-
|
|
6140
|
+
chalk6.red(`
|
|
5194
6141
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
5195
6142
|
`)
|
|
5196
6143
|
);
|
|
@@ -5198,20 +6145,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
5198
6145
|
}
|
|
5199
6146
|
pauseNode9(ms, options.duration);
|
|
5200
6147
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
5201
|
-
console.log(
|
|
6148
|
+
console.log(chalk6.yellow(`
|
|
5202
6149
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
5203
|
-
console.log(
|
|
5204
|
-
console.log(
|
|
6150
|
+
console.log(chalk6.gray(` All tool calls will be allowed without review.`));
|
|
6151
|
+
console.log(chalk6.gray(` Run "node9 resume" to re-enable early.
|
|
5205
6152
|
`));
|
|
5206
6153
|
});
|
|
5207
6154
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
5208
6155
|
const { paused } = checkPause();
|
|
5209
6156
|
if (!paused) {
|
|
5210
|
-
console.log(
|
|
6157
|
+
console.log(chalk6.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
5211
6158
|
return;
|
|
5212
6159
|
}
|
|
5213
6160
|
resumeNode9();
|
|
5214
|
-
console.log(
|
|
6161
|
+
console.log(chalk6.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
5215
6162
|
});
|
|
5216
6163
|
var HOOK_BASED_AGENTS = {
|
|
5217
6164
|
claude: "claude",
|
|
@@ -5224,15 +6171,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
5224
6171
|
if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
|
|
5225
6172
|
const target = HOOK_BASED_AGENTS[firstArg];
|
|
5226
6173
|
console.error(
|
|
5227
|
-
|
|
6174
|
+
chalk6.yellow(`
|
|
5228
6175
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
5229
6176
|
);
|
|
5230
|
-
console.error(
|
|
6177
|
+
console.error(chalk6.white(`
|
|
5231
6178
|
"${target}" uses its own hook system. Use:`));
|
|
5232
6179
|
console.error(
|
|
5233
|
-
|
|
6180
|
+
chalk6.green(` node9 addto ${target} `) + chalk6.gray("# one-time setup")
|
|
5234
6181
|
);
|
|
5235
|
-
console.error(
|
|
6182
|
+
console.error(chalk6.green(` ${target} `) + chalk6.gray("# run normally"));
|
|
5236
6183
|
process.exit(1);
|
|
5237
6184
|
}
|
|
5238
6185
|
const fullCommand = commandArgs.join(" ");
|
|
@@ -5240,7 +6187,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
5240
6187
|
agent: "Terminal"
|
|
5241
6188
|
});
|
|
5242
6189
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
5243
|
-
console.error(
|
|
6190
|
+
console.error(chalk6.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
5244
6191
|
const daemonReady = await autoStartDaemonAndWait();
|
|
5245
6192
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
5246
6193
|
}
|
|
@@ -5249,12 +6196,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
5249
6196
|
}
|
|
5250
6197
|
if (!result.approved) {
|
|
5251
6198
|
console.error(
|
|
5252
|
-
|
|
6199
|
+
chalk6.red(`
|
|
5253
6200
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
5254
6201
|
);
|
|
5255
6202
|
process.exit(1);
|
|
5256
6203
|
}
|
|
5257
|
-
console.error(
|
|
6204
|
+
console.error(chalk6.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
5258
6205
|
await runProxy(fullCommand);
|
|
5259
6206
|
} else {
|
|
5260
6207
|
program.help();
|
|
@@ -5269,22 +6216,22 @@ program.command("undo").description(
|
|
|
5269
6216
|
if (history.length === 0) {
|
|
5270
6217
|
if (!options.all && allHistory.length > 0) {
|
|
5271
6218
|
console.log(
|
|
5272
|
-
|
|
6219
|
+
chalk6.yellow(
|
|
5273
6220
|
`
|
|
5274
6221
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
5275
|
-
Run ${
|
|
6222
|
+
Run ${chalk6.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
5276
6223
|
`
|
|
5277
6224
|
)
|
|
5278
6225
|
);
|
|
5279
6226
|
} else {
|
|
5280
|
-
console.log(
|
|
6227
|
+
console.log(chalk6.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
5281
6228
|
}
|
|
5282
6229
|
return;
|
|
5283
6230
|
}
|
|
5284
6231
|
const idx = history.length - steps;
|
|
5285
6232
|
if (idx < 0) {
|
|
5286
6233
|
console.log(
|
|
5287
|
-
|
|
6234
|
+
chalk6.yellow(
|
|
5288
6235
|
`
|
|
5289
6236
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
5290
6237
|
`
|
|
@@ -5295,18 +6242,18 @@ program.command("undo").description(
|
|
|
5295
6242
|
const snapshot = history[idx];
|
|
5296
6243
|
const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
|
|
5297
6244
|
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
|
|
5298
|
-
console.log(
|
|
6245
|
+
console.log(chalk6.magenta.bold(`
|
|
5299
6246
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
|
|
5300
6247
|
console.log(
|
|
5301
|
-
|
|
5302
|
-
` Tool: ${
|
|
6248
|
+
chalk6.white(
|
|
6249
|
+
` Tool: ${chalk6.cyan(snapshot.tool)}${snapshot.argsSummary ? chalk6.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
5303
6250
|
)
|
|
5304
6251
|
);
|
|
5305
|
-
console.log(
|
|
5306
|
-
console.log(
|
|
6252
|
+
console.log(chalk6.white(` When: ${chalk6.gray(ageStr)}`));
|
|
6253
|
+
console.log(chalk6.white(` Dir: ${chalk6.gray(snapshot.cwd)}`));
|
|
5307
6254
|
if (steps > 1)
|
|
5308
6255
|
console.log(
|
|
5309
|
-
|
|
6256
|
+
chalk6.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
5310
6257
|
);
|
|
5311
6258
|
console.log("");
|
|
5312
6259
|
const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
@@ -5314,21 +6261,21 @@ program.command("undo").description(
|
|
|
5314
6261
|
const lines = diff.split("\n");
|
|
5315
6262
|
for (const line of lines) {
|
|
5316
6263
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
5317
|
-
console.log(
|
|
6264
|
+
console.log(chalk6.bold(line));
|
|
5318
6265
|
} else if (line.startsWith("+")) {
|
|
5319
|
-
console.log(
|
|
6266
|
+
console.log(chalk6.green(line));
|
|
5320
6267
|
} else if (line.startsWith("-")) {
|
|
5321
|
-
console.log(
|
|
6268
|
+
console.log(chalk6.red(line));
|
|
5322
6269
|
} else if (line.startsWith("@@")) {
|
|
5323
|
-
console.log(
|
|
6270
|
+
console.log(chalk6.cyan(line));
|
|
5324
6271
|
} else {
|
|
5325
|
-
console.log(
|
|
6272
|
+
console.log(chalk6.gray(line));
|
|
5326
6273
|
}
|
|
5327
6274
|
}
|
|
5328
6275
|
console.log("");
|
|
5329
6276
|
} else {
|
|
5330
6277
|
console.log(
|
|
5331
|
-
|
|
6278
|
+
chalk6.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
5332
6279
|
);
|
|
5333
6280
|
}
|
|
5334
6281
|
const proceed = await confirm3({
|
|
@@ -5337,42 +6284,42 @@ program.command("undo").description(
|
|
|
5337
6284
|
});
|
|
5338
6285
|
if (proceed) {
|
|
5339
6286
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
5340
|
-
console.log(
|
|
6287
|
+
console.log(chalk6.green("\n\u2705 Reverted successfully.\n"));
|
|
5341
6288
|
} else {
|
|
5342
|
-
console.error(
|
|
6289
|
+
console.error(chalk6.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
5343
6290
|
}
|
|
5344
6291
|
} else {
|
|
5345
|
-
console.log(
|
|
6292
|
+
console.log(chalk6.gray("\nCancelled.\n"));
|
|
5346
6293
|
}
|
|
5347
6294
|
});
|
|
5348
6295
|
var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
|
|
5349
6296
|
shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
|
|
5350
6297
|
const name = resolveShieldName(service);
|
|
5351
6298
|
if (!name) {
|
|
5352
|
-
console.error(
|
|
6299
|
+
console.error(chalk6.red(`
|
|
5353
6300
|
\u274C Unknown shield: "${service}"
|
|
5354
6301
|
`));
|
|
5355
|
-
console.log(`Run ${
|
|
6302
|
+
console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
|
|
5356
6303
|
`);
|
|
5357
6304
|
process.exit(1);
|
|
5358
6305
|
}
|
|
5359
6306
|
const shield = getShield(name);
|
|
5360
6307
|
const active = readActiveShields();
|
|
5361
6308
|
if (active.includes(name)) {
|
|
5362
|
-
console.log(
|
|
6309
|
+
console.log(chalk6.yellow(`
|
|
5363
6310
|
\u2139\uFE0F Shield "${name}" is already active.
|
|
5364
6311
|
`));
|
|
5365
6312
|
return;
|
|
5366
6313
|
}
|
|
5367
6314
|
writeActiveShields([...active, name]);
|
|
5368
|
-
console.log(
|
|
6315
|
+
console.log(chalk6.green(`
|
|
5369
6316
|
\u{1F6E1}\uFE0F Shield "${name}" enabled.`));
|
|
5370
|
-
console.log(
|
|
6317
|
+
console.log(chalk6.gray(` ${shield.smartRules.length} smart rules now active.`));
|
|
5371
6318
|
if (shield.dangerousWords.length > 0)
|
|
5372
|
-
console.log(
|
|
6319
|
+
console.log(chalk6.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
|
|
5373
6320
|
if (name === "filesystem") {
|
|
5374
6321
|
console.log(
|
|
5375
|
-
|
|
6322
|
+
chalk6.yellow(
|
|
5376
6323
|
`
|
|
5377
6324
|
\u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
|
|
5378
6325
|
Tools like unlink, find -delete, or language-level file ops are not intercepted.`
|
|
@@ -5384,51 +6331,51 @@ shieldCmd.command("enable <service>").description("Enable a security shield for
|
|
|
5384
6331
|
shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
|
|
5385
6332
|
const name = resolveShieldName(service);
|
|
5386
6333
|
if (!name) {
|
|
5387
|
-
console.error(
|
|
6334
|
+
console.error(chalk6.red(`
|
|
5388
6335
|
\u274C Unknown shield: "${service}"
|
|
5389
6336
|
`));
|
|
5390
|
-
console.log(`Run ${
|
|
6337
|
+
console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
|
|
5391
6338
|
`);
|
|
5392
6339
|
process.exit(1);
|
|
5393
6340
|
}
|
|
5394
6341
|
const active = readActiveShields();
|
|
5395
6342
|
if (!active.includes(name)) {
|
|
5396
|
-
console.log(
|
|
6343
|
+
console.log(chalk6.yellow(`
|
|
5397
6344
|
\u2139\uFE0F Shield "${name}" is not active.
|
|
5398
6345
|
`));
|
|
5399
6346
|
return;
|
|
5400
6347
|
}
|
|
5401
6348
|
writeActiveShields(active.filter((s) => s !== name));
|
|
5402
|
-
console.log(
|
|
6349
|
+
console.log(chalk6.green(`
|
|
5403
6350
|
\u{1F6E1}\uFE0F Shield "${name}" disabled.
|
|
5404
6351
|
`));
|
|
5405
6352
|
});
|
|
5406
6353
|
shieldCmd.command("list").description("Show all available shields").action(() => {
|
|
5407
6354
|
const active = new Set(readActiveShields());
|
|
5408
|
-
console.log(
|
|
6355
|
+
console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
|
|
5409
6356
|
for (const shield of listShields()) {
|
|
5410
|
-
const status = active.has(shield.name) ?
|
|
5411
|
-
console.log(` ${status} ${
|
|
6357
|
+
const status = active.has(shield.name) ? chalk6.green("\u25CF enabled") : chalk6.gray("\u25CB disabled");
|
|
6358
|
+
console.log(` ${status} ${chalk6.cyan(shield.name.padEnd(12))} ${shield.description}`);
|
|
5412
6359
|
if (shield.aliases.length > 0)
|
|
5413
|
-
console.log(
|
|
6360
|
+
console.log(chalk6.gray(` aliases: ${shield.aliases.join(", ")}`));
|
|
5414
6361
|
}
|
|
5415
6362
|
console.log("");
|
|
5416
6363
|
});
|
|
5417
6364
|
shieldCmd.command("status").description("Show which shields are currently active").action(() => {
|
|
5418
6365
|
const active = readActiveShields();
|
|
5419
6366
|
if (active.length === 0) {
|
|
5420
|
-
console.log(
|
|
5421
|
-
console.log(`Run ${
|
|
6367
|
+
console.log(chalk6.yellow("\n\u2139\uFE0F No shields are active.\n"));
|
|
6368
|
+
console.log(`Run ${chalk6.cyan("node9 shield list")} to see available shields.
|
|
5422
6369
|
`);
|
|
5423
6370
|
return;
|
|
5424
6371
|
}
|
|
5425
|
-
console.log(
|
|
6372
|
+
console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
|
|
5426
6373
|
for (const name of active) {
|
|
5427
6374
|
const shield = getShield(name);
|
|
5428
6375
|
if (!shield) continue;
|
|
5429
|
-
console.log(` ${
|
|
6376
|
+
console.log(` ${chalk6.green("\u25CF")} ${chalk6.cyan(name)}`);
|
|
5430
6377
|
console.log(
|
|
5431
|
-
|
|
6378
|
+
chalk6.gray(
|
|
5432
6379
|
` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
|
|
5433
6380
|
)
|
|
5434
6381
|
);
|
|
@@ -5439,9 +6386,9 @@ process.on("unhandledRejection", (reason) => {
|
|
|
5439
6386
|
const isCheckHook = process.argv[2] === "check";
|
|
5440
6387
|
if (isCheckHook) {
|
|
5441
6388
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
5442
|
-
const logPath =
|
|
6389
|
+
const logPath = path9.join(os7.homedir(), ".node9", "hook-debug.log");
|
|
5443
6390
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
5444
|
-
|
|
6391
|
+
fs7.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
5445
6392
|
`);
|
|
5446
6393
|
}
|
|
5447
6394
|
process.exit(0);
|