@node9/proxy 1.7.0 → 1.8.2
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 +82 -21
- package/dist/cli.js +388 -194
- package/dist/cli.mjs +388 -194
- package/dist/index.js +89 -179
- package/dist/index.mjs +89 -179
- package/dist/shields/builtin/aws.json +59 -0
- package/dist/shields/builtin/bash-safe.json +78 -0
- package/dist/shields/builtin/filesystem.json +30 -0
- package/dist/shields/builtin/github.json +26 -0
- package/dist/shields/builtin/postgres.json +42 -0
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -268,6 +268,70 @@ var init_config_schema = __esm({
|
|
|
268
268
|
});
|
|
269
269
|
|
|
270
270
|
// src/shields.ts
|
|
271
|
+
function validateShieldDefinition(raw, filePath) {
|
|
272
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
273
|
+
process.stderr.write(`[node9] Shield file is not an object: ${filePath}
|
|
274
|
+
`);
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
const r = raw;
|
|
278
|
+
if (typeof r.name !== "string" || !r.name) {
|
|
279
|
+
process.stderr.write(`[node9] Shield file missing 'name': ${filePath}
|
|
280
|
+
`);
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
if (typeof r.description !== "string") {
|
|
284
|
+
process.stderr.write(`[node9] Shield file missing 'description': ${filePath}
|
|
285
|
+
`);
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
if (!Array.isArray(r.aliases)) {
|
|
289
|
+
process.stderr.write(`[node9] Shield file missing 'aliases' array: ${filePath}
|
|
290
|
+
`);
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
if (!Array.isArray(r.smartRules)) {
|
|
294
|
+
process.stderr.write(`[node9] Shield file missing 'smartRules' array: ${filePath}
|
|
295
|
+
`);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
if (!Array.isArray(r.dangerousWords)) {
|
|
299
|
+
process.stderr.write(`[node9] Shield file missing 'dangerousWords' array: ${filePath}
|
|
300
|
+
`);
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
return r;
|
|
304
|
+
}
|
|
305
|
+
function loadShieldsFromDir(dir, label) {
|
|
306
|
+
const result = {};
|
|
307
|
+
let entries;
|
|
308
|
+
try {
|
|
309
|
+
entries = import_fs2.default.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
310
|
+
} catch (err2) {
|
|
311
|
+
if (err2.code !== "ENOENT") {
|
|
312
|
+
process.stderr.write(`[node9] Could not read ${label} shields dir ${dir}: ${String(err2)}
|
|
313
|
+
`);
|
|
314
|
+
}
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
for (const file of entries) {
|
|
318
|
+
const filePath = import_path2.default.join(dir, file);
|
|
319
|
+
try {
|
|
320
|
+
const raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
|
|
321
|
+
const shield = validateShieldDefinition(raw, filePath);
|
|
322
|
+
if (shield) result[shield.name] = shield;
|
|
323
|
+
} catch (err2) {
|
|
324
|
+
process.stderr.write(`[node9] Failed to load ${label} shield ${file}: ${String(err2)}
|
|
325
|
+
`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
function buildSHIELDS() {
|
|
331
|
+
const builtins = loadShieldsFromDir(BUILTIN_DIR, "builtin");
|
|
332
|
+
const userShields = loadShieldsFromDir(USER_SHIELDS_DIR, "user");
|
|
333
|
+
return { ...builtins, ...userShields };
|
|
334
|
+
}
|
|
271
335
|
function resolveShieldName(input) {
|
|
272
336
|
const lower = input.toLowerCase();
|
|
273
337
|
if (SHIELDS[lower]) return lower;
|
|
@@ -374,7 +438,24 @@ function resolveShieldRule(shieldName, identifier) {
|
|
|
374
438
|
}
|
|
375
439
|
return null;
|
|
376
440
|
}
|
|
377
|
-
|
|
441
|
+
function installShield(name, shieldJson) {
|
|
442
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
`Invalid shield name '${name}': only alphanumeric characters, hyphens, and underscores are allowed`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
const shield = validateShieldDefinition(shieldJson, `<downloaded:${name}>`);
|
|
448
|
+
if (!shield) throw new Error(`Downloaded shield '${name}' failed validation`);
|
|
449
|
+
if (shield.name !== name) {
|
|
450
|
+
throw new Error(`Shield name mismatch: file declares '${shield.name}' but expected '${name}'`);
|
|
451
|
+
}
|
|
452
|
+
import_fs2.default.mkdirSync(USER_SHIELDS_DIR, { recursive: true });
|
|
453
|
+
const filePath = import_path2.default.join(USER_SHIELDS_DIR, `${name}.json`);
|
|
454
|
+
const tmp = `${filePath}.${import_crypto2.default.randomBytes(6).toString("hex")}.tmp`;
|
|
455
|
+
import_fs2.default.writeFileSync(tmp, JSON.stringify(shieldJson, null, 2), { mode: 384 });
|
|
456
|
+
import_fs2.default.renameSync(tmp, filePath);
|
|
457
|
+
}
|
|
458
|
+
var import_fs2, import_path2, import_os2, import_crypto2, BUILTIN_DIR, USER_SHIELDS_DIR, SHIELDS, SHIELDS_STATE_FILE;
|
|
378
459
|
var init_shields = __esm({
|
|
379
460
|
"src/shields.ts"() {
|
|
380
461
|
"use strict";
|
|
@@ -382,173 +463,9 @@ var init_shields = __esm({
|
|
|
382
463
|
import_path2 = __toESM(require("path"));
|
|
383
464
|
import_os2 = __toESM(require("os"));
|
|
384
465
|
import_crypto2 = __toESM(require("crypto"));
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
description: "Protects PostgreSQL databases from destructive AI operations",
|
|
389
|
-
aliases: ["pg", "postgresql"],
|
|
390
|
-
smartRules: [
|
|
391
|
-
{
|
|
392
|
-
name: "shield:postgres:block-drop-table",
|
|
393
|
-
tool: "*",
|
|
394
|
-
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
395
|
-
verdict: "block",
|
|
396
|
-
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
397
|
-
},
|
|
398
|
-
{
|
|
399
|
-
name: "shield:postgres:block-truncate",
|
|
400
|
-
tool: "*",
|
|
401
|
-
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
402
|
-
verdict: "block",
|
|
403
|
-
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
name: "shield:postgres:block-drop-column",
|
|
407
|
-
tool: "*",
|
|
408
|
-
conditions: [
|
|
409
|
-
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
410
|
-
],
|
|
411
|
-
verdict: "block",
|
|
412
|
-
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
413
|
-
},
|
|
414
|
-
{
|
|
415
|
-
name: "shield:postgres:review-grant-revoke",
|
|
416
|
-
tool: "*",
|
|
417
|
-
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
418
|
-
verdict: "review",
|
|
419
|
-
reason: "Permission changes require human approval (Postgres shield)"
|
|
420
|
-
}
|
|
421
|
-
],
|
|
422
|
-
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
423
|
-
},
|
|
424
|
-
github: {
|
|
425
|
-
name: "github",
|
|
426
|
-
description: "Protects GitHub repositories from destructive AI operations",
|
|
427
|
-
aliases: ["git"],
|
|
428
|
-
smartRules: [
|
|
429
|
-
{
|
|
430
|
-
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
431
|
-
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
432
|
-
name: "shield:github:review-delete-branch-remote",
|
|
433
|
-
tool: "bash",
|
|
434
|
-
conditions: [
|
|
435
|
-
{
|
|
436
|
-
field: "command",
|
|
437
|
-
op: "matches",
|
|
438
|
-
value: "git\\s+push\\s+.*--delete",
|
|
439
|
-
flags: "i"
|
|
440
|
-
}
|
|
441
|
-
],
|
|
442
|
-
verdict: "review",
|
|
443
|
-
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
444
|
-
},
|
|
445
|
-
{
|
|
446
|
-
name: "shield:github:block-delete-repo",
|
|
447
|
-
tool: "*",
|
|
448
|
-
conditions: [
|
|
449
|
-
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
450
|
-
],
|
|
451
|
-
verdict: "block",
|
|
452
|
-
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
453
|
-
}
|
|
454
|
-
],
|
|
455
|
-
dangerousWords: []
|
|
456
|
-
},
|
|
457
|
-
aws: {
|
|
458
|
-
name: "aws",
|
|
459
|
-
description: "Protects AWS infrastructure from destructive AI operations",
|
|
460
|
-
aliases: ["amazon"],
|
|
461
|
-
smartRules: [
|
|
462
|
-
{
|
|
463
|
-
name: "shield:aws:block-delete-s3-bucket",
|
|
464
|
-
tool: "*",
|
|
465
|
-
conditions: [
|
|
466
|
-
{
|
|
467
|
-
field: "command",
|
|
468
|
-
op: "matches",
|
|
469
|
-
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
470
|
-
flags: "i"
|
|
471
|
-
}
|
|
472
|
-
],
|
|
473
|
-
verdict: "block",
|
|
474
|
-
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
475
|
-
},
|
|
476
|
-
{
|
|
477
|
-
name: "shield:aws:review-iam-changes",
|
|
478
|
-
tool: "*",
|
|
479
|
-
conditions: [
|
|
480
|
-
{
|
|
481
|
-
field: "command",
|
|
482
|
-
op: "matches",
|
|
483
|
-
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
484
|
-
flags: "i"
|
|
485
|
-
}
|
|
486
|
-
],
|
|
487
|
-
verdict: "review",
|
|
488
|
-
reason: "IAM changes require human approval (AWS shield)"
|
|
489
|
-
},
|
|
490
|
-
{
|
|
491
|
-
name: "shield:aws:block-ec2-terminate",
|
|
492
|
-
tool: "*",
|
|
493
|
-
conditions: [
|
|
494
|
-
{
|
|
495
|
-
field: "command",
|
|
496
|
-
op: "matches",
|
|
497
|
-
value: "aws\\s+ec2\\s+terminate-instances",
|
|
498
|
-
flags: "i"
|
|
499
|
-
}
|
|
500
|
-
],
|
|
501
|
-
verdict: "block",
|
|
502
|
-
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
503
|
-
},
|
|
504
|
-
{
|
|
505
|
-
name: "shield:aws:review-rds-delete",
|
|
506
|
-
tool: "*",
|
|
507
|
-
conditions: [
|
|
508
|
-
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
509
|
-
],
|
|
510
|
-
verdict: "review",
|
|
511
|
-
reason: "RDS deletion requires human approval (AWS shield)"
|
|
512
|
-
}
|
|
513
|
-
],
|
|
514
|
-
dangerousWords: []
|
|
515
|
-
},
|
|
516
|
-
filesystem: {
|
|
517
|
-
name: "filesystem",
|
|
518
|
-
description: "Protects the local filesystem from dangerous AI operations",
|
|
519
|
-
aliases: ["fs"],
|
|
520
|
-
smartRules: [
|
|
521
|
-
{
|
|
522
|
-
name: "shield:filesystem:review-chmod-777",
|
|
523
|
-
tool: "bash",
|
|
524
|
-
conditions: [
|
|
525
|
-
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
526
|
-
],
|
|
527
|
-
verdict: "review",
|
|
528
|
-
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
529
|
-
},
|
|
530
|
-
{
|
|
531
|
-
name: "shield:filesystem:review-write-etc",
|
|
532
|
-
tool: "bash",
|
|
533
|
-
conditions: [
|
|
534
|
-
{
|
|
535
|
-
field: "command",
|
|
536
|
-
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
537
|
-
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
538
|
-
op: "matches",
|
|
539
|
-
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
540
|
-
}
|
|
541
|
-
],
|
|
542
|
-
verdict: "review",
|
|
543
|
-
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
544
|
-
}
|
|
545
|
-
],
|
|
546
|
-
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
547
|
-
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
548
|
-
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
549
|
-
dangerousWords: ["wipefs"]
|
|
550
|
-
}
|
|
551
|
-
};
|
|
466
|
+
BUILTIN_DIR = import_path2.default.join(__dirname, "shields", "builtin");
|
|
467
|
+
USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");
|
|
468
|
+
SHIELDS = buildSHIELDS();
|
|
552
469
|
SHIELDS_STATE_FILE = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields.json");
|
|
553
470
|
}
|
|
554
471
|
});
|
|
@@ -2520,7 +2437,7 @@ function isDaemonRunning() {
|
|
|
2520
2437
|
return false;
|
|
2521
2438
|
}
|
|
2522
2439
|
}
|
|
2523
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
|
|
2440
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched) {
|
|
2524
2441
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2525
2442
|
const ctrl = new AbortController();
|
|
2526
2443
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -2541,7 +2458,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2541
2458
|
...cwd && { cwd },
|
|
2542
2459
|
...recoveryCommand && { recoveryCommand },
|
|
2543
2460
|
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
2544
|
-
...viewOnly && { viewOnly: true }
|
|
2461
|
+
...viewOnly && { viewOnly: true },
|
|
2462
|
+
...localSmartRuleMatched && { localSmartRuleMatched: true }
|
|
2545
2463
|
}),
|
|
2546
2464
|
signal: ctrl.signal
|
|
2547
2465
|
});
|
|
@@ -3236,6 +3154,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3236
3154
|
let policyMatchedWord;
|
|
3237
3155
|
let riskMetadata;
|
|
3238
3156
|
let statefulRecoveryCommand;
|
|
3157
|
+
let localSmartRuleMatched = false;
|
|
3239
3158
|
let taintWarning = null;
|
|
3240
3159
|
if (isNetworkTool(toolName, args)) {
|
|
3241
3160
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -3379,6 +3298,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3379
3298
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
3380
3299
|
policyMatchedField = policyResult.matchedField;
|
|
3381
3300
|
policyMatchedWord = policyResult.matchedWord;
|
|
3301
|
+
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
3382
3302
|
riskMetadata = computeRiskMetadata(
|
|
3383
3303
|
args,
|
|
3384
3304
|
policyResult.tier ?? 6,
|
|
@@ -3421,22 +3341,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3421
3341
|
}
|
|
3422
3342
|
let cloudRequestId = null;
|
|
3423
3343
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
3424
|
-
if (cloudEnforced) {
|
|
3344
|
+
if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3425
3345
|
try {
|
|
3426
3346
|
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
3427
3347
|
if (!initResult.pending) {
|
|
3428
3348
|
if (initResult.shadowMode) {
|
|
3429
3349
|
return { approved: true, checkedBy: "cloud" };
|
|
3430
3350
|
}
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3351
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3352
|
+
return {
|
|
3353
|
+
approved: !!initResult.approved,
|
|
3354
|
+
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
3355
|
+
checkedBy: initResult.approved ? "cloud" : void 0,
|
|
3356
|
+
blockedBy: initResult.approved ? void 0 : "team-policy",
|
|
3357
|
+
blockedByLabel: "Organization Policy (SaaS)"
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3362
|
+
cloudRequestId = initResult.requestId || null;
|
|
3438
3363
|
}
|
|
3439
|
-
cloudRequestId = initResult.requestId || null;
|
|
3440
3364
|
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
3441
3365
|
} catch {
|
|
3442
3366
|
}
|
|
@@ -3482,7 +3406,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3482
3406
|
riskMetadata,
|
|
3483
3407
|
options?.activityId,
|
|
3484
3408
|
options?.cwd,
|
|
3485
|
-
statefulRecoveryCommand
|
|
3409
|
+
statefulRecoveryCommand,
|
|
3410
|
+
void 0,
|
|
3411
|
+
void 0,
|
|
3412
|
+
localSmartRuleMatched || options?.localSmartRuleMatched
|
|
3486
3413
|
);
|
|
3487
3414
|
daemonEntryId = entry.id;
|
|
3488
3415
|
daemonAllowCount = entry.allowCount;
|
|
@@ -3490,7 +3417,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3490
3417
|
}
|
|
3491
3418
|
}
|
|
3492
3419
|
}
|
|
3493
|
-
if (cloudEnforced && cloudRequestId) {
|
|
3420
|
+
if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3494
3421
|
racePromises.push(
|
|
3495
3422
|
(async () => {
|
|
3496
3423
|
try {
|
|
@@ -6152,7 +6079,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6152
6079
|
viewOnly = false,
|
|
6153
6080
|
fromCLI = false,
|
|
6154
6081
|
activityId,
|
|
6155
|
-
cwd
|
|
6082
|
+
cwd,
|
|
6083
|
+
localSmartRuleMatched = false
|
|
6156
6084
|
} = JSON.parse(body);
|
|
6157
6085
|
const id = fromCLI && typeof activityId === "string" && activityId || (0, import_crypto6.randomUUID)();
|
|
6158
6086
|
const entry = {
|
|
@@ -6232,7 +6160,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6232
6160
|
agent: typeof agent === "string" ? agent : void 0,
|
|
6233
6161
|
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
|
|
6234
6162
|
},
|
|
6235
|
-
{ calledFromDaemon: true }
|
|
6163
|
+
{ calledFromDaemon: true, localSmartRuleMatched: !!localSmartRuleMatched }
|
|
6236
6164
|
).then((result) => {
|
|
6237
6165
|
const e = pending.get(id);
|
|
6238
6166
|
if (!e) return;
|
|
@@ -7070,6 +6998,7 @@ async function startTail(options = {}) {
|
|
|
7070
6998
|
}
|
|
7071
6999
|
const connectionTime = Date.now();
|
|
7072
7000
|
const activityPending = /* @__PURE__ */ new Map();
|
|
7001
|
+
const orphanedResults = /* @__PURE__ */ new Map();
|
|
7073
7002
|
let csrfToken = "";
|
|
7074
7003
|
const approvalQueue = [];
|
|
7075
7004
|
let cardActive = false;
|
|
@@ -7404,9 +7333,14 @@ async function startTail(options = {}) {
|
|
|
7404
7333
|
renderResult(data, data);
|
|
7405
7334
|
return;
|
|
7406
7335
|
}
|
|
7336
|
+
const orphaned = orphanedResults.get(data.id);
|
|
7337
|
+
if (orphaned) {
|
|
7338
|
+
orphanedResults.delete(data.id);
|
|
7339
|
+
renderResult(data, orphaned);
|
|
7340
|
+
return;
|
|
7341
|
+
}
|
|
7407
7342
|
activityPending.set(data.id, data);
|
|
7408
|
-
|
|
7409
|
-
if (slowTool) renderPending(data);
|
|
7343
|
+
renderPending(data);
|
|
7410
7344
|
}
|
|
7411
7345
|
if (event === "snapshot") {
|
|
7412
7346
|
const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
|
|
@@ -7425,6 +7359,8 @@ async function startTail(options = {}) {
|
|
|
7425
7359
|
if (original) {
|
|
7426
7360
|
renderResult(original, data);
|
|
7427
7361
|
activityPending.delete(data.id);
|
|
7362
|
+
} else {
|
|
7363
|
+
orphanedResults.set(data.id, data);
|
|
7428
7364
|
}
|
|
7429
7365
|
}
|
|
7430
7366
|
}
|
|
@@ -7667,6 +7603,29 @@ function renderOffline() {
|
|
|
7667
7603
|
process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
|
|
7668
7604
|
`);
|
|
7669
7605
|
}
|
|
7606
|
+
function readActiveShieldsHud() {
|
|
7607
|
+
const now = Date.now();
|
|
7608
|
+
if (shieldsCache && now - shieldsCache.ts < SHIELDS_CACHE_TTL_MS) {
|
|
7609
|
+
return shieldsCache.value;
|
|
7610
|
+
}
|
|
7611
|
+
try {
|
|
7612
|
+
const shieldsPath = import_path29.default.join(import_os22.default.homedir(), ".node9", "shields.json");
|
|
7613
|
+
if (!import_fs26.default.existsSync(shieldsPath)) {
|
|
7614
|
+
shieldsCache = { value: [], ts: now };
|
|
7615
|
+
return [];
|
|
7616
|
+
}
|
|
7617
|
+
const parsed = JSON.parse(import_fs26.default.readFileSync(shieldsPath, "utf-8"));
|
|
7618
|
+
if (!Array.isArray(parsed.active)) {
|
|
7619
|
+
shieldsCache = { value: [], ts: now };
|
|
7620
|
+
return [];
|
|
7621
|
+
}
|
|
7622
|
+
const value = parsed.active.filter((s) => typeof s === "string").map((s) => s.slice(0, 64)).slice(0, 20);
|
|
7623
|
+
shieldsCache = { value, ts: now };
|
|
7624
|
+
return value;
|
|
7625
|
+
} catch {
|
|
7626
|
+
return [];
|
|
7627
|
+
}
|
|
7628
|
+
}
|
|
7670
7629
|
function renderSecurityLine(status) {
|
|
7671
7630
|
const parts = [];
|
|
7672
7631
|
parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
|
|
@@ -7684,6 +7643,18 @@ function renderSecurityLine(status) {
|
|
|
7684
7643
|
};
|
|
7685
7644
|
const mc = modeColors[status.mode] ?? WHITE;
|
|
7686
7645
|
parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
|
|
7646
|
+
const activeShields = readActiveShieldsHud();
|
|
7647
|
+
if (activeShields.length > 0) {
|
|
7648
|
+
const shieldAbbrevs = {
|
|
7649
|
+
"bash-safe": "bash",
|
|
7650
|
+
filesystem: "fs",
|
|
7651
|
+
postgres: "pg",
|
|
7652
|
+
github: "gh",
|
|
7653
|
+
aws: "aws"
|
|
7654
|
+
};
|
|
7655
|
+
const labels = activeShields.map((s) => shieldAbbrevs[s] ?? s).join(" ");
|
|
7656
|
+
parts.push(color(DIM, `[${labels}]`));
|
|
7657
|
+
}
|
|
7687
7658
|
if (status.mode === "observe") {
|
|
7688
7659
|
parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
|
|
7689
7660
|
if (status.session.wouldBlock > 0) {
|
|
@@ -7780,7 +7751,7 @@ async function main() {
|
|
|
7780
7751
|
renderOffline();
|
|
7781
7752
|
}
|
|
7782
7753
|
}
|
|
7783
|
-
var import_fs26, import_path29, import_os22, import_http3, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
|
|
7754
|
+
var import_fs26, import_path29, import_os22, import_http3, RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
|
|
7784
7755
|
var init_hud = __esm({
|
|
7785
7756
|
"src/cli/hud.ts"() {
|
|
7786
7757
|
"use strict";
|
|
@@ -7802,6 +7773,8 @@ var init_hud = __esm({
|
|
|
7802
7773
|
BAR_FILLED = "\u2588";
|
|
7803
7774
|
BAR_EMPTY = "\u2591";
|
|
7804
7775
|
BAR_WIDTH = 10;
|
|
7776
|
+
shieldsCache = null;
|
|
7777
|
+
SHIELDS_CACHE_TTL_MS = 2e3;
|
|
7805
7778
|
}
|
|
7806
7779
|
});
|
|
7807
7780
|
|
|
@@ -7815,6 +7788,7 @@ var import_path14 = __toESM(require("path"));
|
|
|
7815
7788
|
var import_os10 = __toESM(require("os"));
|
|
7816
7789
|
var import_chalk = __toESM(require("chalk"));
|
|
7817
7790
|
var import_prompts = require("@inquirer/prompts");
|
|
7791
|
+
var import_smol_toml = require("smol-toml");
|
|
7818
7792
|
var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
|
|
7819
7793
|
function hasNode9McpServer(servers) {
|
|
7820
7794
|
const entry = servers["node9"];
|
|
@@ -8178,7 +8152,8 @@ function detectAgents(homeDir2 = import_os10.default.homedir()) {
|
|
|
8178
8152
|
return {
|
|
8179
8153
|
claude: exists(import_path14.default.join(homeDir2, ".claude")) || exists(import_path14.default.join(homeDir2, ".claude.json")),
|
|
8180
8154
|
gemini: exists(import_path14.default.join(homeDir2, ".gemini")),
|
|
8181
|
-
cursor: exists(import_path14.default.join(homeDir2, ".cursor"))
|
|
8155
|
+
cursor: exists(import_path14.default.join(homeDir2, ".cursor")),
|
|
8156
|
+
codex: exists(import_path14.default.join(homeDir2, ".codex"))
|
|
8182
8157
|
};
|
|
8183
8158
|
}
|
|
8184
8159
|
async function setupCursor() {
|
|
@@ -8243,6 +8218,82 @@ async function setupCursor() {
|
|
|
8243
8218
|
printDaemonTip();
|
|
8244
8219
|
}
|
|
8245
8220
|
}
|
|
8221
|
+
function readToml(filePath) {
|
|
8222
|
+
try {
|
|
8223
|
+
if (import_fs11.default.existsSync(filePath)) {
|
|
8224
|
+
return (0, import_smol_toml.parse)(import_fs11.default.readFileSync(filePath, "utf-8"));
|
|
8225
|
+
}
|
|
8226
|
+
} catch {
|
|
8227
|
+
}
|
|
8228
|
+
return null;
|
|
8229
|
+
}
|
|
8230
|
+
function writeToml(filePath, data) {
|
|
8231
|
+
const dir = import_path14.default.dirname(filePath);
|
|
8232
|
+
if (!import_fs11.default.existsSync(dir)) import_fs11.default.mkdirSync(dir, { recursive: true });
|
|
8233
|
+
import_fs11.default.writeFileSync(filePath, (0, import_smol_toml.stringify)(data));
|
|
8234
|
+
}
|
|
8235
|
+
async function setupCodex() {
|
|
8236
|
+
const homeDir2 = import_os10.default.homedir();
|
|
8237
|
+
const configPath = import_path14.default.join(homeDir2, ".codex", "config.toml");
|
|
8238
|
+
const config = readToml(configPath) ?? {};
|
|
8239
|
+
const servers = config.mcp_servers ?? {};
|
|
8240
|
+
let anythingChanged = false;
|
|
8241
|
+
if (!hasNode9McpServer(servers)) {
|
|
8242
|
+
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
8243
|
+
config.mcp_servers = servers;
|
|
8244
|
+
writeToml(configPath, config);
|
|
8245
|
+
console.log(import_chalk.default.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8246
|
+
anythingChanged = true;
|
|
8247
|
+
}
|
|
8248
|
+
const serversToWrap = [];
|
|
8249
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
8250
|
+
if (!server.command || server.command === "node9") continue;
|
|
8251
|
+
const parts = [server.command, ...server.args ?? []];
|
|
8252
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
8253
|
+
}
|
|
8254
|
+
if (serversToWrap.length > 0) {
|
|
8255
|
+
console.log(import_chalk.default.bold("The following existing entries will be modified:\n"));
|
|
8256
|
+
console.log(import_chalk.default.white(` ${configPath}`));
|
|
8257
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
8258
|
+
console.log(import_chalk.default.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
8259
|
+
}
|
|
8260
|
+
console.log("");
|
|
8261
|
+
const proceed = await (0, import_prompts.confirm)({ message: "Wrap these MCP servers?", default: true });
|
|
8262
|
+
if (proceed) {
|
|
8263
|
+
for (const { name, parts } of serversToWrap) {
|
|
8264
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
8265
|
+
}
|
|
8266
|
+
config.mcp_servers = servers;
|
|
8267
|
+
writeToml(configPath, config);
|
|
8268
|
+
console.log(import_chalk.default.green(`
|
|
8269
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
8270
|
+
anythingChanged = true;
|
|
8271
|
+
} else {
|
|
8272
|
+
console.log(import_chalk.default.yellow(" Skipped MCP server wrapping."));
|
|
8273
|
+
}
|
|
8274
|
+
console.log("");
|
|
8275
|
+
}
|
|
8276
|
+
console.log(
|
|
8277
|
+
import_chalk.default.yellow(
|
|
8278
|
+
" \u26A0\uFE0F Note: Codex does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Codex.\n Native bash and file operations are not monitored."
|
|
8279
|
+
)
|
|
8280
|
+
);
|
|
8281
|
+
console.log("");
|
|
8282
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
8283
|
+
console.log(
|
|
8284
|
+
import_chalk.default.blue(
|
|
8285
|
+
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.codex/config.toml and re-run."
|
|
8286
|
+
)
|
|
8287
|
+
);
|
|
8288
|
+
printDaemonTip();
|
|
8289
|
+
return;
|
|
8290
|
+
}
|
|
8291
|
+
if (anythingChanged) {
|
|
8292
|
+
console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Codex via MCP proxy!"));
|
|
8293
|
+
console.log(import_chalk.default.gray(" Restart Codex for changes to take effect."));
|
|
8294
|
+
printDaemonTip();
|
|
8295
|
+
}
|
|
8296
|
+
}
|
|
8246
8297
|
function setupHud() {
|
|
8247
8298
|
const homeDir2 = import_os10.default.homedir();
|
|
8248
8299
|
const hooksPath = import_path14.default.join(homeDir2, ".claude", "settings.json");
|
|
@@ -8960,7 +9011,7 @@ RAW: ${raw}
|
|
|
8960
9011
|
}
|
|
8961
9012
|
}) + "\n"
|
|
8962
9013
|
);
|
|
8963
|
-
process.exit(
|
|
9014
|
+
process.exit(2);
|
|
8964
9015
|
};
|
|
8965
9016
|
if (!toolName) {
|
|
8966
9017
|
sendBlock("Node9: unrecognised hook payload \u2014 tool name missing.");
|
|
@@ -9195,6 +9246,27 @@ var import_chalk6 = __toESM(require("chalk"));
|
|
|
9195
9246
|
init_shields();
|
|
9196
9247
|
init_audit();
|
|
9197
9248
|
init_config();
|
|
9249
|
+
|
|
9250
|
+
// src/utils/https-fetch.ts
|
|
9251
|
+
var import_https = __toESM(require("https"));
|
|
9252
|
+
function httpsFetch(url) {
|
|
9253
|
+
return new Promise((resolve, reject) => {
|
|
9254
|
+
import_https.default.get(url, (res) => {
|
|
9255
|
+
if (res.statusCode !== 200) {
|
|
9256
|
+
reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
|
|
9257
|
+
res.resume();
|
|
9258
|
+
return;
|
|
9259
|
+
}
|
|
9260
|
+
const chunks = [];
|
|
9261
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
9262
|
+
res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
9263
|
+
res.on("error", reject);
|
|
9264
|
+
}).on("error", reject);
|
|
9265
|
+
});
|
|
9266
|
+
}
|
|
9267
|
+
|
|
9268
|
+
// src/cli/commands/shield.ts
|
|
9269
|
+
var COMMUNITY_INDEX_URL = "https://raw.githubusercontent.com/node9ai/node9-proxy/main/shields/community/index.json";
|
|
9198
9270
|
function registerShieldCommand(program2) {
|
|
9199
9271
|
const shieldCmd = program2.command("shield").description("Manage pre-packaged security shield templates");
|
|
9200
9272
|
shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
|
|
@@ -9254,7 +9326,32 @@ function registerShieldCommand(program2) {
|
|
|
9254
9326
|
\u{1F6E1}\uFE0F Shield "${name}" disabled.
|
|
9255
9327
|
`));
|
|
9256
9328
|
});
|
|
9257
|
-
shieldCmd.command("list").description("Show
|
|
9329
|
+
shieldCmd.command("list").description("Show available shields (add --community to browse the marketplace)").option("--community", "List shields available from the community marketplace").action((opts) => {
|
|
9330
|
+
if (opts.community) {
|
|
9331
|
+
console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Community Shield Marketplace\n"));
|
|
9332
|
+
console.log(import_chalk6.default.gray(" Fetching index\u2026\n"));
|
|
9333
|
+
httpsFetch(COMMUNITY_INDEX_URL).then((body) => {
|
|
9334
|
+
const entries = JSON.parse(body);
|
|
9335
|
+
const installed = new Set(listShields().map((s) => s.name));
|
|
9336
|
+
for (const e of entries) {
|
|
9337
|
+
const tag = installed.has(e.name) ? import_chalk6.default.green("installed") : import_chalk6.default.gray("available");
|
|
9338
|
+
console.log(
|
|
9339
|
+
` ${tag} ${import_chalk6.default.cyan(e.name.padEnd(12))} ${e.description} ${import_chalk6.default.gray(`by ${e.author}`)}`
|
|
9340
|
+
);
|
|
9341
|
+
}
|
|
9342
|
+
console.log("");
|
|
9343
|
+
console.log(
|
|
9344
|
+
import_chalk6.default.gray(` Install a shield: ${import_chalk6.default.cyan("node9 shield install <name>")}
|
|
9345
|
+
`)
|
|
9346
|
+
);
|
|
9347
|
+
}).catch((err2) => {
|
|
9348
|
+
console.error(import_chalk6.default.red(`
|
|
9349
|
+
\u274C Could not fetch community index: ${String(err2)}
|
|
9350
|
+
`));
|
|
9351
|
+
process.exit(1);
|
|
9352
|
+
});
|
|
9353
|
+
return;
|
|
9354
|
+
}
|
|
9258
9355
|
const active = new Set(readActiveShields());
|
|
9259
9356
|
console.log(import_chalk6.default.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
|
|
9260
9357
|
for (const shield of listShields()) {
|
|
@@ -9264,6 +9361,10 @@ function registerShieldCommand(program2) {
|
|
|
9264
9361
|
console.log(import_chalk6.default.gray(` aliases: ${shield.aliases.join(", ")}`));
|
|
9265
9362
|
}
|
|
9266
9363
|
console.log("");
|
|
9364
|
+
console.log(
|
|
9365
|
+
import_chalk6.default.gray(` Browse community shields: ${import_chalk6.default.cyan("node9 shield list --community")}
|
|
9366
|
+
`)
|
|
9367
|
+
);
|
|
9267
9368
|
});
|
|
9268
9369
|
shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
|
|
9269
9370
|
const active = readActiveShields();
|
|
@@ -9401,6 +9502,52 @@ function registerShieldCommand(program2) {
|
|
|
9401
9502
|
`)
|
|
9402
9503
|
);
|
|
9403
9504
|
});
|
|
9505
|
+
shieldCmd.command("install <name>").description("Install a shield from the community marketplace into ~/.node9/shields/").action((name) => {
|
|
9506
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
9507
|
+
console.error(
|
|
9508
|
+
import_chalk6.default.red(
|
|
9509
|
+
`
|
|
9510
|
+
\u274C Invalid shield name: only alphanumeric characters, hyphens, and underscores are allowed
|
|
9511
|
+
`
|
|
9512
|
+
)
|
|
9513
|
+
);
|
|
9514
|
+
process.exit(1);
|
|
9515
|
+
}
|
|
9516
|
+
console.log(import_chalk6.default.bold(`
|
|
9517
|
+
\u{1F6E1}\uFE0F Installing shield "${name}"\u2026
|
|
9518
|
+
`));
|
|
9519
|
+
httpsFetch(COMMUNITY_INDEX_URL).then((indexBody) => {
|
|
9520
|
+
const entries = JSON.parse(indexBody);
|
|
9521
|
+
const entry = entries.find((e) => e.name === name);
|
|
9522
|
+
if (!entry) {
|
|
9523
|
+
const names = entries.map((e) => import_chalk6.default.cyan(e.name)).join(", ");
|
|
9524
|
+
console.error(
|
|
9525
|
+
import_chalk6.default.red(`\u274C Shield "${name}" not found in the community marketplace.
|
|
9526
|
+
`)
|
|
9527
|
+
);
|
|
9528
|
+
console.error(` Available: ${names}
|
|
9529
|
+
`);
|
|
9530
|
+
process.exit(1);
|
|
9531
|
+
}
|
|
9532
|
+
return httpsFetch(entry.url);
|
|
9533
|
+
}).then((shieldBody) => {
|
|
9534
|
+
const shieldJson = JSON.parse(shieldBody);
|
|
9535
|
+
installShield(name, shieldJson);
|
|
9536
|
+
console.log(
|
|
9537
|
+
import_chalk6.default.green(`\u2705 Shield "${name}" installed to ~/.node9/shields/${name}.json`)
|
|
9538
|
+
);
|
|
9539
|
+
console.log(
|
|
9540
|
+
import_chalk6.default.gray(` Activate it with: ${import_chalk6.default.cyan(`node9 shield enable ${name}`)}
|
|
9541
|
+
`)
|
|
9542
|
+
);
|
|
9543
|
+
appendConfigAudit({ event: "shield-install", shield: name });
|
|
9544
|
+
}).catch((err2) => {
|
|
9545
|
+
console.error(import_chalk6.default.red(`
|
|
9546
|
+
\u274C Install failed: ${String(err2)}
|
|
9547
|
+
`));
|
|
9548
|
+
process.exit(1);
|
|
9549
|
+
});
|
|
9550
|
+
});
|
|
9404
9551
|
}
|
|
9405
9552
|
function registerConfigShowCommand(program2) {
|
|
9406
9553
|
program2.command("config show").description(
|
|
@@ -9934,8 +10081,10 @@ var import_chalk11 = __toESM(require("chalk"));
|
|
|
9934
10081
|
var import_fs23 = __toESM(require("fs"));
|
|
9935
10082
|
var import_path25 = __toESM(require("path"));
|
|
9936
10083
|
var import_os19 = __toESM(require("os"));
|
|
9937
|
-
var
|
|
10084
|
+
var import_https2 = __toESM(require("https"));
|
|
9938
10085
|
init_core();
|
|
10086
|
+
init_shields();
|
|
10087
|
+
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
9939
10088
|
function fireTelemetryPing(agents) {
|
|
9940
10089
|
try {
|
|
9941
10090
|
const body = JSON.stringify({
|
|
@@ -9944,7 +10093,7 @@ function fireTelemetryPing(agents) {
|
|
|
9944
10093
|
os: process.platform,
|
|
9945
10094
|
node9_version: process.env.npm_package_version ?? "unknown"
|
|
9946
10095
|
});
|
|
9947
|
-
const req =
|
|
10096
|
+
const req = import_https2.default.request(
|
|
9948
10097
|
{
|
|
9949
10098
|
hostname: "api.node9.ai",
|
|
9950
10099
|
path: "/api/v1/telemetry",
|
|
@@ -9978,7 +10127,17 @@ function registerInitCommand(program2) {
|
|
|
9978
10127
|
message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
|
|
9979
10128
|
default: true
|
|
9980
10129
|
});
|
|
9981
|
-
if (enableShields)
|
|
10130
|
+
if (enableShields) {
|
|
10131
|
+
chosenMode = "standard";
|
|
10132
|
+
try {
|
|
10133
|
+
const current = readActiveShields();
|
|
10134
|
+
const merged = Array.from(/* @__PURE__ */ new Set([...current, ...DEFAULT_SHIELDS]));
|
|
10135
|
+
const hasNewShields = DEFAULT_SHIELDS.some((s) => !current.includes(s));
|
|
10136
|
+
if (hasNewShields) writeActiveShields(merged);
|
|
10137
|
+
} catch (err2) {
|
|
10138
|
+
console.log(import_chalk11.default.yellow(` \u26A0\uFE0F Could not update shields: ${String(err2)}`));
|
|
10139
|
+
}
|
|
10140
|
+
}
|
|
9982
10141
|
console.log("");
|
|
9983
10142
|
}
|
|
9984
10143
|
const configPath = import_path25.default.join(import_os19.default.homedir(), ".node9", "config.json");
|
|
@@ -10016,9 +10175,9 @@ function registerInitCommand(program2) {
|
|
|
10016
10175
|
);
|
|
10017
10176
|
if (found.length === 0) {
|
|
10018
10177
|
console.log(
|
|
10019
|
-
import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, or
|
|
10178
|
+
import_chalk11.default.gray("No AI agents detected. Install Claude Code, Gemini CLI, Cursor, or Codex")
|
|
10020
10179
|
);
|
|
10021
|
-
console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor>"));
|
|
10180
|
+
console.log(import_chalk11.default.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
|
|
10022
10181
|
return;
|
|
10023
10182
|
}
|
|
10024
10183
|
console.log(import_chalk11.default.bold("Detected agents:"));
|
|
@@ -10031,6 +10190,7 @@ function registerInitCommand(program2) {
|
|
|
10031
10190
|
if (agent === "claude") await setupClaude();
|
|
10032
10191
|
else if (agent === "gemini") await setupGemini();
|
|
10033
10192
|
else if (agent === "cursor") await setupCursor();
|
|
10193
|
+
else if (agent === "codex") await setupCodex();
|
|
10034
10194
|
console.log("");
|
|
10035
10195
|
}
|
|
10036
10196
|
{
|
|
@@ -10652,6 +10812,20 @@ var TOOLS = [
|
|
|
10652
10812
|
required: ["service"]
|
|
10653
10813
|
}
|
|
10654
10814
|
},
|
|
10815
|
+
{
|
|
10816
|
+
name: "node9_shield_disable",
|
|
10817
|
+
description: "Disable a node9 shield. Use node9_shield_list to see currently active shields.",
|
|
10818
|
+
inputSchema: {
|
|
10819
|
+
type: "object",
|
|
10820
|
+
properties: {
|
|
10821
|
+
service: {
|
|
10822
|
+
type: "string",
|
|
10823
|
+
description: 'Shield name to disable (e.g. "postgres", "aws", "github", "filesystem").'
|
|
10824
|
+
}
|
|
10825
|
+
},
|
|
10826
|
+
required: ["service"]
|
|
10827
|
+
}
|
|
10828
|
+
},
|
|
10655
10829
|
{
|
|
10656
10830
|
name: "node9_approver_list",
|
|
10657
10831
|
description: "List all node9 approver channels and their current enabled/disabled state. Approvers are the channels through which node9 asks a human to approve risky tool calls. Channels: native (OS popup), browser (web UI), cloud (team policy server), terminal (stdin).",
|
|
@@ -10781,6 +10955,24 @@ function handleShieldEnable(args) {
|
|
|
10781
10955
|
const shield = getShield(name);
|
|
10782
10956
|
return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
|
|
10783
10957
|
}
|
|
10958
|
+
function handleShieldDisable(args) {
|
|
10959
|
+
const service = args.service;
|
|
10960
|
+
if (typeof service !== "string" || !service) {
|
|
10961
|
+
throw new Error("service is required");
|
|
10962
|
+
}
|
|
10963
|
+
const name = resolveShieldName(service);
|
|
10964
|
+
if (!name) {
|
|
10965
|
+
throw new Error(
|
|
10966
|
+
`Unknown shield: "${service}". Run node9_shield_list to see available shields.`
|
|
10967
|
+
);
|
|
10968
|
+
}
|
|
10969
|
+
const active = readActiveShields();
|
|
10970
|
+
if (!active.includes(name)) {
|
|
10971
|
+
return `Shield "${name}" is not active.`;
|
|
10972
|
+
}
|
|
10973
|
+
writeActiveShields(active.filter((s) => s !== name));
|
|
10974
|
+
return `Shield "${name}" disabled.`;
|
|
10975
|
+
}
|
|
10784
10976
|
var GLOBAL_CONFIG_PATH2 = import_path27.default.join(import_os20.default.homedir(), ".node9", "config.json");
|
|
10785
10977
|
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
10786
10978
|
function readGlobalConfigRaw() {
|
|
@@ -10909,6 +11101,8 @@ function runMcpServer() {
|
|
|
10909
11101
|
text = handleShieldList();
|
|
10910
11102
|
} else if (toolName === "node9_shield_enable") {
|
|
10911
11103
|
text = handleShieldEnable(toolArgs);
|
|
11104
|
+
} else if (toolName === "node9_shield_disable") {
|
|
11105
|
+
text = handleShieldDisable(toolArgs);
|
|
10912
11106
|
} else if (toolName === "node9_approver_list") {
|
|
10913
11107
|
text = handleApproverList();
|
|
10914
11108
|
} else if (toolName === "node9_approver_set") {
|