@node9/proxy 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -119
- package/dist/cli.js +2335 -1097
- package/dist/cli.mjs +2315 -1075
- package/dist/index.js +500 -125
- package/dist/index.mjs +500 -125
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
9
16
|
var __copyProps = (to, from, except, desc) => {
|
|
10
17
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
18
|
for (let key of __getOwnPropNames(from))
|
|
@@ -23,25 +30,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
30
|
mod
|
|
24
31
|
));
|
|
25
32
|
|
|
26
|
-
// src/cli.ts
|
|
27
|
-
var import_commander = require("commander");
|
|
28
|
-
|
|
29
|
-
// src/core.ts
|
|
30
|
-
var import_chalk2 = __toESM(require("chalk"));
|
|
31
|
-
var import_prompts = require("@inquirer/prompts");
|
|
32
|
-
var import_fs = __toESM(require("fs"));
|
|
33
|
-
var import_path3 = __toESM(require("path"));
|
|
34
|
-
var import_os = __toESM(require("os"));
|
|
35
|
-
var import_picomatch = __toESM(require("picomatch"));
|
|
36
|
-
var import_sh_syntax = require("sh-syntax");
|
|
37
|
-
|
|
38
|
-
// src/ui/native.ts
|
|
39
|
-
var import_child_process = require("child_process");
|
|
40
|
-
var import_path2 = __toESM(require("path"));
|
|
41
|
-
var import_chalk = __toESM(require("chalk"));
|
|
42
|
-
|
|
43
33
|
// src/context-sniper.ts
|
|
44
|
-
var import_path = __toESM(require("path"));
|
|
45
34
|
function smartTruncate(str, maxLen = 500) {
|
|
46
35
|
if (str.length <= maxLen) return str;
|
|
47
36
|
const edge = Math.floor(maxLen / 2) - 3;
|
|
@@ -71,22 +60,6 @@ function extractContext(text, matchedWord) {
|
|
|
71
60
|
... [${lines.length - end} lines hidden] ...` : "";
|
|
72
61
|
return { snippet: `${head}${snippet}${tail}`, lineIndex };
|
|
73
62
|
}
|
|
74
|
-
var CODE_KEYS = [
|
|
75
|
-
"command",
|
|
76
|
-
"cmd",
|
|
77
|
-
"shell_command",
|
|
78
|
-
"bash_command",
|
|
79
|
-
"script",
|
|
80
|
-
"code",
|
|
81
|
-
"input",
|
|
82
|
-
"sql",
|
|
83
|
-
"query",
|
|
84
|
-
"arguments",
|
|
85
|
-
"args",
|
|
86
|
-
"param",
|
|
87
|
-
"params",
|
|
88
|
-
"text"
|
|
89
|
-
];
|
|
90
63
|
function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
|
|
91
64
|
let intent = "EXEC";
|
|
92
65
|
let contextSnippet;
|
|
@@ -141,11 +114,31 @@ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWo
|
|
|
141
114
|
...ruleName && { ruleName }
|
|
142
115
|
};
|
|
143
116
|
}
|
|
117
|
+
var import_path, CODE_KEYS;
|
|
118
|
+
var init_context_sniper = __esm({
|
|
119
|
+
"src/context-sniper.ts"() {
|
|
120
|
+
"use strict";
|
|
121
|
+
import_path = __toESM(require("path"));
|
|
122
|
+
CODE_KEYS = [
|
|
123
|
+
"command",
|
|
124
|
+
"cmd",
|
|
125
|
+
"shell_command",
|
|
126
|
+
"bash_command",
|
|
127
|
+
"script",
|
|
128
|
+
"code",
|
|
129
|
+
"input",
|
|
130
|
+
"sql",
|
|
131
|
+
"query",
|
|
132
|
+
"arguments",
|
|
133
|
+
"args",
|
|
134
|
+
"param",
|
|
135
|
+
"params",
|
|
136
|
+
"text"
|
|
137
|
+
];
|
|
138
|
+
}
|
|
139
|
+
});
|
|
144
140
|
|
|
145
141
|
// src/ui/native.ts
|
|
146
|
-
var isTestEnv = () => {
|
|
147
|
-
return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
|
|
148
|
-
};
|
|
149
142
|
function formatArgs(args, matchedField, matchedWord) {
|
|
150
143
|
if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
|
|
151
144
|
let parsed = args;
|
|
@@ -355,82 +348,21 @@ end run`;
|
|
|
355
348
|
}
|
|
356
349
|
});
|
|
357
350
|
}
|
|
351
|
+
var import_child_process, import_path2, import_chalk, isTestEnv;
|
|
352
|
+
var init_native = __esm({
|
|
353
|
+
"src/ui/native.ts"() {
|
|
354
|
+
"use strict";
|
|
355
|
+
import_child_process = require("child_process");
|
|
356
|
+
import_path2 = __toESM(require("path"));
|
|
357
|
+
import_chalk = __toESM(require("chalk"));
|
|
358
|
+
init_context_sniper();
|
|
359
|
+
isTestEnv = () => {
|
|
360
|
+
return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
});
|
|
358
364
|
|
|
359
365
|
// src/config-schema.ts
|
|
360
|
-
var import_zod = require("zod");
|
|
361
|
-
var noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
362
|
-
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
363
|
-
});
|
|
364
|
-
var validRegex = noNewlines.refine(
|
|
365
|
-
(s) => {
|
|
366
|
-
try {
|
|
367
|
-
new RegExp(s);
|
|
368
|
-
return true;
|
|
369
|
-
} catch {
|
|
370
|
-
return false;
|
|
371
|
-
}
|
|
372
|
-
},
|
|
373
|
-
{ message: "Value must be a valid regular expression" }
|
|
374
|
-
);
|
|
375
|
-
var SmartConditionSchema = import_zod.z.object({
|
|
376
|
-
field: import_zod.z.string().min(1, "Condition field must not be empty"),
|
|
377
|
-
op: import_zod.z.enum(["matches", "notMatches", "contains", "notContains", "exists", "notExists"], {
|
|
378
|
-
errorMap: () => ({
|
|
379
|
-
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists"
|
|
380
|
-
})
|
|
381
|
-
}),
|
|
382
|
-
value: validRegex.optional(),
|
|
383
|
-
flags: import_zod.z.string().optional()
|
|
384
|
-
});
|
|
385
|
-
var SmartRuleSchema = import_zod.z.object({
|
|
386
|
-
name: import_zod.z.string().optional(),
|
|
387
|
-
tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
|
|
388
|
-
conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
389
|
-
conditionMode: import_zod.z.enum(["all", "any"]).optional(),
|
|
390
|
-
verdict: import_zod.z.enum(["allow", "review", "block"], {
|
|
391
|
-
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
392
|
-
}),
|
|
393
|
-
reason: import_zod.z.string().optional()
|
|
394
|
-
});
|
|
395
|
-
var PolicyRuleSchema = import_zod.z.object({
|
|
396
|
-
action: import_zod.z.string().min(1),
|
|
397
|
-
allowPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
398
|
-
blockPaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
399
|
-
});
|
|
400
|
-
var ConfigFileSchema = import_zod.z.object({
|
|
401
|
-
version: import_zod.z.string().optional(),
|
|
402
|
-
settings: import_zod.z.object({
|
|
403
|
-
mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
|
|
404
|
-
autoStartDaemon: import_zod.z.boolean().optional(),
|
|
405
|
-
enableUndo: import_zod.z.boolean().optional(),
|
|
406
|
-
enableHookLogDebug: import_zod.z.boolean().optional(),
|
|
407
|
-
approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
|
|
408
|
-
approvers: import_zod.z.object({
|
|
409
|
-
native: import_zod.z.boolean().optional(),
|
|
410
|
-
browser: import_zod.z.boolean().optional(),
|
|
411
|
-
cloud: import_zod.z.boolean().optional(),
|
|
412
|
-
terminal: import_zod.z.boolean().optional()
|
|
413
|
-
}).optional(),
|
|
414
|
-
environment: import_zod.z.string().optional(),
|
|
415
|
-
slackEnabled: import_zod.z.boolean().optional(),
|
|
416
|
-
enableTrustSessions: import_zod.z.boolean().optional(),
|
|
417
|
-
allowGlobalPause: import_zod.z.boolean().optional()
|
|
418
|
-
}).optional(),
|
|
419
|
-
policy: import_zod.z.object({
|
|
420
|
-
sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
421
|
-
dangerousWords: import_zod.z.array(noNewlines).optional(),
|
|
422
|
-
ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
423
|
-
toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
|
|
424
|
-
rules: import_zod.z.array(PolicyRuleSchema).optional(),
|
|
425
|
-
smartRules: import_zod.z.array(SmartRuleSchema).optional(),
|
|
426
|
-
snapshot: import_zod.z.object({
|
|
427
|
-
tools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
428
|
-
onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
429
|
-
ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
430
|
-
}).optional()
|
|
431
|
-
}).optional(),
|
|
432
|
-
environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
433
|
-
}).strict({ message: "Config contains unknown top-level keys" });
|
|
434
366
|
function sanitizeConfig(raw) {
|
|
435
367
|
const result = ConfigFileSchema.safeParse(raw);
|
|
436
368
|
if (result.success) {
|
|
@@ -448,8 +380,8 @@ function sanitizeConfig(raw) {
|
|
|
448
380
|
}
|
|
449
381
|
}
|
|
450
382
|
const lines = result.error.issues.map((issue) => {
|
|
451
|
-
const
|
|
452
|
-
return ` \u2022 ${
|
|
383
|
+
const path10 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
384
|
+
return ` \u2022 ${path10}: ${issue.message}`;
|
|
453
385
|
});
|
|
454
386
|
return {
|
|
455
387
|
sanitized,
|
|
@@ -457,19 +389,389 @@ function sanitizeConfig(raw) {
|
|
|
457
389
|
${lines.join("\n")}`
|
|
458
390
|
};
|
|
459
391
|
}
|
|
392
|
+
var import_zod, noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
|
|
393
|
+
var init_config_schema = __esm({
|
|
394
|
+
"src/config-schema.ts"() {
|
|
395
|
+
"use strict";
|
|
396
|
+
import_zod = require("zod");
|
|
397
|
+
noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
|
|
398
|
+
message: "Value must not contain literal newline characters (use \\n instead)"
|
|
399
|
+
});
|
|
400
|
+
SmartConditionSchema = import_zod.z.object({
|
|
401
|
+
field: import_zod.z.string().min(1, "Condition field must not be empty"),
|
|
402
|
+
op: import_zod.z.enum(
|
|
403
|
+
[
|
|
404
|
+
"matches",
|
|
405
|
+
"notMatches",
|
|
406
|
+
"contains",
|
|
407
|
+
"notContains",
|
|
408
|
+
"exists",
|
|
409
|
+
"notExists",
|
|
410
|
+
"matchesGlob",
|
|
411
|
+
"notMatchesGlob"
|
|
412
|
+
],
|
|
413
|
+
{
|
|
414
|
+
errorMap: () => ({
|
|
415
|
+
message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
),
|
|
419
|
+
value: import_zod.z.string().optional(),
|
|
420
|
+
flags: import_zod.z.string().optional()
|
|
421
|
+
});
|
|
422
|
+
SmartRuleSchema = import_zod.z.object({
|
|
423
|
+
name: import_zod.z.string().optional(),
|
|
424
|
+
tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
|
|
425
|
+
conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
|
|
426
|
+
conditionMode: import_zod.z.enum(["all", "any"]).optional(),
|
|
427
|
+
verdict: import_zod.z.enum(["allow", "review", "block"], {
|
|
428
|
+
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
429
|
+
}),
|
|
430
|
+
reason: import_zod.z.string().optional()
|
|
431
|
+
});
|
|
432
|
+
ConfigFileSchema = import_zod.z.object({
|
|
433
|
+
version: import_zod.z.string().optional(),
|
|
434
|
+
settings: import_zod.z.object({
|
|
435
|
+
mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
|
|
436
|
+
autoStartDaemon: import_zod.z.boolean().optional(),
|
|
437
|
+
enableUndo: import_zod.z.boolean().optional(),
|
|
438
|
+
enableHookLogDebug: import_zod.z.boolean().optional(),
|
|
439
|
+
approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
|
|
440
|
+
approvers: import_zod.z.object({
|
|
441
|
+
native: import_zod.z.boolean().optional(),
|
|
442
|
+
browser: import_zod.z.boolean().optional(),
|
|
443
|
+
cloud: import_zod.z.boolean().optional(),
|
|
444
|
+
terminal: import_zod.z.boolean().optional()
|
|
445
|
+
}).optional(),
|
|
446
|
+
environment: import_zod.z.string().optional(),
|
|
447
|
+
slackEnabled: import_zod.z.boolean().optional(),
|
|
448
|
+
enableTrustSessions: import_zod.z.boolean().optional(),
|
|
449
|
+
allowGlobalPause: import_zod.z.boolean().optional()
|
|
450
|
+
}).optional(),
|
|
451
|
+
policy: import_zod.z.object({
|
|
452
|
+
sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
453
|
+
dangerousWords: import_zod.z.array(noNewlines).optional(),
|
|
454
|
+
ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
455
|
+
toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
|
|
456
|
+
smartRules: import_zod.z.array(SmartRuleSchema).optional(),
|
|
457
|
+
snapshot: import_zod.z.object({
|
|
458
|
+
tools: import_zod.z.array(import_zod.z.string()).optional(),
|
|
459
|
+
onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
|
|
460
|
+
ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
|
|
461
|
+
}).optional(),
|
|
462
|
+
dlp: import_zod.z.object({
|
|
463
|
+
enabled: import_zod.z.boolean().optional(),
|
|
464
|
+
scanIgnoredTools: import_zod.z.boolean().optional()
|
|
465
|
+
}).optional()
|
|
466
|
+
}).optional(),
|
|
467
|
+
environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
468
|
+
}).strict({ message: "Config contains unknown top-level keys" });
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// src/shields.ts
|
|
473
|
+
function resolveShieldName(input) {
|
|
474
|
+
const lower = input.toLowerCase();
|
|
475
|
+
if (SHIELDS[lower]) return lower;
|
|
476
|
+
for (const [name, def] of Object.entries(SHIELDS)) {
|
|
477
|
+
if (def.aliases.includes(lower)) return name;
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
function getShield(name) {
|
|
482
|
+
const resolved = resolveShieldName(name);
|
|
483
|
+
return resolved ? SHIELDS[resolved] : null;
|
|
484
|
+
}
|
|
485
|
+
function listShields() {
|
|
486
|
+
return Object.values(SHIELDS);
|
|
487
|
+
}
|
|
488
|
+
function readActiveShields() {
|
|
489
|
+
try {
|
|
490
|
+
const raw = import_fs.default.readFileSync(SHIELDS_STATE_FILE, "utf-8");
|
|
491
|
+
if (!raw.trim()) return [];
|
|
492
|
+
const parsed = JSON.parse(raw);
|
|
493
|
+
if (Array.isArray(parsed.active)) {
|
|
494
|
+
return parsed.active.filter(
|
|
495
|
+
(e) => typeof e === "string" && e.length > 0 && e in SHIELDS
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
} catch (err) {
|
|
499
|
+
if (err.code !== "ENOENT") {
|
|
500
|
+
process.stderr.write(`[node9] Warning: could not read shields state: ${String(err)}
|
|
501
|
+
`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
function writeActiveShields(active) {
|
|
507
|
+
import_fs.default.mkdirSync(import_path3.default.dirname(SHIELDS_STATE_FILE), { recursive: true });
|
|
508
|
+
const tmp = `${SHIELDS_STATE_FILE}.${import_crypto.default.randomBytes(6).toString("hex")}.tmp`;
|
|
509
|
+
import_fs.default.writeFileSync(tmp, JSON.stringify({ active }, null, 2), { mode: 384 });
|
|
510
|
+
import_fs.default.renameSync(tmp, SHIELDS_STATE_FILE);
|
|
511
|
+
}
|
|
512
|
+
var import_fs, import_path3, import_os, import_crypto, SHIELDS, SHIELDS_STATE_FILE;
|
|
513
|
+
var init_shields = __esm({
|
|
514
|
+
"src/shields.ts"() {
|
|
515
|
+
"use strict";
|
|
516
|
+
import_fs = __toESM(require("fs"));
|
|
517
|
+
import_path3 = __toESM(require("path"));
|
|
518
|
+
import_os = __toESM(require("os"));
|
|
519
|
+
import_crypto = __toESM(require("crypto"));
|
|
520
|
+
SHIELDS = {
|
|
521
|
+
postgres: {
|
|
522
|
+
name: "postgres",
|
|
523
|
+
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
524
|
+
aliases: ["pg", "postgresql"],
|
|
525
|
+
smartRules: [
|
|
526
|
+
{
|
|
527
|
+
name: "shield:postgres:block-drop-table",
|
|
528
|
+
tool: "*",
|
|
529
|
+
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
530
|
+
verdict: "block",
|
|
531
|
+
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: "shield:postgres:block-truncate",
|
|
535
|
+
tool: "*",
|
|
536
|
+
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
537
|
+
verdict: "block",
|
|
538
|
+
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "shield:postgres:block-drop-column",
|
|
542
|
+
tool: "*",
|
|
543
|
+
conditions: [
|
|
544
|
+
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
545
|
+
],
|
|
546
|
+
verdict: "block",
|
|
547
|
+
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: "shield:postgres:review-grant-revoke",
|
|
551
|
+
tool: "*",
|
|
552
|
+
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
553
|
+
verdict: "review",
|
|
554
|
+
reason: "Permission changes require human approval (Postgres shield)"
|
|
555
|
+
}
|
|
556
|
+
],
|
|
557
|
+
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
558
|
+
},
|
|
559
|
+
github: {
|
|
560
|
+
name: "github",
|
|
561
|
+
description: "Protects GitHub repositories from destructive AI operations",
|
|
562
|
+
aliases: ["git"],
|
|
563
|
+
smartRules: [
|
|
564
|
+
{
|
|
565
|
+
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
566
|
+
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
567
|
+
name: "shield:github:review-delete-branch-remote",
|
|
568
|
+
tool: "bash",
|
|
569
|
+
conditions: [
|
|
570
|
+
{
|
|
571
|
+
field: "command",
|
|
572
|
+
op: "matches",
|
|
573
|
+
value: "git\\s+push\\s+.*--delete",
|
|
574
|
+
flags: "i"
|
|
575
|
+
}
|
|
576
|
+
],
|
|
577
|
+
verdict: "review",
|
|
578
|
+
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: "shield:github:block-delete-repo",
|
|
582
|
+
tool: "*",
|
|
583
|
+
conditions: [
|
|
584
|
+
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
585
|
+
],
|
|
586
|
+
verdict: "block",
|
|
587
|
+
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
588
|
+
}
|
|
589
|
+
],
|
|
590
|
+
dangerousWords: []
|
|
591
|
+
},
|
|
592
|
+
aws: {
|
|
593
|
+
name: "aws",
|
|
594
|
+
description: "Protects AWS infrastructure from destructive AI operations",
|
|
595
|
+
aliases: ["amazon"],
|
|
596
|
+
smartRules: [
|
|
597
|
+
{
|
|
598
|
+
name: "shield:aws:block-delete-s3-bucket",
|
|
599
|
+
tool: "*",
|
|
600
|
+
conditions: [
|
|
601
|
+
{
|
|
602
|
+
field: "command",
|
|
603
|
+
op: "matches",
|
|
604
|
+
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
605
|
+
flags: "i"
|
|
606
|
+
}
|
|
607
|
+
],
|
|
608
|
+
verdict: "block",
|
|
609
|
+
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
name: "shield:aws:review-iam-changes",
|
|
613
|
+
tool: "*",
|
|
614
|
+
conditions: [
|
|
615
|
+
{
|
|
616
|
+
field: "command",
|
|
617
|
+
op: "matches",
|
|
618
|
+
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
619
|
+
flags: "i"
|
|
620
|
+
}
|
|
621
|
+
],
|
|
622
|
+
verdict: "review",
|
|
623
|
+
reason: "IAM changes require human approval (AWS shield)"
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: "shield:aws:block-ec2-terminate",
|
|
627
|
+
tool: "*",
|
|
628
|
+
conditions: [
|
|
629
|
+
{
|
|
630
|
+
field: "command",
|
|
631
|
+
op: "matches",
|
|
632
|
+
value: "aws\\s+ec2\\s+terminate-instances",
|
|
633
|
+
flags: "i"
|
|
634
|
+
}
|
|
635
|
+
],
|
|
636
|
+
verdict: "block",
|
|
637
|
+
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
name: "shield:aws:review-rds-delete",
|
|
641
|
+
tool: "*",
|
|
642
|
+
conditions: [
|
|
643
|
+
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
644
|
+
],
|
|
645
|
+
verdict: "review",
|
|
646
|
+
reason: "RDS deletion requires human approval (AWS shield)"
|
|
647
|
+
}
|
|
648
|
+
],
|
|
649
|
+
dangerousWords: []
|
|
650
|
+
},
|
|
651
|
+
filesystem: {
|
|
652
|
+
name: "filesystem",
|
|
653
|
+
description: "Protects the local filesystem from dangerous AI operations",
|
|
654
|
+
aliases: ["fs"],
|
|
655
|
+
smartRules: [
|
|
656
|
+
{
|
|
657
|
+
name: "shield:filesystem:review-chmod-777",
|
|
658
|
+
tool: "bash",
|
|
659
|
+
conditions: [
|
|
660
|
+
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
661
|
+
],
|
|
662
|
+
verdict: "review",
|
|
663
|
+
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: "shield:filesystem:review-write-etc",
|
|
667
|
+
tool: "bash",
|
|
668
|
+
conditions: [
|
|
669
|
+
{
|
|
670
|
+
field: "command",
|
|
671
|
+
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
672
|
+
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
673
|
+
op: "matches",
|
|
674
|
+
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
675
|
+
}
|
|
676
|
+
],
|
|
677
|
+
verdict: "review",
|
|
678
|
+
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
679
|
+
}
|
|
680
|
+
],
|
|
681
|
+
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
682
|
+
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
683
|
+
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
684
|
+
dangerousWords: ["wipefs"]
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
SHIELDS_STATE_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "shields.json");
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// src/dlp.ts
|
|
692
|
+
function maskSecret(raw, pattern) {
|
|
693
|
+
const match = raw.match(pattern);
|
|
694
|
+
if (!match) return "****";
|
|
695
|
+
const secret = match[0];
|
|
696
|
+
if (secret.length < 8) return "****";
|
|
697
|
+
const prefix = secret.slice(0, 4);
|
|
698
|
+
const suffix = secret.slice(-4);
|
|
699
|
+
const stars = "*".repeat(Math.min(secret.length - 8, 12));
|
|
700
|
+
return `${prefix}${stars}${suffix}`;
|
|
701
|
+
}
|
|
702
|
+
function scanArgs(args, depth = 0, fieldPath = "args") {
|
|
703
|
+
if (depth > MAX_DEPTH || args === null || args === void 0) return null;
|
|
704
|
+
if (Array.isArray(args)) {
|
|
705
|
+
for (let i = 0; i < args.length; i++) {
|
|
706
|
+
const match = scanArgs(args[i], depth + 1, `${fieldPath}[${i}]`);
|
|
707
|
+
if (match) return match;
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
if (typeof args === "object") {
|
|
712
|
+
for (const [key, value] of Object.entries(args)) {
|
|
713
|
+
const match = scanArgs(value, depth + 1, `${fieldPath}.${key}`);
|
|
714
|
+
if (match) return match;
|
|
715
|
+
}
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
if (typeof args === "string") {
|
|
719
|
+
const text = args.length > MAX_STRING_BYTES ? args.slice(0, MAX_STRING_BYTES) : args;
|
|
720
|
+
for (const pattern of DLP_PATTERNS) {
|
|
721
|
+
if (pattern.regex.test(text)) {
|
|
722
|
+
return {
|
|
723
|
+
patternName: pattern.name,
|
|
724
|
+
fieldPath,
|
|
725
|
+
redactedSample: maskSecret(text, pattern.regex),
|
|
726
|
+
severity: pattern.severity
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (text.length < MAX_JSON_PARSE_BYTES) {
|
|
731
|
+
const trimmed = text.trim();
|
|
732
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
733
|
+
try {
|
|
734
|
+
const parsed = JSON.parse(text);
|
|
735
|
+
const inner = scanArgs(parsed, depth + 1, fieldPath);
|
|
736
|
+
if (inner) return inner;
|
|
737
|
+
} catch {
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
var DLP_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES;
|
|
745
|
+
var init_dlp = __esm({
|
|
746
|
+
"src/dlp.ts"() {
|
|
747
|
+
"use strict";
|
|
748
|
+
DLP_PATTERNS = [
|
|
749
|
+
{ name: "AWS Access Key ID", regex: /\bAKIA[0-9A-Z]{16}\b/, severity: "block" },
|
|
750
|
+
{ name: "GitHub Token", regex: /\bgh[pous]_[A-Za-z0-9]{36}\b/, severity: "block" },
|
|
751
|
+
{ name: "Slack Bot Token", regex: /\bxoxb-[0-9A-Za-z-]+\b/, severity: "block" },
|
|
752
|
+
{ name: "OpenAI API Key", regex: /\bsk-[a-zA-Z0-9_-]{20,}\b/, severity: "block" },
|
|
753
|
+
{ name: "Stripe Secret Key", regex: /\bsk_(?:live|test)_[0-9a-zA-Z]{24}\b/, severity: "block" },
|
|
754
|
+
{
|
|
755
|
+
name: "Private Key (PEM)",
|
|
756
|
+
regex: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
757
|
+
severity: "block"
|
|
758
|
+
},
|
|
759
|
+
{ name: "Bearer Token", regex: /Bearer\s+[a-zA-Z0-9\-._~+/]+=*/i, severity: "review" }
|
|
760
|
+
];
|
|
761
|
+
MAX_DEPTH = 5;
|
|
762
|
+
MAX_STRING_BYTES = 1e5;
|
|
763
|
+
MAX_JSON_PARSE_BYTES = 1e4;
|
|
764
|
+
}
|
|
765
|
+
});
|
|
460
766
|
|
|
461
767
|
// src/core.ts
|
|
462
|
-
var PAUSED_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "PAUSED");
|
|
463
|
-
var TRUST_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "trust.json");
|
|
464
|
-
var LOCAL_AUDIT_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
465
|
-
var HOOK_DEBUG_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
|
|
466
768
|
function checkPause() {
|
|
467
769
|
try {
|
|
468
|
-
if (!
|
|
469
|
-
const state = JSON.parse(
|
|
770
|
+
if (!import_fs2.default.existsSync(PAUSED_FILE)) return { paused: false };
|
|
771
|
+
const state = JSON.parse(import_fs2.default.readFileSync(PAUSED_FILE, "utf-8"));
|
|
470
772
|
if (state.expiry > 0 && Date.now() >= state.expiry) {
|
|
471
773
|
try {
|
|
472
|
-
|
|
774
|
+
import_fs2.default.unlinkSync(PAUSED_FILE);
|
|
473
775
|
} catch {
|
|
474
776
|
}
|
|
475
777
|
return { paused: false };
|
|
@@ -480,11 +782,11 @@ function checkPause() {
|
|
|
480
782
|
}
|
|
481
783
|
}
|
|
482
784
|
function atomicWriteSync(filePath, data, options) {
|
|
483
|
-
const dir =
|
|
484
|
-
if (!
|
|
485
|
-
const tmpPath = `${filePath}.${
|
|
486
|
-
|
|
487
|
-
|
|
785
|
+
const dir = import_path4.default.dirname(filePath);
|
|
786
|
+
if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
|
|
787
|
+
const tmpPath = `${filePath}.${import_os2.default.hostname()}.${process.pid}.tmp`;
|
|
788
|
+
import_fs2.default.writeFileSync(tmpPath, data, options);
|
|
789
|
+
import_fs2.default.renameSync(tmpPath, filePath);
|
|
488
790
|
}
|
|
489
791
|
function pauseNode9(durationMs, durationStr) {
|
|
490
792
|
const state = { expiry: Date.now() + durationMs, duration: durationStr };
|
|
@@ -492,18 +794,18 @@ function pauseNode9(durationMs, durationStr) {
|
|
|
492
794
|
}
|
|
493
795
|
function resumeNode9() {
|
|
494
796
|
try {
|
|
495
|
-
if (
|
|
797
|
+
if (import_fs2.default.existsSync(PAUSED_FILE)) import_fs2.default.unlinkSync(PAUSED_FILE);
|
|
496
798
|
} catch {
|
|
497
799
|
}
|
|
498
800
|
}
|
|
499
801
|
function getActiveTrustSession(toolName) {
|
|
500
802
|
try {
|
|
501
|
-
if (!
|
|
502
|
-
const trust = JSON.parse(
|
|
803
|
+
if (!import_fs2.default.existsSync(TRUST_FILE)) return false;
|
|
804
|
+
const trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
503
805
|
const now = Date.now();
|
|
504
806
|
const active = trust.entries.filter((e) => e.expiry > now);
|
|
505
807
|
if (active.length !== trust.entries.length) {
|
|
506
|
-
|
|
808
|
+
import_fs2.default.writeFileSync(TRUST_FILE, JSON.stringify({ entries: active }, null, 2));
|
|
507
809
|
}
|
|
508
810
|
return active.some((e) => e.tool === toolName || matchesPattern(toolName, e.tool));
|
|
509
811
|
} catch {
|
|
@@ -514,8 +816,8 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
514
816
|
try {
|
|
515
817
|
let trust = { entries: [] };
|
|
516
818
|
try {
|
|
517
|
-
if (
|
|
518
|
-
trust = JSON.parse(
|
|
819
|
+
if (import_fs2.default.existsSync(TRUST_FILE)) {
|
|
820
|
+
trust = JSON.parse(import_fs2.default.readFileSync(TRUST_FILE, "utf-8"));
|
|
519
821
|
}
|
|
520
822
|
} catch {
|
|
521
823
|
}
|
|
@@ -531,9 +833,9 @@ function writeTrustSession(toolName, durationMs) {
|
|
|
531
833
|
}
|
|
532
834
|
function appendToLog(logPath, entry) {
|
|
533
835
|
try {
|
|
534
|
-
const dir =
|
|
535
|
-
if (!
|
|
536
|
-
|
|
836
|
+
const dir = import_path4.default.dirname(logPath);
|
|
837
|
+
if (!import_fs2.default.existsSync(dir)) import_fs2.default.mkdirSync(dir, { recursive: true });
|
|
838
|
+
import_fs2.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
537
839
|
} catch {
|
|
538
840
|
}
|
|
539
841
|
}
|
|
@@ -545,7 +847,7 @@ function appendHookDebug(toolName, args, meta) {
|
|
|
545
847
|
args: safeArgs,
|
|
546
848
|
agent: meta?.agent,
|
|
547
849
|
mcpServer: meta?.mcpServer,
|
|
548
|
-
hostname:
|
|
850
|
+
hostname: import_os2.default.hostname(),
|
|
549
851
|
cwd: process.cwd()
|
|
550
852
|
});
|
|
551
853
|
}
|
|
@@ -559,7 +861,7 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta) {
|
|
|
559
861
|
checkedBy,
|
|
560
862
|
agent: meta?.agent,
|
|
561
863
|
mcpServer: meta?.mcpServer,
|
|
562
|
-
hostname:
|
|
864
|
+
hostname: import_os2.default.hostname()
|
|
563
865
|
});
|
|
564
866
|
}
|
|
565
867
|
function tokenize(toolName) {
|
|
@@ -575,9 +877,9 @@ function matchesPattern(text, patterns) {
|
|
|
575
877
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
576
878
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
577
879
|
}
|
|
578
|
-
function getNestedValue(obj,
|
|
880
|
+
function getNestedValue(obj, path10) {
|
|
579
881
|
if (!obj || typeof obj !== "object") return null;
|
|
580
|
-
return
|
|
882
|
+
return path10.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
581
883
|
}
|
|
582
884
|
function shouldSnapshot(toolName, args, config) {
|
|
583
885
|
if (!config.settings.enableUndo) return false;
|
|
@@ -622,6 +924,10 @@ function evaluateSmartConditions(args, rule) {
|
|
|
622
924
|
return true;
|
|
623
925
|
}
|
|
624
926
|
}
|
|
927
|
+
case "matchesGlob":
|
|
928
|
+
return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
|
|
929
|
+
case "notMatchesGlob":
|
|
930
|
+
return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : true;
|
|
625
931
|
default:
|
|
626
932
|
return false;
|
|
627
933
|
}
|
|
@@ -643,7 +949,6 @@ function isSqlTool(toolName, toolInspection) {
|
|
|
643
949
|
const fieldName = toolInspection[matchingPattern];
|
|
644
950
|
return fieldName === "sql" || fieldName === "query";
|
|
645
951
|
}
|
|
646
|
-
var SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
647
952
|
async function analyzeShellCommand(command) {
|
|
648
953
|
const actions = [];
|
|
649
954
|
const paths = [];
|
|
@@ -725,188 +1030,14 @@ function redactSecrets(text) {
|
|
|
725
1030
|
);
|
|
726
1031
|
return redacted;
|
|
727
1032
|
}
|
|
728
|
-
var DANGEROUS_WORDS = [
|
|
729
|
-
"mkfs",
|
|
730
|
-
// formats/wipes a filesystem partition
|
|
731
|
-
"shred"
|
|
732
|
-
// permanently overwrites file contents (unrecoverable)
|
|
733
|
-
];
|
|
734
|
-
var DEFAULT_CONFIG = {
|
|
735
|
-
settings: {
|
|
736
|
-
mode: "standard",
|
|
737
|
-
autoStartDaemon: true,
|
|
738
|
-
enableUndo: true,
|
|
739
|
-
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
740
|
-
enableHookLogDebug: false,
|
|
741
|
-
approvalTimeoutMs: 0,
|
|
742
|
-
// 0 = disabled; set e.g. 30000 for 30-second auto-deny
|
|
743
|
-
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
744
|
-
},
|
|
745
|
-
policy: {
|
|
746
|
-
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
747
|
-
dangerousWords: DANGEROUS_WORDS,
|
|
748
|
-
ignoredTools: [
|
|
749
|
-
"list_*",
|
|
750
|
-
"get_*",
|
|
751
|
-
"read_*",
|
|
752
|
-
"describe_*",
|
|
753
|
-
"read",
|
|
754
|
-
"glob",
|
|
755
|
-
"grep",
|
|
756
|
-
"ls",
|
|
757
|
-
"notebookread",
|
|
758
|
-
"notebookedit",
|
|
759
|
-
"webfetch",
|
|
760
|
-
"websearch",
|
|
761
|
-
"exitplanmode",
|
|
762
|
-
"askuserquestion",
|
|
763
|
-
"agent",
|
|
764
|
-
"task*",
|
|
765
|
-
"toolsearch",
|
|
766
|
-
"mcp__ide__*",
|
|
767
|
-
"getDiagnostics"
|
|
768
|
-
],
|
|
769
|
-
toolInspection: {
|
|
770
|
-
bash: "command",
|
|
771
|
-
shell: "command",
|
|
772
|
-
run_shell_command: "command",
|
|
773
|
-
"terminal.execute": "command",
|
|
774
|
-
"postgres:query": "sql"
|
|
775
|
-
},
|
|
776
|
-
snapshot: {
|
|
777
|
-
tools: [
|
|
778
|
-
"str_replace_based_edit_tool",
|
|
779
|
-
"write_file",
|
|
780
|
-
"edit_file",
|
|
781
|
-
"create_file",
|
|
782
|
-
"edit",
|
|
783
|
-
"replace"
|
|
784
|
-
],
|
|
785
|
-
onlyPaths: [],
|
|
786
|
-
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
787
|
-
},
|
|
788
|
-
rules: [
|
|
789
|
-
// Only use the legacy rules format for simple path-based rm control.
|
|
790
|
-
// All other command-level enforcement lives in smartRules below.
|
|
791
|
-
{
|
|
792
|
-
action: "rm",
|
|
793
|
-
allowPaths: [
|
|
794
|
-
"**/node_modules/**",
|
|
795
|
-
"dist/**",
|
|
796
|
-
"build/**",
|
|
797
|
-
".next/**",
|
|
798
|
-
"coverage/**",
|
|
799
|
-
".cache/**",
|
|
800
|
-
"tmp/**",
|
|
801
|
-
"temp/**",
|
|
802
|
-
".DS_Store"
|
|
803
|
-
]
|
|
804
|
-
}
|
|
805
|
-
],
|
|
806
|
-
smartRules: [
|
|
807
|
-
// ── SQL safety ────────────────────────────────────────────────────────
|
|
808
|
-
{
|
|
809
|
-
name: "no-delete-without-where",
|
|
810
|
-
tool: "*",
|
|
811
|
-
conditions: [
|
|
812
|
-
{ field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
|
|
813
|
-
{ field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
|
|
814
|
-
],
|
|
815
|
-
conditionMode: "all",
|
|
816
|
-
verdict: "review",
|
|
817
|
-
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
818
|
-
},
|
|
819
|
-
{
|
|
820
|
-
name: "review-drop-truncate-shell",
|
|
821
|
-
tool: "bash",
|
|
822
|
-
conditions: [
|
|
823
|
-
{
|
|
824
|
-
field: "command",
|
|
825
|
-
op: "matches",
|
|
826
|
-
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
827
|
-
flags: "i"
|
|
828
|
-
}
|
|
829
|
-
],
|
|
830
|
-
conditionMode: "all",
|
|
831
|
-
verdict: "review",
|
|
832
|
-
reason: "SQL DDL destructive statement inside a shell command"
|
|
833
|
-
},
|
|
834
|
-
// ── Git safety ────────────────────────────────────────────────────────
|
|
835
|
-
{
|
|
836
|
-
name: "block-force-push",
|
|
837
|
-
tool: "bash",
|
|
838
|
-
conditions: [
|
|
839
|
-
{
|
|
840
|
-
field: "command",
|
|
841
|
-
op: "matches",
|
|
842
|
-
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
843
|
-
flags: "i"
|
|
844
|
-
}
|
|
845
|
-
],
|
|
846
|
-
conditionMode: "all",
|
|
847
|
-
verdict: "block",
|
|
848
|
-
reason: "Force push overwrites remote history and cannot be undone"
|
|
849
|
-
},
|
|
850
|
-
{
|
|
851
|
-
name: "review-git-push",
|
|
852
|
-
tool: "bash",
|
|
853
|
-
conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
|
|
854
|
-
conditionMode: "all",
|
|
855
|
-
verdict: "review",
|
|
856
|
-
reason: "git push sends changes to a shared remote"
|
|
857
|
-
},
|
|
858
|
-
{
|
|
859
|
-
name: "review-git-destructive",
|
|
860
|
-
tool: "bash",
|
|
861
|
-
conditions: [
|
|
862
|
-
{
|
|
863
|
-
field: "command",
|
|
864
|
-
op: "matches",
|
|
865
|
-
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
866
|
-
flags: "i"
|
|
867
|
-
}
|
|
868
|
-
],
|
|
869
|
-
conditionMode: "all",
|
|
870
|
-
verdict: "review",
|
|
871
|
-
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
872
|
-
},
|
|
873
|
-
// ── Shell safety ──────────────────────────────────────────────────────
|
|
874
|
-
{
|
|
875
|
-
name: "review-sudo",
|
|
876
|
-
tool: "bash",
|
|
877
|
-
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
878
|
-
conditionMode: "all",
|
|
879
|
-
verdict: "review",
|
|
880
|
-
reason: "Command requires elevated privileges"
|
|
881
|
-
},
|
|
882
|
-
{
|
|
883
|
-
name: "review-curl-pipe-shell",
|
|
884
|
-
tool: "bash",
|
|
885
|
-
conditions: [
|
|
886
|
-
{
|
|
887
|
-
field: "command",
|
|
888
|
-
op: "matches",
|
|
889
|
-
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
890
|
-
flags: "i"
|
|
891
|
-
}
|
|
892
|
-
],
|
|
893
|
-
conditionMode: "all",
|
|
894
|
-
verdict: "block",
|
|
895
|
-
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
896
|
-
}
|
|
897
|
-
]
|
|
898
|
-
},
|
|
899
|
-
environments: {}
|
|
900
|
-
};
|
|
901
|
-
var cachedConfig = null;
|
|
902
1033
|
function _resetConfigCache() {
|
|
903
1034
|
cachedConfig = null;
|
|
904
1035
|
}
|
|
905
1036
|
function getGlobalSettings() {
|
|
906
1037
|
try {
|
|
907
|
-
const globalConfigPath =
|
|
908
|
-
if (
|
|
909
|
-
const parsed = JSON.parse(
|
|
1038
|
+
const globalConfigPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
|
|
1039
|
+
if (import_fs2.default.existsSync(globalConfigPath)) {
|
|
1040
|
+
const parsed = JSON.parse(import_fs2.default.readFileSync(globalConfigPath, "utf-8"));
|
|
910
1041
|
const settings = parsed.settings || {};
|
|
911
1042
|
return {
|
|
912
1043
|
mode: settings.mode || "standard",
|
|
@@ -928,9 +1059,9 @@ function getGlobalSettings() {
|
|
|
928
1059
|
}
|
|
929
1060
|
function getInternalToken() {
|
|
930
1061
|
try {
|
|
931
|
-
const pidFile =
|
|
932
|
-
if (!
|
|
933
|
-
const data = JSON.parse(
|
|
1062
|
+
const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
|
|
1063
|
+
if (!import_fs2.default.existsSync(pidFile)) return null;
|
|
1064
|
+
const data = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
|
|
934
1065
|
process.kill(data.pid, 0);
|
|
935
1066
|
return data.internalToken ?? null;
|
|
936
1067
|
} catch {
|
|
@@ -945,7 +1076,8 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
945
1076
|
(rule) => matchesPattern(toolName, rule.tool) && evaluateSmartConditions(args, rule)
|
|
946
1077
|
);
|
|
947
1078
|
if (matchedRule) {
|
|
948
|
-
if (matchedRule.verdict === "allow")
|
|
1079
|
+
if (matchedRule.verdict === "allow")
|
|
1080
|
+
return { decision: "allow", ruleName: matchedRule.name ?? matchedRule.tool };
|
|
949
1081
|
return {
|
|
950
1082
|
decision: matchedRule.verdict,
|
|
951
1083
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
@@ -956,13 +1088,11 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
956
1088
|
}
|
|
957
1089
|
}
|
|
958
1090
|
let allTokens = [];
|
|
959
|
-
let actionTokens = [];
|
|
960
1091
|
let pathTokens = [];
|
|
961
1092
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
962
1093
|
if (shellCommand) {
|
|
963
1094
|
const analyzed = await analyzeShellCommand(shellCommand);
|
|
964
1095
|
allTokens = analyzed.allTokens;
|
|
965
|
-
actionTokens = analyzed.actions;
|
|
966
1096
|
pathTokens = analyzed.paths;
|
|
967
1097
|
const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
|
|
968
1098
|
if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
|
|
@@ -970,11 +1100,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
970
1100
|
}
|
|
971
1101
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
972
1102
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
973
|
-
actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
974
1103
|
}
|
|
975
1104
|
} else {
|
|
976
1105
|
allTokens = tokenize(toolName);
|
|
977
|
-
actionTokens = [toolName];
|
|
978
1106
|
if (args && typeof args === "object") {
|
|
979
1107
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
980
1108
|
const extraTokens = flattenedArgs.split(/[^a-zA-Z0-9]+/).filter((t) => t.length > 1);
|
|
@@ -997,29 +1125,6 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
997
1125
|
const allInSandbox = pathTokens.every((p) => matchesPattern(p, config.policy.sandboxPaths));
|
|
998
1126
|
if (allInSandbox) return { decision: "allow" };
|
|
999
1127
|
}
|
|
1000
|
-
for (const action of actionTokens) {
|
|
1001
|
-
const rule = config.policy.rules.find(
|
|
1002
|
-
(r) => r.action === action || matchesPattern(action, r.action)
|
|
1003
|
-
);
|
|
1004
|
-
if (rule) {
|
|
1005
|
-
if (pathTokens.length > 0) {
|
|
1006
|
-
const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
|
|
1007
|
-
if (anyBlocked)
|
|
1008
|
-
return {
|
|
1009
|
-
decision: "review",
|
|
1010
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
|
|
1011
|
-
tier: 5
|
|
1012
|
-
};
|
|
1013
|
-
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
1014
|
-
if (allAllowed) return { decision: "allow" };
|
|
1015
|
-
}
|
|
1016
|
-
return {
|
|
1017
|
-
decision: "review",
|
|
1018
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
|
|
1019
|
-
tier: 5
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
1128
|
let matchedDangerousWord;
|
|
1024
1129
|
const isDangerous = allTokens.some(
|
|
1025
1130
|
(token) => config.policy.dangerousWords.some((word) => {
|
|
@@ -1071,9 +1176,9 @@ async function evaluatePolicy(toolName, args, agent) {
|
|
|
1071
1176
|
}
|
|
1072
1177
|
async function explainPolicy(toolName, args) {
|
|
1073
1178
|
const steps = [];
|
|
1074
|
-
const globalPath =
|
|
1075
|
-
const projectPath =
|
|
1076
|
-
const credsPath =
|
|
1179
|
+
const globalPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
|
|
1180
|
+
const projectPath = import_path4.default.join(process.cwd(), "node9.config.json");
|
|
1181
|
+
const credsPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
|
|
1077
1182
|
const waterfall = [
|
|
1078
1183
|
{
|
|
1079
1184
|
tier: 1,
|
|
@@ -1084,19 +1189,19 @@ async function explainPolicy(toolName, args) {
|
|
|
1084
1189
|
{
|
|
1085
1190
|
tier: 2,
|
|
1086
1191
|
label: "Cloud policy",
|
|
1087
|
-
status:
|
|
1088
|
-
note:
|
|
1192
|
+
status: import_fs2.default.existsSync(credsPath) ? "active" : "missing",
|
|
1193
|
+
note: import_fs2.default.existsSync(credsPath) ? "credentials found (not evaluated in explain mode)" : "not connected \u2014 run: node9 login"
|
|
1089
1194
|
},
|
|
1090
1195
|
{
|
|
1091
1196
|
tier: 3,
|
|
1092
1197
|
label: "Project config",
|
|
1093
|
-
status:
|
|
1198
|
+
status: import_fs2.default.existsSync(projectPath) ? "active" : "missing",
|
|
1094
1199
|
path: projectPath
|
|
1095
1200
|
},
|
|
1096
1201
|
{
|
|
1097
1202
|
tier: 4,
|
|
1098
1203
|
label: "Global config",
|
|
1099
|
-
status:
|
|
1204
|
+
status: import_fs2.default.existsSync(globalPath) ? "active" : "missing",
|
|
1100
1205
|
path: globalPath
|
|
1101
1206
|
},
|
|
1102
1207
|
{
|
|
@@ -1107,7 +1212,28 @@ async function explainPolicy(toolName, args) {
|
|
|
1107
1212
|
}
|
|
1108
1213
|
];
|
|
1109
1214
|
const config = getConfig();
|
|
1110
|
-
|
|
1215
|
+
const wouldBeIgnored = matchesPattern(toolName, config.policy.ignoredTools);
|
|
1216
|
+
if (config.policy.dlp.enabled && (!wouldBeIgnored || config.policy.dlp.scanIgnoredTools)) {
|
|
1217
|
+
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
1218
|
+
if (dlpMatch) {
|
|
1219
|
+
steps.push({
|
|
1220
|
+
name: "DLP Content Scanner",
|
|
1221
|
+
outcome: dlpMatch.severity === "block" ? "block" : "review",
|
|
1222
|
+
detail: `\u{1F6A8} ${dlpMatch.patternName} detected in ${dlpMatch.fieldPath} \u2014 sample: ${dlpMatch.redactedSample}`,
|
|
1223
|
+
isFinal: dlpMatch.severity === "block"
|
|
1224
|
+
});
|
|
1225
|
+
if (dlpMatch.severity === "block") {
|
|
1226
|
+
return { tool: toolName, args, waterfall, steps, decision: "block" };
|
|
1227
|
+
}
|
|
1228
|
+
} else {
|
|
1229
|
+
steps.push({
|
|
1230
|
+
name: "DLP Content Scanner",
|
|
1231
|
+
outcome: "checked",
|
|
1232
|
+
detail: "No sensitive credentials detected in args"
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (wouldBeIgnored) {
|
|
1111
1237
|
steps.push({
|
|
1112
1238
|
name: "Ignored tools",
|
|
1113
1239
|
outcome: "allow",
|
|
@@ -1160,13 +1286,11 @@ async function explainPolicy(toolName, args) {
|
|
|
1160
1286
|
steps.push({ name: "Smart rules", outcome: "skip", detail: "No smart rules configured" });
|
|
1161
1287
|
}
|
|
1162
1288
|
let allTokens = [];
|
|
1163
|
-
let actionTokens = [];
|
|
1164
1289
|
let pathTokens = [];
|
|
1165
1290
|
const shellCommand = extractShellCommand(toolName, args, config.policy.toolInspection);
|
|
1166
1291
|
if (shellCommand) {
|
|
1167
1292
|
const analyzed = await analyzeShellCommand(shellCommand);
|
|
1168
1293
|
allTokens = analyzed.allTokens;
|
|
1169
|
-
actionTokens = analyzed.actions;
|
|
1170
1294
|
pathTokens = analyzed.paths;
|
|
1171
1295
|
const patterns = Object.keys(config.policy.toolInspection);
|
|
1172
1296
|
const matchingPattern = patterns.find((p) => matchesPattern(toolName, p));
|
|
@@ -1200,7 +1324,6 @@ async function explainPolicy(toolName, args) {
|
|
|
1200
1324
|
});
|
|
1201
1325
|
if (isSqlTool(toolName, config.policy.toolInspection)) {
|
|
1202
1326
|
allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
1203
|
-
actionTokens = actionTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
|
|
1204
1327
|
steps.push({
|
|
1205
1328
|
name: "SQL token stripping",
|
|
1206
1329
|
outcome: "checked",
|
|
@@ -1209,7 +1332,6 @@ async function explainPolicy(toolName, args) {
|
|
|
1209
1332
|
}
|
|
1210
1333
|
} else {
|
|
1211
1334
|
allTokens = tokenize(toolName);
|
|
1212
|
-
actionTokens = [toolName];
|
|
1213
1335
|
let detail = `No toolInspection match for "${toolName}" \u2014 tokens: [${allTokens.join(", ")}]`;
|
|
1214
1336
|
if (args && typeof args === "object") {
|
|
1215
1337
|
const flattenedArgs = JSON.stringify(args).toLowerCase();
|
|
@@ -1250,65 +1372,6 @@ async function explainPolicy(toolName, args) {
|
|
|
1250
1372
|
detail: pathTokens.length === 0 ? "No path tokens found in input" : "No sandbox paths configured"
|
|
1251
1373
|
});
|
|
1252
1374
|
}
|
|
1253
|
-
let ruleMatched = false;
|
|
1254
|
-
for (const action of actionTokens) {
|
|
1255
|
-
const rule = config.policy.rules.find(
|
|
1256
|
-
(r) => r.action === action || matchesPattern(action, r.action)
|
|
1257
|
-
);
|
|
1258
|
-
if (rule) {
|
|
1259
|
-
ruleMatched = true;
|
|
1260
|
-
if (pathTokens.length > 0) {
|
|
1261
|
-
const anyBlocked = pathTokens.some((p) => matchesPattern(p, rule.blockPaths || []));
|
|
1262
|
-
if (anyBlocked) {
|
|
1263
|
-
steps.push({
|
|
1264
|
-
name: "Policy rules",
|
|
1265
|
-
outcome: "review",
|
|
1266
|
-
detail: `Rule "${rule.action}" matched + path is in blockPaths`,
|
|
1267
|
-
isFinal: true
|
|
1268
|
-
});
|
|
1269
|
-
return {
|
|
1270
|
-
tool: toolName,
|
|
1271
|
-
args,
|
|
1272
|
-
waterfall,
|
|
1273
|
-
steps,
|
|
1274
|
-
decision: "review",
|
|
1275
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
|
|
1276
|
-
};
|
|
1277
|
-
}
|
|
1278
|
-
const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
|
|
1279
|
-
if (allAllowed) {
|
|
1280
|
-
steps.push({
|
|
1281
|
-
name: "Policy rules",
|
|
1282
|
-
outcome: "allow",
|
|
1283
|
-
detail: `Rule "${rule.action}" matched + all paths are in allowPaths`,
|
|
1284
|
-
isFinal: true
|
|
1285
|
-
});
|
|
1286
|
-
return { tool: toolName, args, waterfall, steps, decision: "allow" };
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
steps.push({
|
|
1290
|
-
name: "Policy rules",
|
|
1291
|
-
outcome: "review",
|
|
1292
|
-
detail: `Rule "${rule.action}" matched \u2014 default block (no path exception)`,
|
|
1293
|
-
isFinal: true
|
|
1294
|
-
});
|
|
1295
|
-
return {
|
|
1296
|
-
tool: toolName,
|
|
1297
|
-
args,
|
|
1298
|
-
waterfall,
|
|
1299
|
-
steps,
|
|
1300
|
-
decision: "review",
|
|
1301
|
-
blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
if (!ruleMatched) {
|
|
1306
|
-
steps.push({
|
|
1307
|
-
name: "Policy rules",
|
|
1308
|
-
outcome: "skip",
|
|
1309
|
-
detail: config.policy.rules.length === 0 ? "No rules configured" : `No rule matched [${actionTokens.join(", ")}]`
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
1375
|
let matchedDangerousWord;
|
|
1313
1376
|
const isDangerous = uniqueTokens.some(
|
|
1314
1377
|
(token) => config.policy.dangerousWords.some((word) => {
|
|
@@ -1373,13 +1436,11 @@ function isIgnoredTool(toolName) {
|
|
|
1373
1436
|
const config = getConfig();
|
|
1374
1437
|
return matchesPattern(toolName, config.policy.ignoredTools);
|
|
1375
1438
|
}
|
|
1376
|
-
var DAEMON_PORT = 7391;
|
|
1377
|
-
var DAEMON_HOST = "127.0.0.1";
|
|
1378
1439
|
function isDaemonRunning() {
|
|
1379
1440
|
try {
|
|
1380
|
-
const pidFile =
|
|
1381
|
-
if (!
|
|
1382
|
-
const { pid, port } = JSON.parse(
|
|
1441
|
+
const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
|
|
1442
|
+
if (!import_fs2.default.existsSync(pidFile)) return false;
|
|
1443
|
+
const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
|
|
1383
1444
|
if (port !== DAEMON_PORT) return false;
|
|
1384
1445
|
process.kill(pid, 0);
|
|
1385
1446
|
return true;
|
|
@@ -1389,16 +1450,16 @@ function isDaemonRunning() {
|
|
|
1389
1450
|
}
|
|
1390
1451
|
function getPersistentDecision(toolName) {
|
|
1391
1452
|
try {
|
|
1392
|
-
const file =
|
|
1393
|
-
if (!
|
|
1394
|
-
const decisions = JSON.parse(
|
|
1453
|
+
const file = import_path4.default.join(import_os2.default.homedir(), ".node9", "decisions.json");
|
|
1454
|
+
if (!import_fs2.default.existsSync(file)) return null;
|
|
1455
|
+
const decisions = JSON.parse(import_fs2.default.readFileSync(file, "utf-8"));
|
|
1395
1456
|
const d = decisions[toolName];
|
|
1396
1457
|
if (d === "allow" || d === "deny") return d;
|
|
1397
1458
|
} catch {
|
|
1398
1459
|
}
|
|
1399
1460
|
return null;
|
|
1400
1461
|
}
|
|
1401
|
-
async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
1462
|
+
async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
|
|
1402
1463
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1403
1464
|
const checkCtrl = new AbortController();
|
|
1404
1465
|
const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
|
|
@@ -1413,6 +1474,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
|
|
|
1413
1474
|
args,
|
|
1414
1475
|
agent: meta?.agent,
|
|
1415
1476
|
mcpServer: meta?.mcpServer,
|
|
1477
|
+
fromCLI: true,
|
|
1478
|
+
// Pass the flight-recorder ID so the daemon uses the same UUID for
|
|
1479
|
+
// activity-result as the CLI used for the pending activity event.
|
|
1480
|
+
// Without this, the two UUIDs never match and tail.ts never resolves
|
|
1481
|
+
// the pending item.
|
|
1482
|
+
activityId,
|
|
1416
1483
|
...riskMetadata && { riskMetadata }
|
|
1417
1484
|
}),
|
|
1418
1485
|
signal: checkCtrl.signal
|
|
@@ -1467,7 +1534,44 @@ async function resolveViaDaemon(id, decision, internalToken) {
|
|
|
1467
1534
|
signal: AbortSignal.timeout(3e3)
|
|
1468
1535
|
});
|
|
1469
1536
|
}
|
|
1537
|
+
function notifyActivity(data) {
|
|
1538
|
+
return new Promise((resolve) => {
|
|
1539
|
+
try {
|
|
1540
|
+
const payload = JSON.stringify(data);
|
|
1541
|
+
const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
|
|
1542
|
+
sock.on("connect", () => {
|
|
1543
|
+
sock.on("close", resolve);
|
|
1544
|
+
sock.end(payload);
|
|
1545
|
+
});
|
|
1546
|
+
sock.on("error", resolve);
|
|
1547
|
+
} catch {
|
|
1548
|
+
resolve();
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1470
1552
|
async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1553
|
+
if (!options?.calledFromDaemon) {
|
|
1554
|
+
const actId = (0, import_crypto2.randomUUID)();
|
|
1555
|
+
const actTs = Date.now();
|
|
1556
|
+
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
|
|
1557
|
+
const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
|
|
1558
|
+
...options,
|
|
1559
|
+
activityId: actId
|
|
1560
|
+
});
|
|
1561
|
+
if (!result.noApprovalMechanism) {
|
|
1562
|
+
await notifyActivity({
|
|
1563
|
+
id: actId,
|
|
1564
|
+
tool: toolName,
|
|
1565
|
+
ts: actTs,
|
|
1566
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
1567
|
+
label: result.blockedByLabel
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
return result;
|
|
1571
|
+
}
|
|
1572
|
+
return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
|
|
1573
|
+
}
|
|
1574
|
+
async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
|
|
1471
1575
|
if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
|
|
1472
1576
|
const pauseState = checkPause();
|
|
1473
1577
|
if (pauseState.paused) return { approved: true, checkedBy: "paused" };
|
|
@@ -1490,10 +1594,27 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1490
1594
|
let policyMatchedField;
|
|
1491
1595
|
let policyMatchedWord;
|
|
1492
1596
|
let riskMetadata;
|
|
1493
|
-
if (config.
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1597
|
+
if (config.policy.dlp.enabled && (!isIgnoredTool(toolName) || config.policy.dlp.scanIgnoredTools)) {
|
|
1598
|
+
const dlpMatch = scanArgs(args);
|
|
1599
|
+
if (dlpMatch) {
|
|
1600
|
+
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
1601
|
+
if (dlpMatch.severity === "block") {
|
|
1602
|
+
if (!isManual) appendLocalAudit(toolName, args, "deny", "dlp-block", meta);
|
|
1603
|
+
return {
|
|
1604
|
+
approved: false,
|
|
1605
|
+
reason: dlpReason,
|
|
1606
|
+
blockedBy: "local-config",
|
|
1607
|
+
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
|
|
1611
|
+
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (config.settings.mode === "audit") {
|
|
1615
|
+
if (!isIgnoredTool(toolName)) {
|
|
1616
|
+
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
1617
|
+
if (policyResult.decision === "review") {
|
|
1497
1618
|
appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
|
|
1498
1619
|
if (approvers.cloud && creds?.apiKey) {
|
|
1499
1620
|
await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
|
|
@@ -1709,7 +1830,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1709
1830
|
console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
|
|
1710
1831
|
`));
|
|
1711
1832
|
}
|
|
1712
|
-
const daemonDecision = await askDaemon(
|
|
1833
|
+
const daemonDecision = await askDaemon(
|
|
1834
|
+
toolName,
|
|
1835
|
+
args,
|
|
1836
|
+
meta,
|
|
1837
|
+
signal,
|
|
1838
|
+
riskMetadata,
|
|
1839
|
+
options?.activityId
|
|
1840
|
+
);
|
|
1713
1841
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
1714
1842
|
const isApproved = daemonDecision === "allow";
|
|
1715
1843
|
return {
|
|
@@ -1729,7 +1857,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
|
|
|
1729
1857
|
racePromises.push(
|
|
1730
1858
|
(async () => {
|
|
1731
1859
|
try {
|
|
1732
|
-
|
|
1860
|
+
if (explainableLabel.includes("DLP")) {
|
|
1861
|
+
console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
1862
|
+
console.log(
|
|
1863
|
+
import_chalk2.default.red.bold(` A sensitive secret was detected in the tool arguments!`)
|
|
1864
|
+
);
|
|
1865
|
+
} else {
|
|
1866
|
+
console.log(import_chalk2.default.bgRed.white.bold(` \u{1F6D1} NODE9 INTERCEPTOR `));
|
|
1867
|
+
}
|
|
1733
1868
|
console.log(`${import_chalk2.default.bold("Action:")} ${import_chalk2.default.red(toolName)}`);
|
|
1734
1869
|
console.log(`${import_chalk2.default.bold("Flagged By:")} ${import_chalk2.default.yellow(explainableLabel)}`);
|
|
1735
1870
|
if (isRemoteLocked) {
|
|
@@ -1834,8 +1969,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
1834
1969
|
}
|
|
1835
1970
|
function getConfig() {
|
|
1836
1971
|
if (cachedConfig) return cachedConfig;
|
|
1837
|
-
const globalPath =
|
|
1838
|
-
const projectPath =
|
|
1972
|
+
const globalPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "config.json");
|
|
1973
|
+
const projectPath = import_path4.default.join(process.cwd(), "node9.config.json");
|
|
1839
1974
|
const globalConfig = tryLoadConfig(globalPath);
|
|
1840
1975
|
const projectConfig = tryLoadConfig(projectPath);
|
|
1841
1976
|
const mergedSettings = {
|
|
@@ -1847,13 +1982,13 @@ function getConfig() {
|
|
|
1847
1982
|
dangerousWords: [...DEFAULT_CONFIG.policy.dangerousWords],
|
|
1848
1983
|
ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
|
|
1849
1984
|
toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
|
|
1850
|
-
rules: [...DEFAULT_CONFIG.policy.rules],
|
|
1851
1985
|
smartRules: [...DEFAULT_CONFIG.policy.smartRules],
|
|
1852
1986
|
snapshot: {
|
|
1853
1987
|
tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
|
|
1854
1988
|
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
1855
1989
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
1856
|
-
}
|
|
1990
|
+
},
|
|
1991
|
+
dlp: { ...DEFAULT_CONFIG.policy.dlp }
|
|
1857
1992
|
};
|
|
1858
1993
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
1859
1994
|
const applyLayer = (source) => {
|
|
@@ -1873,7 +2008,6 @@ function getConfig() {
|
|
|
1873
2008
|
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
1874
2009
|
if (p.toolInspection)
|
|
1875
2010
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
1876
|
-
if (p.rules) mergedPolicy.rules.push(...p.rules);
|
|
1877
2011
|
if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
|
|
1878
2012
|
if (p.snapshot) {
|
|
1879
2013
|
const s2 = p.snapshot;
|
|
@@ -1881,6 +2015,11 @@ function getConfig() {
|
|
|
1881
2015
|
if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
|
|
1882
2016
|
if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
|
|
1883
2017
|
}
|
|
2018
|
+
if (p.dlp) {
|
|
2019
|
+
const d = p.dlp;
|
|
2020
|
+
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
2021
|
+
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
2022
|
+
}
|
|
1884
2023
|
const envs = source.environments || {};
|
|
1885
2024
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
1886
2025
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -1895,6 +2034,22 @@ function getConfig() {
|
|
|
1895
2034
|
};
|
|
1896
2035
|
applyLayer(globalConfig);
|
|
1897
2036
|
applyLayer(projectConfig);
|
|
2037
|
+
for (const shieldName of readActiveShields()) {
|
|
2038
|
+
const shield = getShield(shieldName);
|
|
2039
|
+
if (!shield) continue;
|
|
2040
|
+
const existingRuleNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2041
|
+
for (const rule of shield.smartRules) {
|
|
2042
|
+
if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
2043
|
+
}
|
|
2044
|
+
const existingWords = new Set(mergedPolicy.dangerousWords);
|
|
2045
|
+
for (const word of shield.dangerousWords) {
|
|
2046
|
+
if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
|
|
2050
|
+
for (const rule of ADVISORY_SMART_RULES) {
|
|
2051
|
+
if (!existingAdvisoryNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
|
|
2052
|
+
}
|
|
1898
2053
|
if (process.env.NODE9_MODE) mergedSettings.mode = process.env.NODE9_MODE;
|
|
1899
2054
|
mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
|
|
1900
2055
|
mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
|
|
@@ -1910,10 +2065,10 @@ function getConfig() {
|
|
|
1910
2065
|
return cachedConfig;
|
|
1911
2066
|
}
|
|
1912
2067
|
function tryLoadConfig(filePath) {
|
|
1913
|
-
if (!
|
|
2068
|
+
if (!import_fs2.default.existsSync(filePath)) return null;
|
|
1914
2069
|
let raw;
|
|
1915
2070
|
try {
|
|
1916
|
-
raw = JSON.parse(
|
|
2071
|
+
raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
|
|
1917
2072
|
} catch (err) {
|
|
1918
2073
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1919
2074
|
process.stderr.write(
|
|
@@ -1975,9 +2130,9 @@ function getCredentials() {
|
|
|
1975
2130
|
};
|
|
1976
2131
|
}
|
|
1977
2132
|
try {
|
|
1978
|
-
const credPath =
|
|
1979
|
-
if (
|
|
1980
|
-
const creds = JSON.parse(
|
|
2133
|
+
const credPath = import_path4.default.join(import_os2.default.homedir(), ".node9", "credentials.json");
|
|
2134
|
+
if (import_fs2.default.existsSync(credPath)) {
|
|
2135
|
+
const creds = JSON.parse(import_fs2.default.readFileSync(credPath, "utf-8"));
|
|
1981
2136
|
const profileName = process.env.NODE9_PROFILE || "default";
|
|
1982
2137
|
const profile = creds[profileName];
|
|
1983
2138
|
if (profile?.apiKey) {
|
|
@@ -2008,9 +2163,9 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
2008
2163
|
context: {
|
|
2009
2164
|
agent: meta?.agent,
|
|
2010
2165
|
mcpServer: meta?.mcpServer,
|
|
2011
|
-
hostname:
|
|
2166
|
+
hostname: import_os2.default.hostname(),
|
|
2012
2167
|
cwd: process.cwd(),
|
|
2013
|
-
platform:
|
|
2168
|
+
platform: import_os2.default.platform()
|
|
2014
2169
|
}
|
|
2015
2170
|
}),
|
|
2016
2171
|
signal: AbortSignal.timeout(5e3)
|
|
@@ -2031,9 +2186,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
2031
2186
|
context: {
|
|
2032
2187
|
agent: meta?.agent,
|
|
2033
2188
|
mcpServer: meta?.mcpServer,
|
|
2034
|
-
hostname:
|
|
2189
|
+
hostname: import_os2.default.hostname(),
|
|
2035
2190
|
cwd: process.cwd(),
|
|
2036
|
-
platform:
|
|
2191
|
+
platform: import_os2.default.platform()
|
|
2037
2192
|
},
|
|
2038
2193
|
...riskMetadata && { riskMetadata }
|
|
2039
2194
|
}),
|
|
@@ -2090,295 +2245,272 @@ async function resolveNode9SaaS(requestId, creds, approved) {
|
|
|
2090
2245
|
} catch {
|
|
2091
2246
|
}
|
|
2092
2247
|
}
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2248
|
+
var import_chalk2, import_prompts, import_fs2, import_path4, import_os2, import_net, import_crypto2, import_picomatch, import_sh_syntax, PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
|
|
2249
|
+
var init_core = __esm({
|
|
2250
|
+
"src/core.ts"() {
|
|
2251
|
+
"use strict";
|
|
2252
|
+
import_chalk2 = __toESM(require("chalk"));
|
|
2253
|
+
import_prompts = require("@inquirer/prompts");
|
|
2254
|
+
import_fs2 = __toESM(require("fs"));
|
|
2255
|
+
import_path4 = __toESM(require("path"));
|
|
2256
|
+
import_os2 = __toESM(require("os"));
|
|
2257
|
+
import_net = __toESM(require("net"));
|
|
2258
|
+
import_crypto2 = require("crypto");
|
|
2259
|
+
import_picomatch = __toESM(require("picomatch"));
|
|
2260
|
+
import_sh_syntax = require("sh-syntax");
|
|
2261
|
+
init_native();
|
|
2262
|
+
init_context_sniper();
|
|
2263
|
+
init_config_schema();
|
|
2264
|
+
init_shields();
|
|
2265
|
+
init_dlp();
|
|
2266
|
+
PAUSED_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "PAUSED");
|
|
2267
|
+
TRUST_FILE = import_path4.default.join(import_os2.default.homedir(), ".node9", "trust.json");
|
|
2268
|
+
LOCAL_AUDIT_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "audit.log");
|
|
2269
|
+
HOOK_DEBUG_LOG = import_path4.default.join(import_os2.default.homedir(), ".node9", "hook-debug.log");
|
|
2270
|
+
SQL_DML_KEYWORDS = /* @__PURE__ */ new Set(["select", "insert", "update", "delete", "merge", "upsert"]);
|
|
2271
|
+
DANGEROUS_WORDS = [
|
|
2272
|
+
"mkfs",
|
|
2273
|
+
// formats/wipes a filesystem partition
|
|
2274
|
+
"shred"
|
|
2275
|
+
// permanently overwrites file contents (unrecoverable)
|
|
2276
|
+
];
|
|
2277
|
+
DEFAULT_CONFIG = {
|
|
2278
|
+
settings: {
|
|
2279
|
+
mode: "standard",
|
|
2280
|
+
autoStartDaemon: true,
|
|
2281
|
+
enableUndo: true,
|
|
2282
|
+
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
2283
|
+
enableHookLogDebug: false,
|
|
2284
|
+
approvalTimeoutMs: 0,
|
|
2285
|
+
// 0 = disabled; set e.g. 30000 for 30-second auto-deny
|
|
2286
|
+
approvers: { native: true, browser: true, cloud: true, terminal: true }
|
|
2287
|
+
},
|
|
2288
|
+
policy: {
|
|
2289
|
+
sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
|
|
2290
|
+
dangerousWords: DANGEROUS_WORDS,
|
|
2291
|
+
ignoredTools: [
|
|
2292
|
+
"list_*",
|
|
2293
|
+
"get_*",
|
|
2294
|
+
"read_*",
|
|
2295
|
+
"describe_*",
|
|
2296
|
+
"read",
|
|
2297
|
+
"glob",
|
|
2298
|
+
"grep",
|
|
2299
|
+
"ls",
|
|
2300
|
+
"notebookread",
|
|
2301
|
+
"notebookedit",
|
|
2302
|
+
"webfetch",
|
|
2303
|
+
"websearch",
|
|
2304
|
+
"exitplanmode",
|
|
2305
|
+
"askuserquestion",
|
|
2306
|
+
"agent",
|
|
2307
|
+
"task*",
|
|
2308
|
+
"toolsearch",
|
|
2309
|
+
"mcp__ide__*",
|
|
2310
|
+
"getDiagnostics"
|
|
2311
|
+
],
|
|
2312
|
+
toolInspection: {
|
|
2313
|
+
bash: "command",
|
|
2314
|
+
shell: "command",
|
|
2315
|
+
run_shell_command: "command",
|
|
2316
|
+
"terminal.execute": "command",
|
|
2317
|
+
"postgres:query": "sql"
|
|
2318
|
+
},
|
|
2319
|
+
snapshot: {
|
|
2320
|
+
tools: [
|
|
2321
|
+
"str_replace_based_edit_tool",
|
|
2322
|
+
"write_file",
|
|
2323
|
+
"edit_file",
|
|
2324
|
+
"create_file",
|
|
2325
|
+
"edit",
|
|
2326
|
+
"replace"
|
|
2327
|
+
],
|
|
2328
|
+
onlyPaths: [],
|
|
2329
|
+
ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
|
|
2330
|
+
},
|
|
2331
|
+
smartRules: [
|
|
2332
|
+
// ── rm safety (critical — always evaluated first) ──────────────────────
|
|
2333
|
+
{
|
|
2334
|
+
name: "block-rm-rf-home",
|
|
2335
|
+
tool: "bash",
|
|
2336
|
+
conditionMode: "all",
|
|
2337
|
+
conditions: [
|
|
2338
|
+
{
|
|
2339
|
+
field: "command",
|
|
2340
|
+
op: "matches",
|
|
2341
|
+
value: "rm\\b.*(-[rRfF]*[rR][rRfF]*|--recursive)"
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
field: "command",
|
|
2345
|
+
op: "matches",
|
|
2346
|
+
value: "(~|\\/root(\\/|$)|\\$HOME|\\/home\\/)"
|
|
2347
|
+
}
|
|
2348
|
+
],
|
|
2349
|
+
verdict: "block",
|
|
2350
|
+
reason: "Recursive delete of home directory is irreversible"
|
|
2351
|
+
},
|
|
2352
|
+
// ── SQL safety ────────────────────────────────────────────────────────
|
|
2353
|
+
{
|
|
2354
|
+
name: "no-delete-without-where",
|
|
2355
|
+
tool: "*",
|
|
2356
|
+
conditions: [
|
|
2357
|
+
{ field: "sql", op: "matches", value: "^(DELETE|UPDATE)\\s", flags: "i" },
|
|
2358
|
+
{ field: "sql", op: "notMatches", value: "\\bWHERE\\b", flags: "i" }
|
|
2359
|
+
],
|
|
2360
|
+
conditionMode: "all",
|
|
2361
|
+
verdict: "review",
|
|
2362
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
2363
|
+
},
|
|
2364
|
+
{
|
|
2365
|
+
name: "review-drop-truncate-shell",
|
|
2366
|
+
tool: "bash",
|
|
2367
|
+
conditions: [
|
|
2368
|
+
{
|
|
2369
|
+
field: "command",
|
|
2370
|
+
op: "matches",
|
|
2371
|
+
value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
|
|
2372
|
+
flags: "i"
|
|
2373
|
+
}
|
|
2374
|
+
],
|
|
2375
|
+
conditionMode: "all",
|
|
2376
|
+
verdict: "review",
|
|
2377
|
+
reason: "SQL DDL destructive statement inside a shell command"
|
|
2378
|
+
},
|
|
2379
|
+
// ── Git safety ────────────────────────────────────────────────────────
|
|
2380
|
+
{
|
|
2381
|
+
name: "block-force-push",
|
|
2382
|
+
tool: "bash",
|
|
2383
|
+
conditions: [
|
|
2384
|
+
{
|
|
2385
|
+
field: "command",
|
|
2386
|
+
op: "matches",
|
|
2387
|
+
value: "git push.*(--force|--force-with-lease|-f\\b)",
|
|
2388
|
+
flags: "i"
|
|
2389
|
+
}
|
|
2390
|
+
],
|
|
2391
|
+
conditionMode: "all",
|
|
2392
|
+
verdict: "block",
|
|
2393
|
+
reason: "Force push overwrites remote history and cannot be undone"
|
|
2394
|
+
},
|
|
2395
|
+
{
|
|
2396
|
+
name: "review-git-push",
|
|
2397
|
+
tool: "bash",
|
|
2398
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*git\\s+push\\b", flags: "i" }],
|
|
2399
|
+
conditionMode: "all",
|
|
2400
|
+
verdict: "review",
|
|
2401
|
+
reason: "git push sends changes to a shared remote"
|
|
2402
|
+
},
|
|
2403
|
+
{
|
|
2404
|
+
name: "review-git-destructive",
|
|
2405
|
+
tool: "bash",
|
|
2406
|
+
conditions: [
|
|
2407
|
+
{
|
|
2408
|
+
field: "command",
|
|
2409
|
+
op: "matches",
|
|
2410
|
+
value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
|
|
2411
|
+
flags: "i"
|
|
2412
|
+
}
|
|
2413
|
+
],
|
|
2414
|
+
conditionMode: "all",
|
|
2415
|
+
verdict: "review",
|
|
2416
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
2417
|
+
},
|
|
2418
|
+
// ── Shell safety ──────────────────────────────────────────────────────
|
|
2419
|
+
{
|
|
2420
|
+
name: "review-sudo",
|
|
2421
|
+
tool: "bash",
|
|
2422
|
+
conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
|
|
2423
|
+
conditionMode: "all",
|
|
2424
|
+
verdict: "review",
|
|
2425
|
+
reason: "Command requires elevated privileges"
|
|
2426
|
+
},
|
|
2427
|
+
{
|
|
2428
|
+
name: "review-curl-pipe-shell",
|
|
2429
|
+
tool: "bash",
|
|
2430
|
+
conditions: [
|
|
2431
|
+
{
|
|
2432
|
+
field: "command",
|
|
2433
|
+
op: "matches",
|
|
2434
|
+
value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
|
|
2435
|
+
flags: "i"
|
|
2436
|
+
}
|
|
2437
|
+
],
|
|
2438
|
+
conditionMode: "all",
|
|
2439
|
+
verdict: "block",
|
|
2440
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
2441
|
+
}
|
|
2442
|
+
],
|
|
2443
|
+
dlp: { enabled: true, scanIgnoredTools: true }
|
|
2444
|
+
},
|
|
2445
|
+
environments: {}
|
|
2446
|
+
};
|
|
2447
|
+
ADVISORY_SMART_RULES = [
|
|
2448
|
+
{
|
|
2449
|
+
name: "allow-rm-safe-paths",
|
|
2450
|
+
tool: "*",
|
|
2451
|
+
conditionMode: "all",
|
|
2452
|
+
conditions: [
|
|
2453
|
+
{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" },
|
|
2454
|
+
{
|
|
2455
|
+
field: "command",
|
|
2456
|
+
op: "matches",
|
|
2457
|
+
// Matches known-safe build artifact paths in the command.
|
|
2458
|
+
value: "(node_modules|\\bdist\\b|\\.next|\\bcoverage\\b|\\.cache|\\btmp\\b|\\btemp\\b|\\.DS_Store)(\\/|\\s|$)"
|
|
2459
|
+
}
|
|
2460
|
+
],
|
|
2461
|
+
verdict: "allow",
|
|
2462
|
+
reason: "Deleting a known-safe build artifact path"
|
|
2463
|
+
},
|
|
2464
|
+
{
|
|
2465
|
+
name: "review-rm",
|
|
2466
|
+
tool: "*",
|
|
2467
|
+
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
2468
|
+
verdict: "review",
|
|
2469
|
+
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
2179
2470
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
} else {
|
|
2186
|
-
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
2187
|
-
}
|
|
2188
|
-
console.log("");
|
|
2189
|
-
}
|
|
2190
|
-
if (!anythingChanged && serversToWrap.length === 0) {
|
|
2191
|
-
console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
2192
|
-
printDaemonTip();
|
|
2193
|
-
return;
|
|
2194
|
-
}
|
|
2195
|
-
if (anythingChanged) {
|
|
2196
|
-
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
2197
|
-
console.log(import_chalk3.default.gray(" Restart Claude Code for changes to take effect."));
|
|
2198
|
-
printDaemonTip();
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
async function setupGemini() {
|
|
2202
|
-
const homeDir2 = import_os2.default.homedir();
|
|
2203
|
-
const settingsPath = import_path4.default.join(homeDir2, ".gemini", "settings.json");
|
|
2204
|
-
const settings = readJson(settingsPath) ?? {};
|
|
2205
|
-
const servers = settings.mcpServers ?? {};
|
|
2206
|
-
let anythingChanged = false;
|
|
2207
|
-
if (!settings.hooks) settings.hooks = {};
|
|
2208
|
-
const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
|
|
2209
|
-
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
2210
|
-
);
|
|
2211
|
-
if (!hasBeforeHook) {
|
|
2212
|
-
if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
|
|
2213
|
-
if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
|
|
2214
|
-
settings.hooks.BeforeTool.push({
|
|
2215
|
-
matcher: ".*",
|
|
2216
|
-
hooks: [
|
|
2217
|
-
{
|
|
2218
|
-
name: "node9-check",
|
|
2219
|
-
type: "command",
|
|
2220
|
-
command: fullPathCommand("check"),
|
|
2221
|
-
timeout: 6e5
|
|
2222
|
-
}
|
|
2223
|
-
]
|
|
2224
|
-
});
|
|
2225
|
-
console.log(import_chalk3.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
2226
|
-
anythingChanged = true;
|
|
2227
|
-
}
|
|
2228
|
-
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
2229
|
-
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
2230
|
-
);
|
|
2231
|
-
if (!hasAfterHook) {
|
|
2232
|
-
if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
|
|
2233
|
-
if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
|
|
2234
|
-
settings.hooks.AfterTool.push({
|
|
2235
|
-
matcher: ".*",
|
|
2236
|
-
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
2237
|
-
});
|
|
2238
|
-
console.log(import_chalk3.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
2239
|
-
anythingChanged = true;
|
|
2240
|
-
}
|
|
2241
|
-
if (anythingChanged) {
|
|
2242
|
-
writeJson(settingsPath, settings);
|
|
2243
|
-
console.log("");
|
|
2244
|
-
}
|
|
2245
|
-
const serversToWrap = [];
|
|
2246
|
-
for (const [name, server] of Object.entries(servers)) {
|
|
2247
|
-
if (!server.command || server.command === "node9") continue;
|
|
2248
|
-
const parts = [server.command, ...server.args ?? []];
|
|
2249
|
-
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
2471
|
+
];
|
|
2472
|
+
cachedConfig = null;
|
|
2473
|
+
DAEMON_PORT = 7391;
|
|
2474
|
+
DAEMON_HOST = "127.0.0.1";
|
|
2475
|
+
ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path4.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
|
|
2250
2476
|
}
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
}
|
|
2278
|
-
if (anythingChanged) {
|
|
2279
|
-
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
2280
|
-
console.log(import_chalk3.default.gray(" Restart Gemini CLI for changes to take effect."));
|
|
2281
|
-
printDaemonTip();
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
async function setupCursor() {
|
|
2285
|
-
const homeDir2 = import_os2.default.homedir();
|
|
2286
|
-
const mcpPath = import_path4.default.join(homeDir2, ".cursor", "mcp.json");
|
|
2287
|
-
const hooksPath = import_path4.default.join(homeDir2, ".cursor", "hooks.json");
|
|
2288
|
-
const mcpConfig = readJson(mcpPath) ?? {};
|
|
2289
|
-
const hooksFile = readJson(hooksPath) ?? { version: 1 };
|
|
2290
|
-
const servers = mcpConfig.mcpServers ?? {};
|
|
2291
|
-
let anythingChanged = false;
|
|
2292
|
-
if (!hooksFile.hooks) hooksFile.hooks = {};
|
|
2293
|
-
const hasPreHook = hooksFile.hooks.preToolUse?.some(
|
|
2294
|
-
(h) => h.command === "node9" && h.args?.includes("check") || h.command?.includes("cli.js")
|
|
2295
|
-
);
|
|
2296
|
-
if (!hasPreHook) {
|
|
2297
|
-
if (!hooksFile.hooks.preToolUse) hooksFile.hooks.preToolUse = [];
|
|
2298
|
-
hooksFile.hooks.preToolUse.push({ command: fullPathCommand("check") });
|
|
2299
|
-
console.log(import_chalk3.default.green(" \u2705 preToolUse hook added \u2192 node9 check"));
|
|
2300
|
-
anythingChanged = true;
|
|
2301
|
-
}
|
|
2302
|
-
const hasPostHook = hooksFile.hooks.postToolUse?.some(
|
|
2303
|
-
(h) => h.command === "node9" && h.args?.includes("log") || h.command?.includes("cli.js")
|
|
2304
|
-
);
|
|
2305
|
-
if (!hasPostHook) {
|
|
2306
|
-
if (!hooksFile.hooks.postToolUse) hooksFile.hooks.postToolUse = [];
|
|
2307
|
-
hooksFile.hooks.postToolUse.push({ command: fullPathCommand("log") });
|
|
2308
|
-
console.log(import_chalk3.default.green(" \u2705 postToolUse hook added \u2192 node9 log"));
|
|
2309
|
-
anythingChanged = true;
|
|
2310
|
-
}
|
|
2311
|
-
if (anythingChanged) {
|
|
2312
|
-
writeJson(hooksPath, hooksFile);
|
|
2313
|
-
console.log("");
|
|
2314
|
-
}
|
|
2315
|
-
const serversToWrap = [];
|
|
2316
|
-
for (const [name, server] of Object.entries(servers)) {
|
|
2317
|
-
if (!server.command || server.command === "node9") continue;
|
|
2318
|
-
const parts = [server.command, ...server.args ?? []];
|
|
2319
|
-
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
2320
|
-
}
|
|
2321
|
-
if (serversToWrap.length > 0) {
|
|
2322
|
-
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
2323
|
-
console.log(import_chalk3.default.white(` ${mcpPath}`));
|
|
2324
|
-
for (const { name, originalCmd } of serversToWrap) {
|
|
2325
|
-
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
2326
|
-
}
|
|
2327
|
-
console.log("");
|
|
2328
|
-
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
2329
|
-
if (proceed) {
|
|
2330
|
-
for (const { name, parts } of serversToWrap) {
|
|
2331
|
-
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
2332
|
-
}
|
|
2333
|
-
mcpConfig.mcpServers = servers;
|
|
2334
|
-
writeJson(mcpPath, mcpConfig);
|
|
2335
|
-
console.log(import_chalk3.default.green(`
|
|
2336
|
-
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
2337
|
-
anythingChanged = true;
|
|
2338
|
-
} else {
|
|
2339
|
-
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
2340
|
-
}
|
|
2341
|
-
console.log("");
|
|
2342
|
-
}
|
|
2343
|
-
if (!anythingChanged && serversToWrap.length === 0) {
|
|
2344
|
-
console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Cursor."));
|
|
2345
|
-
printDaemonTip();
|
|
2346
|
-
return;
|
|
2347
|
-
}
|
|
2348
|
-
if (anythingChanged) {
|
|
2349
|
-
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor!"));
|
|
2350
|
-
console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
|
|
2351
|
-
printDaemonTip();
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
// src/daemon/ui.html
|
|
2356
|
-
var ui_default = `<!doctype html>
|
|
2357
|
-
<html lang="en">
|
|
2358
|
-
<head>
|
|
2359
|
-
<meta charset="UTF-8" />
|
|
2360
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2361
|
-
<title>Node9 Security Guard</title>
|
|
2362
|
-
<style>
|
|
2363
|
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Fira+Code:wght@400;500&display=swap');
|
|
2364
|
-
:root {
|
|
2365
|
-
--bg: #0a0c10;
|
|
2366
|
-
--card: #1c2128;
|
|
2367
|
-
--panel: #161b22;
|
|
2368
|
-
--border: #30363d;
|
|
2369
|
-
--text: #adbac7;
|
|
2370
|
-
--text-bright: #cdd9e5;
|
|
2371
|
-
--muted: #768390;
|
|
2372
|
-
--primary: #f0883e;
|
|
2373
|
-
--success: #347d39;
|
|
2374
|
-
--danger: #c93c37;
|
|
2375
|
-
--accent: #539bf5;
|
|
2477
|
+
});
|
|
2478
|
+
|
|
2479
|
+
// src/daemon/ui.html
|
|
2480
|
+
var ui_default;
|
|
2481
|
+
var init_ui = __esm({
|
|
2482
|
+
"src/daemon/ui.html"() {
|
|
2483
|
+
ui_default = `<!doctype html>
|
|
2484
|
+
<html lang="en">
|
|
2485
|
+
<head>
|
|
2486
|
+
<meta charset="UTF-8" />
|
|
2487
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2488
|
+
<title>Node9 Security Guard</title>
|
|
2489
|
+
<style>
|
|
2490
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Fira+Code:wght@400;500&display=swap');
|
|
2491
|
+
:root {
|
|
2492
|
+
--bg: #0a0c10;
|
|
2493
|
+
--card: #1c2128;
|
|
2494
|
+
--panel: #161b22;
|
|
2495
|
+
--border: #30363d;
|
|
2496
|
+
--text: #adbac7;
|
|
2497
|
+
--text-bright: #cdd9e5;
|
|
2498
|
+
--muted: #768390;
|
|
2499
|
+
--primary: #f0883e;
|
|
2500
|
+
--success: #347d39;
|
|
2501
|
+
--danger: #c93c37;
|
|
2502
|
+
--accent: #539bf5;
|
|
2376
2503
|
}
|
|
2377
2504
|
* {
|
|
2378
2505
|
box-sizing: border-box;
|
|
2379
2506
|
margin: 0;
|
|
2380
2507
|
padding: 0;
|
|
2381
2508
|
}
|
|
2509
|
+
html,
|
|
2510
|
+
body {
|
|
2511
|
+
height: 100%;
|
|
2512
|
+
overflow: hidden;
|
|
2513
|
+
}
|
|
2382
2514
|
body {
|
|
2383
2515
|
background: var(--bg);
|
|
2384
2516
|
color: var(--text);
|
|
@@ -2386,16 +2518,17 @@ var ui_default = `<!doctype html>
|
|
|
2386
2518
|
'Inter',
|
|
2387
2519
|
-apple-system,
|
|
2388
2520
|
sans-serif;
|
|
2389
|
-
min-height: 100vh;
|
|
2390
2521
|
}
|
|
2391
2522
|
|
|
2392
2523
|
.shell {
|
|
2393
|
-
max-width:
|
|
2524
|
+
max-width: 1440px;
|
|
2525
|
+
height: 100vh;
|
|
2394
2526
|
margin: 0 auto;
|
|
2395
|
-
padding:
|
|
2527
|
+
padding: 16px 20px 16px;
|
|
2396
2528
|
display: grid;
|
|
2397
2529
|
grid-template-rows: auto 1fr;
|
|
2398
|
-
gap:
|
|
2530
|
+
gap: 16px;
|
|
2531
|
+
overflow: hidden;
|
|
2399
2532
|
}
|
|
2400
2533
|
header {
|
|
2401
2534
|
display: flex;
|
|
@@ -2432,9 +2565,10 @@ var ui_default = `<!doctype html>
|
|
|
2432
2565
|
|
|
2433
2566
|
.body {
|
|
2434
2567
|
display: grid;
|
|
2435
|
-
grid-template-columns: 1fr
|
|
2436
|
-
gap:
|
|
2437
|
-
|
|
2568
|
+
grid-template-columns: 360px 1fr 270px;
|
|
2569
|
+
gap: 16px;
|
|
2570
|
+
min-height: 0;
|
|
2571
|
+
overflow: hidden;
|
|
2438
2572
|
}
|
|
2439
2573
|
|
|
2440
2574
|
.warning-banner {
|
|
@@ -2454,6 +2588,10 @@ var ui_default = `<!doctype html>
|
|
|
2454
2588
|
|
|
2455
2589
|
.main {
|
|
2456
2590
|
min-width: 0;
|
|
2591
|
+
min-height: 0;
|
|
2592
|
+
overflow-y: auto;
|
|
2593
|
+
scrollbar-width: thin;
|
|
2594
|
+
scrollbar-color: var(--border) transparent;
|
|
2457
2595
|
}
|
|
2458
2596
|
.section-title {
|
|
2459
2597
|
font-size: 11px;
|
|
@@ -2484,14 +2622,64 @@ var ui_default = `<!doctype html>
|
|
|
2484
2622
|
background: var(--card);
|
|
2485
2623
|
border: 1px solid var(--border);
|
|
2486
2624
|
border-radius: 14px;
|
|
2487
|
-
padding:
|
|
2488
|
-
margin-bottom:
|
|
2625
|
+
padding: 20px;
|
|
2626
|
+
margin-bottom: 14px;
|
|
2489
2627
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
2490
2628
|
animation: pop 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
2491
2629
|
}
|
|
2492
2630
|
.card.slack-viewer {
|
|
2493
2631
|
border-color: rgba(83, 155, 245, 0.3);
|
|
2494
2632
|
}
|
|
2633
|
+
.card-header {
|
|
2634
|
+
display: flex;
|
|
2635
|
+
align-items: center;
|
|
2636
|
+
gap: 8px;
|
|
2637
|
+
margin-bottom: 12px;
|
|
2638
|
+
padding-bottom: 12px;
|
|
2639
|
+
border-bottom: 1px solid var(--border);
|
|
2640
|
+
}
|
|
2641
|
+
.card-header-icon {
|
|
2642
|
+
font-size: 16px;
|
|
2643
|
+
}
|
|
2644
|
+
.card-header-title {
|
|
2645
|
+
font-size: 12px;
|
|
2646
|
+
font-weight: 700;
|
|
2647
|
+
color: var(--text-bright);
|
|
2648
|
+
text-transform: uppercase;
|
|
2649
|
+
letter-spacing: 0.5px;
|
|
2650
|
+
}
|
|
2651
|
+
.card-timer {
|
|
2652
|
+
margin-left: auto;
|
|
2653
|
+
font-size: 11px;
|
|
2654
|
+
font-family: 'Fira Code', monospace;
|
|
2655
|
+
color: var(--muted);
|
|
2656
|
+
background: rgba(48, 54, 61, 0.6);
|
|
2657
|
+
padding: 2px 8px;
|
|
2658
|
+
border-radius: 5px;
|
|
2659
|
+
}
|
|
2660
|
+
.card-timer.urgent {
|
|
2661
|
+
color: var(--danger);
|
|
2662
|
+
background: rgba(201, 60, 55, 0.1);
|
|
2663
|
+
}
|
|
2664
|
+
.btn-allow {
|
|
2665
|
+
background: var(--success);
|
|
2666
|
+
color: #fff;
|
|
2667
|
+
grid-column: span 2;
|
|
2668
|
+
font-size: 14px;
|
|
2669
|
+
padding: 13px 14px;
|
|
2670
|
+
}
|
|
2671
|
+
.btn-deny {
|
|
2672
|
+
background: rgba(201, 60, 55, 0.15);
|
|
2673
|
+
color: #e5534b;
|
|
2674
|
+
border: 1px solid rgba(201, 60, 55, 0.3);
|
|
2675
|
+
grid-column: span 2;
|
|
2676
|
+
}
|
|
2677
|
+
.btn-deny:hover:not(:disabled) {
|
|
2678
|
+
background: var(--danger);
|
|
2679
|
+
color: #fff;
|
|
2680
|
+
border-color: transparent;
|
|
2681
|
+
filter: none;
|
|
2682
|
+
}
|
|
2495
2683
|
@keyframes pop {
|
|
2496
2684
|
from {
|
|
2497
2685
|
opacity: 0;
|
|
@@ -2699,24 +2887,178 @@ var ui_default = `<!doctype html>
|
|
|
2699
2887
|
cursor: not-allowed;
|
|
2700
2888
|
}
|
|
2701
2889
|
|
|
2890
|
+
.flight-col {
|
|
2891
|
+
display: flex;
|
|
2892
|
+
flex-direction: column;
|
|
2893
|
+
min-height: 0;
|
|
2894
|
+
overflow: hidden;
|
|
2895
|
+
}
|
|
2896
|
+
.flight-panel {
|
|
2897
|
+
flex: 1;
|
|
2898
|
+
min-height: 0;
|
|
2899
|
+
display: flex;
|
|
2900
|
+
flex-direction: column;
|
|
2901
|
+
overflow: hidden;
|
|
2902
|
+
}
|
|
2702
2903
|
.sidebar {
|
|
2703
2904
|
display: flex;
|
|
2704
2905
|
flex-direction: column;
|
|
2705
2906
|
gap: 12px;
|
|
2706
|
-
|
|
2707
|
-
|
|
2907
|
+
min-height: 0;
|
|
2908
|
+
overflow-y: auto;
|
|
2909
|
+
scrollbar-width: thin;
|
|
2910
|
+
scrollbar-color: var(--border) transparent;
|
|
2708
2911
|
}
|
|
2709
2912
|
.panel {
|
|
2710
2913
|
background: var(--panel);
|
|
2711
2914
|
border: 1px solid var(--border);
|
|
2712
2915
|
border-radius: 12px;
|
|
2713
|
-
padding:
|
|
2916
|
+
padding: 14px;
|
|
2917
|
+
}
|
|
2918
|
+
/* \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
2919
|
+
#activity-feed {
|
|
2920
|
+
display: flex;
|
|
2921
|
+
flex-direction: column;
|
|
2922
|
+
gap: 4px;
|
|
2923
|
+
margin-top: 4px;
|
|
2924
|
+
flex: 1;
|
|
2925
|
+
min-height: 0;
|
|
2926
|
+
overflow-y: auto;
|
|
2927
|
+
scrollbar-width: thin;
|
|
2928
|
+
scrollbar-color: var(--border) transparent;
|
|
2929
|
+
}
|
|
2930
|
+
.feed-row {
|
|
2931
|
+
display: grid;
|
|
2932
|
+
grid-template-columns: 58px 20px 1fr 48px;
|
|
2933
|
+
align-items: start;
|
|
2934
|
+
gap: 6px;
|
|
2935
|
+
background: rgba(22, 27, 34, 0.6);
|
|
2936
|
+
border: 1px solid var(--border);
|
|
2937
|
+
padding: 7px 10px;
|
|
2938
|
+
border-radius: 7px;
|
|
2939
|
+
font-size: 11px;
|
|
2940
|
+
animation: frSlideIn 0.15s ease-out;
|
|
2941
|
+
transition: background 0.1s;
|
|
2942
|
+
cursor: default;
|
|
2943
|
+
}
|
|
2944
|
+
.feed-row:hover {
|
|
2945
|
+
background: rgba(30, 38, 48, 0.9);
|
|
2946
|
+
border-color: rgba(83, 155, 245, 0.2);
|
|
2947
|
+
}
|
|
2948
|
+
@keyframes frSlideIn {
|
|
2949
|
+
from {
|
|
2950
|
+
opacity: 0;
|
|
2951
|
+
transform: translateX(-4px);
|
|
2952
|
+
}
|
|
2953
|
+
to {
|
|
2954
|
+
opacity: 1;
|
|
2955
|
+
transform: none;
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
.feed-ts {
|
|
2959
|
+
color: var(--muted);
|
|
2960
|
+
font-family: monospace;
|
|
2961
|
+
font-size: 9px;
|
|
2962
|
+
}
|
|
2963
|
+
.feed-icon {
|
|
2964
|
+
text-align: center;
|
|
2965
|
+
font-size: 13px;
|
|
2966
|
+
}
|
|
2967
|
+
.feed-content {
|
|
2968
|
+
min-width: 0;
|
|
2969
|
+
color: var(--text-bright);
|
|
2970
|
+
word-break: break-all;
|
|
2971
|
+
}
|
|
2972
|
+
.feed-args {
|
|
2973
|
+
display: block;
|
|
2974
|
+
color: var(--muted);
|
|
2975
|
+
font-family: monospace;
|
|
2976
|
+
margin-top: 2px;
|
|
2977
|
+
font-size: 10px;
|
|
2978
|
+
word-break: break-all;
|
|
2979
|
+
}
|
|
2980
|
+
.feed-badge {
|
|
2981
|
+
text-align: right;
|
|
2982
|
+
font-weight: 700;
|
|
2983
|
+
font-size: 9px;
|
|
2984
|
+
letter-spacing: 0.03em;
|
|
2985
|
+
}
|
|
2986
|
+
.fr-pending {
|
|
2987
|
+
color: var(--muted);
|
|
2988
|
+
}
|
|
2989
|
+
.fr-allow {
|
|
2990
|
+
color: #57ab5a;
|
|
2991
|
+
}
|
|
2992
|
+
.fr-block {
|
|
2993
|
+
color: var(--danger);
|
|
2994
|
+
}
|
|
2995
|
+
.fr-dlp {
|
|
2996
|
+
color: var(--primary);
|
|
2997
|
+
animation: frBlink 1s infinite;
|
|
2998
|
+
}
|
|
2999
|
+
@keyframes frBlink {
|
|
3000
|
+
50% {
|
|
3001
|
+
opacity: 0.4;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
.fr-dlp-row {
|
|
3005
|
+
border-color: var(--primary) !important;
|
|
3006
|
+
}
|
|
3007
|
+
.feed-clear-btn {
|
|
3008
|
+
background: transparent;
|
|
3009
|
+
border: none;
|
|
3010
|
+
color: var(--muted);
|
|
3011
|
+
font-size: 10px;
|
|
3012
|
+
padding: 0;
|
|
3013
|
+
cursor: pointer;
|
|
3014
|
+
margin-left: auto;
|
|
3015
|
+
font-family: inherit;
|
|
3016
|
+
font-weight: 500;
|
|
3017
|
+
transition: color 0.15s;
|
|
3018
|
+
}
|
|
3019
|
+
.feed-clear-btn:hover {
|
|
3020
|
+
color: var(--text);
|
|
3021
|
+
filter: none;
|
|
3022
|
+
transform: none;
|
|
3023
|
+
}
|
|
3024
|
+
/* \u2500\u2500 Shields \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
3025
|
+
.shield-row {
|
|
3026
|
+
display: flex;
|
|
3027
|
+
align-items: flex-start;
|
|
3028
|
+
gap: 10px;
|
|
3029
|
+
padding: 8px 0;
|
|
3030
|
+
border-bottom: 1px solid var(--border);
|
|
3031
|
+
}
|
|
3032
|
+
.shield-row:last-child {
|
|
3033
|
+
border-bottom: none;
|
|
3034
|
+
padding-bottom: 0;
|
|
3035
|
+
}
|
|
3036
|
+
.shield-row:first-child {
|
|
3037
|
+
padding-top: 0;
|
|
3038
|
+
}
|
|
3039
|
+
.shield-info {
|
|
3040
|
+
flex: 1;
|
|
3041
|
+
min-width: 0;
|
|
3042
|
+
}
|
|
3043
|
+
.shield-name {
|
|
3044
|
+
font-size: 12px;
|
|
3045
|
+
color: var(--text-bright);
|
|
3046
|
+
font-weight: 600;
|
|
3047
|
+
font-family: 'Fira Code', monospace;
|
|
3048
|
+
}
|
|
3049
|
+
.shield-desc {
|
|
3050
|
+
font-size: 10px;
|
|
3051
|
+
color: var(--muted);
|
|
3052
|
+
margin-top: 2px;
|
|
3053
|
+
line-height: 1.4;
|
|
2714
3054
|
}
|
|
3055
|
+
|
|
2715
3056
|
.panel-title {
|
|
2716
3057
|
font-size: 12px;
|
|
2717
3058
|
font-weight: 700;
|
|
2718
3059
|
color: var(--text-bright);
|
|
2719
3060
|
margin-bottom: 12px;
|
|
3061
|
+
flex-shrink: 0;
|
|
2720
3062
|
display: flex;
|
|
2721
3063
|
align-items: center;
|
|
2722
3064
|
gap: 6px;
|
|
@@ -2724,8 +3066,8 @@ var ui_default = `<!doctype html>
|
|
|
2724
3066
|
.setting-row {
|
|
2725
3067
|
display: flex;
|
|
2726
3068
|
align-items: flex-start;
|
|
2727
|
-
gap:
|
|
2728
|
-
margin-bottom:
|
|
3069
|
+
gap: 10px;
|
|
3070
|
+
margin-bottom: 8px;
|
|
2729
3071
|
}
|
|
2730
3072
|
.setting-row:last-child {
|
|
2731
3073
|
margin-bottom: 0;
|
|
@@ -2734,20 +3076,21 @@ var ui_default = `<!doctype html>
|
|
|
2734
3076
|
flex: 1;
|
|
2735
3077
|
}
|
|
2736
3078
|
.setting-label {
|
|
2737
|
-
font-size:
|
|
3079
|
+
font-size: 11px;
|
|
2738
3080
|
color: var(--text-bright);
|
|
2739
|
-
margin-bottom:
|
|
3081
|
+
margin-bottom: 2px;
|
|
3082
|
+
font-weight: 600;
|
|
2740
3083
|
}
|
|
2741
3084
|
.setting-desc {
|
|
2742
|
-
font-size:
|
|
3085
|
+
font-size: 10px;
|
|
2743
3086
|
color: var(--muted);
|
|
2744
|
-
line-height: 1.
|
|
3087
|
+
line-height: 1.4;
|
|
2745
3088
|
}
|
|
2746
3089
|
.toggle {
|
|
2747
3090
|
position: relative;
|
|
2748
3091
|
display: inline-block;
|
|
2749
|
-
width:
|
|
2750
|
-
height:
|
|
3092
|
+
width: 34px;
|
|
3093
|
+
height: 19px;
|
|
2751
3094
|
flex-shrink: 0;
|
|
2752
3095
|
margin-top: 1px;
|
|
2753
3096
|
}
|
|
@@ -2767,8 +3110,8 @@ var ui_default = `<!doctype html>
|
|
|
2767
3110
|
.slider:before {
|
|
2768
3111
|
content: '';
|
|
2769
3112
|
position: absolute;
|
|
2770
|
-
width:
|
|
2771
|
-
height:
|
|
3113
|
+
width: 13px;
|
|
3114
|
+
height: 13px;
|
|
2772
3115
|
left: 3px;
|
|
2773
3116
|
bottom: 3px;
|
|
2774
3117
|
background: #fff;
|
|
@@ -2779,7 +3122,7 @@ var ui_default = `<!doctype html>
|
|
|
2779
3122
|
background: var(--success);
|
|
2780
3123
|
}
|
|
2781
3124
|
input:checked + .slider:before {
|
|
2782
|
-
transform: translateX(
|
|
3125
|
+
transform: translateX(15px);
|
|
2783
3126
|
}
|
|
2784
3127
|
input:disabled + .slider {
|
|
2785
3128
|
opacity: 0.4;
|
|
@@ -2938,12 +3281,17 @@ var ui_default = `<!doctype html>
|
|
|
2938
3281
|
border: 1px solid var(--border);
|
|
2939
3282
|
}
|
|
2940
3283
|
|
|
2941
|
-
@media (max-width:
|
|
3284
|
+
@media (max-width: 960px) {
|
|
2942
3285
|
.body {
|
|
2943
|
-
grid-template-columns: 1fr;
|
|
3286
|
+
grid-template-columns: 1fr 220px;
|
|
2944
3287
|
}
|
|
2945
|
-
.
|
|
2946
|
-
|
|
3288
|
+
.flight-col {
|
|
3289
|
+
display: none;
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
@media (max-width: 640px) {
|
|
3293
|
+
.body {
|
|
3294
|
+
grid-template-columns: 1fr;
|
|
2947
3295
|
}
|
|
2948
3296
|
}
|
|
2949
3297
|
</style>
|
|
@@ -2957,6 +3305,19 @@ var ui_default = `<!doctype html>
|
|
|
2957
3305
|
</header>
|
|
2958
3306
|
|
|
2959
3307
|
<div class="body">
|
|
3308
|
+
<div class="flight-col">
|
|
3309
|
+
<div class="panel flight-panel">
|
|
3310
|
+
<div class="panel-title">
|
|
3311
|
+
\u{1F6F0}\uFE0F Flight Recorder
|
|
3312
|
+
<span style="font-weight: 400; color: var(--muted); font-size: 11px">live</span>
|
|
3313
|
+
<button class="feed-clear-btn" onclick="clearFeed()">clear</button>
|
|
3314
|
+
</div>
|
|
3315
|
+
<div id="activity-feed">
|
|
3316
|
+
<span class="decisions-empty">Waiting for agent activity\u2026</span>
|
|
3317
|
+
</div>
|
|
3318
|
+
</div>
|
|
3319
|
+
</div>
|
|
3320
|
+
|
|
2960
3321
|
<div class="main">
|
|
2961
3322
|
<div id="warnBanner" class="warning-banner">
|
|
2962
3323
|
\u26A0\uFE0F Auto-start is off \u2014 daemon started manually. Run "node9 daemon stop" to stop it, or
|
|
@@ -3037,6 +3398,11 @@ var ui_default = `<!doctype html>
|
|
|
3037
3398
|
<div id="slackStatusLine" class="slack-status-line">No key saved</div>
|
|
3038
3399
|
</div>
|
|
3039
3400
|
|
|
3401
|
+
<div class="panel">
|
|
3402
|
+
<div class="panel-title">\u{1F6E1}\uFE0F Active Shields</div>
|
|
3403
|
+
<div id="shieldsList"><span class="decisions-empty">Loading\u2026</span></div>
|
|
3404
|
+
</div>
|
|
3405
|
+
|
|
3040
3406
|
<div class="panel">
|
|
3041
3407
|
<div class="panel-title">\u{1F4CB} Persistent Decisions</div>
|
|
3042
3408
|
<div id="decisionsList"><span class="decisions-empty">None yet.</span></div>
|
|
@@ -3082,14 +3448,23 @@ var ui_default = `<!doctype html>
|
|
|
3082
3448
|
|
|
3083
3449
|
function updateDenyButton(id, timestamp) {
|
|
3084
3450
|
const btn = document.querySelector('#c-' + id + ' .btn-deny');
|
|
3451
|
+
const timer = document.querySelector('#timer-' + id);
|
|
3085
3452
|
if (!btn) return;
|
|
3086
3453
|
const elapsed = Date.now() - timestamp;
|
|
3087
3454
|
const remaining = Math.max(0, Math.ceil((autoDenyMs - elapsed) / 1000));
|
|
3088
3455
|
if (remaining <= 0) {
|
|
3089
|
-
btn.textContent = 'Auto-Denying
|
|
3456
|
+
btn.textContent = '\u23F3 Auto-Denying\u2026';
|
|
3090
3457
|
btn.disabled = true;
|
|
3458
|
+
if (timer) {
|
|
3459
|
+
timer.textContent = 'auto-deny';
|
|
3460
|
+
timer.className = 'card-timer urgent';
|
|
3461
|
+
}
|
|
3091
3462
|
} else {
|
|
3092
|
-
btn.textContent = 'Block Action
|
|
3463
|
+
btn.textContent = '\u{1F6AB} Block this Action';
|
|
3464
|
+
if (timer) {
|
|
3465
|
+
timer.textContent = remaining + 's';
|
|
3466
|
+
timer.className = 'card-timer' + (remaining < 15 ? ' urgent' : '');
|
|
3467
|
+
}
|
|
3093
3468
|
setTimeout(() => updateDenyButton(id, timestamp), 1000);
|
|
3094
3469
|
}
|
|
3095
3470
|
}
|
|
@@ -3105,34 +3480,61 @@ var ui_default = `<!doctype html>
|
|
|
3105
3480
|
empty.style.display = requests.size === 0 ? 'block' : 'none';
|
|
3106
3481
|
}
|
|
3107
3482
|
|
|
3108
|
-
function
|
|
3483
|
+
function setCardBusy(card, busy) {
|
|
3484
|
+
if (!card) return;
|
|
3485
|
+
card.querySelectorAll('button').forEach((b) => (b.disabled = busy));
|
|
3486
|
+
card.style.opacity = busy ? '0.5' : '1';
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
function showCardError(card, msg) {
|
|
3490
|
+
if (!card) return;
|
|
3491
|
+
card.style.outline = '2px solid #f87171';
|
|
3492
|
+
let err = card.querySelector('.card-error');
|
|
3493
|
+
if (!err) {
|
|
3494
|
+
err = document.createElement('p');
|
|
3495
|
+
err.className = 'card-error';
|
|
3496
|
+
err.style.cssText = 'color:#f87171;font-size:11px;margin:6px 0 0;';
|
|
3497
|
+
card.appendChild(err);
|
|
3498
|
+
}
|
|
3499
|
+
err.textContent = '\u26A0 ' + msg + ' \u2014 please try again or refresh.';
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
async function sendDecision(id, decision, persist) {
|
|
3109
3503
|
const card = document.getElementById('c-' + id);
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3504
|
+
setCardBusy(card, true);
|
|
3505
|
+
try {
|
|
3506
|
+
const res = await fetch('/decision/' + id, {
|
|
3507
|
+
method: 'POST',
|
|
3508
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3509
|
+
body: JSON.stringify({ decision, persist: !!persist }),
|
|
3510
|
+
});
|
|
3511
|
+
if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
|
|
3117
3512
|
card?.remove();
|
|
3118
3513
|
requests.delete(id);
|
|
3119
3514
|
refresh();
|
|
3120
|
-
}
|
|
3515
|
+
} catch (err) {
|
|
3516
|
+
setCardBusy(card, false);
|
|
3517
|
+
showCardError(card, err.message || 'Network error');
|
|
3518
|
+
}
|
|
3121
3519
|
}
|
|
3122
3520
|
|
|
3123
|
-
function sendTrust(id, duration) {
|
|
3521
|
+
async function sendTrust(id, duration) {
|
|
3124
3522
|
const card = document.getElementById('c-' + id);
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3523
|
+
setCardBusy(card, true);
|
|
3524
|
+
try {
|
|
3525
|
+
const res = await fetch('/decision/' + id, {
|
|
3526
|
+
method: 'POST',
|
|
3527
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3528
|
+
body: JSON.stringify({ decision: 'trust', trustDuration: duration }),
|
|
3529
|
+
});
|
|
3530
|
+
if (!res.ok) throw new Error('Request failed (HTTP ' + res.status + ')');
|
|
3132
3531
|
card?.remove();
|
|
3133
3532
|
requests.delete(id);
|
|
3134
3533
|
refresh();
|
|
3135
|
-
}
|
|
3534
|
+
} catch (err) {
|
|
3535
|
+
setCardBusy(card, false);
|
|
3536
|
+
showCardError(card, err.message || 'Network error');
|
|
3537
|
+
}
|
|
3136
3538
|
}
|
|
3137
3539
|
|
|
3138
3540
|
function renderPayload(req) {
|
|
@@ -3183,16 +3585,21 @@ var ui_default = `<!doctype html>
|
|
|
3183
3585
|
const mcpLabel = req.mcpServer ? esc(req.mcpServer) : null;
|
|
3184
3586
|
const dis = isSlack ? 'disabled' : '';
|
|
3185
3587
|
card.innerHTML = \`
|
|
3588
|
+
<div class="card-header">
|
|
3589
|
+
<span class="card-header-icon">\${isSlack ? '\u26A1' : '\u26A0\uFE0F'}</span>
|
|
3590
|
+
<span class="card-header-title">\${isSlack ? 'Awaiting Cloud Approval' : 'Action Required'}</span>
|
|
3591
|
+
<span class="card-timer" id="timer-\${req.id}">\${autoDenyMs > 0 ? Math.ceil(autoDenyMs / 1000) + 's' : ''}</span>
|
|
3592
|
+
</div>
|
|
3186
3593
|
<div class="source-row">
|
|
3187
3594
|
<span class="agent-badge">\${agentLabel}</span>
|
|
3188
3595
|
\${mcpLabel ? \`<span class="source-arrow">\u2192</span><span class="mcp-badge">mcp::\${mcpLabel}</span>\` : ''}
|
|
3189
3596
|
</div>
|
|
3190
3597
|
<div class="tool-chip">\${esc(req.toolName)}</div>
|
|
3191
|
-
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting
|
|
3598
|
+
\${isSlack ? '<div class="slack-indicator">\u26A1 Awaiting Cloud approval \u2014 view only</div>' : ''}
|
|
3192
3599
|
\${renderPayload(req)}
|
|
3193
3600
|
<div class="actions" id="act-\${req.id}">
|
|
3194
|
-
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}
|
|
3195
|
-
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}
|
|
3601
|
+
<button class="btn-allow" onclick="sendDecision('\${req.id}','allow',false)" \${dis}>\u2705 Allow this Action</button>
|
|
3602
|
+
<button class="btn-deny" onclick="sendDecision('\${req.id}','deny',false)" \${dis}>\u{1F6AB} Block this Action</button>
|
|
3196
3603
|
<div class="trust-row\${trustEnabled ? ' show' : ''}" id="tr-\${req.id}">
|
|
3197
3604
|
<button class="btn-trust" onclick="sendTrust('\${req.id}','30m')" \${dis}>\u23F1 Trust 30m</button>
|
|
3198
3605
|
<button class="btn-trust" onclick="sendTrust('\${req.id}','1h')" \${dis}>\u23F1 Trust 1h</button>
|
|
@@ -3252,9 +3659,84 @@ var ui_default = `<!doctype html>
|
|
|
3252
3659
|
ev.addEventListener('slack-status', (e) => {
|
|
3253
3660
|
applySlackStatus(JSON.parse(e.data));
|
|
3254
3661
|
});
|
|
3662
|
+
ev.addEventListener('shields-status', (e) => {
|
|
3663
|
+
renderShields(JSON.parse(e.data).shields);
|
|
3664
|
+
});
|
|
3665
|
+
|
|
3666
|
+
// \u2500\u2500 Flight Recorder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3667
|
+
ev.addEventListener('activity', (e) => {
|
|
3668
|
+
const data = JSON.parse(e.data);
|
|
3669
|
+
const feed = document.getElementById('activity-feed');
|
|
3670
|
+
// Remove placeholder on first item
|
|
3671
|
+
const placeholder = feed.querySelector('.decisions-empty');
|
|
3672
|
+
if (placeholder) placeholder.remove();
|
|
3673
|
+
|
|
3674
|
+
const time = new Date(data.ts).toLocaleTimeString([], {
|
|
3675
|
+
hour12: false,
|
|
3676
|
+
hour: '2-digit',
|
|
3677
|
+
minute: '2-digit',
|
|
3678
|
+
second: '2-digit',
|
|
3679
|
+
});
|
|
3680
|
+
const icon = frIcon(data.tool);
|
|
3681
|
+
const argsStr = JSON.stringify(data.args ?? {});
|
|
3682
|
+
const argsPreview = esc(argsStr.length > 120 ? argsStr.slice(0, 120) + '\u2026' : argsStr);
|
|
3683
|
+
|
|
3684
|
+
const row = document.createElement('div');
|
|
3685
|
+
row.className = 'feed-row';
|
|
3686
|
+
row.id = 'fr-' + data.id;
|
|
3687
|
+
row.innerHTML = \`
|
|
3688
|
+
<span class="feed-ts">\${time}</span>
|
|
3689
|
+
<span class="feed-icon">\${icon}</span>
|
|
3690
|
+
<span class="feed-content"><strong>\${esc(data.tool)}</strong><span class="feed-args">\${argsPreview}</span></span>
|
|
3691
|
+
<span class="feed-badge fr-pending">\u25CF</span>
|
|
3692
|
+
\`;
|
|
3693
|
+
feed.prepend(row);
|
|
3694
|
+
if (feed.children.length > 100) feed.lastChild.remove();
|
|
3695
|
+
});
|
|
3696
|
+
|
|
3697
|
+
ev.addEventListener('activity-result', (e) => {
|
|
3698
|
+
const { id, status, label } = JSON.parse(e.data);
|
|
3699
|
+
const row = document.getElementById('fr-' + id);
|
|
3700
|
+
if (!row) return;
|
|
3701
|
+
const badge = row.querySelector('.feed-badge');
|
|
3702
|
+
if (status === 'allow') {
|
|
3703
|
+
badge.textContent = 'ALLOW';
|
|
3704
|
+
badge.className = 'feed-badge fr-allow';
|
|
3705
|
+
} else if (status === 'dlp') {
|
|
3706
|
+
badge.textContent = '\u{1F6E1}\uFE0F DLP';
|
|
3707
|
+
badge.className = 'feed-badge fr-dlp';
|
|
3708
|
+
row.classList.add('fr-dlp-row');
|
|
3709
|
+
} else {
|
|
3710
|
+
badge.textContent = 'BLOCK';
|
|
3711
|
+
badge.className = 'feed-badge fr-block';
|
|
3712
|
+
}
|
|
3713
|
+
});
|
|
3255
3714
|
}
|
|
3256
3715
|
connect();
|
|
3257
3716
|
|
|
3717
|
+
const FR_ICONS = {
|
|
3718
|
+
bash: '\u{1F4BB}',
|
|
3719
|
+
read: '\u{1F4D6}',
|
|
3720
|
+
edit: '\u270F\uFE0F',
|
|
3721
|
+
write: '\u270F\uFE0F',
|
|
3722
|
+
glob: '\u{1F4C2}',
|
|
3723
|
+
grep: '\u{1F50D}',
|
|
3724
|
+
agent: '\u{1F916}',
|
|
3725
|
+
search: '\u{1F50D}',
|
|
3726
|
+
sql: '\u{1F5C4}\uFE0F',
|
|
3727
|
+
query: '\u{1F5C4}\uFE0F',
|
|
3728
|
+
list: '\u{1F4C2}',
|
|
3729
|
+
delete: '\u{1F5D1}\uFE0F',
|
|
3730
|
+
web: '\u{1F310}',
|
|
3731
|
+
};
|
|
3732
|
+
function frIcon(tool) {
|
|
3733
|
+
const t = (tool || '').toLowerCase();
|
|
3734
|
+
for (const [k, v] of Object.entries(FR_ICONS)) {
|
|
3735
|
+
if (t.includes(k)) return v;
|
|
3736
|
+
}
|
|
3737
|
+
return '\u{1F6E0}\uFE0F';
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3258
3740
|
function saveSetting(key, value) {
|
|
3259
3741
|
fetch('/settings', {
|
|
3260
3742
|
method: 'POST',
|
|
@@ -3344,6 +3826,49 @@ var ui_default = `<!doctype html>
|
|
|
3344
3826
|
}
|
|
3345
3827
|
}
|
|
3346
3828
|
|
|
3829
|
+
function clearFeed() {
|
|
3830
|
+
const feed = document.getElementById('activity-feed');
|
|
3831
|
+
feed.innerHTML = '<span class="decisions-empty">Feed cleared.</span>';
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
function renderShields(shields) {
|
|
3835
|
+
const list = document.getElementById('shieldsList');
|
|
3836
|
+
if (!shields || shields.length === 0) {
|
|
3837
|
+
list.innerHTML = '<span class="decisions-empty">No shields available.</span>';
|
|
3838
|
+
return;
|
|
3839
|
+
}
|
|
3840
|
+
list.innerHTML = shields
|
|
3841
|
+
.map(
|
|
3842
|
+
(s) => \`
|
|
3843
|
+
<div class="shield-row">
|
|
3844
|
+
<div class="shield-info">
|
|
3845
|
+
<div class="shield-name">\${esc(s.name)}</div>
|
|
3846
|
+
<div class="shield-desc">\${esc(s.description)}</div>
|
|
3847
|
+
</div>
|
|
3848
|
+
<label class="toggle">
|
|
3849
|
+
<input type="checkbox" \${s.active ? 'checked' : ''}
|
|
3850
|
+
onchange="toggleShield('\${esc(s.name)}', this.checked)" />
|
|
3851
|
+
<span class="slider"></span>
|
|
3852
|
+
</label>
|
|
3853
|
+
</div>
|
|
3854
|
+
\`
|
|
3855
|
+
)
|
|
3856
|
+
.join('');
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
function toggleShield(name, active) {
|
|
3860
|
+
fetch('/shields', {
|
|
3861
|
+
method: 'POST',
|
|
3862
|
+
headers: { 'Content-Type': 'application/json', 'X-Node9-Token': CSRF_TOKEN },
|
|
3863
|
+
body: JSON.stringify({ name, active }),
|
|
3864
|
+
}).catch(() => {});
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
fetch('/shields')
|
|
3868
|
+
.then((r) => r.json())
|
|
3869
|
+
.then(({ shields }) => renderShields(shields))
|
|
3870
|
+
.catch(() => {});
|
|
3871
|
+
|
|
3347
3872
|
function renderDecisions(decisions) {
|
|
3348
3873
|
const dl = document.getElementById('decisionsList');
|
|
3349
3874
|
const entries = Object.entries(decisions);
|
|
@@ -3390,40 +3915,33 @@ var ui_default = `<!doctype html>
|
|
|
3390
3915
|
</body>
|
|
3391
3916
|
</html>
|
|
3392
3917
|
`;
|
|
3918
|
+
}
|
|
3919
|
+
});
|
|
3393
3920
|
|
|
3394
3921
|
// src/daemon/ui.ts
|
|
3395
|
-
var UI_HTML_TEMPLATE
|
|
3922
|
+
var UI_HTML_TEMPLATE;
|
|
3923
|
+
var init_ui2 = __esm({
|
|
3924
|
+
"src/daemon/ui.ts"() {
|
|
3925
|
+
"use strict";
|
|
3926
|
+
init_ui();
|
|
3927
|
+
UI_HTML_TEMPLATE = ui_default;
|
|
3928
|
+
}
|
|
3929
|
+
});
|
|
3396
3930
|
|
|
3397
3931
|
// src/daemon/index.ts
|
|
3398
|
-
var import_http = __toESM(require("http"));
|
|
3399
|
-
var import_fs3 = __toESM(require("fs"));
|
|
3400
|
-
var import_path5 = __toESM(require("path"));
|
|
3401
|
-
var import_os3 = __toESM(require("os"));
|
|
3402
|
-
var import_child_process2 = require("child_process");
|
|
3403
|
-
var import_crypto = require("crypto");
|
|
3404
|
-
var import_chalk4 = __toESM(require("chalk"));
|
|
3405
|
-
var DAEMON_PORT2 = 7391;
|
|
3406
|
-
var DAEMON_HOST2 = "127.0.0.1";
|
|
3407
|
-
var homeDir = import_os3.default.homedir();
|
|
3408
|
-
var DAEMON_PID_FILE = import_path5.default.join(homeDir, ".node9", "daemon.pid");
|
|
3409
|
-
var DECISIONS_FILE = import_path5.default.join(homeDir, ".node9", "decisions.json");
|
|
3410
|
-
var GLOBAL_CONFIG_FILE = import_path5.default.join(homeDir, ".node9", "config.json");
|
|
3411
|
-
var CREDENTIALS_FILE = import_path5.default.join(homeDir, ".node9", "credentials.json");
|
|
3412
|
-
var AUDIT_LOG_FILE = import_path5.default.join(homeDir, ".node9", "audit.log");
|
|
3413
|
-
var TRUST_FILE2 = import_path5.default.join(homeDir, ".node9", "trust.json");
|
|
3414
3932
|
function atomicWriteSync2(filePath, data, options) {
|
|
3415
|
-
const dir =
|
|
3416
|
-
if (!
|
|
3417
|
-
const tmpPath = `${filePath}.${(0,
|
|
3418
|
-
|
|
3419
|
-
|
|
3933
|
+
const dir = import_path6.default.dirname(filePath);
|
|
3934
|
+
if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
3935
|
+
const tmpPath = `${filePath}.${(0, import_crypto3.randomUUID)()}.tmp`;
|
|
3936
|
+
import_fs4.default.writeFileSync(tmpPath, data, options);
|
|
3937
|
+
import_fs4.default.renameSync(tmpPath, filePath);
|
|
3420
3938
|
}
|
|
3421
3939
|
function writeTrustEntry(toolName, durationMs) {
|
|
3422
3940
|
try {
|
|
3423
3941
|
let trust = { entries: [] };
|
|
3424
3942
|
try {
|
|
3425
|
-
if (
|
|
3426
|
-
trust = JSON.parse(
|
|
3943
|
+
if (import_fs4.default.existsSync(TRUST_FILE2))
|
|
3944
|
+
trust = JSON.parse(import_fs4.default.readFileSync(TRUST_FILE2, "utf-8"));
|
|
3427
3945
|
} catch {
|
|
3428
3946
|
}
|
|
3429
3947
|
trust.entries = trust.entries.filter((e) => e.tool !== toolName && e.expiry > Date.now());
|
|
@@ -3432,12 +3950,6 @@ function writeTrustEntry(toolName, durationMs) {
|
|
|
3432
3950
|
} catch {
|
|
3433
3951
|
}
|
|
3434
3952
|
}
|
|
3435
|
-
var TRUST_DURATIONS = {
|
|
3436
|
-
"30m": 30 * 6e4,
|
|
3437
|
-
"1h": 60 * 6e4,
|
|
3438
|
-
"2h": 2 * 60 * 6e4
|
|
3439
|
-
};
|
|
3440
|
-
var SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
3441
3953
|
function redactArgs(value) {
|
|
3442
3954
|
if (!value || typeof value !== "object") return value;
|
|
3443
3955
|
if (Array.isArray(value)) return value.map(redactArgs);
|
|
@@ -3456,41 +3968,39 @@ function appendAuditLog(data) {
|
|
|
3456
3968
|
decision: data.decision,
|
|
3457
3969
|
source: "daemon"
|
|
3458
3970
|
};
|
|
3459
|
-
const dir =
|
|
3460
|
-
if (!
|
|
3461
|
-
|
|
3971
|
+
const dir = import_path6.default.dirname(AUDIT_LOG_FILE);
|
|
3972
|
+
if (!import_fs4.default.existsSync(dir)) import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
3973
|
+
import_fs4.default.appendFileSync(AUDIT_LOG_FILE, JSON.stringify(entry) + "\n");
|
|
3462
3974
|
} catch {
|
|
3463
3975
|
}
|
|
3464
3976
|
}
|
|
3465
3977
|
function getAuditHistory(limit = 20) {
|
|
3466
3978
|
try {
|
|
3467
|
-
if (!
|
|
3468
|
-
const lines =
|
|
3979
|
+
if (!import_fs4.default.existsSync(AUDIT_LOG_FILE)) return [];
|
|
3980
|
+
const lines = import_fs4.default.readFileSync(AUDIT_LOG_FILE, "utf-8").trim().split("\n");
|
|
3469
3981
|
if (lines.length === 1 && lines[0] === "") return [];
|
|
3470
3982
|
return lines.slice(-limit).map((l) => JSON.parse(l)).reverse();
|
|
3471
3983
|
} catch {
|
|
3472
3984
|
return [];
|
|
3473
3985
|
}
|
|
3474
3986
|
}
|
|
3475
|
-
var AUTO_DENY_MS = 12e4;
|
|
3476
3987
|
function getOrgName() {
|
|
3477
3988
|
try {
|
|
3478
|
-
if (
|
|
3989
|
+
if (import_fs4.default.existsSync(CREDENTIALS_FILE)) {
|
|
3479
3990
|
return "Node9 Cloud";
|
|
3480
3991
|
}
|
|
3481
3992
|
} catch {
|
|
3482
3993
|
}
|
|
3483
3994
|
return null;
|
|
3484
3995
|
}
|
|
3485
|
-
var autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
3486
3996
|
function hasStoredSlackKey() {
|
|
3487
|
-
return
|
|
3997
|
+
return import_fs4.default.existsSync(CREDENTIALS_FILE);
|
|
3488
3998
|
}
|
|
3489
3999
|
function writeGlobalSetting(key, value) {
|
|
3490
4000
|
let config = {};
|
|
3491
4001
|
try {
|
|
3492
|
-
if (
|
|
3493
|
-
config = JSON.parse(
|
|
4002
|
+
if (import_fs4.default.existsSync(GLOBAL_CONFIG_FILE)) {
|
|
4003
|
+
config = JSON.parse(import_fs4.default.readFileSync(GLOBAL_CONFIG_FILE, "utf-8"));
|
|
3494
4004
|
}
|
|
3495
4005
|
} catch {
|
|
3496
4006
|
}
|
|
@@ -3498,11 +4008,6 @@ function writeGlobalSetting(key, value) {
|
|
|
3498
4008
|
config.settings[key] = value;
|
|
3499
4009
|
atomicWriteSync2(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3500
4010
|
}
|
|
3501
|
-
var pending = /* @__PURE__ */ new Map();
|
|
3502
|
-
var sseClients = /* @__PURE__ */ new Set();
|
|
3503
|
-
var abandonTimer = null;
|
|
3504
|
-
var daemonServer = null;
|
|
3505
|
-
var hadBrowserClient = false;
|
|
3506
4011
|
function abandonPending() {
|
|
3507
4012
|
abandonTimer = null;
|
|
3508
4013
|
pending.forEach((entry, id) => {
|
|
@@ -3514,7 +4019,7 @@ function abandonPending() {
|
|
|
3514
4019
|
});
|
|
3515
4020
|
if (autoStarted) {
|
|
3516
4021
|
try {
|
|
3517
|
-
|
|
4022
|
+
import_fs4.default.unlinkSync(DAEMON_PID_FILE);
|
|
3518
4023
|
} catch {
|
|
3519
4024
|
}
|
|
3520
4025
|
setTimeout(() => {
|
|
@@ -3524,6 +4029,18 @@ function abandonPending() {
|
|
|
3524
4029
|
}
|
|
3525
4030
|
}
|
|
3526
4031
|
function broadcast(event, data) {
|
|
4032
|
+
if (event === "activity") {
|
|
4033
|
+
activityRing.push({ event, data });
|
|
4034
|
+
if (activityRing.length > ACTIVITY_RING_SIZE) activityRing.shift();
|
|
4035
|
+
} else if (event === "activity-result") {
|
|
4036
|
+
const { id, status, label } = data;
|
|
4037
|
+
for (let i = activityRing.length - 1; i >= 0; i--) {
|
|
4038
|
+
if (activityRing[i].data.id === id) {
|
|
4039
|
+
Object.assign(activityRing[i].data, { status, label });
|
|
4040
|
+
break;
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
3527
4044
|
const msg = `event: ${event}
|
|
3528
4045
|
data: ${JSON.stringify(data)}
|
|
3529
4046
|
|
|
@@ -3552,8 +4069,8 @@ function readBody(req) {
|
|
|
3552
4069
|
}
|
|
3553
4070
|
function readPersistentDecisions() {
|
|
3554
4071
|
try {
|
|
3555
|
-
if (
|
|
3556
|
-
return JSON.parse(
|
|
4072
|
+
if (import_fs4.default.existsSync(DECISIONS_FILE)) {
|
|
4073
|
+
return JSON.parse(import_fs4.default.readFileSync(DECISIONS_FILE, "utf-8"));
|
|
3557
4074
|
}
|
|
3558
4075
|
} catch {
|
|
3559
4076
|
}
|
|
@@ -3569,18 +4086,20 @@ function writePersistentDecision(toolName, decision) {
|
|
|
3569
4086
|
}
|
|
3570
4087
|
}
|
|
3571
4088
|
function startDaemon() {
|
|
3572
|
-
const csrfToken = (0,
|
|
3573
|
-
const internalToken = (0,
|
|
4089
|
+
const csrfToken = (0, import_crypto3.randomUUID)();
|
|
4090
|
+
const internalToken = (0, import_crypto3.randomUUID)();
|
|
3574
4091
|
const UI_HTML = UI_HTML_TEMPLATE.replace("{{CSRF_TOKEN}}", csrfToken);
|
|
3575
4092
|
const validToken = (req) => req.headers["x-node9-token"] === csrfToken;
|
|
3576
4093
|
const IDLE_TIMEOUT_MS = 12 * 60 * 60 * 1e3;
|
|
4094
|
+
const watchMode = process.env.NODE9_WATCH_MODE === "1";
|
|
3577
4095
|
let idleTimer;
|
|
3578
4096
|
function resetIdleTimer() {
|
|
4097
|
+
if (watchMode) return;
|
|
3579
4098
|
if (idleTimer) clearTimeout(idleTimer);
|
|
3580
4099
|
idleTimer = setTimeout(() => {
|
|
3581
4100
|
if (autoStarted) {
|
|
3582
4101
|
try {
|
|
3583
|
-
|
|
4102
|
+
import_fs4.default.unlinkSync(DAEMON_PID_FILE);
|
|
3584
4103
|
} catch {
|
|
3585
4104
|
}
|
|
3586
4105
|
}
|
|
@@ -3630,6 +4149,12 @@ data: ${JSON.stringify({
|
|
|
3630
4149
|
data: ${JSON.stringify(readPersistentDecisions())}
|
|
3631
4150
|
|
|
3632
4151
|
`);
|
|
4152
|
+
for (const item of activityRing) {
|
|
4153
|
+
res.write(`event: ${item.event}
|
|
4154
|
+
data: ${JSON.stringify(item.data)}
|
|
4155
|
+
|
|
4156
|
+
`);
|
|
4157
|
+
}
|
|
3633
4158
|
return req.on("close", () => {
|
|
3634
4159
|
sseClients.delete(res);
|
|
3635
4160
|
if (sseClients.size === 0 && pending.size > 0) {
|
|
@@ -3649,9 +4174,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3649
4174
|
slackDelegated = false,
|
|
3650
4175
|
agent,
|
|
3651
4176
|
mcpServer,
|
|
3652
|
-
riskMetadata
|
|
4177
|
+
riskMetadata,
|
|
4178
|
+
fromCLI = false,
|
|
4179
|
+
activityId
|
|
3653
4180
|
} = JSON.parse(body);
|
|
3654
|
-
const id = (0,
|
|
4181
|
+
const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto3.randomUUID)();
|
|
3655
4182
|
const entry = {
|
|
3656
4183
|
id,
|
|
3657
4184
|
toolName,
|
|
@@ -3682,6 +4209,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3682
4209
|
}, AUTO_DENY_MS)
|
|
3683
4210
|
};
|
|
3684
4211
|
pending.set(id, entry);
|
|
4212
|
+
if (!fromCLI) {
|
|
4213
|
+
broadcast("activity", {
|
|
4214
|
+
id,
|
|
4215
|
+
ts: entry.timestamp,
|
|
4216
|
+
tool: toolName,
|
|
4217
|
+
args: redactArgs(args),
|
|
4218
|
+
status: "pending"
|
|
4219
|
+
});
|
|
4220
|
+
}
|
|
3685
4221
|
const browserEnabled = getConfig().settings.approvers?.browser !== false;
|
|
3686
4222
|
if (browserEnabled) {
|
|
3687
4223
|
broadcast("add", {
|
|
@@ -3711,6 +4247,11 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3711
4247
|
const e = pending.get(id);
|
|
3712
4248
|
if (!e) return;
|
|
3713
4249
|
if (result.noApprovalMechanism) return;
|
|
4250
|
+
broadcast("activity-result", {
|
|
4251
|
+
id,
|
|
4252
|
+
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
|
|
4253
|
+
label: result.blockedByLabel
|
|
4254
|
+
});
|
|
3714
4255
|
clearTimeout(e.timer);
|
|
3715
4256
|
const decision = result.approved ? "allow" : "deny";
|
|
3716
4257
|
appendAuditLog({ toolName: e.toolName, args: e.args, decision });
|
|
@@ -3745,8 +4286,8 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3745
4286
|
const entry = pending.get(id);
|
|
3746
4287
|
if (!entry) return res.writeHead(404).end();
|
|
3747
4288
|
if (entry.earlyDecision) {
|
|
4289
|
+
clearTimeout(entry.timer);
|
|
3748
4290
|
pending.delete(id);
|
|
3749
|
-
broadcast("remove", { id });
|
|
3750
4291
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3751
4292
|
const body = { decision: entry.earlyDecision };
|
|
3752
4293
|
if (entry.earlyReason) body.reason = entry.earlyReason;
|
|
@@ -3776,10 +4317,15 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3776
4317
|
decision: `trust:${trustDuration}`
|
|
3777
4318
|
});
|
|
3778
4319
|
clearTimeout(entry.timer);
|
|
3779
|
-
if (entry.waiter)
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
4320
|
+
if (entry.waiter) {
|
|
4321
|
+
entry.waiter("allow");
|
|
4322
|
+
pending.delete(id);
|
|
4323
|
+
broadcast("remove", { id });
|
|
4324
|
+
} else {
|
|
4325
|
+
entry.earlyDecision = "allow";
|
|
4326
|
+
broadcast("remove", { id });
|
|
4327
|
+
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
4328
|
+
}
|
|
3783
4329
|
res.writeHead(200);
|
|
3784
4330
|
return res.end(JSON.stringify({ ok: true }));
|
|
3785
4331
|
}
|
|
@@ -3791,13 +4337,16 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3791
4337
|
decision: resolvedDecision
|
|
3792
4338
|
});
|
|
3793
4339
|
clearTimeout(entry.timer);
|
|
3794
|
-
if (entry.waiter)
|
|
3795
|
-
|
|
4340
|
+
if (entry.waiter) {
|
|
4341
|
+
entry.waiter(resolvedDecision, reason);
|
|
4342
|
+
pending.delete(id);
|
|
4343
|
+
broadcast("remove", { id });
|
|
4344
|
+
} else {
|
|
3796
4345
|
entry.earlyDecision = resolvedDecision;
|
|
3797
4346
|
entry.earlyReason = reason;
|
|
4347
|
+
broadcast("remove", { id });
|
|
4348
|
+
entry.timer = setTimeout(() => pending.delete(id), 3e4);
|
|
3798
4349
|
}
|
|
3799
|
-
pending.delete(id);
|
|
3800
|
-
broadcast("remove", { id });
|
|
3801
4350
|
res.writeHead(200);
|
|
3802
4351
|
return res.end(JSON.stringify({ ok: true }));
|
|
3803
4352
|
} catch {
|
|
@@ -3888,99 +4437,666 @@ data: ${JSON.stringify(readPersistentDecisions())}
|
|
|
3888
4437
|
res.writeHead(400).end();
|
|
3889
4438
|
}
|
|
3890
4439
|
}
|
|
3891
|
-
if (req.method === "GET" && pathname === "/audit") {
|
|
3892
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3893
|
-
return res.end(JSON.stringify(getAuditHistory()));
|
|
4440
|
+
if (req.method === "GET" && pathname === "/audit") {
|
|
4441
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4442
|
+
return res.end(JSON.stringify(getAuditHistory()));
|
|
4443
|
+
}
|
|
4444
|
+
if (req.method === "GET" && pathname === "/shields") {
|
|
4445
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4446
|
+
const active = readActiveShields();
|
|
4447
|
+
const shields = Object.values(SHIELDS).map((s) => ({
|
|
4448
|
+
name: s.name,
|
|
4449
|
+
description: s.description,
|
|
4450
|
+
active: active.includes(s.name)
|
|
4451
|
+
}));
|
|
4452
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4453
|
+
return res.end(JSON.stringify({ shields }));
|
|
4454
|
+
}
|
|
4455
|
+
if (req.method === "POST" && pathname === "/shields") {
|
|
4456
|
+
if (!validToken(req)) return res.writeHead(403).end();
|
|
4457
|
+
try {
|
|
4458
|
+
const { name, active } = JSON.parse(await readBody(req));
|
|
4459
|
+
if (!SHIELDS[name]) return res.writeHead(400).end();
|
|
4460
|
+
const current = readActiveShields();
|
|
4461
|
+
const updated = active ? [.../* @__PURE__ */ new Set([...current, name])] : current.filter((n) => n !== name);
|
|
4462
|
+
writeActiveShields(updated);
|
|
4463
|
+
_resetConfigCache();
|
|
4464
|
+
const shieldsPayload = Object.values(SHIELDS).map((s) => ({
|
|
4465
|
+
name: s.name,
|
|
4466
|
+
description: s.description,
|
|
4467
|
+
active: updated.includes(s.name)
|
|
4468
|
+
}));
|
|
4469
|
+
broadcast("shields-status", { shields: shieldsPayload });
|
|
4470
|
+
res.writeHead(200);
|
|
4471
|
+
return res.end(JSON.stringify({ ok: true }));
|
|
4472
|
+
} catch {
|
|
4473
|
+
res.writeHead(400).end();
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4476
|
+
res.writeHead(404).end();
|
|
4477
|
+
});
|
|
4478
|
+
daemonServer = server;
|
|
4479
|
+
server.on("error", (e) => {
|
|
4480
|
+
if (e.code === "EADDRINUSE") {
|
|
4481
|
+
try {
|
|
4482
|
+
if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
|
|
4483
|
+
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4484
|
+
process.kill(pid, 0);
|
|
4485
|
+
return process.exit(0);
|
|
4486
|
+
}
|
|
4487
|
+
} catch {
|
|
4488
|
+
try {
|
|
4489
|
+
import_fs4.default.unlinkSync(DAEMON_PID_FILE);
|
|
4490
|
+
} catch {
|
|
4491
|
+
}
|
|
4492
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2);
|
|
4493
|
+
return;
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
console.error(import_chalk4.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
|
|
4497
|
+
process.exit(1);
|
|
4498
|
+
});
|
|
4499
|
+
server.listen(DAEMON_PORT2, DAEMON_HOST2, () => {
|
|
4500
|
+
atomicWriteSync2(
|
|
4501
|
+
DAEMON_PID_FILE,
|
|
4502
|
+
JSON.stringify({ pid: process.pid, port: DAEMON_PORT2, internalToken, autoStarted }),
|
|
4503
|
+
{ mode: 384 }
|
|
4504
|
+
);
|
|
4505
|
+
console.log(import_chalk4.default.green(`\u{1F6E1}\uFE0F Node9 Guard LIVE: http://127.0.0.1:${DAEMON_PORT2}`));
|
|
4506
|
+
});
|
|
4507
|
+
if (watchMode) {
|
|
4508
|
+
console.log(import_chalk4.default.cyan("\u{1F6F0}\uFE0F Flight Recorder active \u2014 daemon will not idle-timeout"));
|
|
4509
|
+
}
|
|
4510
|
+
try {
|
|
4511
|
+
import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4512
|
+
} catch {
|
|
4513
|
+
}
|
|
4514
|
+
const ACTIVITY_MAX_BYTES = 1024 * 1024;
|
|
4515
|
+
const unixServer = import_net2.default.createServer((socket) => {
|
|
4516
|
+
const chunks = [];
|
|
4517
|
+
let bytesReceived = 0;
|
|
4518
|
+
socket.on("data", (chunk) => {
|
|
4519
|
+
bytesReceived += chunk.length;
|
|
4520
|
+
if (bytesReceived > ACTIVITY_MAX_BYTES) {
|
|
4521
|
+
socket.destroy();
|
|
4522
|
+
return;
|
|
4523
|
+
}
|
|
4524
|
+
chunks.push(chunk);
|
|
4525
|
+
});
|
|
4526
|
+
socket.on("end", () => {
|
|
4527
|
+
try {
|
|
4528
|
+
const data = JSON.parse(Buffer.concat(chunks).toString());
|
|
4529
|
+
if (data.status === "pending") {
|
|
4530
|
+
broadcast("activity", {
|
|
4531
|
+
id: data.id,
|
|
4532
|
+
ts: data.ts,
|
|
4533
|
+
tool: data.tool,
|
|
4534
|
+
args: redactArgs(data.args),
|
|
4535
|
+
status: "pending"
|
|
4536
|
+
});
|
|
4537
|
+
} else {
|
|
4538
|
+
broadcast("activity-result", {
|
|
4539
|
+
id: data.id,
|
|
4540
|
+
status: data.status,
|
|
4541
|
+
label: data.label
|
|
4542
|
+
});
|
|
4543
|
+
}
|
|
4544
|
+
} catch {
|
|
4545
|
+
}
|
|
4546
|
+
});
|
|
4547
|
+
socket.on("error", () => {
|
|
4548
|
+
});
|
|
4549
|
+
});
|
|
4550
|
+
unixServer.listen(ACTIVITY_SOCKET_PATH2);
|
|
4551
|
+
process.on("exit", () => {
|
|
4552
|
+
try {
|
|
4553
|
+
import_fs4.default.unlinkSync(ACTIVITY_SOCKET_PATH2);
|
|
4554
|
+
} catch {
|
|
4555
|
+
}
|
|
4556
|
+
});
|
|
4557
|
+
}
|
|
4558
|
+
function stopDaemon() {
|
|
4559
|
+
if (!import_fs4.default.existsSync(DAEMON_PID_FILE)) return console.log(import_chalk4.default.yellow("Not running."));
|
|
4560
|
+
try {
|
|
4561
|
+
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4562
|
+
process.kill(pid, "SIGTERM");
|
|
4563
|
+
console.log(import_chalk4.default.green("\u2705 Stopped."));
|
|
4564
|
+
} catch {
|
|
4565
|
+
console.log(import_chalk4.default.gray("Cleaned up stale PID file."));
|
|
4566
|
+
} finally {
|
|
4567
|
+
try {
|
|
4568
|
+
import_fs4.default.unlinkSync(DAEMON_PID_FILE);
|
|
4569
|
+
} catch {
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
function daemonStatus() {
|
|
4574
|
+
if (!import_fs4.default.existsSync(DAEMON_PID_FILE))
|
|
4575
|
+
return console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
|
|
4576
|
+
try {
|
|
4577
|
+
const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
|
|
4578
|
+
process.kill(pid, 0);
|
|
4579
|
+
console.log(import_chalk4.default.green("Node9 daemon: running"));
|
|
4580
|
+
} catch {
|
|
4581
|
+
console.log(import_chalk4.default.yellow("Node9 daemon: not running (stale PID)"));
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
var import_http, import_net2, import_fs4, import_path6, import_os4, import_child_process2, import_crypto3, import_chalk4, ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
|
|
4585
|
+
var init_daemon = __esm({
|
|
4586
|
+
"src/daemon/index.ts"() {
|
|
4587
|
+
"use strict";
|
|
4588
|
+
init_ui2();
|
|
4589
|
+
import_http = __toESM(require("http"));
|
|
4590
|
+
import_net2 = __toESM(require("net"));
|
|
4591
|
+
import_fs4 = __toESM(require("fs"));
|
|
4592
|
+
import_path6 = __toESM(require("path"));
|
|
4593
|
+
import_os4 = __toESM(require("os"));
|
|
4594
|
+
import_child_process2 = require("child_process");
|
|
4595
|
+
import_crypto3 = require("crypto");
|
|
4596
|
+
import_chalk4 = __toESM(require("chalk"));
|
|
4597
|
+
init_core();
|
|
4598
|
+
init_shields();
|
|
4599
|
+
ACTIVITY_SOCKET_PATH2 = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path6.default.join(import_os4.default.tmpdir(), "node9-activity.sock");
|
|
4600
|
+
DAEMON_PORT2 = 7391;
|
|
4601
|
+
DAEMON_HOST2 = "127.0.0.1";
|
|
4602
|
+
homeDir = import_os4.default.homedir();
|
|
4603
|
+
DAEMON_PID_FILE = import_path6.default.join(homeDir, ".node9", "daemon.pid");
|
|
4604
|
+
DECISIONS_FILE = import_path6.default.join(homeDir, ".node9", "decisions.json");
|
|
4605
|
+
GLOBAL_CONFIG_FILE = import_path6.default.join(homeDir, ".node9", "config.json");
|
|
4606
|
+
CREDENTIALS_FILE = import_path6.default.join(homeDir, ".node9", "credentials.json");
|
|
4607
|
+
AUDIT_LOG_FILE = import_path6.default.join(homeDir, ".node9", "audit.log");
|
|
4608
|
+
TRUST_FILE2 = import_path6.default.join(homeDir, ".node9", "trust.json");
|
|
4609
|
+
TRUST_DURATIONS = {
|
|
4610
|
+
"30m": 30 * 6e4,
|
|
4611
|
+
"1h": 60 * 6e4,
|
|
4612
|
+
"2h": 2 * 60 * 6e4
|
|
4613
|
+
};
|
|
4614
|
+
SECRET_KEY_RE = /password|secret|token|key|apikey|credential|auth/i;
|
|
4615
|
+
AUTO_DENY_MS = 12e4;
|
|
4616
|
+
autoStarted = process.env.NODE9_AUTO_STARTED === "1";
|
|
4617
|
+
pending = /* @__PURE__ */ new Map();
|
|
4618
|
+
sseClients = /* @__PURE__ */ new Set();
|
|
4619
|
+
abandonTimer = null;
|
|
4620
|
+
daemonServer = null;
|
|
4621
|
+
hadBrowserClient = false;
|
|
4622
|
+
ACTIVITY_RING_SIZE = 100;
|
|
4623
|
+
activityRing = [];
|
|
4624
|
+
}
|
|
4625
|
+
});
|
|
4626
|
+
|
|
4627
|
+
// src/tui/tail.ts
|
|
4628
|
+
var tail_exports = {};
|
|
4629
|
+
__export(tail_exports, {
|
|
4630
|
+
startTail: () => startTail
|
|
4631
|
+
});
|
|
4632
|
+
function getIcon(tool) {
|
|
4633
|
+
const t = tool.toLowerCase();
|
|
4634
|
+
for (const [k, v] of Object.entries(ICONS)) {
|
|
4635
|
+
if (t.includes(k)) return v;
|
|
4636
|
+
}
|
|
4637
|
+
return "\u{1F6E0}\uFE0F";
|
|
4638
|
+
}
|
|
4639
|
+
function formatBase(activity) {
|
|
4640
|
+
const time = new Date(activity.ts).toLocaleTimeString([], { hour12: false });
|
|
4641
|
+
const icon = getIcon(activity.tool);
|
|
4642
|
+
const toolName = activity.tool.slice(0, 16).padEnd(16);
|
|
4643
|
+
const argsStr = JSON.stringify(activity.args ?? {}).replace(/\s+/g, " ");
|
|
4644
|
+
const argsPreview = argsStr.length > 70 ? argsStr.slice(0, 70) + "\u2026" : argsStr;
|
|
4645
|
+
return `${import_chalk5.default.gray(time)} ${icon} ${import_chalk5.default.white.bold(toolName)} ${import_chalk5.default.dim(argsPreview)}`;
|
|
4646
|
+
}
|
|
4647
|
+
function renderResult(activity, result) {
|
|
4648
|
+
const base = formatBase(activity);
|
|
4649
|
+
let status;
|
|
4650
|
+
if (result.status === "allow") {
|
|
4651
|
+
status = import_chalk5.default.green("\u2713 ALLOW");
|
|
4652
|
+
} else if (result.status === "dlp") {
|
|
4653
|
+
status = import_chalk5.default.bgRed.white.bold(" \u{1F6E1}\uFE0F DLP ");
|
|
4654
|
+
} else {
|
|
4655
|
+
status = import_chalk5.default.red("\u2717 BLOCK");
|
|
4656
|
+
}
|
|
4657
|
+
if (process.stdout.isTTY) {
|
|
4658
|
+
import_readline.default.clearLine(process.stdout, 0);
|
|
4659
|
+
import_readline.default.cursorTo(process.stdout, 0);
|
|
4660
|
+
}
|
|
4661
|
+
console.log(`${base} ${status}`);
|
|
4662
|
+
}
|
|
4663
|
+
function renderPending(activity) {
|
|
4664
|
+
if (!process.stdout.isTTY) return;
|
|
4665
|
+
process.stdout.write(`${formatBase(activity)} ${import_chalk5.default.yellow("\u25CF \u2026")}\r`);
|
|
4666
|
+
}
|
|
4667
|
+
async function ensureDaemon() {
|
|
4668
|
+
if (import_fs6.default.existsSync(PID_FILE)) {
|
|
4669
|
+
try {
|
|
4670
|
+
const { port } = JSON.parse(import_fs6.default.readFileSync(PID_FILE, "utf-8"));
|
|
4671
|
+
return port;
|
|
4672
|
+
} catch {
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
console.log(import_chalk5.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
4676
|
+
const child = (0, import_child_process4.spawn)(process.execPath, [process.argv[1], "daemon"], {
|
|
4677
|
+
detached: true,
|
|
4678
|
+
stdio: "ignore",
|
|
4679
|
+
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
4680
|
+
});
|
|
4681
|
+
child.unref();
|
|
4682
|
+
for (let i = 0; i < 20; i++) {
|
|
4683
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
4684
|
+
if (!import_fs6.default.existsSync(PID_FILE)) continue;
|
|
4685
|
+
try {
|
|
4686
|
+
const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
|
|
4687
|
+
signal: AbortSignal.timeout(500)
|
|
4688
|
+
});
|
|
4689
|
+
if (res.ok) {
|
|
4690
|
+
const { port } = JSON.parse(import_fs6.default.readFileSync(PID_FILE, "utf-8"));
|
|
4691
|
+
return port;
|
|
4692
|
+
}
|
|
4693
|
+
} catch {
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
console.error(import_chalk5.default.red("\u274C Daemon failed to start. Try: node9 daemon start"));
|
|
4697
|
+
process.exit(1);
|
|
4698
|
+
}
|
|
4699
|
+
async function startTail(options = {}) {
|
|
4700
|
+
const port = await ensureDaemon();
|
|
4701
|
+
const connectionTime = Date.now();
|
|
4702
|
+
const pending2 = /* @__PURE__ */ new Map();
|
|
4703
|
+
console.log(import_chalk5.default.cyan.bold(`
|
|
4704
|
+
\u{1F6F0}\uFE0F Node9 tail `) + import_chalk5.default.dim(`\u2192 localhost:${port}`));
|
|
4705
|
+
if (options.history) {
|
|
4706
|
+
console.log(import_chalk5.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
|
|
4707
|
+
} else {
|
|
4708
|
+
console.log(
|
|
4709
|
+
import_chalk5.default.dim("Showing live events only. Use --history to include past. Press Ctrl+C to exit.\n")
|
|
4710
|
+
);
|
|
4711
|
+
}
|
|
4712
|
+
process.on("SIGINT", () => {
|
|
4713
|
+
if (process.stdout.isTTY) {
|
|
4714
|
+
import_readline.default.clearLine(process.stdout, 0);
|
|
4715
|
+
import_readline.default.cursorTo(process.stdout, 0);
|
|
4716
|
+
}
|
|
4717
|
+
console.log(import_chalk5.default.dim("\n\u{1F6F0}\uFE0F Disconnected."));
|
|
4718
|
+
process.exit(0);
|
|
4719
|
+
});
|
|
4720
|
+
const req = import_http2.default.get(`http://127.0.0.1:${port}/events`, (res) => {
|
|
4721
|
+
if (res.statusCode !== 200) {
|
|
4722
|
+
console.error(import_chalk5.default.red(`Failed to connect: HTTP ${res.statusCode}`));
|
|
4723
|
+
process.exit(1);
|
|
4724
|
+
}
|
|
4725
|
+
let currentEvent = "";
|
|
4726
|
+
let currentData = "";
|
|
4727
|
+
res.on("error", () => {
|
|
4728
|
+
});
|
|
4729
|
+
const rl = import_readline.default.createInterface({ input: res, crlfDelay: Infinity });
|
|
4730
|
+
rl.on("error", () => {
|
|
4731
|
+
});
|
|
4732
|
+
rl.on("line", (line) => {
|
|
4733
|
+
if (line.startsWith("event:")) {
|
|
4734
|
+
currentEvent = line.slice(6).trim();
|
|
4735
|
+
} else if (line.startsWith("data:")) {
|
|
4736
|
+
currentData = line.slice(5).trim();
|
|
4737
|
+
} else if (line === "") {
|
|
4738
|
+
if (currentEvent && currentData) {
|
|
4739
|
+
handleMessage(currentEvent, currentData);
|
|
4740
|
+
}
|
|
4741
|
+
currentEvent = "";
|
|
4742
|
+
currentData = "";
|
|
4743
|
+
}
|
|
4744
|
+
});
|
|
4745
|
+
rl.on("close", () => {
|
|
4746
|
+
if (process.stdout.isTTY) {
|
|
4747
|
+
import_readline.default.clearLine(process.stdout, 0);
|
|
4748
|
+
import_readline.default.cursorTo(process.stdout, 0);
|
|
4749
|
+
}
|
|
4750
|
+
console.log(import_chalk5.default.red("\n\u274C Daemon disconnected."));
|
|
4751
|
+
process.exit(1);
|
|
4752
|
+
});
|
|
4753
|
+
});
|
|
4754
|
+
function handleMessage(event, rawData) {
|
|
4755
|
+
let data;
|
|
4756
|
+
try {
|
|
4757
|
+
data = JSON.parse(rawData);
|
|
4758
|
+
} catch {
|
|
4759
|
+
return;
|
|
4760
|
+
}
|
|
4761
|
+
if (event === "activity") {
|
|
4762
|
+
if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
|
|
4763
|
+
if (data.status && data.status !== "pending") {
|
|
4764
|
+
renderResult(data, data);
|
|
4765
|
+
return;
|
|
4766
|
+
}
|
|
4767
|
+
pending2.set(data.id, data);
|
|
4768
|
+
const slowTool = /bash|shell|query|sql|agent/i.test(data.tool);
|
|
4769
|
+
if (slowTool) renderPending(data);
|
|
4770
|
+
}
|
|
4771
|
+
if (event === "activity-result") {
|
|
4772
|
+
const original = pending2.get(data.id);
|
|
4773
|
+
if (original) {
|
|
4774
|
+
renderResult(original, data);
|
|
4775
|
+
pending2.delete(data.id);
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
req.on("error", (err) => {
|
|
4780
|
+
const msg = err.code === "ECONNREFUSED" ? "Daemon is not running. Start it with: node9 daemon start" : err.message;
|
|
4781
|
+
console.error(import_chalk5.default.red(`
|
|
4782
|
+
\u274C ${msg}`));
|
|
4783
|
+
process.exit(1);
|
|
4784
|
+
});
|
|
4785
|
+
}
|
|
4786
|
+
var import_http2, import_chalk5, import_fs6, import_os6, import_path8, import_readline, import_child_process4, PID_FILE, ICONS;
|
|
4787
|
+
var init_tail = __esm({
|
|
4788
|
+
"src/tui/tail.ts"() {
|
|
4789
|
+
"use strict";
|
|
4790
|
+
import_http2 = __toESM(require("http"));
|
|
4791
|
+
import_chalk5 = __toESM(require("chalk"));
|
|
4792
|
+
import_fs6 = __toESM(require("fs"));
|
|
4793
|
+
import_os6 = __toESM(require("os"));
|
|
4794
|
+
import_path8 = __toESM(require("path"));
|
|
4795
|
+
import_readline = __toESM(require("readline"));
|
|
4796
|
+
import_child_process4 = require("child_process");
|
|
4797
|
+
init_daemon();
|
|
4798
|
+
PID_FILE = import_path8.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
|
|
4799
|
+
ICONS = {
|
|
4800
|
+
bash: "\u{1F4BB}",
|
|
4801
|
+
shell: "\u{1F4BB}",
|
|
4802
|
+
terminal: "\u{1F4BB}",
|
|
4803
|
+
read: "\u{1F4D6}",
|
|
4804
|
+
edit: "\u270F\uFE0F",
|
|
4805
|
+
write: "\u270F\uFE0F",
|
|
4806
|
+
glob: "\u{1F4C2}",
|
|
4807
|
+
grep: "\u{1F50D}",
|
|
4808
|
+
agent: "\u{1F916}",
|
|
4809
|
+
search: "\u{1F50D}",
|
|
4810
|
+
sql: "\u{1F5C4}\uFE0F",
|
|
4811
|
+
query: "\u{1F5C4}\uFE0F",
|
|
4812
|
+
list: "\u{1F4C2}",
|
|
4813
|
+
delete: "\u{1F5D1}\uFE0F",
|
|
4814
|
+
web: "\u{1F310}"
|
|
4815
|
+
};
|
|
4816
|
+
}
|
|
4817
|
+
});
|
|
4818
|
+
|
|
4819
|
+
// src/cli.ts
|
|
4820
|
+
var import_commander = require("commander");
|
|
4821
|
+
init_core();
|
|
4822
|
+
|
|
4823
|
+
// src/setup.ts
|
|
4824
|
+
var import_fs3 = __toESM(require("fs"));
|
|
4825
|
+
var import_path5 = __toESM(require("path"));
|
|
4826
|
+
var import_os3 = __toESM(require("os"));
|
|
4827
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
4828
|
+
var import_prompts2 = require("@inquirer/prompts");
|
|
4829
|
+
function printDaemonTip() {
|
|
4830
|
+
console.log(
|
|
4831
|
+
import_chalk3.default.cyan("\n \u{1F4A1} Node9 will protect you automatically using Native OS popups.") + import_chalk3.default.white("\n To view your history or manage persistent rules, run:") + import_chalk3.default.green("\n node9 daemon --openui")
|
|
4832
|
+
);
|
|
4833
|
+
}
|
|
4834
|
+
function fullPathCommand(subcommand) {
|
|
4835
|
+
if (process.env.NODE9_TESTING === "1") return `node9 ${subcommand}`;
|
|
4836
|
+
const nodeExec = process.execPath;
|
|
4837
|
+
const cliScript = process.argv[1];
|
|
4838
|
+
return `${nodeExec} ${cliScript} ${subcommand}`;
|
|
4839
|
+
}
|
|
4840
|
+
function readJson(filePath) {
|
|
4841
|
+
try {
|
|
4842
|
+
if (import_fs3.default.existsSync(filePath)) {
|
|
4843
|
+
return JSON.parse(import_fs3.default.readFileSync(filePath, "utf-8"));
|
|
4844
|
+
}
|
|
4845
|
+
} catch {
|
|
4846
|
+
}
|
|
4847
|
+
return null;
|
|
4848
|
+
}
|
|
4849
|
+
function writeJson(filePath, data) {
|
|
4850
|
+
const dir = import_path5.default.dirname(filePath);
|
|
4851
|
+
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
4852
|
+
import_fs3.default.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
4853
|
+
}
|
|
4854
|
+
async function setupClaude() {
|
|
4855
|
+
const homeDir2 = import_os3.default.homedir();
|
|
4856
|
+
const mcpPath = import_path5.default.join(homeDir2, ".claude.json");
|
|
4857
|
+
const hooksPath = import_path5.default.join(homeDir2, ".claude", "settings.json");
|
|
4858
|
+
const claudeConfig = readJson(mcpPath) ?? {};
|
|
4859
|
+
const settings = readJson(hooksPath) ?? {};
|
|
4860
|
+
const servers = claudeConfig.mcpServers ?? {};
|
|
4861
|
+
let anythingChanged = false;
|
|
4862
|
+
if (!settings.hooks) settings.hooks = {};
|
|
4863
|
+
const hasPreHook = settings.hooks.PreToolUse?.some(
|
|
4864
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
4865
|
+
);
|
|
4866
|
+
if (!hasPreHook) {
|
|
4867
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
4868
|
+
settings.hooks.PreToolUse.push({
|
|
4869
|
+
matcher: ".*",
|
|
4870
|
+
hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 60 }]
|
|
4871
|
+
});
|
|
4872
|
+
console.log(import_chalk3.default.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
|
|
4873
|
+
anythingChanged = true;
|
|
4874
|
+
}
|
|
4875
|
+
const hasPostHook = settings.hooks.PostToolUse?.some(
|
|
4876
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
4877
|
+
);
|
|
4878
|
+
if (!hasPostHook) {
|
|
4879
|
+
if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
|
|
4880
|
+
settings.hooks.PostToolUse.push({
|
|
4881
|
+
matcher: ".*",
|
|
4882
|
+
hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
|
|
4883
|
+
});
|
|
4884
|
+
console.log(import_chalk3.default.green(" \u2705 PostToolUse hook added \u2192 node9 log"));
|
|
4885
|
+
anythingChanged = true;
|
|
4886
|
+
}
|
|
4887
|
+
if (anythingChanged) {
|
|
4888
|
+
writeJson(hooksPath, settings);
|
|
4889
|
+
console.log("");
|
|
4890
|
+
}
|
|
4891
|
+
const serversToWrap = [];
|
|
4892
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
4893
|
+
if (!server.command || server.command === "node9") continue;
|
|
4894
|
+
const parts = [server.command, ...server.args ?? []];
|
|
4895
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
4896
|
+
}
|
|
4897
|
+
if (serversToWrap.length > 0) {
|
|
4898
|
+
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
4899
|
+
console.log(import_chalk3.default.white(` ${mcpPath}`));
|
|
4900
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
4901
|
+
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
4902
|
+
}
|
|
4903
|
+
console.log("");
|
|
4904
|
+
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
4905
|
+
if (proceed) {
|
|
4906
|
+
for (const { name, parts } of serversToWrap) {
|
|
4907
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
4908
|
+
}
|
|
4909
|
+
claudeConfig.mcpServers = servers;
|
|
4910
|
+
writeJson(mcpPath, claudeConfig);
|
|
4911
|
+
console.log(import_chalk3.default.green(`
|
|
4912
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
4913
|
+
anythingChanged = true;
|
|
4914
|
+
} else {
|
|
4915
|
+
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
3894
4916
|
}
|
|
3895
|
-
|
|
3896
|
-
}
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
4917
|
+
console.log("");
|
|
4918
|
+
}
|
|
4919
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
4920
|
+
console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Claude Code."));
|
|
4921
|
+
printDaemonTip();
|
|
4922
|
+
return;
|
|
4923
|
+
}
|
|
4924
|
+
if (anythingChanged) {
|
|
4925
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Claude Code!"));
|
|
4926
|
+
console.log(import_chalk3.default.gray(" Restart Claude Code for changes to take effect."));
|
|
4927
|
+
printDaemonTip();
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
async function setupGemini() {
|
|
4931
|
+
const homeDir2 = import_os3.default.homedir();
|
|
4932
|
+
const settingsPath = import_path5.default.join(homeDir2, ".gemini", "settings.json");
|
|
4933
|
+
const settings = readJson(settingsPath) ?? {};
|
|
4934
|
+
const servers = settings.mcpServers ?? {};
|
|
4935
|
+
let anythingChanged = false;
|
|
4936
|
+
if (!settings.hooks) settings.hooks = {};
|
|
4937
|
+
const hasBeforeHook = Array.isArray(settings.hooks.BeforeTool) && settings.hooks.BeforeTool.some(
|
|
4938
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 check") || h.command?.includes("cli.js check"))
|
|
4939
|
+
);
|
|
4940
|
+
if (!hasBeforeHook) {
|
|
4941
|
+
if (!settings.hooks.BeforeTool) settings.hooks.BeforeTool = [];
|
|
4942
|
+
if (!Array.isArray(settings.hooks.BeforeTool)) settings.hooks.BeforeTool = [];
|
|
4943
|
+
settings.hooks.BeforeTool.push({
|
|
4944
|
+
matcher: ".*",
|
|
4945
|
+
hooks: [
|
|
4946
|
+
{
|
|
4947
|
+
name: "node9-check",
|
|
4948
|
+
type: "command",
|
|
4949
|
+
command: fullPathCommand("check"),
|
|
4950
|
+
timeout: 6e5
|
|
3910
4951
|
}
|
|
3911
|
-
|
|
3912
|
-
|
|
4952
|
+
]
|
|
4953
|
+
});
|
|
4954
|
+
console.log(import_chalk3.default.green(" \u2705 BeforeTool hook added \u2192 node9 check"));
|
|
4955
|
+
anythingChanged = true;
|
|
4956
|
+
}
|
|
4957
|
+
const hasAfterHook = Array.isArray(settings.hooks.AfterTool) && settings.hooks.AfterTool.some(
|
|
4958
|
+
(m) => m.hooks.some((h) => h.command?.includes("node9 log") || h.command?.includes("cli.js log"))
|
|
4959
|
+
);
|
|
4960
|
+
if (!hasAfterHook) {
|
|
4961
|
+
if (!settings.hooks.AfterTool) settings.hooks.AfterTool = [];
|
|
4962
|
+
if (!Array.isArray(settings.hooks.AfterTool)) settings.hooks.AfterTool = [];
|
|
4963
|
+
settings.hooks.AfterTool.push({
|
|
4964
|
+
matcher: ".*",
|
|
4965
|
+
hooks: [{ name: "node9-log", type: "command", command: fullPathCommand("log") }]
|
|
4966
|
+
});
|
|
4967
|
+
console.log(import_chalk3.default.green(" \u2705 AfterTool hook added \u2192 node9 log"));
|
|
4968
|
+
anythingChanged = true;
|
|
4969
|
+
}
|
|
4970
|
+
if (anythingChanged) {
|
|
4971
|
+
writeJson(settingsPath, settings);
|
|
4972
|
+
console.log("");
|
|
4973
|
+
}
|
|
4974
|
+
const serversToWrap = [];
|
|
4975
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
4976
|
+
if (!server.command || server.command === "node9") continue;
|
|
4977
|
+
const parts = [server.command, ...server.args ?? []];
|
|
4978
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
4979
|
+
}
|
|
4980
|
+
if (serversToWrap.length > 0) {
|
|
4981
|
+
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
4982
|
+
console.log(import_chalk3.default.white(` ${settingsPath} (mcpServers)`));
|
|
4983
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
4984
|
+
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
4985
|
+
}
|
|
4986
|
+
console.log("");
|
|
4987
|
+
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
4988
|
+
if (proceed) {
|
|
4989
|
+
for (const { name, parts } of serversToWrap) {
|
|
4990
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
3913
4991
|
}
|
|
4992
|
+
settings.mcpServers = servers;
|
|
4993
|
+
writeJson(settingsPath, settings);
|
|
4994
|
+
console.log(import_chalk3.default.green(`
|
|
4995
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
4996
|
+
anythingChanged = true;
|
|
4997
|
+
} else {
|
|
4998
|
+
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
3914
4999
|
}
|
|
3915
|
-
console.
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
);
|
|
3924
|
-
console.log(
|
|
3925
|
-
|
|
5000
|
+
console.log("");
|
|
5001
|
+
}
|
|
5002
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
5003
|
+
console.log(import_chalk3.default.blue("\u2139\uFE0F Node9 is already fully configured for Gemini CLI."));
|
|
5004
|
+
printDaemonTip();
|
|
5005
|
+
return;
|
|
5006
|
+
}
|
|
5007
|
+
if (anythingChanged) {
|
|
5008
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Gemini CLI!"));
|
|
5009
|
+
console.log(import_chalk3.default.gray(" Restart Gemini CLI for changes to take effect."));
|
|
5010
|
+
printDaemonTip();
|
|
5011
|
+
}
|
|
3926
5012
|
}
|
|
3927
|
-
function
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
5013
|
+
async function setupCursor() {
|
|
5014
|
+
const homeDir2 = import_os3.default.homedir();
|
|
5015
|
+
const mcpPath = import_path5.default.join(homeDir2, ".cursor", "mcp.json");
|
|
5016
|
+
const mcpConfig = readJson(mcpPath) ?? {};
|
|
5017
|
+
const servers = mcpConfig.mcpServers ?? {};
|
|
5018
|
+
let anythingChanged = false;
|
|
5019
|
+
const serversToWrap = [];
|
|
5020
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
5021
|
+
if (!server.command || server.command === "node9") continue;
|
|
5022
|
+
const parts = [server.command, ...server.args ?? []];
|
|
5023
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
5024
|
+
}
|
|
5025
|
+
if (serversToWrap.length > 0) {
|
|
5026
|
+
console.log(import_chalk3.default.bold("The following existing entries will be modified:\n"));
|
|
5027
|
+
console.log(import_chalk3.default.white(` ${mcpPath}`));
|
|
5028
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
5029
|
+
console.log(import_chalk3.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
5030
|
+
}
|
|
5031
|
+
console.log("");
|
|
5032
|
+
const proceed = await (0, import_prompts2.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
5033
|
+
if (proceed) {
|
|
5034
|
+
for (const { name, parts } of serversToWrap) {
|
|
5035
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
5036
|
+
}
|
|
5037
|
+
mcpConfig.mcpServers = servers;
|
|
5038
|
+
writeJson(mcpPath, mcpConfig);
|
|
5039
|
+
console.log(import_chalk3.default.green(`
|
|
5040
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
5041
|
+
anythingChanged = true;
|
|
5042
|
+
} else {
|
|
5043
|
+
console.log(import_chalk3.default.yellow(" Skipped MCP server wrapping."));
|
|
3939
5044
|
}
|
|
5045
|
+
console.log("");
|
|
3940
5046
|
}
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
console.log(
|
|
3949
|
-
|
|
3950
|
-
|
|
5047
|
+
console.log(
|
|
5048
|
+
import_chalk3.default.yellow(
|
|
5049
|
+
" \u26A0\uFE0F Note: Cursor does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Cursor."
|
|
5050
|
+
)
|
|
5051
|
+
);
|
|
5052
|
+
console.log("");
|
|
5053
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
5054
|
+
console.log(
|
|
5055
|
+
import_chalk3.default.blue(
|
|
5056
|
+
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.cursor/mcp.json and re-run."
|
|
5057
|
+
)
|
|
5058
|
+
);
|
|
5059
|
+
printDaemonTip();
|
|
5060
|
+
return;
|
|
5061
|
+
}
|
|
5062
|
+
if (anythingChanged) {
|
|
5063
|
+
console.log(import_chalk3.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Cursor via MCP proxy!"));
|
|
5064
|
+
console.log(import_chalk3.default.gray(" Restart Cursor for changes to take effect."));
|
|
5065
|
+
printDaemonTip();
|
|
3951
5066
|
}
|
|
3952
5067
|
}
|
|
3953
5068
|
|
|
3954
5069
|
// src/cli.ts
|
|
3955
|
-
|
|
5070
|
+
init_daemon();
|
|
5071
|
+
var import_child_process5 = require("child_process");
|
|
3956
5072
|
var import_execa = require("execa");
|
|
3957
5073
|
var import_execa2 = require("execa");
|
|
3958
|
-
var
|
|
3959
|
-
var
|
|
3960
|
-
var
|
|
3961
|
-
var
|
|
3962
|
-
var
|
|
5074
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
5075
|
+
var import_readline2 = __toESM(require("readline"));
|
|
5076
|
+
var import_fs7 = __toESM(require("fs"));
|
|
5077
|
+
var import_path9 = __toESM(require("path"));
|
|
5078
|
+
var import_os7 = __toESM(require("os"));
|
|
3963
5079
|
|
|
3964
5080
|
// src/undo.ts
|
|
3965
5081
|
var import_child_process3 = require("child_process");
|
|
3966
|
-
var
|
|
3967
|
-
var
|
|
3968
|
-
var
|
|
3969
|
-
var SNAPSHOT_STACK_PATH =
|
|
3970
|
-
var UNDO_LATEST_PATH =
|
|
5082
|
+
var import_fs5 = __toESM(require("fs"));
|
|
5083
|
+
var import_path7 = __toESM(require("path"));
|
|
5084
|
+
var import_os5 = __toESM(require("os"));
|
|
5085
|
+
var SNAPSHOT_STACK_PATH = import_path7.default.join(import_os5.default.homedir(), ".node9", "snapshots.json");
|
|
5086
|
+
var UNDO_LATEST_PATH = import_path7.default.join(import_os5.default.homedir(), ".node9", "undo_latest.txt");
|
|
3971
5087
|
var MAX_SNAPSHOTS = 10;
|
|
3972
5088
|
function readStack() {
|
|
3973
5089
|
try {
|
|
3974
|
-
if (
|
|
3975
|
-
return JSON.parse(
|
|
5090
|
+
if (import_fs5.default.existsSync(SNAPSHOT_STACK_PATH))
|
|
5091
|
+
return JSON.parse(import_fs5.default.readFileSync(SNAPSHOT_STACK_PATH, "utf-8"));
|
|
3976
5092
|
} catch {
|
|
3977
5093
|
}
|
|
3978
5094
|
return [];
|
|
3979
5095
|
}
|
|
3980
5096
|
function writeStack(stack) {
|
|
3981
|
-
const dir =
|
|
3982
|
-
if (!
|
|
3983
|
-
|
|
5097
|
+
const dir = import_path7.default.dirname(SNAPSHOT_STACK_PATH);
|
|
5098
|
+
if (!import_fs5.default.existsSync(dir)) import_fs5.default.mkdirSync(dir, { recursive: true });
|
|
5099
|
+
import_fs5.default.writeFileSync(SNAPSHOT_STACK_PATH, JSON.stringify(stack, null, 2));
|
|
3984
5100
|
}
|
|
3985
5101
|
function buildArgsSummary(tool, args) {
|
|
3986
5102
|
if (!args || typeof args !== "object") return "";
|
|
@@ -3996,13 +5112,13 @@ function buildArgsSummary(tool, args) {
|
|
|
3996
5112
|
async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
3997
5113
|
try {
|
|
3998
5114
|
const cwd = process.cwd();
|
|
3999
|
-
if (!
|
|
4000
|
-
const tempIndex =
|
|
5115
|
+
if (!import_fs5.default.existsSync(import_path7.default.join(cwd, ".git"))) return null;
|
|
5116
|
+
const tempIndex = import_path7.default.join(cwd, ".git", `node9_index_${Date.now()}`);
|
|
4001
5117
|
const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
|
|
4002
5118
|
(0, import_child_process3.spawnSync)("git", ["add", "-A"], { env });
|
|
4003
5119
|
const treeRes = (0, import_child_process3.spawnSync)("git", ["write-tree"], { env });
|
|
4004
5120
|
const treeHash = treeRes.stdout.toString().trim();
|
|
4005
|
-
if (
|
|
5121
|
+
if (import_fs5.default.existsSync(tempIndex)) import_fs5.default.unlinkSync(tempIndex);
|
|
4006
5122
|
if (!treeHash || treeRes.status !== 0) return null;
|
|
4007
5123
|
const commitRes = (0, import_child_process3.spawnSync)("git", [
|
|
4008
5124
|
"commit-tree",
|
|
@@ -4023,7 +5139,7 @@ async function createShadowSnapshot(tool = "unknown", args = {}) {
|
|
|
4023
5139
|
stack.push(entry);
|
|
4024
5140
|
if (stack.length > MAX_SNAPSHOTS) stack.splice(0, stack.length - MAX_SNAPSHOTS);
|
|
4025
5141
|
writeStack(stack);
|
|
4026
|
-
|
|
5142
|
+
import_fs5.default.writeFileSync(UNDO_LATEST_PATH, commitHash);
|
|
4027
5143
|
return commitHash;
|
|
4028
5144
|
} catch (err) {
|
|
4029
5145
|
if (process.env.NODE9_DEBUG === "1") console.error("[Node9 Undo Engine Error]:", err);
|
|
@@ -4061,9 +5177,9 @@ function applyUndo(hash, cwd) {
|
|
|
4061
5177
|
const tracked = (0, import_child_process3.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
4062
5178
|
const untracked = (0, import_child_process3.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
|
|
4063
5179
|
for (const file of [...tracked, ...untracked]) {
|
|
4064
|
-
const fullPath =
|
|
4065
|
-
if (!snapshotFiles.has(file) &&
|
|
4066
|
-
|
|
5180
|
+
const fullPath = import_path7.default.join(dir, file);
|
|
5181
|
+
if (!snapshotFiles.has(file) && import_fs5.default.existsSync(fullPath)) {
|
|
5182
|
+
import_fs5.default.unlinkSync(fullPath);
|
|
4067
5183
|
}
|
|
4068
5184
|
}
|
|
4069
5185
|
return true;
|
|
@@ -4073,9 +5189,10 @@ function applyUndo(hash, cwd) {
|
|
|
4073
5189
|
}
|
|
4074
5190
|
|
|
4075
5191
|
// src/cli.ts
|
|
5192
|
+
init_shields();
|
|
4076
5193
|
var import_prompts3 = require("@inquirer/prompts");
|
|
4077
5194
|
var { version } = JSON.parse(
|
|
4078
|
-
|
|
5195
|
+
import_fs7.default.readFileSync(import_path9.default.join(__dirname, "../package.json"), "utf-8")
|
|
4079
5196
|
);
|
|
4080
5197
|
function parseDuration(str) {
|
|
4081
5198
|
const m = str.trim().match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)?$/i);
|
|
@@ -4107,6 +5224,15 @@ INSTRUCTIONS:
|
|
|
4107
5224
|
- If you believe this action is critical, explain your reasoning and ask them to run "node9 pause 15m" to proceed.`;
|
|
4108
5225
|
}
|
|
4109
5226
|
const label = blockedByLabel.toLowerCase();
|
|
5227
|
+
if (label.includes("dlp") || label.includes("secret detected") || label.includes("credential review")) {
|
|
5228
|
+
return `NODE9 SECURITY ALERT: A sensitive credential (API key, token, or private key) was found in your tool call arguments.
|
|
5229
|
+
CRITICAL INSTRUCTION: Do NOT retry this action.
|
|
5230
|
+
REQUIRED ACTIONS:
|
|
5231
|
+
1. Remove the hardcoded credential from your command or code.
|
|
5232
|
+
2. Use an environment variable or a dedicated secrets manager instead.
|
|
5233
|
+
3. Treat the leaked credential as compromised and rotate it immediately.
|
|
5234
|
+
Do NOT attempt to bypass this check or pass the credential through another tool.`;
|
|
5235
|
+
}
|
|
4110
5236
|
if (label.includes("sql safety") && label.includes("delete without where")) {
|
|
4111
5237
|
return `NODE9: Blocked \u2014 DELETE without WHERE clause would wipe the entire table.
|
|
4112
5238
|
INSTRUCTION: Add a WHERE clause to scope the deletion (e.g. WHERE id = <value>).
|
|
@@ -4154,15 +5280,15 @@ function openBrowserLocal() {
|
|
|
4154
5280
|
const url = `http://${DAEMON_HOST2}:${DAEMON_PORT2}/`;
|
|
4155
5281
|
try {
|
|
4156
5282
|
const opts = { stdio: "ignore" };
|
|
4157
|
-
if (process.platform === "darwin") (0,
|
|
4158
|
-
else if (process.platform === "win32") (0,
|
|
4159
|
-
else (0,
|
|
5283
|
+
if (process.platform === "darwin") (0, import_child_process5.execSync)(`open "${url}"`, opts);
|
|
5284
|
+
else if (process.platform === "win32") (0, import_child_process5.execSync)(`cmd /c start "" "${url}"`, opts);
|
|
5285
|
+
else (0, import_child_process5.execSync)(`xdg-open "${url}"`, opts);
|
|
4160
5286
|
} catch {
|
|
4161
5287
|
}
|
|
4162
5288
|
}
|
|
4163
5289
|
async function autoStartDaemonAndWait() {
|
|
4164
5290
|
try {
|
|
4165
|
-
const child = (0,
|
|
5291
|
+
const child = (0, import_child_process5.spawn)("node9", ["daemon"], {
|
|
4166
5292
|
detached: true,
|
|
4167
5293
|
stdio: "ignore",
|
|
4168
5294
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -4198,14 +5324,14 @@ async function runProxy(targetCommand) {
|
|
|
4198
5324
|
if (stdout) executable = stdout.trim();
|
|
4199
5325
|
} catch {
|
|
4200
5326
|
}
|
|
4201
|
-
console.log(
|
|
4202
|
-
const child = (0,
|
|
5327
|
+
console.log(import_chalk6.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
5328
|
+
const child = (0, import_child_process5.spawn)(executable, args, {
|
|
4203
5329
|
stdio: ["pipe", "pipe", "inherit"],
|
|
4204
5330
|
// We control STDIN and STDOUT
|
|
4205
5331
|
shell: false,
|
|
4206
5332
|
env: { ...process.env, FORCE_COLOR: "1" }
|
|
4207
5333
|
});
|
|
4208
|
-
const agentIn =
|
|
5334
|
+
const agentIn = import_readline2.default.createInterface({ input: process.stdin, terminal: false });
|
|
4209
5335
|
agentIn.on("line", async (line) => {
|
|
4210
5336
|
let message;
|
|
4211
5337
|
try {
|
|
@@ -4223,10 +5349,10 @@ async function runProxy(targetCommand) {
|
|
|
4223
5349
|
agent: "Proxy/MCP"
|
|
4224
5350
|
});
|
|
4225
5351
|
if (!result.approved) {
|
|
4226
|
-
console.error(
|
|
5352
|
+
console.error(import_chalk6.default.red(`
|
|
4227
5353
|
\u{1F6D1} Node9 Sudo: Action Blocked`));
|
|
4228
|
-
console.error(
|
|
4229
|
-
console.error(
|
|
5354
|
+
console.error(import_chalk6.default.gray(` Tool: ${name}`));
|
|
5355
|
+
console.error(import_chalk6.default.gray(` Reason: ${result.reason || "Security Policy"}
|
|
4230
5356
|
`));
|
|
4231
5357
|
const blockedByLabel = result.blockedByLabel ?? result.reason ?? "Security Policy";
|
|
4232
5358
|
const isHuman = blockedByLabel.toLowerCase().includes("user") || blockedByLabel.toLowerCase().includes("daemon") || blockedByLabel.toLowerCase().includes("decision");
|
|
@@ -4268,14 +5394,14 @@ async function runProxy(targetCommand) {
|
|
|
4268
5394
|
}
|
|
4269
5395
|
program.command("login").argument("<apiKey>").option("--local", "Save key for audit/logging only \u2014 local config still controls all decisions").option("--profile <name>", 'Save as a named profile (default: "default")').action((apiKey, options) => {
|
|
4270
5396
|
const DEFAULT_API_URL = "https://api.node9.ai/api/v1/intercept";
|
|
4271
|
-
const credPath =
|
|
4272
|
-
if (!
|
|
4273
|
-
|
|
5397
|
+
const credPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "credentials.json");
|
|
5398
|
+
if (!import_fs7.default.existsSync(import_path9.default.dirname(credPath)))
|
|
5399
|
+
import_fs7.default.mkdirSync(import_path9.default.dirname(credPath), { recursive: true });
|
|
4274
5400
|
const profileName = options.profile || "default";
|
|
4275
5401
|
let existingCreds = {};
|
|
4276
5402
|
try {
|
|
4277
|
-
if (
|
|
4278
|
-
const raw = JSON.parse(
|
|
5403
|
+
if (import_fs7.default.existsSync(credPath)) {
|
|
5404
|
+
const raw = JSON.parse(import_fs7.default.readFileSync(credPath, "utf-8"));
|
|
4279
5405
|
if (raw.apiKey) {
|
|
4280
5406
|
existingCreds = {
|
|
4281
5407
|
default: { apiKey: raw.apiKey, apiUrl: raw.apiUrl || DEFAULT_API_URL }
|
|
@@ -4287,13 +5413,13 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4287
5413
|
} catch {
|
|
4288
5414
|
}
|
|
4289
5415
|
existingCreds[profileName] = { apiKey, apiUrl: DEFAULT_API_URL };
|
|
4290
|
-
|
|
5416
|
+
import_fs7.default.writeFileSync(credPath, JSON.stringify(existingCreds, null, 2), { mode: 384 });
|
|
4291
5417
|
if (profileName === "default") {
|
|
4292
|
-
const configPath =
|
|
5418
|
+
const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
|
|
4293
5419
|
let config = {};
|
|
4294
5420
|
try {
|
|
4295
|
-
if (
|
|
4296
|
-
config = JSON.parse(
|
|
5421
|
+
if (import_fs7.default.existsSync(configPath))
|
|
5422
|
+
config = JSON.parse(import_fs7.default.readFileSync(configPath, "utf-8"));
|
|
4297
5423
|
} catch {
|
|
4298
5424
|
}
|
|
4299
5425
|
if (!config.settings || typeof config.settings !== "object") config.settings = {};
|
|
@@ -4308,36 +5434,36 @@ program.command("login").argument("<apiKey>").option("--local", "Save key for au
|
|
|
4308
5434
|
approvers.cloud = false;
|
|
4309
5435
|
}
|
|
4310
5436
|
s.approvers = approvers;
|
|
4311
|
-
if (!
|
|
4312
|
-
|
|
4313
|
-
|
|
5437
|
+
if (!import_fs7.default.existsSync(import_path9.default.dirname(configPath)))
|
|
5438
|
+
import_fs7.default.mkdirSync(import_path9.default.dirname(configPath), { recursive: true });
|
|
5439
|
+
import_fs7.default.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
4314
5440
|
}
|
|
4315
5441
|
if (options.profile && profileName !== "default") {
|
|
4316
|
-
console.log(
|
|
4317
|
-
console.log(
|
|
5442
|
+
console.log(import_chalk6.default.green(`\u2705 Profile "${profileName}" saved`));
|
|
5443
|
+
console.log(import_chalk6.default.gray(` Switch to it per-session: NODE9_PROFILE=${profileName} claude`));
|
|
4318
5444
|
} else if (options.local) {
|
|
4319
|
-
console.log(
|
|
4320
|
-
console.log(
|
|
5445
|
+
console.log(import_chalk6.default.green(`\u2705 Privacy mode \u{1F6E1}\uFE0F`));
|
|
5446
|
+
console.log(import_chalk6.default.gray(` All decisions stay on this machine.`));
|
|
4321
5447
|
} else {
|
|
4322
|
-
console.log(
|
|
4323
|
-
console.log(
|
|
5448
|
+
console.log(import_chalk6.default.green(`\u2705 Logged in \u2014 agent mode`));
|
|
5449
|
+
console.log(import_chalk6.default.gray(` Team policy enforced for all calls via Node9 cloud.`));
|
|
4324
5450
|
}
|
|
4325
5451
|
});
|
|
4326
5452
|
program.command("addto").description("Integrate Node9 with an AI agent").addHelpText("after", "\n Supported targets: claude gemini cursor").argument("<target>", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
4327
5453
|
if (target === "gemini") return await setupGemini();
|
|
4328
5454
|
if (target === "claude") return await setupClaude();
|
|
4329
5455
|
if (target === "cursor") return await setupCursor();
|
|
4330
|
-
console.error(
|
|
5456
|
+
console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
4331
5457
|
process.exit(1);
|
|
4332
5458
|
});
|
|
4333
5459
|
program.command("setup").description('Alias for "addto" \u2014 integrate Node9 with an AI agent').addHelpText("after", "\n Supported targets: claude gemini cursor").argument("[target]", "The agent to protect: claude | gemini | cursor").action(async (target) => {
|
|
4334
5460
|
if (!target) {
|
|
4335
|
-
console.log(
|
|
4336
|
-
console.log(" Usage: " +
|
|
5461
|
+
console.log(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9 Setup \u2014 integrate with your AI agent\n"));
|
|
5462
|
+
console.log(" Usage: " + import_chalk6.default.white("node9 setup <target>") + "\n");
|
|
4337
5463
|
console.log(" Targets:");
|
|
4338
|
-
console.log(" " +
|
|
4339
|
-
console.log(" " +
|
|
4340
|
-
console.log(" " +
|
|
5464
|
+
console.log(" " + import_chalk6.default.green("claude") + " \u2014 Claude Code (hook mode)");
|
|
5465
|
+
console.log(" " + import_chalk6.default.green("gemini") + " \u2014 Gemini CLI (hook mode)");
|
|
5466
|
+
console.log(" " + import_chalk6.default.green("cursor") + " \u2014 Cursor (hook mode)");
|
|
4341
5467
|
console.log("");
|
|
4342
5468
|
return;
|
|
4343
5469
|
}
|
|
@@ -4345,33 +5471,33 @@ program.command("setup").description('Alias for "addto" \u2014 integrate Node9 w
|
|
|
4345
5471
|
if (t === "gemini") return await setupGemini();
|
|
4346
5472
|
if (t === "claude") return await setupClaude();
|
|
4347
5473
|
if (t === "cursor") return await setupCursor();
|
|
4348
|
-
console.error(
|
|
5474
|
+
console.error(import_chalk6.default.red(`Unknown target: "${target}". Supported: claude, gemini, cursor`));
|
|
4349
5475
|
process.exit(1);
|
|
4350
5476
|
});
|
|
4351
5477
|
program.command("doctor").description("Check that Node9 is installed and configured correctly").action(() => {
|
|
4352
|
-
const homeDir2 =
|
|
5478
|
+
const homeDir2 = import_os7.default.homedir();
|
|
4353
5479
|
let failures = 0;
|
|
4354
5480
|
function pass(msg) {
|
|
4355
|
-
console.log(
|
|
5481
|
+
console.log(import_chalk6.default.green(" \u2705 ") + msg);
|
|
4356
5482
|
}
|
|
4357
5483
|
function fail(msg, hint) {
|
|
4358
|
-
console.log(
|
|
4359
|
-
if (hint) console.log(
|
|
5484
|
+
console.log(import_chalk6.default.red(" \u274C ") + msg);
|
|
5485
|
+
if (hint) console.log(import_chalk6.default.gray(" " + hint));
|
|
4360
5486
|
failures++;
|
|
4361
5487
|
}
|
|
4362
5488
|
function warn(msg, hint) {
|
|
4363
|
-
console.log(
|
|
4364
|
-
if (hint) console.log(
|
|
5489
|
+
console.log(import_chalk6.default.yellow(" \u26A0\uFE0F ") + msg);
|
|
5490
|
+
if (hint) console.log(import_chalk6.default.gray(" " + hint));
|
|
4365
5491
|
}
|
|
4366
5492
|
function section(title) {
|
|
4367
|
-
console.log("\n" +
|
|
5493
|
+
console.log("\n" + import_chalk6.default.bold(title));
|
|
4368
5494
|
}
|
|
4369
|
-
console.log(
|
|
5495
|
+
console.log(import_chalk6.default.cyan.bold(`
|
|
4370
5496
|
\u{1F6E1}\uFE0F Node9 Doctor v${version}
|
|
4371
5497
|
`));
|
|
4372
5498
|
section("Binary");
|
|
4373
5499
|
try {
|
|
4374
|
-
const which = (0,
|
|
5500
|
+
const which = (0, import_child_process5.execSync)("which node9", { encoding: "utf-8" }).trim();
|
|
4375
5501
|
pass(`node9 found at ${which}`);
|
|
4376
5502
|
} catch {
|
|
4377
5503
|
warn("node9 not found in $PATH \u2014 hooks may not find it", "Run: npm install -g @node9/proxy");
|
|
@@ -4386,7 +5512,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4386
5512
|
);
|
|
4387
5513
|
}
|
|
4388
5514
|
try {
|
|
4389
|
-
const gitVersion = (0,
|
|
5515
|
+
const gitVersion = (0, import_child_process5.execSync)("git --version", { encoding: "utf-8" }).trim();
|
|
4390
5516
|
pass(gitVersion);
|
|
4391
5517
|
} catch {
|
|
4392
5518
|
warn(
|
|
@@ -4395,10 +5521,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4395
5521
|
);
|
|
4396
5522
|
}
|
|
4397
5523
|
section("Configuration");
|
|
4398
|
-
const globalConfigPath =
|
|
4399
|
-
if (
|
|
5524
|
+
const globalConfigPath = import_path9.default.join(homeDir2, ".node9", "config.json");
|
|
5525
|
+
if (import_fs7.default.existsSync(globalConfigPath)) {
|
|
4400
5526
|
try {
|
|
4401
|
-
JSON.parse(
|
|
5527
|
+
JSON.parse(import_fs7.default.readFileSync(globalConfigPath, "utf-8"));
|
|
4402
5528
|
pass("~/.node9/config.json found and valid");
|
|
4403
5529
|
} catch {
|
|
4404
5530
|
fail("~/.node9/config.json is invalid JSON", "Run: node9 init --force");
|
|
@@ -4406,17 +5532,17 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4406
5532
|
} else {
|
|
4407
5533
|
warn("~/.node9/config.json not found (using defaults)", "Run: node9 init");
|
|
4408
5534
|
}
|
|
4409
|
-
const projectConfigPath =
|
|
4410
|
-
if (
|
|
5535
|
+
const projectConfigPath = import_path9.default.join(process.cwd(), "node9.config.json");
|
|
5536
|
+
if (import_fs7.default.existsSync(projectConfigPath)) {
|
|
4411
5537
|
try {
|
|
4412
|
-
JSON.parse(
|
|
5538
|
+
JSON.parse(import_fs7.default.readFileSync(projectConfigPath, "utf-8"));
|
|
4413
5539
|
pass("node9.config.json found and valid (project)");
|
|
4414
5540
|
} catch {
|
|
4415
5541
|
fail("node9.config.json is invalid JSON", "Fix the JSON or delete it and run: node9 init");
|
|
4416
5542
|
}
|
|
4417
5543
|
}
|
|
4418
|
-
const credsPath =
|
|
4419
|
-
if (
|
|
5544
|
+
const credsPath = import_path9.default.join(homeDir2, ".node9", "credentials.json");
|
|
5545
|
+
if (import_fs7.default.existsSync(credsPath)) {
|
|
4420
5546
|
pass("Cloud credentials found (~/.node9/credentials.json)");
|
|
4421
5547
|
} else {
|
|
4422
5548
|
warn(
|
|
@@ -4425,10 +5551,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4425
5551
|
);
|
|
4426
5552
|
}
|
|
4427
5553
|
section("Agent Hooks");
|
|
4428
|
-
const claudeSettingsPath =
|
|
4429
|
-
if (
|
|
5554
|
+
const claudeSettingsPath = import_path9.default.join(homeDir2, ".claude", "settings.json");
|
|
5555
|
+
if (import_fs7.default.existsSync(claudeSettingsPath)) {
|
|
4430
5556
|
try {
|
|
4431
|
-
const cs = JSON.parse(
|
|
5557
|
+
const cs = JSON.parse(import_fs7.default.readFileSync(claudeSettingsPath, "utf-8"));
|
|
4432
5558
|
const hasHook = cs.hooks?.PreToolUse?.some(
|
|
4433
5559
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4434
5560
|
);
|
|
@@ -4441,10 +5567,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4441
5567
|
} else {
|
|
4442
5568
|
warn("Claude Code \u2014 not configured", "Run: node9 setup claude");
|
|
4443
5569
|
}
|
|
4444
|
-
const geminiSettingsPath =
|
|
4445
|
-
if (
|
|
5570
|
+
const geminiSettingsPath = import_path9.default.join(homeDir2, ".gemini", "settings.json");
|
|
5571
|
+
if (import_fs7.default.existsSync(geminiSettingsPath)) {
|
|
4446
5572
|
try {
|
|
4447
|
-
const gs = JSON.parse(
|
|
5573
|
+
const gs = JSON.parse(import_fs7.default.readFileSync(geminiSettingsPath, "utf-8"));
|
|
4448
5574
|
const hasHook = gs.hooks?.BeforeTool?.some(
|
|
4449
5575
|
(m) => m.hooks.some((h) => h.command?.includes("node9") || h.command?.includes("cli.js"))
|
|
4450
5576
|
);
|
|
@@ -4457,10 +5583,10 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4457
5583
|
} else {
|
|
4458
5584
|
warn("Gemini CLI \u2014 not configured", "Run: node9 setup gemini (skip if not using Gemini)");
|
|
4459
5585
|
}
|
|
4460
|
-
const cursorHooksPath =
|
|
4461
|
-
if (
|
|
5586
|
+
const cursorHooksPath = import_path9.default.join(homeDir2, ".cursor", "hooks.json");
|
|
5587
|
+
if (import_fs7.default.existsSync(cursorHooksPath)) {
|
|
4462
5588
|
try {
|
|
4463
|
-
const cur = JSON.parse(
|
|
5589
|
+
const cur = JSON.parse(import_fs7.default.readFileSync(cursorHooksPath, "utf-8"));
|
|
4464
5590
|
const hasHook = cur.hooks?.preToolUse?.some(
|
|
4465
5591
|
(h) => h.command?.includes("node9") || h.command?.includes("cli.js")
|
|
4466
5592
|
);
|
|
@@ -4481,9 +5607,9 @@ program.command("doctor").description("Check that Node9 is installed and configu
|
|
|
4481
5607
|
}
|
|
4482
5608
|
console.log("");
|
|
4483
5609
|
if (failures === 0) {
|
|
4484
|
-
console.log(
|
|
5610
|
+
console.log(import_chalk6.default.green.bold(" All checks passed. Node9 is ready.\n"));
|
|
4485
5611
|
} else {
|
|
4486
|
-
console.log(
|
|
5612
|
+
console.log(import_chalk6.default.red.bold(` ${failures} check(s) failed. See hints above.
|
|
4487
5613
|
`));
|
|
4488
5614
|
process.exit(1);
|
|
4489
5615
|
}
|
|
@@ -4498,7 +5624,7 @@ program.command("explain").description(
|
|
|
4498
5624
|
try {
|
|
4499
5625
|
args = JSON.parse(trimmed);
|
|
4500
5626
|
} catch {
|
|
4501
|
-
console.error(
|
|
5627
|
+
console.error(import_chalk6.default.red(`
|
|
4502
5628
|
\u274C Invalid JSON: ${trimmed}
|
|
4503
5629
|
`));
|
|
4504
5630
|
process.exit(1);
|
|
@@ -4509,63 +5635,63 @@ program.command("explain").description(
|
|
|
4509
5635
|
}
|
|
4510
5636
|
const result = await explainPolicy(tool, args);
|
|
4511
5637
|
console.log("");
|
|
4512
|
-
console.log(
|
|
5638
|
+
console.log(import_chalk6.default.cyan.bold("\u{1F6E1}\uFE0F Node9 Explain"));
|
|
4513
5639
|
console.log("");
|
|
4514
|
-
console.log(` ${
|
|
5640
|
+
console.log(` ${import_chalk6.default.bold("Tool:")} ${import_chalk6.default.white(result.tool)}`);
|
|
4515
5641
|
if (argsRaw) {
|
|
4516
5642
|
const preview = argsRaw.length > 80 ? argsRaw.slice(0, 77) + "\u2026" : argsRaw;
|
|
4517
|
-
console.log(` ${
|
|
5643
|
+
console.log(` ${import_chalk6.default.bold("Input:")} ${import_chalk6.default.gray(preview)}`);
|
|
4518
5644
|
}
|
|
4519
5645
|
console.log("");
|
|
4520
|
-
console.log(
|
|
5646
|
+
console.log(import_chalk6.default.bold("Config Sources (Waterfall):"));
|
|
4521
5647
|
for (const tier of result.waterfall) {
|
|
4522
|
-
const num =
|
|
5648
|
+
const num = import_chalk6.default.gray(` ${tier.tier}.`);
|
|
4523
5649
|
const label = tier.label.padEnd(16);
|
|
4524
5650
|
let statusStr;
|
|
4525
5651
|
if (tier.tier === 1) {
|
|
4526
|
-
statusStr =
|
|
5652
|
+
statusStr = import_chalk6.default.gray(tier.note ?? "");
|
|
4527
5653
|
} else if (tier.status === "active") {
|
|
4528
|
-
const loc = tier.path ?
|
|
4529
|
-
const note = tier.note ?
|
|
4530
|
-
statusStr =
|
|
5654
|
+
const loc = tier.path ? import_chalk6.default.gray(tier.path) : "";
|
|
5655
|
+
const note = tier.note ? import_chalk6.default.gray(`(${tier.note})`) : "";
|
|
5656
|
+
statusStr = import_chalk6.default.green("\u2713 active") + (loc ? " " + loc : "") + (note ? " " + note : "");
|
|
4531
5657
|
} else {
|
|
4532
|
-
statusStr =
|
|
5658
|
+
statusStr = import_chalk6.default.gray("\u25CB " + (tier.note ?? "not found"));
|
|
4533
5659
|
}
|
|
4534
|
-
console.log(`${num} ${
|
|
5660
|
+
console.log(`${num} ${import_chalk6.default.white(label)} ${statusStr}`);
|
|
4535
5661
|
}
|
|
4536
5662
|
console.log("");
|
|
4537
|
-
console.log(
|
|
5663
|
+
console.log(import_chalk6.default.bold("Policy Evaluation:"));
|
|
4538
5664
|
for (const step of result.steps) {
|
|
4539
5665
|
const isFinal = step.isFinal;
|
|
4540
5666
|
let icon;
|
|
4541
|
-
if (step.outcome === "allow") icon =
|
|
4542
|
-
else if (step.outcome === "review") icon =
|
|
4543
|
-
else if (step.outcome === "skip") icon =
|
|
4544
|
-
else icon =
|
|
5667
|
+
if (step.outcome === "allow") icon = import_chalk6.default.green(" \u2705");
|
|
5668
|
+
else if (step.outcome === "review") icon = import_chalk6.default.red(" \u{1F534}");
|
|
5669
|
+
else if (step.outcome === "skip") icon = import_chalk6.default.gray(" \u2500 ");
|
|
5670
|
+
else icon = import_chalk6.default.gray(" \u25CB ");
|
|
4545
5671
|
const name = step.name.padEnd(18);
|
|
4546
|
-
const nameStr = isFinal ?
|
|
4547
|
-
const detail = isFinal ?
|
|
4548
|
-
const arrow = isFinal ?
|
|
5672
|
+
const nameStr = isFinal ? import_chalk6.default.white.bold(name) : import_chalk6.default.white(name);
|
|
5673
|
+
const detail = isFinal ? import_chalk6.default.white(step.detail) : import_chalk6.default.gray(step.detail);
|
|
5674
|
+
const arrow = isFinal ? import_chalk6.default.yellow(" \u2190 STOP") : "";
|
|
4549
5675
|
console.log(`${icon} ${nameStr} ${detail}${arrow}`);
|
|
4550
5676
|
}
|
|
4551
5677
|
console.log("");
|
|
4552
5678
|
if (result.decision === "allow") {
|
|
4553
|
-
console.log(
|
|
5679
|
+
console.log(import_chalk6.default.green.bold(" Decision: \u2705 ALLOW") + import_chalk6.default.gray(" \u2014 no approval needed"));
|
|
4554
5680
|
} else {
|
|
4555
5681
|
console.log(
|
|
4556
|
-
|
|
5682
|
+
import_chalk6.default.red.bold(" Decision: \u{1F534} REVIEW") + import_chalk6.default.gray(" \u2014 human approval required")
|
|
4557
5683
|
);
|
|
4558
5684
|
if (result.blockedByLabel) {
|
|
4559
|
-
console.log(
|
|
5685
|
+
console.log(import_chalk6.default.gray(` Reason: ${result.blockedByLabel}`));
|
|
4560
5686
|
}
|
|
4561
5687
|
}
|
|
4562
5688
|
console.log("");
|
|
4563
5689
|
});
|
|
4564
5690
|
program.command("init").description("Create ~/.node9/config.json with default policy (safe to run multiple times)").option("--force", "Overwrite existing config").option("-m, --mode <mode>", "Set initial security mode (standard, strict, audit)", "standard").action((options) => {
|
|
4565
|
-
const configPath =
|
|
4566
|
-
if (
|
|
4567
|
-
console.log(
|
|
4568
|
-
console.log(
|
|
5691
|
+
const configPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
|
|
5692
|
+
if (import_fs7.default.existsSync(configPath) && !options.force) {
|
|
5693
|
+
console.log(import_chalk6.default.yellow(`\u2139\uFE0F Global config already exists: ${configPath}`));
|
|
5694
|
+
console.log(import_chalk6.default.gray(` Run with --force to overwrite.`));
|
|
4569
5695
|
return;
|
|
4570
5696
|
}
|
|
4571
5697
|
const requestedMode = options.mode.toLowerCase();
|
|
@@ -4577,13 +5703,13 @@ program.command("init").description("Create ~/.node9/config.json with default po
|
|
|
4577
5703
|
mode: safeMode
|
|
4578
5704
|
}
|
|
4579
5705
|
};
|
|
4580
|
-
const dir =
|
|
4581
|
-
if (!
|
|
4582
|
-
|
|
4583
|
-
console.log(
|
|
4584
|
-
console.log(
|
|
5706
|
+
const dir = import_path9.default.dirname(configPath);
|
|
5707
|
+
if (!import_fs7.default.existsSync(dir)) import_fs7.default.mkdirSync(dir, { recursive: true });
|
|
5708
|
+
import_fs7.default.writeFileSync(configPath, JSON.stringify(configToSave, null, 2));
|
|
5709
|
+
console.log(import_chalk6.default.green(`\u2705 Global config created: ${configPath}`));
|
|
5710
|
+
console.log(import_chalk6.default.cyan(` Mode set to: ${safeMode}`));
|
|
4585
5711
|
console.log(
|
|
4586
|
-
|
|
5712
|
+
import_chalk6.default.gray(` Undo Engine is ENABLED by default. Use 'node9 undo' to revert AI changes.`)
|
|
4587
5713
|
);
|
|
4588
5714
|
});
|
|
4589
5715
|
function formatRelativeTime(timestamp) {
|
|
@@ -4597,14 +5723,14 @@ function formatRelativeTime(timestamp) {
|
|
|
4597
5723
|
return new Date(timestamp).toLocaleDateString();
|
|
4598
5724
|
}
|
|
4599
5725
|
program.command("audit").description("View local execution audit log").option("--tail <n>", "Number of entries to show", "20").option("--tool <pattern>", "Filter by tool name (substring match)").option("--deny", "Show only denied actions").option("--json", "Output raw JSON").action((options) => {
|
|
4600
|
-
const logPath =
|
|
4601
|
-
if (!
|
|
5726
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
|
|
5727
|
+
if (!import_fs7.default.existsSync(logPath)) {
|
|
4602
5728
|
console.log(
|
|
4603
|
-
|
|
5729
|
+
import_chalk6.default.yellow("No audit logs found. Run node9 with an agent to generate entries.")
|
|
4604
5730
|
);
|
|
4605
5731
|
return;
|
|
4606
5732
|
}
|
|
4607
|
-
const raw =
|
|
5733
|
+
const raw = import_fs7.default.readFileSync(logPath, "utf-8");
|
|
4608
5734
|
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
4609
5735
|
let entries = lines.flatMap((line) => {
|
|
4610
5736
|
try {
|
|
@@ -4626,31 +5752,31 @@ program.command("audit").description("View local execution audit log").option("-
|
|
|
4626
5752
|
return;
|
|
4627
5753
|
}
|
|
4628
5754
|
if (entries.length === 0) {
|
|
4629
|
-
console.log(
|
|
5755
|
+
console.log(import_chalk6.default.yellow("No matching audit entries."));
|
|
4630
5756
|
return;
|
|
4631
5757
|
}
|
|
4632
5758
|
console.log(
|
|
4633
5759
|
`
|
|
4634
|
-
${
|
|
5760
|
+
${import_chalk6.default.bold("Node9 Audit Log")} ${import_chalk6.default.dim(`(${entries.length} entries)`)}`
|
|
4635
5761
|
);
|
|
4636
|
-
console.log(
|
|
5762
|
+
console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
|
|
4637
5763
|
console.log(
|
|
4638
5764
|
` ${"Time".padEnd(12)} ${"Tool".padEnd(18)} ${"Result".padEnd(10)} ${"By".padEnd(15)} Agent`
|
|
4639
5765
|
);
|
|
4640
|
-
console.log(
|
|
5766
|
+
console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
|
|
4641
5767
|
for (const e of entries) {
|
|
4642
5768
|
const time = formatRelativeTime(String(e.ts)).padEnd(12);
|
|
4643
5769
|
const tool = String(e.tool).slice(0, 17).padEnd(18);
|
|
4644
|
-
const result = e.decision === "allow" ?
|
|
5770
|
+
const result = e.decision === "allow" ? import_chalk6.default.green("ALLOW".padEnd(10)) : import_chalk6.default.red("DENY".padEnd(10));
|
|
4645
5771
|
const checker = String(e.checkedBy || "unknown").slice(0, 14).padEnd(15);
|
|
4646
5772
|
const agent = String(e.agent || "unknown");
|
|
4647
5773
|
console.log(` ${time} ${tool} ${result} ${checker} ${agent}`);
|
|
4648
5774
|
}
|
|
4649
5775
|
const allowed = entries.filter((e) => e.decision === "allow").length;
|
|
4650
5776
|
const denied = entries.filter((e) => e.decision === "deny").length;
|
|
4651
|
-
console.log(
|
|
5777
|
+
console.log(import_chalk6.default.dim(" " + "\u2500".repeat(65)));
|
|
4652
5778
|
console.log(
|
|
4653
|
-
` ${entries.length} entries | ${
|
|
5779
|
+
` ${entries.length} entries | ${import_chalk6.default.green(allowed + " allowed")} | ${import_chalk6.default.red(denied + " denied")}
|
|
4654
5780
|
`
|
|
4655
5781
|
);
|
|
4656
5782
|
});
|
|
@@ -4661,43 +5787,43 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4661
5787
|
const settings = mergedConfig.settings;
|
|
4662
5788
|
console.log("");
|
|
4663
5789
|
if (creds && settings.approvers.cloud) {
|
|
4664
|
-
console.log(
|
|
5790
|
+
console.log(import_chalk6.default.green(" \u25CF Agent mode") + import_chalk6.default.gray(" \u2014 cloud team policy enforced"));
|
|
4665
5791
|
} else if (creds && !settings.approvers.cloud) {
|
|
4666
5792
|
console.log(
|
|
4667
|
-
|
|
5793
|
+
import_chalk6.default.blue(" \u25CF Privacy mode \u{1F6E1}\uFE0F") + import_chalk6.default.gray(" \u2014 all decisions stay on this machine")
|
|
4668
5794
|
);
|
|
4669
5795
|
} else {
|
|
4670
5796
|
console.log(
|
|
4671
|
-
|
|
5797
|
+
import_chalk6.default.yellow(" \u25CB Privacy mode \u{1F6E1}\uFE0F") + import_chalk6.default.gray(" \u2014 no API key (Local rules only)")
|
|
4672
5798
|
);
|
|
4673
5799
|
}
|
|
4674
5800
|
console.log("");
|
|
4675
5801
|
if (daemonRunning) {
|
|
4676
5802
|
console.log(
|
|
4677
|
-
|
|
5803
|
+
import_chalk6.default.green(" \u25CF Daemon running") + import_chalk6.default.gray(` \u2192 http://127.0.0.1:${DAEMON_PORT2}/`)
|
|
4678
5804
|
);
|
|
4679
5805
|
} else {
|
|
4680
|
-
console.log(
|
|
5806
|
+
console.log(import_chalk6.default.gray(" \u25CB Daemon stopped"));
|
|
4681
5807
|
}
|
|
4682
5808
|
if (settings.enableUndo) {
|
|
4683
5809
|
console.log(
|
|
4684
|
-
|
|
5810
|
+
import_chalk6.default.magenta(" \u25CF Undo Engine") + import_chalk6.default.gray(` \u2192 Auto-snapshotting Git repos on AI change`)
|
|
4685
5811
|
);
|
|
4686
5812
|
}
|
|
4687
5813
|
console.log("");
|
|
4688
|
-
const modeLabel = settings.mode === "audit" ?
|
|
5814
|
+
const modeLabel = settings.mode === "audit" ? import_chalk6.default.blue("audit") : settings.mode === "strict" ? import_chalk6.default.red("strict") : import_chalk6.default.white("standard");
|
|
4689
5815
|
console.log(` Mode: ${modeLabel}`);
|
|
4690
|
-
const projectConfig =
|
|
4691
|
-
const globalConfig =
|
|
5816
|
+
const projectConfig = import_path9.default.join(process.cwd(), "node9.config.json");
|
|
5817
|
+
const globalConfig = import_path9.default.join(import_os7.default.homedir(), ".node9", "config.json");
|
|
4692
5818
|
console.log(
|
|
4693
|
-
` Local: ${
|
|
5819
|
+
` Local: ${import_fs7.default.existsSync(projectConfig) ? import_chalk6.default.green("Active (node9.config.json)") : import_chalk6.default.gray("Not present")}`
|
|
4694
5820
|
);
|
|
4695
5821
|
console.log(
|
|
4696
|
-
` Global: ${
|
|
5822
|
+
` Global: ${import_fs7.default.existsSync(globalConfig) ? import_chalk6.default.green("Active (~/.node9/config.json)") : import_chalk6.default.gray("Not present")}`
|
|
4697
5823
|
);
|
|
4698
5824
|
if (mergedConfig.policy.sandboxPaths.length > 0) {
|
|
4699
5825
|
console.log(
|
|
4700
|
-
` Sandbox: ${
|
|
5826
|
+
` Sandbox: ${import_chalk6.default.green(`${mergedConfig.policy.sandboxPaths.length} safe zones active`)}`
|
|
4701
5827
|
);
|
|
4702
5828
|
}
|
|
4703
5829
|
const pauseState = checkPause();
|
|
@@ -4705,47 +5831,63 @@ program.command("status").description("Show current Node9 mode, policy source, a
|
|
|
4705
5831
|
const expiresAt = pauseState.expiresAt ? new Date(pauseState.expiresAt).toLocaleTimeString() : "indefinitely";
|
|
4706
5832
|
console.log("");
|
|
4707
5833
|
console.log(
|
|
4708
|
-
|
|
5834
|
+
import_chalk6.default.yellow(` \u23F8 PAUSED until ${expiresAt}`) + import_chalk6.default.gray(" \u2014 all tool calls allowed")
|
|
4709
5835
|
);
|
|
4710
5836
|
}
|
|
4711
5837
|
console.log("");
|
|
4712
5838
|
});
|
|
4713
|
-
program.command("daemon").description("Run the local approval server").argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").
|
|
5839
|
+
program.command("daemon").description("Run the local approval server").argument("[action]", "start | stop | status (default: start)").option("-b, --background", "Start the daemon in the background (detached)").option("-o, --openui", "Start in background and open browser").option(
|
|
5840
|
+
"-w, --watch",
|
|
5841
|
+
"Start daemon + open browser, stay alive permanently (Flight Recorder mode)"
|
|
5842
|
+
).action(
|
|
4714
5843
|
async (action, options) => {
|
|
4715
5844
|
const cmd = (action ?? "start").toLowerCase();
|
|
4716
5845
|
if (cmd === "stop") return stopDaemon();
|
|
4717
5846
|
if (cmd === "status") return daemonStatus();
|
|
4718
5847
|
if (cmd !== "start" && action !== void 0) {
|
|
4719
|
-
console.error(
|
|
5848
|
+
console.error(import_chalk6.default.red(`Unknown daemon action: "${action}". Use: start | stop | status`));
|
|
4720
5849
|
process.exit(1);
|
|
4721
5850
|
}
|
|
5851
|
+
if (options.watch) {
|
|
5852
|
+
process.env.NODE9_WATCH_MODE = "1";
|
|
5853
|
+
setTimeout(() => {
|
|
5854
|
+
openBrowserLocal();
|
|
5855
|
+
console.log(import_chalk6.default.cyan(`\u{1F6F0}\uFE0F Flight Recorder: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
5856
|
+
}, 600);
|
|
5857
|
+
startDaemon();
|
|
5858
|
+
return;
|
|
5859
|
+
}
|
|
4722
5860
|
if (options.openui) {
|
|
4723
5861
|
if (isDaemonRunning()) {
|
|
4724
5862
|
openBrowserLocal();
|
|
4725
|
-
console.log(
|
|
5863
|
+
console.log(import_chalk6.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
|
|
4726
5864
|
process.exit(0);
|
|
4727
5865
|
}
|
|
4728
|
-
const child = (0,
|
|
5866
|
+
const child = (0, import_child_process5.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
4729
5867
|
child.unref();
|
|
4730
5868
|
for (let i = 0; i < 12; i++) {
|
|
4731
5869
|
await new Promise((r) => setTimeout(r, 250));
|
|
4732
5870
|
if (isDaemonRunning()) break;
|
|
4733
5871
|
}
|
|
4734
5872
|
openBrowserLocal();
|
|
4735
|
-
console.log(
|
|
5873
|
+
console.log(import_chalk6.default.green(`
|
|
4736
5874
|
\u{1F6E1}\uFE0F Node9 daemon started + browser opened`));
|
|
4737
5875
|
process.exit(0);
|
|
4738
5876
|
}
|
|
4739
5877
|
if (options.background) {
|
|
4740
|
-
const child = (0,
|
|
5878
|
+
const child = (0, import_child_process5.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
|
|
4741
5879
|
child.unref();
|
|
4742
|
-
console.log(
|
|
5880
|
+
console.log(import_chalk6.default.green(`
|
|
4743
5881
|
\u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
|
|
4744
5882
|
process.exit(0);
|
|
4745
5883
|
}
|
|
4746
5884
|
startDaemon();
|
|
4747
5885
|
}
|
|
4748
5886
|
);
|
|
5887
|
+
program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).action(async (options) => {
|
|
5888
|
+
const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
|
|
5889
|
+
await startTail2(options);
|
|
5890
|
+
});
|
|
4749
5891
|
program.command("check").description("Hook handler \u2014 evaluates a tool call before execution").argument("[data]", "JSON string of the tool call").action(async (data) => {
|
|
4750
5892
|
const processPayload = async (raw) => {
|
|
4751
5893
|
try {
|
|
@@ -4756,9 +5898,9 @@ program.command("check").description("Hook handler \u2014 evaluates a tool call
|
|
|
4756
5898
|
} catch (err) {
|
|
4757
5899
|
const tempConfig = getConfig();
|
|
4758
5900
|
if (process.env.NODE9_DEBUG === "1" || tempConfig.settings.enableHookLogDebug) {
|
|
4759
|
-
const logPath =
|
|
5901
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
|
|
4760
5902
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4761
|
-
|
|
5903
|
+
import_fs7.default.appendFileSync(
|
|
4762
5904
|
logPath,
|
|
4763
5905
|
`[${(/* @__PURE__ */ new Date()).toISOString()}] JSON_PARSE_ERROR: ${errMsg}
|
|
4764
5906
|
RAW: ${raw}
|
|
@@ -4776,10 +5918,10 @@ RAW: ${raw}
|
|
|
4776
5918
|
}
|
|
4777
5919
|
const config = getConfig();
|
|
4778
5920
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
4779
|
-
const logPath =
|
|
4780
|
-
if (!
|
|
4781
|
-
|
|
4782
|
-
|
|
5921
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
|
|
5922
|
+
if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
|
|
5923
|
+
import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
|
|
5924
|
+
import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
|
|
4783
5925
|
`);
|
|
4784
5926
|
}
|
|
4785
5927
|
const toolName = sanitize(payload.tool_name ?? payload.name ?? "");
|
|
@@ -4790,13 +5932,19 @@ RAW: ${raw}
|
|
|
4790
5932
|
const sendBlock = (msg, result2) => {
|
|
4791
5933
|
const blockedByContext = result2?.blockedByLabel || result2?.blockedBy || "Local Security Policy";
|
|
4792
5934
|
const isHumanDecision = blockedByContext.toLowerCase().includes("user") || blockedByContext.toLowerCase().includes("daemon") || blockedByContext.toLowerCase().includes("decision");
|
|
4793
|
-
|
|
5935
|
+
if (blockedByContext.includes("DLP") || blockedByContext.includes("Secret Detected") || blockedByContext.includes("Credential Review")) {
|
|
5936
|
+
console.error(import_chalk6.default.bgRed.white.bold(`
|
|
5937
|
+
\u{1F6A8} NODE9 DLP ALERT \u2014 CREDENTIAL DETECTED `));
|
|
5938
|
+
console.error(import_chalk6.default.red.bold(` A sensitive secret was found in the tool arguments!`));
|
|
5939
|
+
} else {
|
|
5940
|
+
console.error(import_chalk6.default.red(`
|
|
4794
5941
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
4795
|
-
|
|
4796
|
-
|
|
5942
|
+
}
|
|
5943
|
+
console.error(import_chalk6.default.gray(` Triggered by: ${blockedByContext}`));
|
|
5944
|
+
if (result2?.changeHint) console.error(import_chalk6.default.cyan(` To change: ${result2.changeHint}`));
|
|
4797
5945
|
console.error("");
|
|
4798
5946
|
const aiFeedbackMessage = buildNegotiationMessage(blockedByContext, isHumanDecision, msg);
|
|
4799
|
-
console.error(
|
|
5947
|
+
console.error(import_chalk6.default.dim(` (Detailed instructions sent to AI agent)`));
|
|
4800
5948
|
process.stdout.write(
|
|
4801
5949
|
JSON.stringify({
|
|
4802
5950
|
decision: "block",
|
|
@@ -4827,7 +5975,7 @@ RAW: ${raw}
|
|
|
4827
5975
|
process.exit(0);
|
|
4828
5976
|
}
|
|
4829
5977
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && !process.stdout.isTTY && config.settings.autoStartDaemon) {
|
|
4830
|
-
console.error(
|
|
5978
|
+
console.error(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
4831
5979
|
const daemonReady = await autoStartDaemonAndWait();
|
|
4832
5980
|
if (daemonReady) {
|
|
4833
5981
|
const retry = await authorizeHeadless(toolName, toolInput, false, meta);
|
|
@@ -4850,9 +5998,9 @@ RAW: ${raw}
|
|
|
4850
5998
|
});
|
|
4851
5999
|
} catch (err) {
|
|
4852
6000
|
if (process.env.NODE9_DEBUG === "1") {
|
|
4853
|
-
const logPath =
|
|
6001
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
|
|
4854
6002
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4855
|
-
|
|
6003
|
+
import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${errMsg}
|
|
4856
6004
|
`);
|
|
4857
6005
|
}
|
|
4858
6006
|
process.exit(0);
|
|
@@ -4897,10 +6045,10 @@ program.command("log").description("PostToolUse hook \u2014 records executed too
|
|
|
4897
6045
|
decision: "allowed",
|
|
4898
6046
|
source: "post-hook"
|
|
4899
6047
|
};
|
|
4900
|
-
const logPath =
|
|
4901
|
-
if (!
|
|
4902
|
-
|
|
4903
|
-
|
|
6048
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "audit.log");
|
|
6049
|
+
if (!import_fs7.default.existsSync(import_path9.default.dirname(logPath)))
|
|
6050
|
+
import_fs7.default.mkdirSync(import_path9.default.dirname(logPath), { recursive: true });
|
|
6051
|
+
import_fs7.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
4904
6052
|
const config = getConfig();
|
|
4905
6053
|
if (shouldSnapshot(tool, {}, config)) {
|
|
4906
6054
|
await createShadowSnapshot();
|
|
@@ -4927,7 +6075,7 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
4927
6075
|
const ms = parseDuration(options.duration);
|
|
4928
6076
|
if (ms === null) {
|
|
4929
6077
|
console.error(
|
|
4930
|
-
|
|
6078
|
+
import_chalk6.default.red(`
|
|
4931
6079
|
\u274C Invalid duration: "${options.duration}". Use format like 15m, 1h, 30s.
|
|
4932
6080
|
`)
|
|
4933
6081
|
);
|
|
@@ -4935,20 +6083,20 @@ program.command("pause").description("Temporarily disable Node9 protection for a
|
|
|
4935
6083
|
}
|
|
4936
6084
|
pauseNode9(ms, options.duration);
|
|
4937
6085
|
const expiresAt = new Date(Date.now() + ms).toLocaleTimeString();
|
|
4938
|
-
console.log(
|
|
6086
|
+
console.log(import_chalk6.default.yellow(`
|
|
4939
6087
|
\u23F8 Node9 paused until ${expiresAt}`));
|
|
4940
|
-
console.log(
|
|
4941
|
-
console.log(
|
|
6088
|
+
console.log(import_chalk6.default.gray(` All tool calls will be allowed without review.`));
|
|
6089
|
+
console.log(import_chalk6.default.gray(` Run "node9 resume" to re-enable early.
|
|
4942
6090
|
`));
|
|
4943
6091
|
});
|
|
4944
6092
|
program.command("resume").description("Re-enable Node9 protection immediately").action(() => {
|
|
4945
6093
|
const { paused } = checkPause();
|
|
4946
6094
|
if (!paused) {
|
|
4947
|
-
console.log(
|
|
6095
|
+
console.log(import_chalk6.default.gray("\nNode9 is already active \u2014 nothing to resume.\n"));
|
|
4948
6096
|
return;
|
|
4949
6097
|
}
|
|
4950
6098
|
resumeNode9();
|
|
4951
|
-
console.log(
|
|
6099
|
+
console.log(import_chalk6.default.green("\n\u25B6 Node9 resumed \u2014 protection is active.\n"));
|
|
4952
6100
|
});
|
|
4953
6101
|
var HOOK_BASED_AGENTS = {
|
|
4954
6102
|
claude: "claude",
|
|
@@ -4961,15 +6109,15 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
4961
6109
|
if (HOOK_BASED_AGENTS[firstArg] !== void 0) {
|
|
4962
6110
|
const target = HOOK_BASED_AGENTS[firstArg];
|
|
4963
6111
|
console.error(
|
|
4964
|
-
|
|
6112
|
+
import_chalk6.default.yellow(`
|
|
4965
6113
|
\u26A0\uFE0F Node9 proxy mode does not support "${target}" directly.`)
|
|
4966
6114
|
);
|
|
4967
|
-
console.error(
|
|
6115
|
+
console.error(import_chalk6.default.white(`
|
|
4968
6116
|
"${target}" uses its own hook system. Use:`));
|
|
4969
6117
|
console.error(
|
|
4970
|
-
|
|
6118
|
+
import_chalk6.default.green(` node9 addto ${target} `) + import_chalk6.default.gray("# one-time setup")
|
|
4971
6119
|
);
|
|
4972
|
-
console.error(
|
|
6120
|
+
console.error(import_chalk6.default.green(` ${target} `) + import_chalk6.default.gray("# run normally"));
|
|
4973
6121
|
process.exit(1);
|
|
4974
6122
|
}
|
|
4975
6123
|
const fullCommand = commandArgs.join(" ");
|
|
@@ -4977,7 +6125,7 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
4977
6125
|
agent: "Terminal"
|
|
4978
6126
|
});
|
|
4979
6127
|
if (result.noApprovalMechanism && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON && getConfig().settings.autoStartDaemon) {
|
|
4980
|
-
console.error(
|
|
6128
|
+
console.error(import_chalk6.default.cyan("\n\u{1F6E1}\uFE0F Node9: Starting approval daemon automatically..."));
|
|
4981
6129
|
const daemonReady = await autoStartDaemonAndWait();
|
|
4982
6130
|
if (daemonReady) result = await authorizeHeadless("shell", { command: fullCommand });
|
|
4983
6131
|
}
|
|
@@ -4986,12 +6134,12 @@ program.argument("[command...]", "The agent command to run (e.g., gemini)").acti
|
|
|
4986
6134
|
}
|
|
4987
6135
|
if (!result.approved) {
|
|
4988
6136
|
console.error(
|
|
4989
|
-
|
|
6137
|
+
import_chalk6.default.red(`
|
|
4990
6138
|
\u274C Node9 Blocked: ${result.reason || "Dangerous command detected."}`)
|
|
4991
6139
|
);
|
|
4992
6140
|
process.exit(1);
|
|
4993
6141
|
}
|
|
4994
|
-
console.error(
|
|
6142
|
+
console.error(import_chalk6.default.green("\n\u2705 Approved \u2014 running command...\n"));
|
|
4995
6143
|
await runProxy(fullCommand);
|
|
4996
6144
|
} else {
|
|
4997
6145
|
program.help();
|
|
@@ -5006,22 +6154,22 @@ program.command("undo").description(
|
|
|
5006
6154
|
if (history.length === 0) {
|
|
5007
6155
|
if (!options.all && allHistory.length > 0) {
|
|
5008
6156
|
console.log(
|
|
5009
|
-
|
|
6157
|
+
import_chalk6.default.yellow(
|
|
5010
6158
|
`
|
|
5011
6159
|
\u2139\uFE0F No snapshots found for the current directory (${process.cwd()}).
|
|
5012
|
-
Run ${
|
|
6160
|
+
Run ${import_chalk6.default.cyan("node9 undo --all")} to see snapshots from all projects.
|
|
5013
6161
|
`
|
|
5014
6162
|
)
|
|
5015
6163
|
);
|
|
5016
6164
|
} else {
|
|
5017
|
-
console.log(
|
|
6165
|
+
console.log(import_chalk6.default.yellow("\n\u2139\uFE0F No undo snapshots found.\n"));
|
|
5018
6166
|
}
|
|
5019
6167
|
return;
|
|
5020
6168
|
}
|
|
5021
6169
|
const idx = history.length - steps;
|
|
5022
6170
|
if (idx < 0) {
|
|
5023
6171
|
console.log(
|
|
5024
|
-
|
|
6172
|
+
import_chalk6.default.yellow(
|
|
5025
6173
|
`
|
|
5026
6174
|
\u2139\uFE0F Only ${history.length} snapshot(s) available, cannot go back ${steps}.
|
|
5027
6175
|
`
|
|
@@ -5032,18 +6180,18 @@ program.command("undo").description(
|
|
|
5032
6180
|
const snapshot = history[idx];
|
|
5033
6181
|
const age = Math.round((Date.now() - snapshot.timestamp) / 1e3);
|
|
5034
6182
|
const ageStr = age < 60 ? `${age}s ago` : age < 3600 ? `${Math.round(age / 60)}m ago` : `${Math.round(age / 3600)}h ago`;
|
|
5035
|
-
console.log(
|
|
6183
|
+
console.log(import_chalk6.default.magenta.bold(`
|
|
5036
6184
|
\u23EA Node9 Undo${steps > 1 ? ` (${steps} steps back)` : ""}`));
|
|
5037
6185
|
console.log(
|
|
5038
|
-
|
|
5039
|
-
` Tool: ${
|
|
6186
|
+
import_chalk6.default.white(
|
|
6187
|
+
` Tool: ${import_chalk6.default.cyan(snapshot.tool)}${snapshot.argsSummary ? import_chalk6.default.gray(" \u2192 " + snapshot.argsSummary) : ""}`
|
|
5040
6188
|
)
|
|
5041
6189
|
);
|
|
5042
|
-
console.log(
|
|
5043
|
-
console.log(
|
|
6190
|
+
console.log(import_chalk6.default.white(` When: ${import_chalk6.default.gray(ageStr)}`));
|
|
6191
|
+
console.log(import_chalk6.default.white(` Dir: ${import_chalk6.default.gray(snapshot.cwd)}`));
|
|
5044
6192
|
if (steps > 1)
|
|
5045
6193
|
console.log(
|
|
5046
|
-
|
|
6194
|
+
import_chalk6.default.yellow(` Note: This will also undo the ${steps - 1} action(s) after it.`)
|
|
5047
6195
|
);
|
|
5048
6196
|
console.log("");
|
|
5049
6197
|
const diff = computeUndoDiff(snapshot.hash, snapshot.cwd);
|
|
@@ -5051,21 +6199,21 @@ program.command("undo").description(
|
|
|
5051
6199
|
const lines = diff.split("\n");
|
|
5052
6200
|
for (const line of lines) {
|
|
5053
6201
|
if (line.startsWith("+++") || line.startsWith("---")) {
|
|
5054
|
-
console.log(
|
|
6202
|
+
console.log(import_chalk6.default.bold(line));
|
|
5055
6203
|
} else if (line.startsWith("+")) {
|
|
5056
|
-
console.log(
|
|
6204
|
+
console.log(import_chalk6.default.green(line));
|
|
5057
6205
|
} else if (line.startsWith("-")) {
|
|
5058
|
-
console.log(
|
|
6206
|
+
console.log(import_chalk6.default.red(line));
|
|
5059
6207
|
} else if (line.startsWith("@@")) {
|
|
5060
|
-
console.log(
|
|
6208
|
+
console.log(import_chalk6.default.cyan(line));
|
|
5061
6209
|
} else {
|
|
5062
|
-
console.log(
|
|
6210
|
+
console.log(import_chalk6.default.gray(line));
|
|
5063
6211
|
}
|
|
5064
6212
|
}
|
|
5065
6213
|
console.log("");
|
|
5066
6214
|
} else {
|
|
5067
6215
|
console.log(
|
|
5068
|
-
|
|
6216
|
+
import_chalk6.default.gray(" (no diff available \u2014 working tree may already match snapshot)\n")
|
|
5069
6217
|
);
|
|
5070
6218
|
}
|
|
5071
6219
|
const proceed = await (0, import_prompts3.confirm)({
|
|
@@ -5074,21 +6222,111 @@ program.command("undo").description(
|
|
|
5074
6222
|
});
|
|
5075
6223
|
if (proceed) {
|
|
5076
6224
|
if (applyUndo(snapshot.hash, snapshot.cwd)) {
|
|
5077
|
-
console.log(
|
|
6225
|
+
console.log(import_chalk6.default.green("\n\u2705 Reverted successfully.\n"));
|
|
5078
6226
|
} else {
|
|
5079
|
-
console.error(
|
|
6227
|
+
console.error(import_chalk6.default.red("\n\u274C Undo failed. Ensure you are in a Git repository.\n"));
|
|
5080
6228
|
}
|
|
5081
6229
|
} else {
|
|
5082
|
-
console.log(
|
|
6230
|
+
console.log(import_chalk6.default.gray("\nCancelled.\n"));
|
|
6231
|
+
}
|
|
6232
|
+
});
|
|
6233
|
+
var shieldCmd = program.command("shield").description("Manage pre-packaged security shield templates");
|
|
6234
|
+
shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
|
|
6235
|
+
const name = resolveShieldName(service);
|
|
6236
|
+
if (!name) {
|
|
6237
|
+
console.error(import_chalk6.default.red(`
|
|
6238
|
+
\u274C Unknown shield: "${service}"
|
|
6239
|
+
`));
|
|
6240
|
+
console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
|
|
6241
|
+
`);
|
|
6242
|
+
process.exit(1);
|
|
6243
|
+
}
|
|
6244
|
+
const shield = getShield(name);
|
|
6245
|
+
const active = readActiveShields();
|
|
6246
|
+
if (active.includes(name)) {
|
|
6247
|
+
console.log(import_chalk6.default.yellow(`
|
|
6248
|
+
\u2139\uFE0F Shield "${name}" is already active.
|
|
6249
|
+
`));
|
|
6250
|
+
return;
|
|
6251
|
+
}
|
|
6252
|
+
writeActiveShields([...active, name]);
|
|
6253
|
+
console.log(import_chalk6.default.green(`
|
|
6254
|
+
\u{1F6E1}\uFE0F Shield "${name}" enabled.`));
|
|
6255
|
+
console.log(import_chalk6.default.gray(` ${shield.smartRules.length} smart rules now active.`));
|
|
6256
|
+
if (shield.dangerousWords.length > 0)
|
|
6257
|
+
console.log(import_chalk6.default.gray(` ${shield.dangerousWords.length} dangerous words now active.`));
|
|
6258
|
+
if (name === "filesystem") {
|
|
6259
|
+
console.log(
|
|
6260
|
+
import_chalk6.default.yellow(
|
|
6261
|
+
`
|
|
6262
|
+
\u26A0\uFE0F Note: filesystem rules cover common rm -rf patterns but not all variants.
|
|
6263
|
+
Tools like unlink, find -delete, or language-level file ops are not intercepted.`
|
|
6264
|
+
)
|
|
6265
|
+
);
|
|
6266
|
+
}
|
|
6267
|
+
console.log("");
|
|
6268
|
+
});
|
|
6269
|
+
shieldCmd.command("disable <service>").description("Disable a security shield").action((service) => {
|
|
6270
|
+
const name = resolveShieldName(service);
|
|
6271
|
+
if (!name) {
|
|
6272
|
+
console.error(import_chalk6.default.red(`
|
|
6273
|
+
\u274C Unknown shield: "${service}"
|
|
6274
|
+
`));
|
|
6275
|
+
console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
|
|
6276
|
+
`);
|
|
6277
|
+
process.exit(1);
|
|
5083
6278
|
}
|
|
6279
|
+
const active = readActiveShields();
|
|
6280
|
+
if (!active.includes(name)) {
|
|
6281
|
+
console.log(import_chalk6.default.yellow(`
|
|
6282
|
+
\u2139\uFE0F Shield "${name}" is not active.
|
|
6283
|
+
`));
|
|
6284
|
+
return;
|
|
6285
|
+
}
|
|
6286
|
+
writeActiveShields(active.filter((s) => s !== name));
|
|
6287
|
+
console.log(import_chalk6.default.green(`
|
|
6288
|
+
\u{1F6E1}\uFE0F Shield "${name}" disabled.
|
|
6289
|
+
`));
|
|
6290
|
+
});
|
|
6291
|
+
shieldCmd.command("list").description("Show all available shields").action(() => {
|
|
6292
|
+
const active = new Set(readActiveShields());
|
|
6293
|
+
console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
|
|
6294
|
+
for (const shield of listShields()) {
|
|
6295
|
+
const status = active.has(shield.name) ? import_chalk6.default.green("\u25CF enabled") : import_chalk6.default.gray("\u25CB disabled");
|
|
6296
|
+
console.log(` ${status} ${import_chalk6.default.cyan(shield.name.padEnd(12))} ${shield.description}`);
|
|
6297
|
+
if (shield.aliases.length > 0)
|
|
6298
|
+
console.log(import_chalk6.default.gray(` aliases: ${shield.aliases.join(", ")}`));
|
|
6299
|
+
}
|
|
6300
|
+
console.log("");
|
|
6301
|
+
});
|
|
6302
|
+
shieldCmd.command("status").description("Show which shields are currently active").action(() => {
|
|
6303
|
+
const active = readActiveShields();
|
|
6304
|
+
if (active.length === 0) {
|
|
6305
|
+
console.log(import_chalk6.default.yellow("\n\u2139\uFE0F No shields are active.\n"));
|
|
6306
|
+
console.log(`Run ${import_chalk6.default.cyan("node9 shield list")} to see available shields.
|
|
6307
|
+
`);
|
|
6308
|
+
return;
|
|
6309
|
+
}
|
|
6310
|
+
console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Active Shields\n"));
|
|
6311
|
+
for (const name of active) {
|
|
6312
|
+
const shield = getShield(name);
|
|
6313
|
+
if (!shield) continue;
|
|
6314
|
+
console.log(` ${import_chalk6.default.green("\u25CF")} ${import_chalk6.default.cyan(name)}`);
|
|
6315
|
+
console.log(
|
|
6316
|
+
import_chalk6.default.gray(
|
|
6317
|
+
` ${shield.smartRules.length} smart rules \xB7 ${shield.dangerousWords.length} dangerous words`
|
|
6318
|
+
)
|
|
6319
|
+
);
|
|
6320
|
+
}
|
|
6321
|
+
console.log("");
|
|
5084
6322
|
});
|
|
5085
6323
|
process.on("unhandledRejection", (reason) => {
|
|
5086
6324
|
const isCheckHook = process.argv[2] === "check";
|
|
5087
6325
|
if (isCheckHook) {
|
|
5088
6326
|
if (process.env.NODE9_DEBUG === "1" || getConfig().settings.enableHookLogDebug) {
|
|
5089
|
-
const logPath =
|
|
6327
|
+
const logPath = import_path9.default.join(import_os7.default.homedir(), ".node9", "hook-debug.log");
|
|
5090
6328
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
5091
|
-
|
|
6329
|
+
import_fs7.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] UNHANDLED: ${msg}
|
|
5092
6330
|
`);
|
|
5093
6331
|
}
|
|
5094
6332
|
process.exit(0);
|