@node9/proxy 1.7.1 → 1.8.3
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 +50 -657
- package/dist/cli.js +404 -333
- package/dist/cli.mjs +396 -325
- package/dist/index.js +90 -260
- package/dist/index.mjs +90 -260
- package/dist/shields/builtin/aws.json +59 -0
- package/dist/shields/builtin/bash-safe.json +78 -0
- package/dist/shields/builtin/docker.json +120 -0
- package/dist/shields/builtin/filesystem.json +30 -0
- package/dist/shields/builtin/github.json +26 -0
- package/dist/shields/builtin/k8s.json +92 -0
- package/dist/shields/builtin/mongodb.json +78 -0
- package/dist/shields/builtin/postgres.json +42 -0
- package/dist/shields/builtin/redis.json +78 -0
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -250,6 +250,70 @@ import fs2 from "fs";
|
|
|
250
250
|
import path2 from "path";
|
|
251
251
|
import os2 from "os";
|
|
252
252
|
import crypto from "crypto";
|
|
253
|
+
function validateShieldDefinition(raw, filePath) {
|
|
254
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
255
|
+
process.stderr.write(`[node9] Shield file is not an object: ${filePath}
|
|
256
|
+
`);
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const r = raw;
|
|
260
|
+
if (typeof r.name !== "string" || !r.name) {
|
|
261
|
+
process.stderr.write(`[node9] Shield file missing 'name': ${filePath}
|
|
262
|
+
`);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
if (typeof r.description !== "string") {
|
|
266
|
+
process.stderr.write(`[node9] Shield file missing 'description': ${filePath}
|
|
267
|
+
`);
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
if (!Array.isArray(r.aliases)) {
|
|
271
|
+
process.stderr.write(`[node9] Shield file missing 'aliases' array: ${filePath}
|
|
272
|
+
`);
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
if (!Array.isArray(r.smartRules)) {
|
|
276
|
+
process.stderr.write(`[node9] Shield file missing 'smartRules' array: ${filePath}
|
|
277
|
+
`);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
if (!Array.isArray(r.dangerousWords)) {
|
|
281
|
+
process.stderr.write(`[node9] Shield file missing 'dangerousWords' array: ${filePath}
|
|
282
|
+
`);
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
return r;
|
|
286
|
+
}
|
|
287
|
+
function loadShieldsFromDir(dir, label) {
|
|
288
|
+
const result = {};
|
|
289
|
+
let entries;
|
|
290
|
+
try {
|
|
291
|
+
entries = fs2.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
292
|
+
} catch (err2) {
|
|
293
|
+
if (err2.code !== "ENOENT") {
|
|
294
|
+
process.stderr.write(`[node9] Could not read ${label} shields dir ${dir}: ${String(err2)}
|
|
295
|
+
`);
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
for (const file of entries) {
|
|
300
|
+
const filePath = path2.join(dir, file);
|
|
301
|
+
try {
|
|
302
|
+
const raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
303
|
+
const shield = validateShieldDefinition(raw, filePath);
|
|
304
|
+
if (shield) result[shield.name] = shield;
|
|
305
|
+
} catch (err2) {
|
|
306
|
+
process.stderr.write(`[node9] Failed to load ${label} shield ${file}: ${String(err2)}
|
|
307
|
+
`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
function buildSHIELDS() {
|
|
313
|
+
const builtins = loadShieldsFromDir(BUILTIN_DIR, "builtin");
|
|
314
|
+
const userShields = loadShieldsFromDir(USER_SHIELDS_DIR, "user");
|
|
315
|
+
return { ...builtins, ...userShields };
|
|
316
|
+
}
|
|
253
317
|
function resolveShieldName(input) {
|
|
254
318
|
const lower = input.toLowerCase();
|
|
255
319
|
if (SHIELDS[lower]) return lower;
|
|
@@ -356,255 +420,30 @@ function resolveShieldRule(shieldName, identifier) {
|
|
|
356
420
|
}
|
|
357
421
|
return null;
|
|
358
422
|
}
|
|
359
|
-
|
|
423
|
+
function installShield(name, shieldJson) {
|
|
424
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`Invalid shield name '${name}': only alphanumeric characters, hyphens, and underscores are allowed`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
const shield = validateShieldDefinition(shieldJson, `<downloaded:${name}>`);
|
|
430
|
+
if (!shield) throw new Error(`Downloaded shield '${name}' failed validation`);
|
|
431
|
+
if (shield.name !== name) {
|
|
432
|
+
throw new Error(`Shield name mismatch: file declares '${shield.name}' but expected '${name}'`);
|
|
433
|
+
}
|
|
434
|
+
fs2.mkdirSync(USER_SHIELDS_DIR, { recursive: true });
|
|
435
|
+
const filePath = path2.join(USER_SHIELDS_DIR, `${name}.json`);
|
|
436
|
+
const tmp = `${filePath}.${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
437
|
+
fs2.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
|
|
438
|
+
fs2.renameSync(tmp, filePath);
|
|
439
|
+
}
|
|
440
|
+
var BUILTIN_DIR, USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE;
|
|
360
441
|
var init_shields = __esm({
|
|
361
442
|
"src/shields.ts"() {
|
|
362
443
|
"use strict";
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
367
|
-
aliases: ["pg", "postgresql"],
|
|
368
|
-
smartRules: [
|
|
369
|
-
{
|
|
370
|
-
name: "shield:postgres:block-drop-table",
|
|
371
|
-
tool: "*",
|
|
372
|
-
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
373
|
-
verdict: "block",
|
|
374
|
-
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
name: "shield:postgres:block-truncate",
|
|
378
|
-
tool: "*",
|
|
379
|
-
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
380
|
-
verdict: "block",
|
|
381
|
-
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
382
|
-
},
|
|
383
|
-
{
|
|
384
|
-
name: "shield:postgres:block-drop-column",
|
|
385
|
-
tool: "*",
|
|
386
|
-
conditions: [
|
|
387
|
-
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
388
|
-
],
|
|
389
|
-
verdict: "block",
|
|
390
|
-
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
391
|
-
},
|
|
392
|
-
{
|
|
393
|
-
name: "shield:postgres:review-grant-revoke",
|
|
394
|
-
tool: "*",
|
|
395
|
-
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
396
|
-
verdict: "review",
|
|
397
|
-
reason: "Permission changes require human approval (Postgres shield)"
|
|
398
|
-
}
|
|
399
|
-
],
|
|
400
|
-
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
401
|
-
},
|
|
402
|
-
github: {
|
|
403
|
-
name: "github",
|
|
404
|
-
description: "Protects GitHub repositories from destructive AI operations",
|
|
405
|
-
aliases: ["git"],
|
|
406
|
-
smartRules: [
|
|
407
|
-
{
|
|
408
|
-
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
409
|
-
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
410
|
-
name: "shield:github:review-delete-branch-remote",
|
|
411
|
-
tool: "bash",
|
|
412
|
-
conditions: [
|
|
413
|
-
{
|
|
414
|
-
field: "command",
|
|
415
|
-
op: "matches",
|
|
416
|
-
value: "git\\s+push\\s+.*--delete",
|
|
417
|
-
flags: "i"
|
|
418
|
-
}
|
|
419
|
-
],
|
|
420
|
-
verdict: "review",
|
|
421
|
-
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
422
|
-
},
|
|
423
|
-
{
|
|
424
|
-
name: "shield:github:block-delete-repo",
|
|
425
|
-
tool: "*",
|
|
426
|
-
conditions: [
|
|
427
|
-
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
428
|
-
],
|
|
429
|
-
verdict: "block",
|
|
430
|
-
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
431
|
-
}
|
|
432
|
-
],
|
|
433
|
-
dangerousWords: []
|
|
434
|
-
},
|
|
435
|
-
aws: {
|
|
436
|
-
name: "aws",
|
|
437
|
-
description: "Protects AWS infrastructure from destructive AI operations",
|
|
438
|
-
aliases: ["amazon"],
|
|
439
|
-
smartRules: [
|
|
440
|
-
{
|
|
441
|
-
name: "shield:aws:block-delete-s3-bucket",
|
|
442
|
-
tool: "*",
|
|
443
|
-
conditions: [
|
|
444
|
-
{
|
|
445
|
-
field: "command",
|
|
446
|
-
op: "matches",
|
|
447
|
-
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
448
|
-
flags: "i"
|
|
449
|
-
}
|
|
450
|
-
],
|
|
451
|
-
verdict: "block",
|
|
452
|
-
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
453
|
-
},
|
|
454
|
-
{
|
|
455
|
-
name: "shield:aws:review-iam-changes",
|
|
456
|
-
tool: "*",
|
|
457
|
-
conditions: [
|
|
458
|
-
{
|
|
459
|
-
field: "command",
|
|
460
|
-
op: "matches",
|
|
461
|
-
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
462
|
-
flags: "i"
|
|
463
|
-
}
|
|
464
|
-
],
|
|
465
|
-
verdict: "review",
|
|
466
|
-
reason: "IAM changes require human approval (AWS shield)"
|
|
467
|
-
},
|
|
468
|
-
{
|
|
469
|
-
name: "shield:aws:block-ec2-terminate",
|
|
470
|
-
tool: "*",
|
|
471
|
-
conditions: [
|
|
472
|
-
{
|
|
473
|
-
field: "command",
|
|
474
|
-
op: "matches",
|
|
475
|
-
value: "aws\\s+ec2\\s+terminate-instances",
|
|
476
|
-
flags: "i"
|
|
477
|
-
}
|
|
478
|
-
],
|
|
479
|
-
verdict: "block",
|
|
480
|
-
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
481
|
-
},
|
|
482
|
-
{
|
|
483
|
-
name: "shield:aws:review-rds-delete",
|
|
484
|
-
tool: "*",
|
|
485
|
-
conditions: [
|
|
486
|
-
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
487
|
-
],
|
|
488
|
-
verdict: "review",
|
|
489
|
-
reason: "RDS deletion requires human approval (AWS shield)"
|
|
490
|
-
}
|
|
491
|
-
],
|
|
492
|
-
dangerousWords: []
|
|
493
|
-
},
|
|
494
|
-
"bash-safe": {
|
|
495
|
-
name: "bash-safe",
|
|
496
|
-
description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
|
|
497
|
-
aliases: ["bash", "shell"],
|
|
498
|
-
smartRules: [
|
|
499
|
-
{
|
|
500
|
-
name: "shield:bash-safe:block-pipe-to-shell",
|
|
501
|
-
tool: "bash",
|
|
502
|
-
conditions: [
|
|
503
|
-
{
|
|
504
|
-
field: "command",
|
|
505
|
-
op: "matches",
|
|
506
|
-
value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
|
|
507
|
-
flags: "i"
|
|
508
|
-
}
|
|
509
|
-
],
|
|
510
|
-
verdict: "block",
|
|
511
|
-
reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
|
|
512
|
-
},
|
|
513
|
-
{
|
|
514
|
-
name: "shield:bash-safe:block-obfuscated-exec",
|
|
515
|
-
tool: "bash",
|
|
516
|
-
conditions: [
|
|
517
|
-
{
|
|
518
|
-
field: "command",
|
|
519
|
-
op: "matches",
|
|
520
|
-
value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
|
|
521
|
-
flags: "i"
|
|
522
|
-
}
|
|
523
|
-
],
|
|
524
|
-
verdict: "block",
|
|
525
|
-
reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
|
|
526
|
-
},
|
|
527
|
-
{
|
|
528
|
-
name: "shield:bash-safe:block-rm-root",
|
|
529
|
-
tool: "bash",
|
|
530
|
-
conditions: [
|
|
531
|
-
{
|
|
532
|
-
field: "command",
|
|
533
|
-
op: "matches",
|
|
534
|
-
value: "rm\\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)[a-zA-Z]*\\s+(\\/|~|\\$HOME|\\$\\{HOME\\})\\s*$",
|
|
535
|
-
flags: "i"
|
|
536
|
-
}
|
|
537
|
-
],
|
|
538
|
-
verdict: "block",
|
|
539
|
-
reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
|
|
540
|
-
},
|
|
541
|
-
{
|
|
542
|
-
name: "shield:bash-safe:block-disk-overwrite",
|
|
543
|
-
tool: "bash",
|
|
544
|
-
conditions: [
|
|
545
|
-
{
|
|
546
|
-
field: "command",
|
|
547
|
-
op: "matches",
|
|
548
|
-
value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
|
|
549
|
-
flags: "i"
|
|
550
|
-
}
|
|
551
|
-
],
|
|
552
|
-
verdict: "block",
|
|
553
|
-
reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
|
|
554
|
-
},
|
|
555
|
-
{
|
|
556
|
-
name: "shield:bash-safe:review-eval",
|
|
557
|
-
tool: "bash",
|
|
558
|
-
conditions: [
|
|
559
|
-
{
|
|
560
|
-
field: "command",
|
|
561
|
-
op: "matches",
|
|
562
|
-
value: '\\beval\\s+[\\$`("]',
|
|
563
|
-
flags: "i"
|
|
564
|
-
}
|
|
565
|
-
],
|
|
566
|
-
verdict: "review",
|
|
567
|
-
reason: "eval of dynamic content requires human approval (bash-safe shield)"
|
|
568
|
-
}
|
|
569
|
-
],
|
|
570
|
-
dangerousWords: []
|
|
571
|
-
},
|
|
572
|
-
filesystem: {
|
|
573
|
-
name: "filesystem",
|
|
574
|
-
description: "Protects the local filesystem from dangerous AI operations",
|
|
575
|
-
aliases: ["fs"],
|
|
576
|
-
smartRules: [
|
|
577
|
-
{
|
|
578
|
-
name: "shield:filesystem:review-chmod-777",
|
|
579
|
-
tool: "bash",
|
|
580
|
-
conditions: [
|
|
581
|
-
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
582
|
-
],
|
|
583
|
-
verdict: "review",
|
|
584
|
-
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
585
|
-
},
|
|
586
|
-
{
|
|
587
|
-
name: "shield:filesystem:review-write-etc",
|
|
588
|
-
tool: "bash",
|
|
589
|
-
conditions: [
|
|
590
|
-
{
|
|
591
|
-
field: "command",
|
|
592
|
-
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
593
|
-
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
594
|
-
op: "matches",
|
|
595
|
-
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
596
|
-
}
|
|
597
|
-
],
|
|
598
|
-
verdict: "review",
|
|
599
|
-
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
600
|
-
}
|
|
601
|
-
],
|
|
602
|
-
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
603
|
-
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
604
|
-
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
605
|
-
dangerousWords: ["wipefs"]
|
|
606
|
-
}
|
|
607
|
-
};
|
|
444
|
+
BUILTIN_DIR = path2.join(__dirname, "shields", "builtin");
|
|
445
|
+
USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
|
|
446
|
+
SHIELDS = buildSHIELDS();
|
|
608
447
|
SHIELDS_STATE_FILE = path2.join(os2.homedir(), ".node9", "shields.json");
|
|
609
448
|
}
|
|
610
449
|
});
|
|
@@ -2581,7 +2420,7 @@ function isDaemonRunning() {
|
|
|
2581
2420
|
return false;
|
|
2582
2421
|
}
|
|
2583
2422
|
}
|
|
2584
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
|
|
2423
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched) {
|
|
2585
2424
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2586
2425
|
const ctrl = new AbortController();
|
|
2587
2426
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -2602,7 +2441,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2602
2441
|
...cwd && { cwd },
|
|
2603
2442
|
...recoveryCommand && { recoveryCommand },
|
|
2604
2443
|
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
2605
|
-
...viewOnly && { viewOnly: true }
|
|
2444
|
+
...viewOnly && { viewOnly: true },
|
|
2445
|
+
...localSmartRuleMatched && { localSmartRuleMatched: true }
|
|
2606
2446
|
}),
|
|
2607
2447
|
signal: ctrl.signal
|
|
2608
2448
|
});
|
|
@@ -3012,9 +2852,7 @@ end run`;
|
|
|
3012
2852
|
"--text",
|
|
3013
2853
|
pangoMessage,
|
|
3014
2854
|
"--ok-label",
|
|
3015
|
-
locked ? "Waiting..." : "Allow \u21B5"
|
|
3016
|
-
"--timeout",
|
|
3017
|
-
"300"
|
|
2855
|
+
locked ? "Waiting..." : "Allow \u21B5"
|
|
3018
2856
|
];
|
|
3019
2857
|
if (!locked) {
|
|
3020
2858
|
argsList.push("--cancel-label", "Block \u238B");
|
|
@@ -3292,6 +3130,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3292
3130
|
let policyMatchedWord;
|
|
3293
3131
|
let riskMetadata;
|
|
3294
3132
|
let statefulRecoveryCommand;
|
|
3133
|
+
let localSmartRuleMatched = false;
|
|
3295
3134
|
let taintWarning = null;
|
|
3296
3135
|
if (isNetworkTool(toolName, args)) {
|
|
3297
3136
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -3435,6 +3274,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3435
3274
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
3436
3275
|
policyMatchedField = policyResult.matchedField;
|
|
3437
3276
|
policyMatchedWord = policyResult.matchedWord;
|
|
3277
|
+
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
3438
3278
|
riskMetadata = computeRiskMetadata(
|
|
3439
3279
|
args,
|
|
3440
3280
|
policyResult.tier ?? 6,
|
|
@@ -3477,22 +3317,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3477
3317
|
}
|
|
3478
3318
|
let cloudRequestId = null;
|
|
3479
3319
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
3480
|
-
if (cloudEnforced) {
|
|
3320
|
+
if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3481
3321
|
try {
|
|
3482
3322
|
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
3483
3323
|
if (!initResult.pending) {
|
|
3484
3324
|
if (initResult.shadowMode) {
|
|
3485
3325
|
return { approved: true, checkedBy: "cloud" };
|
|
3486
3326
|
}
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3327
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3328
|
+
return {
|
|
3329
|
+
approved: !!initResult.approved,
|
|
3330
|
+
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
3331
|
+
checkedBy: initResult.approved ? "cloud" : void 0,
|
|
3332
|
+
blockedBy: initResult.approved ? void 0 : "team-policy",
|
|
3333
|
+
blockedByLabel: "Organization Policy (SaaS)"
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3338
|
+
cloudRequestId = initResult.requestId || null;
|
|
3494
3339
|
}
|
|
3495
|
-
cloudRequestId = initResult.requestId || null;
|
|
3496
3340
|
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
3497
3341
|
} catch {
|
|
3498
3342
|
}
|
|
@@ -3538,7 +3382,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3538
3382
|
riskMetadata,
|
|
3539
3383
|
options?.activityId,
|
|
3540
3384
|
options?.cwd,
|
|
3541
|
-
statefulRecoveryCommand
|
|
3385
|
+
statefulRecoveryCommand,
|
|
3386
|
+
void 0,
|
|
3387
|
+
void 0,
|
|
3388
|
+
localSmartRuleMatched || options?.localSmartRuleMatched
|
|
3542
3389
|
);
|
|
3543
3390
|
daemonEntryId = entry.id;
|
|
3544
3391
|
daemonAllowCount = entry.allowCount;
|
|
@@ -3546,7 +3393,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3546
3393
|
}
|
|
3547
3394
|
}
|
|
3548
3395
|
}
|
|
3549
|
-
if (cloudEnforced && cloudRequestId) {
|
|
3396
|
+
if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3550
3397
|
racePromises.push(
|
|
3551
3398
|
(async () => {
|
|
3552
3399
|
try {
|
|
@@ -5825,6 +5672,12 @@ function estimateToolCost(tool, args) {
|
|
|
5825
5672
|
const newStr = a.new_string ?? "";
|
|
5826
5673
|
return String(newStr).length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
|
|
5827
5674
|
}
|
|
5675
|
+
if (t === "bash" || t === "shell" || t === "run_shell_command" || t === "terminal_execute") {
|
|
5676
|
+
const command = String(a.command ?? a.cmd ?? a.input ?? "");
|
|
5677
|
+
if (command.length > 0) {
|
|
5678
|
+
return command.length / BYTES_PER_TOKEN / 1e6 * OUTPUT_PRICE_PER_1M;
|
|
5679
|
+
}
|
|
5680
|
+
}
|
|
5828
5681
|
return void 0;
|
|
5829
5682
|
}
|
|
5830
5683
|
function broadcast(event, data) {
|
|
@@ -6213,7 +6066,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6213
6066
|
viewOnly = false,
|
|
6214
6067
|
fromCLI = false,
|
|
6215
6068
|
activityId,
|
|
6216
|
-
cwd
|
|
6069
|
+
cwd,
|
|
6070
|
+
localSmartRuleMatched = false
|
|
6217
6071
|
} = JSON.parse(body);
|
|
6218
6072
|
const id = fromCLI && typeof activityId === "string" && activityId || randomUUID4();
|
|
6219
6073
|
const entry = {
|
|
@@ -6293,7 +6147,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6293
6147
|
agent: typeof agent === "string" ? agent : void 0,
|
|
6294
6148
|
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
|
|
6295
6149
|
},
|
|
6296
|
-
{ calledFromDaemon: true }
|
|
6150
|
+
{ calledFromDaemon: true, localSmartRuleMatched: !!localSmartRuleMatched }
|
|
6297
6151
|
).then((result) => {
|
|
6298
6152
|
const e = pending.get(id);
|
|
6299
6153
|
if (!e) return;
|
|
@@ -6898,7 +6752,7 @@ import fs25 from "fs";
|
|
|
6898
6752
|
import os21 from "os";
|
|
6899
6753
|
import path28 from "path";
|
|
6900
6754
|
import readline5 from "readline";
|
|
6901
|
-
import { spawn as
|
|
6755
|
+
import { spawn as spawn10, execSync as execSync3 } from "child_process";
|
|
6902
6756
|
function getIcon(tool) {
|
|
6903
6757
|
const t = tool.toLowerCase();
|
|
6904
6758
|
for (const [k, v] of Object.entries(ICONS)) {
|
|
@@ -6955,7 +6809,7 @@ async function ensureDaemon() {
|
|
|
6955
6809
|
} catch {
|
|
6956
6810
|
}
|
|
6957
6811
|
console.log(chalk17.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
|
|
6958
|
-
const child =
|
|
6812
|
+
const child = spawn10(process.execPath, [process.argv[1], "daemon"], {
|
|
6959
6813
|
detached: true,
|
|
6960
6814
|
stdio: "ignore",
|
|
6961
6815
|
env: { ...process.env, NODE9_AUTO_STARTED: "1" }
|
|
@@ -7129,6 +6983,7 @@ async function startTail(options = {}) {
|
|
|
7129
6983
|
return;
|
|
7130
6984
|
}
|
|
7131
6985
|
const connectionTime = Date.now();
|
|
6986
|
+
let initialReplayDone = false;
|
|
7132
6987
|
const activityPending = /* @__PURE__ */ new Map();
|
|
7133
6988
|
const orphanedResults = /* @__PURE__ */ new Map();
|
|
7134
6989
|
let csrfToken = "";
|
|
@@ -7460,11 +7315,17 @@ async function startTail(options = {}) {
|
|
|
7460
7315
|
return;
|
|
7461
7316
|
}
|
|
7462
7317
|
if (event === "activity") {
|
|
7318
|
+
const isReplayEvent = data.status && data.status !== "pending";
|
|
7319
|
+
if (isReplayEvent && !initialReplayDone) {
|
|
7320
|
+
renderResult(data, data);
|
|
7321
|
+
return;
|
|
7322
|
+
}
|
|
7463
7323
|
if (!options.history && data.ts > 0 && data.ts < connectionTime) return;
|
|
7464
|
-
if (
|
|
7324
|
+
if (isReplayEvent) {
|
|
7465
7325
|
renderResult(data, data);
|
|
7466
7326
|
return;
|
|
7467
7327
|
}
|
|
7328
|
+
if (!initialReplayDone) initialReplayDone = true;
|
|
7468
7329
|
const orphaned = orphanedResults.get(data.id);
|
|
7469
7330
|
if (orphaned) {
|
|
7470
7331
|
orphanedResults.delete(data.id);
|
|
@@ -7845,6 +7706,24 @@ function renderContextLine(stdin) {
|
|
|
7845
7706
|
async function main() {
|
|
7846
7707
|
try {
|
|
7847
7708
|
const [stdin, daemonStatus2] = await Promise.all([readStdin(), queryDaemon()]);
|
|
7709
|
+
if (fs26.existsSync(path29.join(os22.homedir(), ".node9", "hud-debug"))) {
|
|
7710
|
+
try {
|
|
7711
|
+
const logPath = path29.join(os22.homedir(), ".node9", "hud-debug.log");
|
|
7712
|
+
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
7713
|
+
let size = 0;
|
|
7714
|
+
try {
|
|
7715
|
+
size = fs26.statSync(logPath).size;
|
|
7716
|
+
} catch {
|
|
7717
|
+
}
|
|
7718
|
+
if (size < MAX_LOG_SIZE) {
|
|
7719
|
+
fs26.appendFileSync(
|
|
7720
|
+
logPath,
|
|
7721
|
+
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), stdin }) + "\n"
|
|
7722
|
+
);
|
|
7723
|
+
}
|
|
7724
|
+
} catch {
|
|
7725
|
+
}
|
|
7726
|
+
}
|
|
7848
7727
|
if (!daemonStatus2) {
|
|
7849
7728
|
renderOffline();
|
|
7850
7729
|
return;
|
|
@@ -7957,7 +7836,7 @@ function isNode9Hook(cmd) {
|
|
|
7957
7836
|
function teardownClaude() {
|
|
7958
7837
|
const homeDir2 = os10.homedir();
|
|
7959
7838
|
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
7960
|
-
const mcpPath = path14.join(homeDir2, ".claude.json");
|
|
7839
|
+
const mcpPath = path14.join(homeDir2, ".claude", ".mcp.json");
|
|
7961
7840
|
let changed = false;
|
|
7962
7841
|
const settings = readJson(hooksPath);
|
|
7963
7842
|
if (settings?.hooks) {
|
|
@@ -7983,11 +7862,12 @@ function teardownClaude() {
|
|
|
7983
7862
|
let mcpChanged = false;
|
|
7984
7863
|
if (removeNode9McpServer(claudeConfig.mcpServers)) {
|
|
7985
7864
|
mcpChanged = true;
|
|
7986
|
-
console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.claude.json"));
|
|
7865
|
+
console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.claude/.mcp.json"));
|
|
7987
7866
|
}
|
|
7988
7867
|
for (const [name, server] of Object.entries(claudeConfig.mcpServers)) {
|
|
7989
|
-
|
|
7990
|
-
|
|
7868
|
+
const args = server.args;
|
|
7869
|
+
if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
|
|
7870
|
+
const [originalCmd, ...originalArgs] = args[2].split(" ");
|
|
7991
7871
|
claudeConfig.mcpServers[name] = {
|
|
7992
7872
|
...server,
|
|
7993
7873
|
command: originalCmd,
|
|
@@ -7995,16 +7875,11 @@ function teardownClaude() {
|
|
|
7995
7875
|
};
|
|
7996
7876
|
mcpChanged = true;
|
|
7997
7877
|
} else if (server.command === "node9") {
|
|
7998
|
-
console.warn(
|
|
7999
|
-
chalk.yellow(
|
|
8000
|
-
` \u26A0\uFE0F Cannot unwrap MCP server "${name}" in ~/.claude.json \u2014 args is empty. Remove it manually.`
|
|
8001
|
-
)
|
|
8002
|
-
);
|
|
8003
7878
|
}
|
|
8004
7879
|
}
|
|
8005
7880
|
if (mcpChanged) {
|
|
8006
7881
|
writeJson(mcpPath, claudeConfig);
|
|
8007
|
-
console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.claude.json"));
|
|
7882
|
+
console.log(chalk.green(" \u2705 Unwrapped MCP servers in ~/.claude/.mcp.json"));
|
|
8008
7883
|
}
|
|
8009
7884
|
}
|
|
8010
7885
|
}
|
|
@@ -8033,8 +7908,9 @@ function teardownGemini() {
|
|
|
8033
7908
|
console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.gemini/settings.json"));
|
|
8034
7909
|
}
|
|
8035
7910
|
for (const [name, server] of Object.entries(settings.mcpServers)) {
|
|
8036
|
-
|
|
8037
|
-
|
|
7911
|
+
const args = server.args;
|
|
7912
|
+
if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
|
|
7913
|
+
const [originalCmd, ...originalArgs] = args[2].split(" ");
|
|
8038
7914
|
settings.mcpServers[name] = {
|
|
8039
7915
|
...server,
|
|
8040
7916
|
command: originalCmd,
|
|
@@ -8065,8 +7941,9 @@ function teardownCursor() {
|
|
|
8065
7941
|
console.log(chalk.green(" \u2705 Removed node9 MCP server entry from ~/.cursor/mcp.json"));
|
|
8066
7942
|
}
|
|
8067
7943
|
for (const [name, server] of Object.entries(mcpConfig.mcpServers)) {
|
|
8068
|
-
|
|
8069
|
-
|
|
7944
|
+
const args = server.args;
|
|
7945
|
+
if (server.command === "node9" && Array.isArray(args) && args[0] === "mcp" && args[1] === "--upstream" && typeof args[2] === "string") {
|
|
7946
|
+
const [originalCmd, ...originalArgs] = args[2].split(" ");
|
|
8070
7947
|
mcpConfig.mcpServers[name] = {
|
|
8071
7948
|
...server,
|
|
8072
7949
|
command: originalCmd,
|
|
@@ -8084,7 +7961,7 @@ function teardownCursor() {
|
|
|
8084
7961
|
}
|
|
8085
7962
|
async function setupClaude() {
|
|
8086
7963
|
const homeDir2 = os10.homedir();
|
|
8087
|
-
const mcpPath = path14.join(homeDir2, ".claude.json");
|
|
7964
|
+
const mcpPath = path14.join(homeDir2, ".claude", ".mcp.json");
|
|
8088
7965
|
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
8089
7966
|
const claudeConfig = readJson(mcpPath) ?? {};
|
|
8090
7967
|
const settings = readJson(hooksPath) ?? {};
|
|
@@ -8099,7 +7976,7 @@ async function setupClaude() {
|
|
|
8099
7976
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
8100
7977
|
settings.hooks.PreToolUse.push({
|
|
8101
7978
|
matcher: ".*",
|
|
8102
|
-
hooks: [{ type: "command", command: fullPathCommand("check"), timeout:
|
|
7979
|
+
hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 600 }]
|
|
8103
7980
|
});
|
|
8104
7981
|
console.log(chalk.green(" \u2705 PreToolUse hook added \u2192 node9 check"));
|
|
8105
7982
|
hooksChanged = true;
|
|
@@ -8125,6 +8002,15 @@ async function setupClaude() {
|
|
|
8125
8002
|
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8126
8003
|
anythingChanged = true;
|
|
8127
8004
|
}
|
|
8005
|
+
const hudCommand = fullPathCommand("hud");
|
|
8006
|
+
const statusLineObj = { type: "command", command: hudCommand };
|
|
8007
|
+
const existingStatusLine = settings.statusLine;
|
|
8008
|
+
const existingStatusCommand = typeof existingStatusLine === "object" ? existingStatusLine?.command : existingStatusLine;
|
|
8009
|
+
if (existingStatusCommand !== hudCommand) {
|
|
8010
|
+
settings.statusLine = statusLineObj;
|
|
8011
|
+
hooksChanged = true;
|
|
8012
|
+
anythingChanged = true;
|
|
8013
|
+
}
|
|
8128
8014
|
if (hooksChanged) {
|
|
8129
8015
|
writeJson(hooksPath, settings);
|
|
8130
8016
|
console.log("");
|
|
@@ -8132,20 +8018,24 @@ async function setupClaude() {
|
|
|
8132
8018
|
const serversToWrap = [];
|
|
8133
8019
|
for (const [name, server] of Object.entries(servers)) {
|
|
8134
8020
|
if (!server.command || server.command === "node9") continue;
|
|
8135
|
-
const
|
|
8136
|
-
serversToWrap.push({ name,
|
|
8021
|
+
const upstream = [server.command, ...server.args ?? []].join(" ");
|
|
8022
|
+
serversToWrap.push({ name, upstream });
|
|
8137
8023
|
}
|
|
8138
8024
|
if (serversToWrap.length > 0) {
|
|
8139
8025
|
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
8140
8026
|
console.log(chalk.white(` ${mcpPath}`));
|
|
8141
|
-
for (const { name,
|
|
8142
|
-
console.log(chalk.gray(` \u2022 ${name}: "${
|
|
8027
|
+
for (const { name, upstream } of serversToWrap) {
|
|
8028
|
+
console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
8143
8029
|
}
|
|
8144
8030
|
console.log("");
|
|
8145
8031
|
const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
|
|
8146
8032
|
if (proceed) {
|
|
8147
|
-
for (const { name,
|
|
8148
|
-
servers[name] = {
|
|
8033
|
+
for (const { name, upstream } of serversToWrap) {
|
|
8034
|
+
servers[name] = {
|
|
8035
|
+
...servers[name],
|
|
8036
|
+
command: "node9",
|
|
8037
|
+
args: ["mcp", "--upstream", upstream]
|
|
8038
|
+
};
|
|
8149
8039
|
}
|
|
8150
8040
|
claudeConfig.mcpServers = servers;
|
|
8151
8041
|
writeJson(mcpPath, claudeConfig);
|
|
@@ -8225,20 +8115,24 @@ async function setupGemini() {
|
|
|
8225
8115
|
const serversToWrap = [];
|
|
8226
8116
|
for (const [name, server] of Object.entries(servers)) {
|
|
8227
8117
|
if (!server.command || server.command === "node9") continue;
|
|
8228
|
-
const
|
|
8229
|
-
serversToWrap.push({ name,
|
|
8118
|
+
const upstream = [server.command, ...server.args ?? []].join(" ");
|
|
8119
|
+
serversToWrap.push({ name, upstream });
|
|
8230
8120
|
}
|
|
8231
8121
|
if (serversToWrap.length > 0) {
|
|
8232
8122
|
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
8233
8123
|
console.log(chalk.white(` ${settingsPath} (mcpServers)`));
|
|
8234
|
-
for (const { name,
|
|
8235
|
-
console.log(chalk.gray(` \u2022 ${name}: "${
|
|
8124
|
+
for (const { name, upstream } of serversToWrap) {
|
|
8125
|
+
console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
8236
8126
|
}
|
|
8237
8127
|
console.log("");
|
|
8238
8128
|
const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
|
|
8239
8129
|
if (proceed) {
|
|
8240
|
-
for (const { name,
|
|
8241
|
-
servers[name] = {
|
|
8130
|
+
for (const { name, upstream } of serversToWrap) {
|
|
8131
|
+
servers[name] = {
|
|
8132
|
+
...servers[name],
|
|
8133
|
+
command: "node9",
|
|
8134
|
+
args: ["mcp", "--upstream", upstream]
|
|
8135
|
+
};
|
|
8242
8136
|
}
|
|
8243
8137
|
settings.mcpServers = servers;
|
|
8244
8138
|
writeJson(settingsPath, settings);
|
|
@@ -8297,20 +8191,24 @@ async function setupCursor() {
|
|
|
8297
8191
|
const serversToWrap = [];
|
|
8298
8192
|
for (const [name, server] of Object.entries(servers)) {
|
|
8299
8193
|
if (!server.command || server.command === "node9") continue;
|
|
8300
|
-
const
|
|
8301
|
-
serversToWrap.push({ name,
|
|
8194
|
+
const upstream = [server.command, ...server.args ?? []].join(" ");
|
|
8195
|
+
serversToWrap.push({ name, upstream });
|
|
8302
8196
|
}
|
|
8303
8197
|
if (serversToWrap.length > 0) {
|
|
8304
8198
|
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
8305
8199
|
console.log(chalk.white(` ${mcpPath}`));
|
|
8306
|
-
for (const { name,
|
|
8307
|
-
console.log(chalk.gray(` \u2022 ${name}: "${
|
|
8200
|
+
for (const { name, upstream } of serversToWrap) {
|
|
8201
|
+
console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
8308
8202
|
}
|
|
8309
8203
|
console.log("");
|
|
8310
8204
|
const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
|
|
8311
8205
|
if (proceed) {
|
|
8312
|
-
for (const { name,
|
|
8313
|
-
servers[name] = {
|
|
8206
|
+
for (const { name, upstream } of serversToWrap) {
|
|
8207
|
+
servers[name] = {
|
|
8208
|
+
...servers[name],
|
|
8209
|
+
command: "node9",
|
|
8210
|
+
args: ["mcp", "--upstream", upstream]
|
|
8211
|
+
};
|
|
8314
8212
|
}
|
|
8315
8213
|
mcpConfig.mcpServers = servers;
|
|
8316
8214
|
writeJson(mcpPath, mcpConfig);
|
|
@@ -8373,20 +8271,24 @@ async function setupCodex() {
|
|
|
8373
8271
|
const serversToWrap = [];
|
|
8374
8272
|
for (const [name, server] of Object.entries(servers)) {
|
|
8375
8273
|
if (!server.command || server.command === "node9") continue;
|
|
8376
|
-
const
|
|
8377
|
-
serversToWrap.push({ name,
|
|
8274
|
+
const upstream = [server.command, ...server.args ?? []].join(" ");
|
|
8275
|
+
serversToWrap.push({ name, upstream });
|
|
8378
8276
|
}
|
|
8379
8277
|
if (serversToWrap.length > 0) {
|
|
8380
8278
|
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
8381
8279
|
console.log(chalk.white(` ${configPath}`));
|
|
8382
|
-
for (const { name,
|
|
8383
|
-
console.log(chalk.gray(` \u2022 ${name}: "${
|
|
8280
|
+
for (const { name, upstream } of serversToWrap) {
|
|
8281
|
+
console.log(chalk.gray(` \u2022 ${name}: "${upstream}" \u2192 node9 mcp --upstream "${upstream}"`));
|
|
8384
8282
|
}
|
|
8385
8283
|
console.log("");
|
|
8386
8284
|
const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
|
|
8387
8285
|
if (proceed) {
|
|
8388
|
-
for (const { name,
|
|
8389
|
-
servers[name] = {
|
|
8286
|
+
for (const { name, upstream } of serversToWrap) {
|
|
8287
|
+
servers[name] = {
|
|
8288
|
+
...servers[name],
|
|
8289
|
+
command: "node9",
|
|
8290
|
+
args: ["mcp", "--upstream", upstream]
|
|
8291
|
+
};
|
|
8390
8292
|
}
|
|
8391
8293
|
config.mcp_servers = servers;
|
|
8392
8294
|
writeToml(configPath, config);
|
|
@@ -8575,18 +8477,20 @@ async function runProxy(targetCommand) {
|
|
|
8575
8477
|
const cmd = commandParts[0];
|
|
8576
8478
|
const args = commandParts.slice(1);
|
|
8577
8479
|
let executable = cmd;
|
|
8480
|
+
let useShell = false;
|
|
8578
8481
|
try {
|
|
8579
8482
|
const { stdout } = await execa("which", [cmd]);
|
|
8580
8483
|
if (stdout) executable = stdout.trim();
|
|
8581
8484
|
} catch {
|
|
8485
|
+
useShell = true;
|
|
8582
8486
|
}
|
|
8583
8487
|
console.error(chalk4.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
|
|
8584
|
-
const
|
|
8488
|
+
const spawnEnv = { ...process.env, FORCE_COLOR: "1" };
|
|
8489
|
+
const child = useShell ? spawn3("/bin/bash", ["-c", targetCommand], {
|
|
8585
8490
|
stdio: ["pipe", "pipe", "inherit"],
|
|
8586
|
-
// We control STDIN and STDOUT
|
|
8587
8491
|
shell: false,
|
|
8588
|
-
env:
|
|
8589
|
-
});
|
|
8492
|
+
env: spawnEnv
|
|
8493
|
+
}) : spawn3(executable, args, { stdio: ["pipe", "pipe", "inherit"], shell: false, env: spawnEnv });
|
|
8590
8494
|
const agentIn = readline.createInterface({ input: process.stdin, terminal: false });
|
|
8591
8495
|
agentIn.on("line", async (line) => {
|
|
8592
8496
|
let message;
|
|
@@ -8699,6 +8603,7 @@ init_config();
|
|
|
8699
8603
|
init_policy();
|
|
8700
8604
|
import chalk5 from "chalk";
|
|
8701
8605
|
import fs18 from "fs";
|
|
8606
|
+
import { spawn as spawn6 } from "child_process";
|
|
8702
8607
|
import path20 from "path";
|
|
8703
8608
|
import os14 from "os";
|
|
8704
8609
|
|
|
@@ -9078,6 +8983,37 @@ RAW: ${raw}
|
|
|
9078
8983
|
process.exit(0);
|
|
9079
8984
|
}
|
|
9080
8985
|
const config = getConfig(payload.cwd || void 0);
|
|
8986
|
+
if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
|
|
8987
|
+
try {
|
|
8988
|
+
const scriptPath = process.argv[1];
|
|
8989
|
+
if (typeof scriptPath !== "string" || !path20.isAbsolute(scriptPath))
|
|
8990
|
+
throw new Error("node9: argv[1] is not an absolute path");
|
|
8991
|
+
const resolvedScript = fs18.realpathSync(scriptPath);
|
|
8992
|
+
const expectedCli = fs18.realpathSync(path20.resolve(__dirname, "../../cli.js"));
|
|
8993
|
+
if (resolvedScript !== expectedCli)
|
|
8994
|
+
throw new Error(
|
|
8995
|
+
"node9: daemon spawn aborted \u2014 argv[1] does not resolve to the node9 CLI"
|
|
8996
|
+
);
|
|
8997
|
+
const safeEnv = { ...process.env };
|
|
8998
|
+
for (const key of [
|
|
8999
|
+
"NODE_OPTIONS",
|
|
9000
|
+
"LD_PRELOAD",
|
|
9001
|
+
"LD_LIBRARY_PATH",
|
|
9002
|
+
"DYLD_INSERT_LIBRARIES",
|
|
9003
|
+
"NODE_PATH",
|
|
9004
|
+
"ELECTRON_RUN_AS_NODE"
|
|
9005
|
+
]) {
|
|
9006
|
+
delete safeEnv[key];
|
|
9007
|
+
}
|
|
9008
|
+
const d = spawn6(process.execPath, [scriptPath, "daemon"], {
|
|
9009
|
+
detached: true,
|
|
9010
|
+
stdio: "ignore",
|
|
9011
|
+
env: { ...safeEnv, NODE9_AUTO_STARTED: "1", NODE9_BROWSER_OPENED: "1" }
|
|
9012
|
+
});
|
|
9013
|
+
d.unref();
|
|
9014
|
+
} catch {
|
|
9015
|
+
}
|
|
9016
|
+
}
|
|
9081
9017
|
if (process.env.NODE9_DEBUG === "1" || config.settings.enableHookLogDebug) {
|
|
9082
9018
|
const logPath = path20.join(os14.homedir(), ".node9", "hook-debug.log");
|
|
9083
9019
|
if (!fs18.existsSync(path20.dirname(logPath)))
|
|
@@ -9136,7 +9072,7 @@ RAW: ${raw}
|
|
|
9136
9072
|
}
|
|
9137
9073
|
}) + "\n"
|
|
9138
9074
|
);
|
|
9139
|
-
process.exit(
|
|
9075
|
+
process.exit(2);
|
|
9140
9076
|
};
|
|
9141
9077
|
if (!toolName) {
|
|
9142
9078
|
sendBlock("Node9: unrecognised hook payload \u2014 tool name missing.");
|
|
@@ -9371,6 +9307,27 @@ init_shields();
|
|
|
9371
9307
|
init_audit();
|
|
9372
9308
|
init_config();
|
|
9373
9309
|
import chalk6 from "chalk";
|
|
9310
|
+
|
|
9311
|
+
// src/utils/https-fetch.ts
|
|
9312
|
+
import https from "https";
|
|
9313
|
+
function httpsFetch(url) {
|
|
9314
|
+
return new Promise((resolve, reject) => {
|
|
9315
|
+
https.get(url, (res) => {
|
|
9316
|
+
if (res.statusCode !== 200) {
|
|
9317
|
+
reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
|
|
9318
|
+
res.resume();
|
|
9319
|
+
return;
|
|
9320
|
+
}
|
|
9321
|
+
const chunks = [];
|
|
9322
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
9323
|
+
res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
9324
|
+
res.on("error", reject);
|
|
9325
|
+
}).on("error", reject);
|
|
9326
|
+
});
|
|
9327
|
+
}
|
|
9328
|
+
|
|
9329
|
+
// src/cli/commands/shield.ts
|
|
9330
|
+
var COMMUNITY_INDEX_URL = "https://raw.githubusercontent.com/node9ai/node9-proxy/main/shields/community/index.json";
|
|
9374
9331
|
function registerShieldCommand(program2) {
|
|
9375
9332
|
const shieldCmd = program2.command("shield").description("Manage pre-packaged security shield templates");
|
|
9376
9333
|
shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
|
|
@@ -9430,7 +9387,32 @@ function registerShieldCommand(program2) {
|
|
|
9430
9387
|
\u{1F6E1}\uFE0F Shield "${name}" disabled.
|
|
9431
9388
|
`));
|
|
9432
9389
|
});
|
|
9433
|
-
shieldCmd.command("list").description("Show
|
|
9390
|
+
shieldCmd.command("list").description("Show available shields (add --community to browse the marketplace)").option("--community", "List shields available from the community marketplace").action((opts) => {
|
|
9391
|
+
if (opts.community) {
|
|
9392
|
+
console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Community Shield Marketplace\n"));
|
|
9393
|
+
console.log(chalk6.gray(" Fetching index\u2026\n"));
|
|
9394
|
+
httpsFetch(COMMUNITY_INDEX_URL).then((body) => {
|
|
9395
|
+
const entries = JSON.parse(body);
|
|
9396
|
+
const installed = new Set(listShields().map((s) => s.name));
|
|
9397
|
+
for (const e of entries) {
|
|
9398
|
+
const tag = installed.has(e.name) ? chalk6.green("installed") : chalk6.gray("available");
|
|
9399
|
+
console.log(
|
|
9400
|
+
` ${tag} ${chalk6.cyan(e.name.padEnd(12))} ${e.description} ${chalk6.gray(`by ${e.author}`)}`
|
|
9401
|
+
);
|
|
9402
|
+
}
|
|
9403
|
+
console.log("");
|
|
9404
|
+
console.log(
|
|
9405
|
+
chalk6.gray(` Install a shield: ${chalk6.cyan("node9 shield install <name>")}
|
|
9406
|
+
`)
|
|
9407
|
+
);
|
|
9408
|
+
}).catch((err2) => {
|
|
9409
|
+
console.error(chalk6.red(`
|
|
9410
|
+
\u274C Could not fetch community index: ${String(err2)}
|
|
9411
|
+
`));
|
|
9412
|
+
process.exit(1);
|
|
9413
|
+
});
|
|
9414
|
+
return;
|
|
9415
|
+
}
|
|
9434
9416
|
const active = new Set(readActiveShields());
|
|
9435
9417
|
console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
|
|
9436
9418
|
for (const shield of listShields()) {
|
|
@@ -9440,6 +9422,10 @@ function registerShieldCommand(program2) {
|
|
|
9440
9422
|
console.log(chalk6.gray(` aliases: ${shield.aliases.join(", ")}`));
|
|
9441
9423
|
}
|
|
9442
9424
|
console.log("");
|
|
9425
|
+
console.log(
|
|
9426
|
+
chalk6.gray(` Browse community shields: ${chalk6.cyan("node9 shield list --community")}
|
|
9427
|
+
`)
|
|
9428
|
+
);
|
|
9443
9429
|
});
|
|
9444
9430
|
shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
|
|
9445
9431
|
const active = readActiveShields();
|
|
@@ -9577,6 +9563,52 @@ function registerShieldCommand(program2) {
|
|
|
9577
9563
|
`)
|
|
9578
9564
|
);
|
|
9579
9565
|
});
|
|
9566
|
+
shieldCmd.command("install <name>").description("Install a shield from the community marketplace into ~/.node9/shields/").action((name) => {
|
|
9567
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
9568
|
+
console.error(
|
|
9569
|
+
chalk6.red(
|
|
9570
|
+
`
|
|
9571
|
+
\u274C Invalid shield name: only alphanumeric characters, hyphens, and underscores are allowed
|
|
9572
|
+
`
|
|
9573
|
+
)
|
|
9574
|
+
);
|
|
9575
|
+
process.exit(1);
|
|
9576
|
+
}
|
|
9577
|
+
console.log(chalk6.bold(`
|
|
9578
|
+
\u{1F6E1}\uFE0F Installing shield "${name}"\u2026
|
|
9579
|
+
`));
|
|
9580
|
+
httpsFetch(COMMUNITY_INDEX_URL).then((indexBody) => {
|
|
9581
|
+
const entries = JSON.parse(indexBody);
|
|
9582
|
+
const entry = entries.find((e) => e.name === name);
|
|
9583
|
+
if (!entry) {
|
|
9584
|
+
const names = entries.map((e) => chalk6.cyan(e.name)).join(", ");
|
|
9585
|
+
console.error(
|
|
9586
|
+
chalk6.red(`\u274C Shield "${name}" not found in the community marketplace.
|
|
9587
|
+
`)
|
|
9588
|
+
);
|
|
9589
|
+
console.error(` Available: ${names}
|
|
9590
|
+
`);
|
|
9591
|
+
process.exit(1);
|
|
9592
|
+
}
|
|
9593
|
+
return httpsFetch(entry.url);
|
|
9594
|
+
}).then((shieldBody) => {
|
|
9595
|
+
const shieldJson = JSON.parse(shieldBody);
|
|
9596
|
+
installShield(name, shieldJson);
|
|
9597
|
+
console.log(
|
|
9598
|
+
chalk6.green(`\u2705 Shield "${name}" installed to ~/.node9/shields/${name}.json`)
|
|
9599
|
+
);
|
|
9600
|
+
console.log(
|
|
9601
|
+
chalk6.gray(` Activate it with: ${chalk6.cyan(`node9 shield enable ${name}`)}
|
|
9602
|
+
`)
|
|
9603
|
+
);
|
|
9604
|
+
appendConfigAudit({ event: "shield-install", shield: name });
|
|
9605
|
+
}).catch((err2) => {
|
|
9606
|
+
console.error(chalk6.red(`
|
|
9607
|
+
\u274C Install failed: ${String(err2)}
|
|
9608
|
+
`));
|
|
9609
|
+
process.exit(1);
|
|
9610
|
+
});
|
|
9611
|
+
});
|
|
9580
9612
|
}
|
|
9581
9613
|
function registerConfigShowCommand(program2) {
|
|
9582
9614
|
program2.command("config show").description(
|
|
@@ -9893,7 +9925,7 @@ function registerAuditCommand(program2) {
|
|
|
9893
9925
|
init_daemon2();
|
|
9894
9926
|
init_daemon();
|
|
9895
9927
|
import chalk9 from "chalk";
|
|
9896
|
-
import { spawn as
|
|
9928
|
+
import { spawn as spawn7 } from "child_process";
|
|
9897
9929
|
function registerDaemonCommand(program2) {
|
|
9898
9930
|
program2.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(
|
|
9899
9931
|
"-w, --watch",
|
|
@@ -9924,7 +9956,7 @@ function registerDaemonCommand(program2) {
|
|
|
9924
9956
|
console.log(chalk9.green(`\u{1F310} Opened browser: http://${DAEMON_HOST}:${DAEMON_PORT}/`));
|
|
9925
9957
|
process.exit(0);
|
|
9926
9958
|
}
|
|
9927
|
-
const child =
|
|
9959
|
+
const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
|
|
9928
9960
|
detached: true,
|
|
9929
9961
|
stdio: "ignore"
|
|
9930
9962
|
});
|
|
@@ -9939,7 +9971,7 @@ function registerDaemonCommand(program2) {
|
|
|
9939
9971
|
process.exit(0);
|
|
9940
9972
|
}
|
|
9941
9973
|
if (options.background) {
|
|
9942
|
-
const child =
|
|
9974
|
+
const child = spawn7(process.execPath, [process.argv[1], "daemon"], {
|
|
9943
9975
|
detached: true,
|
|
9944
9976
|
stdio: "ignore"
|
|
9945
9977
|
});
|
|
@@ -10111,7 +10143,7 @@ import chalk11 from "chalk";
|
|
|
10111
10143
|
import fs23 from "fs";
|
|
10112
10144
|
import path25 from "path";
|
|
10113
10145
|
import os19 from "os";
|
|
10114
|
-
import
|
|
10146
|
+
import https2 from "https";
|
|
10115
10147
|
init_shields();
|
|
10116
10148
|
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
10117
10149
|
function fireTelemetryPing(agents) {
|
|
@@ -10122,7 +10154,7 @@ function fireTelemetryPing(agents) {
|
|
|
10122
10154
|
os: process.platform,
|
|
10123
10155
|
node9_version: process.env.npm_package_version ?? "unknown"
|
|
10124
10156
|
});
|
|
10125
|
-
const req =
|
|
10157
|
+
const req = https2.request(
|
|
10126
10158
|
{
|
|
10127
10159
|
hostname: "api.node9.ai",
|
|
10128
10160
|
path: "/api/v1/telemetry",
|
|
@@ -10528,7 +10560,7 @@ function registerUndoCommand(program2) {
|
|
|
10528
10560
|
// src/cli/commands/watch.ts
|
|
10529
10561
|
init_daemon();
|
|
10530
10562
|
import chalk14 from "chalk";
|
|
10531
|
-
import { spawn as
|
|
10563
|
+
import { spawn as spawn8, spawnSync as spawnSync5 } from "child_process";
|
|
10532
10564
|
function registerWatchCommand(program2) {
|
|
10533
10565
|
program2.command("watch").description("Run a command under Node9 watch mode (daemon stays alive for the session)").argument("<command>", "Command to run").argument("[args...]", "Arguments for the command").action(async (cmd, args) => {
|
|
10534
10566
|
let port = DAEMON_PORT;
|
|
@@ -10544,7 +10576,7 @@ function registerWatchCommand(program2) {
|
|
|
10544
10576
|
}
|
|
10545
10577
|
} catch {
|
|
10546
10578
|
console.error(chalk14.dim("\u{1F6E1}\uFE0F Starting Node9 daemon (watch mode)..."));
|
|
10547
|
-
const child =
|
|
10579
|
+
const child = spawn8(process.execPath, [process.argv[1], "daemon"], {
|
|
10548
10580
|
detached: true,
|
|
10549
10581
|
stdio: "ignore",
|
|
10550
10582
|
env: { ...process.env, NODE9_AUTO_STARTED: "1", NODE9_WATCH_MODE: "1" }
|
|
@@ -10590,7 +10622,7 @@ function registerWatchCommand(program2) {
|
|
|
10590
10622
|
init_orchestrator();
|
|
10591
10623
|
import readline3 from "readline";
|
|
10592
10624
|
import chalk15 from "chalk";
|
|
10593
|
-
import { spawn as
|
|
10625
|
+
import { spawn as spawn9 } from "child_process";
|
|
10594
10626
|
import { execa as execa2 } from "execa";
|
|
10595
10627
|
init_provenance();
|
|
10596
10628
|
function sanitize4(value) {
|
|
@@ -10677,7 +10709,7 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10677
10709
|
const safeEnv = Object.fromEntries(
|
|
10678
10710
|
Object.entries(process.env).filter(([k]) => !UPSTREAM_INJECTOR_VARS.has(k))
|
|
10679
10711
|
);
|
|
10680
|
-
const child =
|
|
10712
|
+
const child = spawn9(executable, cmdArgs, {
|
|
10681
10713
|
stdio: ["pipe", "pipe", "inherit"],
|
|
10682
10714
|
// control stdin/stdout; inherit stderr
|
|
10683
10715
|
shell: false,
|
|
@@ -10760,8 +10792,11 @@ async function runMcpGateway(upstreamCommand) {
|
|
|
10760
10792
|
return;
|
|
10761
10793
|
} finally {
|
|
10762
10794
|
authPending = false;
|
|
10763
|
-
|
|
10764
|
-
|
|
10795
|
+
if (deferredStdinEnd) {
|
|
10796
|
+
child.stdin.end();
|
|
10797
|
+
} else {
|
|
10798
|
+
agentIn.resume();
|
|
10799
|
+
}
|
|
10765
10800
|
if (deferredExitCode !== null) process.exit(deferredExitCode);
|
|
10766
10801
|
}
|
|
10767
10802
|
return;
|
|
@@ -11496,7 +11531,43 @@ registerMcpGatewayCommand(program);
|
|
|
11496
11531
|
registerMcpServerCommand(program);
|
|
11497
11532
|
registerCheckCommand(program);
|
|
11498
11533
|
registerLogCommand(program);
|
|
11499
|
-
program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").
|
|
11534
|
+
program.command("hud").description("Render node9 security statusline (spawned by Claude Code statusLine)").addHelpText(
|
|
11535
|
+
"after",
|
|
11536
|
+
`
|
|
11537
|
+
Outputs up to 3 lines to stdout, then exits:
|
|
11538
|
+
|
|
11539
|
+
Line 1 \u2014 Security state (always shown):
|
|
11540
|
+
\u{1F6E1} node9 | <mode> [shields] | \u2705 allowed \u{1F6D1} blocked \u{1F6A8} dlp ~$cost
|
|
11541
|
+
Shows "offline" if the node9 daemon is not running.
|
|
11542
|
+
|
|
11543
|
+
Line 2 \u2014 Claude context & rate limits (shown when available):
|
|
11544
|
+
<model> \u2502 ctx \u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591\u2591\u2591 61% \u2502 5h \u2588\u2588\u2588\u2588\u2591\u2591\u2591\u2591\u2591\u2591 40% (2h 10m left)
|
|
11545
|
+
Only appears when Claude Code passes context_window / rate_limits data via stdin.
|
|
11546
|
+
|
|
11547
|
+
Line 3 \u2014 Environment counts (shown when non-zero):
|
|
11548
|
+
2 CLAUDE.md | 5 rules | 4 MCPs | 3 hooks
|
|
11549
|
+
Counts CLAUDE.md files, rules/, MCP servers, and hook entries across user + project scope.
|
|
11550
|
+
Disable with: { "settings": { "hud": { "showEnvironmentCounts": false } } } in node9.config.json
|
|
11551
|
+
|
|
11552
|
+
Claude Code spawns this command every ~300ms and writes a JSON payload to stdin.
|
|
11553
|
+
Run "node9 addto claude" to register it as the statusLine.`
|
|
11554
|
+
).argument("[subcommand]", 'Optional: "debug on" / "debug off" to toggle stdin logging').argument("[state]", 'on|off \u2014 used with "debug" subcommand').action(async (subcommand, state) => {
|
|
11555
|
+
if (subcommand === "debug") {
|
|
11556
|
+
const flagFile = path30.join(os23.homedir(), ".node9", "hud-debug");
|
|
11557
|
+
if (state === "on") {
|
|
11558
|
+
fs27.mkdirSync(path30.dirname(flagFile), { recursive: true });
|
|
11559
|
+
fs27.writeFileSync(flagFile, "");
|
|
11560
|
+
console.log("HUD debug logging enabled \u2192 ~/.node9/hud-debug.log");
|
|
11561
|
+
console.log("Tail it with: tail -f ~/.node9/hud-debug.log");
|
|
11562
|
+
} else if (state === "off") {
|
|
11563
|
+
if (fs27.existsSync(flagFile)) fs27.unlinkSync(flagFile);
|
|
11564
|
+
console.log("HUD debug logging disabled.");
|
|
11565
|
+
} else {
|
|
11566
|
+
console.error("Usage: node9 hud debug on|off");
|
|
11567
|
+
process.exit(1);
|
|
11568
|
+
}
|
|
11569
|
+
return;
|
|
11570
|
+
}
|
|
11500
11571
|
const { main: main2 } = await Promise.resolve().then(() => (init_hud(), hud_exports));
|
|
11501
11572
|
await main2();
|
|
11502
11573
|
});
|