@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.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,177 +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
|
-
filesystem: {
|
|
495
|
-
name: "filesystem",
|
|
496
|
-
description: "Protects the local filesystem from dangerous AI operations",
|
|
497
|
-
aliases: ["fs"],
|
|
498
|
-
smartRules: [
|
|
499
|
-
{
|
|
500
|
-
name: "shield:filesystem:review-chmod-777",
|
|
501
|
-
tool: "bash",
|
|
502
|
-
conditions: [
|
|
503
|
-
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
504
|
-
],
|
|
505
|
-
verdict: "review",
|
|
506
|
-
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
507
|
-
},
|
|
508
|
-
{
|
|
509
|
-
name: "shield:filesystem:review-write-etc",
|
|
510
|
-
tool: "bash",
|
|
511
|
-
conditions: [
|
|
512
|
-
{
|
|
513
|
-
field: "command",
|
|
514
|
-
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
515
|
-
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
516
|
-
op: "matches",
|
|
517
|
-
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
518
|
-
}
|
|
519
|
-
],
|
|
520
|
-
verdict: "review",
|
|
521
|
-
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
522
|
-
}
|
|
523
|
-
],
|
|
524
|
-
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
525
|
-
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
526
|
-
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
527
|
-
dangerousWords: ["wipefs"]
|
|
528
|
-
}
|
|
529
|
-
};
|
|
444
|
+
BUILTIN_DIR = path2.join(__dirname, "shields", "builtin");
|
|
445
|
+
USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
|
|
446
|
+
SHIELDS = buildSHIELDS();
|
|
530
447
|
SHIELDS_STATE_FILE = path2.join(os2.homedir(), ".node9", "shields.json");
|
|
531
448
|
}
|
|
532
449
|
});
|
|
@@ -2503,7 +2420,7 @@ function isDaemonRunning() {
|
|
|
2503
2420
|
return false;
|
|
2504
2421
|
}
|
|
2505
2422
|
}
|
|
2506
|
-
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) {
|
|
2507
2424
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2508
2425
|
const ctrl = new AbortController();
|
|
2509
2426
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -2524,7 +2441,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2524
2441
|
...cwd && { cwd },
|
|
2525
2442
|
...recoveryCommand && { recoveryCommand },
|
|
2526
2443
|
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
2527
|
-
...viewOnly && { viewOnly: true }
|
|
2444
|
+
...viewOnly && { viewOnly: true },
|
|
2445
|
+
...localSmartRuleMatched && { localSmartRuleMatched: true }
|
|
2528
2446
|
}),
|
|
2529
2447
|
signal: ctrl.signal
|
|
2530
2448
|
});
|
|
@@ -3214,6 +3132,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3214
3132
|
let policyMatchedWord;
|
|
3215
3133
|
let riskMetadata;
|
|
3216
3134
|
let statefulRecoveryCommand;
|
|
3135
|
+
let localSmartRuleMatched = false;
|
|
3217
3136
|
let taintWarning = null;
|
|
3218
3137
|
if (isNetworkTool(toolName, args)) {
|
|
3219
3138
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -3357,6 +3276,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3357
3276
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
3358
3277
|
policyMatchedField = policyResult.matchedField;
|
|
3359
3278
|
policyMatchedWord = policyResult.matchedWord;
|
|
3279
|
+
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
3360
3280
|
riskMetadata = computeRiskMetadata(
|
|
3361
3281
|
args,
|
|
3362
3282
|
policyResult.tier ?? 6,
|
|
@@ -3399,22 +3319,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3399
3319
|
}
|
|
3400
3320
|
let cloudRequestId = null;
|
|
3401
3321
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
3402
|
-
if (cloudEnforced) {
|
|
3322
|
+
if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3403
3323
|
try {
|
|
3404
3324
|
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
3405
3325
|
if (!initResult.pending) {
|
|
3406
3326
|
if (initResult.shadowMode) {
|
|
3407
3327
|
return { approved: true, checkedBy: "cloud" };
|
|
3408
3328
|
}
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3329
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3330
|
+
return {
|
|
3331
|
+
approved: !!initResult.approved,
|
|
3332
|
+
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
3333
|
+
checkedBy: initResult.approved ? "cloud" : void 0,
|
|
3334
|
+
blockedBy: initResult.approved ? void 0 : "team-policy",
|
|
3335
|
+
blockedByLabel: "Organization Policy (SaaS)"
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3340
|
+
cloudRequestId = initResult.requestId || null;
|
|
3416
3341
|
}
|
|
3417
|
-
cloudRequestId = initResult.requestId || null;
|
|
3418
3342
|
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
3419
3343
|
} catch {
|
|
3420
3344
|
}
|
|
@@ -3460,7 +3384,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3460
3384
|
riskMetadata,
|
|
3461
3385
|
options?.activityId,
|
|
3462
3386
|
options?.cwd,
|
|
3463
|
-
statefulRecoveryCommand
|
|
3387
|
+
statefulRecoveryCommand,
|
|
3388
|
+
void 0,
|
|
3389
|
+
void 0,
|
|
3390
|
+
localSmartRuleMatched || options?.localSmartRuleMatched
|
|
3464
3391
|
);
|
|
3465
3392
|
daemonEntryId = entry.id;
|
|
3466
3393
|
daemonAllowCount = entry.allowCount;
|
|
@@ -3468,7 +3395,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3468
3395
|
}
|
|
3469
3396
|
}
|
|
3470
3397
|
}
|
|
3471
|
-
if (cloudEnforced && cloudRequestId) {
|
|
3398
|
+
if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3472
3399
|
racePromises.push(
|
|
3473
3400
|
(async () => {
|
|
3474
3401
|
try {
|
|
@@ -6135,7 +6062,8 @@ data: ${JSON.stringify(item.data)}
|
|
|
6135
6062
|
viewOnly = false,
|
|
6136
6063
|
fromCLI = false,
|
|
6137
6064
|
activityId,
|
|
6138
|
-
cwd
|
|
6065
|
+
cwd,
|
|
6066
|
+
localSmartRuleMatched = false
|
|
6139
6067
|
} = JSON.parse(body);
|
|
6140
6068
|
const id = fromCLI && typeof activityId === "string" && activityId || randomUUID4();
|
|
6141
6069
|
const entry = {
|
|
@@ -6215,7 +6143,7 @@ data: ${JSON.stringify(item.data)}
|
|
|
6215
6143
|
agent: typeof agent === "string" ? agent : void 0,
|
|
6216
6144
|
mcpServer: typeof mcpServer === "string" ? mcpServer : void 0
|
|
6217
6145
|
},
|
|
6218
|
-
{ calledFromDaemon: true }
|
|
6146
|
+
{ calledFromDaemon: true, localSmartRuleMatched: !!localSmartRuleMatched }
|
|
6219
6147
|
).then((result) => {
|
|
6220
6148
|
const e = pending.get(id);
|
|
6221
6149
|
if (!e) return;
|
|
@@ -7052,6 +6980,7 @@ async function startTail(options = {}) {
|
|
|
7052
6980
|
}
|
|
7053
6981
|
const connectionTime = Date.now();
|
|
7054
6982
|
const activityPending = /* @__PURE__ */ new Map();
|
|
6983
|
+
const orphanedResults = /* @__PURE__ */ new Map();
|
|
7055
6984
|
let csrfToken = "";
|
|
7056
6985
|
const approvalQueue = [];
|
|
7057
6986
|
let cardActive = false;
|
|
@@ -7386,9 +7315,14 @@ async function startTail(options = {}) {
|
|
|
7386
7315
|
renderResult(data, data);
|
|
7387
7316
|
return;
|
|
7388
7317
|
}
|
|
7318
|
+
const orphaned = orphanedResults.get(data.id);
|
|
7319
|
+
if (orphaned) {
|
|
7320
|
+
orphanedResults.delete(data.id);
|
|
7321
|
+
renderResult(data, orphaned);
|
|
7322
|
+
return;
|
|
7323
|
+
}
|
|
7389
7324
|
activityPending.set(data.id, data);
|
|
7390
|
-
|
|
7391
|
-
if (slowTool) renderPending(data);
|
|
7325
|
+
renderPending(data);
|
|
7392
7326
|
}
|
|
7393
7327
|
if (event === "snapshot") {
|
|
7394
7328
|
const time = new Date(data.ts).toLocaleTimeString([], { hour12: false });
|
|
@@ -7407,6 +7341,8 @@ async function startTail(options = {}) {
|
|
|
7407
7341
|
if (original) {
|
|
7408
7342
|
renderResult(original, data);
|
|
7409
7343
|
activityPending.delete(data.id);
|
|
7344
|
+
} else {
|
|
7345
|
+
orphanedResults.set(data.id, data);
|
|
7410
7346
|
}
|
|
7411
7347
|
}
|
|
7412
7348
|
}
|
|
@@ -7646,6 +7582,29 @@ function renderOffline() {
|
|
|
7646
7582
|
process.stdout.write(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")} ${dim("|")} ${dim("offline")}
|
|
7647
7583
|
`);
|
|
7648
7584
|
}
|
|
7585
|
+
function readActiveShieldsHud() {
|
|
7586
|
+
const now = Date.now();
|
|
7587
|
+
if (shieldsCache && now - shieldsCache.ts < SHIELDS_CACHE_TTL_MS) {
|
|
7588
|
+
return shieldsCache.value;
|
|
7589
|
+
}
|
|
7590
|
+
try {
|
|
7591
|
+
const shieldsPath = path29.join(os22.homedir(), ".node9", "shields.json");
|
|
7592
|
+
if (!fs26.existsSync(shieldsPath)) {
|
|
7593
|
+
shieldsCache = { value: [], ts: now };
|
|
7594
|
+
return [];
|
|
7595
|
+
}
|
|
7596
|
+
const parsed = JSON.parse(fs26.readFileSync(shieldsPath, "utf-8"));
|
|
7597
|
+
if (!Array.isArray(parsed.active)) {
|
|
7598
|
+
shieldsCache = { value: [], ts: now };
|
|
7599
|
+
return [];
|
|
7600
|
+
}
|
|
7601
|
+
const value = parsed.active.filter((s) => typeof s === "string").map((s) => s.slice(0, 64)).slice(0, 20);
|
|
7602
|
+
shieldsCache = { value, ts: now };
|
|
7603
|
+
return value;
|
|
7604
|
+
} catch {
|
|
7605
|
+
return [];
|
|
7606
|
+
}
|
|
7607
|
+
}
|
|
7649
7608
|
function renderSecurityLine(status) {
|
|
7650
7609
|
const parts = [];
|
|
7651
7610
|
parts.push(`${color(BLUE, "\u{1F6E1}")} ${bold("node9")}`);
|
|
@@ -7663,6 +7622,18 @@ function renderSecurityLine(status) {
|
|
|
7663
7622
|
};
|
|
7664
7623
|
const mc = modeColors[status.mode] ?? WHITE;
|
|
7665
7624
|
parts.push(`${dim("|")} ${color(mc, modeIcon[status.mode] ?? "")}${color(mc, status.mode)}`);
|
|
7625
|
+
const activeShields = readActiveShieldsHud();
|
|
7626
|
+
if (activeShields.length > 0) {
|
|
7627
|
+
const shieldAbbrevs = {
|
|
7628
|
+
"bash-safe": "bash",
|
|
7629
|
+
filesystem: "fs",
|
|
7630
|
+
postgres: "pg",
|
|
7631
|
+
github: "gh",
|
|
7632
|
+
aws: "aws"
|
|
7633
|
+
};
|
|
7634
|
+
const labels = activeShields.map((s) => shieldAbbrevs[s] ?? s).join(" ");
|
|
7635
|
+
parts.push(color(DIM, `[${labels}]`));
|
|
7636
|
+
}
|
|
7666
7637
|
if (status.mode === "observe") {
|
|
7667
7638
|
parts.push(`${dim("|")} ${color(GREEN2, `\u2705 ${status.session.allowed} passed`)}`);
|
|
7668
7639
|
if (status.session.wouldBlock > 0) {
|
|
@@ -7759,7 +7730,7 @@ async function main() {
|
|
|
7759
7730
|
renderOffline();
|
|
7760
7731
|
}
|
|
7761
7732
|
}
|
|
7762
|
-
var RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH;
|
|
7733
|
+
var RESET3, BOLD3, DIM, RED2, GREEN2, YELLOW2, BLUE, MAGENTA, CYAN2, WHITE, BAR_FILLED, BAR_EMPTY, BAR_WIDTH, shieldsCache, SHIELDS_CACHE_TTL_MS;
|
|
7763
7734
|
var init_hud = __esm({
|
|
7764
7735
|
"src/cli/hud.ts"() {
|
|
7765
7736
|
"use strict";
|
|
@@ -7777,6 +7748,8 @@ var init_hud = __esm({
|
|
|
7777
7748
|
BAR_FILLED = "\u2588";
|
|
7778
7749
|
BAR_EMPTY = "\u2591";
|
|
7779
7750
|
BAR_WIDTH = 10;
|
|
7751
|
+
shieldsCache = null;
|
|
7752
|
+
SHIELDS_CACHE_TTL_MS = 2e3;
|
|
7780
7753
|
}
|
|
7781
7754
|
});
|
|
7782
7755
|
|
|
@@ -7790,6 +7763,7 @@ import path14 from "path";
|
|
|
7790
7763
|
import os10 from "os";
|
|
7791
7764
|
import chalk from "chalk";
|
|
7792
7765
|
import { confirm } from "@inquirer/prompts";
|
|
7766
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
7793
7767
|
var NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
|
|
7794
7768
|
function hasNode9McpServer(servers) {
|
|
7795
7769
|
const entry = servers["node9"];
|
|
@@ -8153,7 +8127,8 @@ function detectAgents(homeDir2 = os10.homedir()) {
|
|
|
8153
8127
|
return {
|
|
8154
8128
|
claude: exists(path14.join(homeDir2, ".claude")) || exists(path14.join(homeDir2, ".claude.json")),
|
|
8155
8129
|
gemini: exists(path14.join(homeDir2, ".gemini")),
|
|
8156
|
-
cursor: exists(path14.join(homeDir2, ".cursor"))
|
|
8130
|
+
cursor: exists(path14.join(homeDir2, ".cursor")),
|
|
8131
|
+
codex: exists(path14.join(homeDir2, ".codex"))
|
|
8157
8132
|
};
|
|
8158
8133
|
}
|
|
8159
8134
|
async function setupCursor() {
|
|
@@ -8218,6 +8193,82 @@ async function setupCursor() {
|
|
|
8218
8193
|
printDaemonTip();
|
|
8219
8194
|
}
|
|
8220
8195
|
}
|
|
8196
|
+
function readToml(filePath) {
|
|
8197
|
+
try {
|
|
8198
|
+
if (fs11.existsSync(filePath)) {
|
|
8199
|
+
return parseToml(fs11.readFileSync(filePath, "utf-8"));
|
|
8200
|
+
}
|
|
8201
|
+
} catch {
|
|
8202
|
+
}
|
|
8203
|
+
return null;
|
|
8204
|
+
}
|
|
8205
|
+
function writeToml(filePath, data) {
|
|
8206
|
+
const dir = path14.dirname(filePath);
|
|
8207
|
+
if (!fs11.existsSync(dir)) fs11.mkdirSync(dir, { recursive: true });
|
|
8208
|
+
fs11.writeFileSync(filePath, stringifyToml(data));
|
|
8209
|
+
}
|
|
8210
|
+
async function setupCodex() {
|
|
8211
|
+
const homeDir2 = os10.homedir();
|
|
8212
|
+
const configPath = path14.join(homeDir2, ".codex", "config.toml");
|
|
8213
|
+
const config = readToml(configPath) ?? {};
|
|
8214
|
+
const servers = config.mcp_servers ?? {};
|
|
8215
|
+
let anythingChanged = false;
|
|
8216
|
+
if (!hasNode9McpServer(servers)) {
|
|
8217
|
+
servers["node9"] = NODE9_MCP_SERVER_ENTRY;
|
|
8218
|
+
config.mcp_servers = servers;
|
|
8219
|
+
writeToml(configPath, config);
|
|
8220
|
+
console.log(chalk.green(" \u2705 node9 MCP server added \u2192 node9 mcp-server"));
|
|
8221
|
+
anythingChanged = true;
|
|
8222
|
+
}
|
|
8223
|
+
const serversToWrap = [];
|
|
8224
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
8225
|
+
if (!server.command || server.command === "node9") continue;
|
|
8226
|
+
const parts = [server.command, ...server.args ?? []];
|
|
8227
|
+
serversToWrap.push({ name, originalCmd: parts.join(" "), parts });
|
|
8228
|
+
}
|
|
8229
|
+
if (serversToWrap.length > 0) {
|
|
8230
|
+
console.log(chalk.bold("The following existing entries will be modified:\n"));
|
|
8231
|
+
console.log(chalk.white(` ${configPath}`));
|
|
8232
|
+
for (const { name, originalCmd } of serversToWrap) {
|
|
8233
|
+
console.log(chalk.gray(` \u2022 ${name}: "${originalCmd}" \u2192 node9 ${originalCmd}`));
|
|
8234
|
+
}
|
|
8235
|
+
console.log("");
|
|
8236
|
+
const proceed = await confirm({ message: "Wrap these MCP servers?", default: true });
|
|
8237
|
+
if (proceed) {
|
|
8238
|
+
for (const { name, parts } of serversToWrap) {
|
|
8239
|
+
servers[name] = { ...servers[name], command: "node9", args: parts };
|
|
8240
|
+
}
|
|
8241
|
+
config.mcp_servers = servers;
|
|
8242
|
+
writeToml(configPath, config);
|
|
8243
|
+
console.log(chalk.green(`
|
|
8244
|
+
\u2705 ${serversToWrap.length} MCP server(s) wrapped`));
|
|
8245
|
+
anythingChanged = true;
|
|
8246
|
+
} else {
|
|
8247
|
+
console.log(chalk.yellow(" Skipped MCP server wrapping."));
|
|
8248
|
+
}
|
|
8249
|
+
console.log("");
|
|
8250
|
+
}
|
|
8251
|
+
console.log(
|
|
8252
|
+
chalk.yellow(
|
|
8253
|
+
" \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."
|
|
8254
|
+
)
|
|
8255
|
+
);
|
|
8256
|
+
console.log("");
|
|
8257
|
+
if (!anythingChanged && serversToWrap.length === 0) {
|
|
8258
|
+
console.log(
|
|
8259
|
+
chalk.blue(
|
|
8260
|
+
"\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.codex/config.toml and re-run."
|
|
8261
|
+
)
|
|
8262
|
+
);
|
|
8263
|
+
printDaemonTip();
|
|
8264
|
+
return;
|
|
8265
|
+
}
|
|
8266
|
+
if (anythingChanged) {
|
|
8267
|
+
console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Codex via MCP proxy!"));
|
|
8268
|
+
console.log(chalk.gray(" Restart Codex for changes to take effect."));
|
|
8269
|
+
printDaemonTip();
|
|
8270
|
+
}
|
|
8271
|
+
}
|
|
8221
8272
|
function setupHud() {
|
|
8222
8273
|
const homeDir2 = os10.homedir();
|
|
8223
8274
|
const hooksPath = path14.join(homeDir2, ".claude", "settings.json");
|
|
@@ -8935,7 +8986,7 @@ RAW: ${raw}
|
|
|
8935
8986
|
}
|
|
8936
8987
|
}) + "\n"
|
|
8937
8988
|
);
|
|
8938
|
-
process.exit(
|
|
8989
|
+
process.exit(2);
|
|
8939
8990
|
};
|
|
8940
8991
|
if (!toolName) {
|
|
8941
8992
|
sendBlock("Node9: unrecognised hook payload \u2014 tool name missing.");
|
|
@@ -9170,6 +9221,27 @@ init_shields();
|
|
|
9170
9221
|
init_audit();
|
|
9171
9222
|
init_config();
|
|
9172
9223
|
import chalk6 from "chalk";
|
|
9224
|
+
|
|
9225
|
+
// src/utils/https-fetch.ts
|
|
9226
|
+
import https from "https";
|
|
9227
|
+
function httpsFetch(url) {
|
|
9228
|
+
return new Promise((resolve, reject) => {
|
|
9229
|
+
https.get(url, (res) => {
|
|
9230
|
+
if (res.statusCode !== 200) {
|
|
9231
|
+
reject(new Error(`HTTP ${String(res.statusCode)} for ${url}`));
|
|
9232
|
+
res.resume();
|
|
9233
|
+
return;
|
|
9234
|
+
}
|
|
9235
|
+
const chunks = [];
|
|
9236
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
9237
|
+
res.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
9238
|
+
res.on("error", reject);
|
|
9239
|
+
}).on("error", reject);
|
|
9240
|
+
});
|
|
9241
|
+
}
|
|
9242
|
+
|
|
9243
|
+
// src/cli/commands/shield.ts
|
|
9244
|
+
var COMMUNITY_INDEX_URL = "https://raw.githubusercontent.com/node9ai/node9-proxy/main/shields/community/index.json";
|
|
9173
9245
|
function registerShieldCommand(program2) {
|
|
9174
9246
|
const shieldCmd = program2.command("shield").description("Manage pre-packaged security shield templates");
|
|
9175
9247
|
shieldCmd.command("enable <service>").description("Enable a security shield for a specific service").action((service) => {
|
|
@@ -9229,7 +9301,32 @@ function registerShieldCommand(program2) {
|
|
|
9229
9301
|
\u{1F6E1}\uFE0F Shield "${name}" disabled.
|
|
9230
9302
|
`));
|
|
9231
9303
|
});
|
|
9232
|
-
shieldCmd.command("list").description("Show
|
|
9304
|
+
shieldCmd.command("list").description("Show available shields (add --community to browse the marketplace)").option("--community", "List shields available from the community marketplace").action((opts) => {
|
|
9305
|
+
if (opts.community) {
|
|
9306
|
+
console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Community Shield Marketplace\n"));
|
|
9307
|
+
console.log(chalk6.gray(" Fetching index\u2026\n"));
|
|
9308
|
+
httpsFetch(COMMUNITY_INDEX_URL).then((body) => {
|
|
9309
|
+
const entries = JSON.parse(body);
|
|
9310
|
+
const installed = new Set(listShields().map((s) => s.name));
|
|
9311
|
+
for (const e of entries) {
|
|
9312
|
+
const tag = installed.has(e.name) ? chalk6.green("installed") : chalk6.gray("available");
|
|
9313
|
+
console.log(
|
|
9314
|
+
` ${tag} ${chalk6.cyan(e.name.padEnd(12))} ${e.description} ${chalk6.gray(`by ${e.author}`)}`
|
|
9315
|
+
);
|
|
9316
|
+
}
|
|
9317
|
+
console.log("");
|
|
9318
|
+
console.log(
|
|
9319
|
+
chalk6.gray(` Install a shield: ${chalk6.cyan("node9 shield install <name>")}
|
|
9320
|
+
`)
|
|
9321
|
+
);
|
|
9322
|
+
}).catch((err2) => {
|
|
9323
|
+
console.error(chalk6.red(`
|
|
9324
|
+
\u274C Could not fetch community index: ${String(err2)}
|
|
9325
|
+
`));
|
|
9326
|
+
process.exit(1);
|
|
9327
|
+
});
|
|
9328
|
+
return;
|
|
9329
|
+
}
|
|
9233
9330
|
const active = new Set(readActiveShields());
|
|
9234
9331
|
console.log(chalk6.bold("\n\u{1F6E1}\uFE0F Available Shields\n"));
|
|
9235
9332
|
for (const shield of listShields()) {
|
|
@@ -9239,6 +9336,10 @@ function registerShieldCommand(program2) {
|
|
|
9239
9336
|
console.log(chalk6.gray(` aliases: ${shield.aliases.join(", ")}`));
|
|
9240
9337
|
}
|
|
9241
9338
|
console.log("");
|
|
9339
|
+
console.log(
|
|
9340
|
+
chalk6.gray(` Browse community shields: ${chalk6.cyan("node9 shield list --community")}
|
|
9341
|
+
`)
|
|
9342
|
+
);
|
|
9242
9343
|
});
|
|
9243
9344
|
shieldCmd.command("status").description("Show active shields and their individual rules with verdicts").action(() => {
|
|
9244
9345
|
const active = readActiveShields();
|
|
@@ -9376,6 +9477,52 @@ function registerShieldCommand(program2) {
|
|
|
9376
9477
|
`)
|
|
9377
9478
|
);
|
|
9378
9479
|
});
|
|
9480
|
+
shieldCmd.command("install <name>").description("Install a shield from the community marketplace into ~/.node9/shields/").action((name) => {
|
|
9481
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
9482
|
+
console.error(
|
|
9483
|
+
chalk6.red(
|
|
9484
|
+
`
|
|
9485
|
+
\u274C Invalid shield name: only alphanumeric characters, hyphens, and underscores are allowed
|
|
9486
|
+
`
|
|
9487
|
+
)
|
|
9488
|
+
);
|
|
9489
|
+
process.exit(1);
|
|
9490
|
+
}
|
|
9491
|
+
console.log(chalk6.bold(`
|
|
9492
|
+
\u{1F6E1}\uFE0F Installing shield "${name}"\u2026
|
|
9493
|
+
`));
|
|
9494
|
+
httpsFetch(COMMUNITY_INDEX_URL).then((indexBody) => {
|
|
9495
|
+
const entries = JSON.parse(indexBody);
|
|
9496
|
+
const entry = entries.find((e) => e.name === name);
|
|
9497
|
+
if (!entry) {
|
|
9498
|
+
const names = entries.map((e) => chalk6.cyan(e.name)).join(", ");
|
|
9499
|
+
console.error(
|
|
9500
|
+
chalk6.red(`\u274C Shield "${name}" not found in the community marketplace.
|
|
9501
|
+
`)
|
|
9502
|
+
);
|
|
9503
|
+
console.error(` Available: ${names}
|
|
9504
|
+
`);
|
|
9505
|
+
process.exit(1);
|
|
9506
|
+
}
|
|
9507
|
+
return httpsFetch(entry.url);
|
|
9508
|
+
}).then((shieldBody) => {
|
|
9509
|
+
const shieldJson = JSON.parse(shieldBody);
|
|
9510
|
+
installShield(name, shieldJson);
|
|
9511
|
+
console.log(
|
|
9512
|
+
chalk6.green(`\u2705 Shield "${name}" installed to ~/.node9/shields/${name}.json`)
|
|
9513
|
+
);
|
|
9514
|
+
console.log(
|
|
9515
|
+
chalk6.gray(` Activate it with: ${chalk6.cyan(`node9 shield enable ${name}`)}
|
|
9516
|
+
`)
|
|
9517
|
+
);
|
|
9518
|
+
appendConfigAudit({ event: "shield-install", shield: name });
|
|
9519
|
+
}).catch((err2) => {
|
|
9520
|
+
console.error(chalk6.red(`
|
|
9521
|
+
\u274C Install failed: ${String(err2)}
|
|
9522
|
+
`));
|
|
9523
|
+
process.exit(1);
|
|
9524
|
+
});
|
|
9525
|
+
});
|
|
9379
9526
|
}
|
|
9380
9527
|
function registerConfigShowCommand(program2) {
|
|
9381
9528
|
program2.command("config show").description(
|
|
@@ -9910,7 +10057,9 @@ import chalk11 from "chalk";
|
|
|
9910
10057
|
import fs23 from "fs";
|
|
9911
10058
|
import path25 from "path";
|
|
9912
10059
|
import os19 from "os";
|
|
9913
|
-
import
|
|
10060
|
+
import https2 from "https";
|
|
10061
|
+
init_shields();
|
|
10062
|
+
var DEFAULT_SHIELDS = ["bash-safe", "filesystem", "postgres"];
|
|
9914
10063
|
function fireTelemetryPing(agents) {
|
|
9915
10064
|
try {
|
|
9916
10065
|
const body = JSON.stringify({
|
|
@@ -9919,7 +10068,7 @@ function fireTelemetryPing(agents) {
|
|
|
9919
10068
|
os: process.platform,
|
|
9920
10069
|
node9_version: process.env.npm_package_version ?? "unknown"
|
|
9921
10070
|
});
|
|
9922
|
-
const req =
|
|
10071
|
+
const req = https2.request(
|
|
9923
10072
|
{
|
|
9924
10073
|
hostname: "api.node9.ai",
|
|
9925
10074
|
path: "/api/v1/telemetry",
|
|
@@ -9953,7 +10102,17 @@ function registerInitCommand(program2) {
|
|
|
9953
10102
|
message: "Enable recommended safety shields? (blocks rm -rf, SQL drops, pipe-to-shell)",
|
|
9954
10103
|
default: true
|
|
9955
10104
|
});
|
|
9956
|
-
if (enableShields)
|
|
10105
|
+
if (enableShields) {
|
|
10106
|
+
chosenMode = "standard";
|
|
10107
|
+
try {
|
|
10108
|
+
const current = readActiveShields();
|
|
10109
|
+
const merged = Array.from(/* @__PURE__ */ new Set([...current, ...DEFAULT_SHIELDS]));
|
|
10110
|
+
const hasNewShields = DEFAULT_SHIELDS.some((s) => !current.includes(s));
|
|
10111
|
+
if (hasNewShields) writeActiveShields(merged);
|
|
10112
|
+
} catch (err2) {
|
|
10113
|
+
console.log(chalk11.yellow(` \u26A0\uFE0F Could not update shields: ${String(err2)}`));
|
|
10114
|
+
}
|
|
10115
|
+
}
|
|
9957
10116
|
console.log("");
|
|
9958
10117
|
}
|
|
9959
10118
|
const configPath = path25.join(os19.homedir(), ".node9", "config.json");
|
|
@@ -9991,9 +10150,9 @@ function registerInitCommand(program2) {
|
|
|
9991
10150
|
);
|
|
9992
10151
|
if (found.length === 0) {
|
|
9993
10152
|
console.log(
|
|
9994
|
-
chalk11.gray("No AI agents detected. Install Claude Code, Gemini CLI, or
|
|
10153
|
+
chalk11.gray("No AI agents detected. Install Claude Code, Gemini CLI, Cursor, or Codex")
|
|
9995
10154
|
);
|
|
9996
|
-
console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor>"));
|
|
10155
|
+
console.log(chalk11.gray("then run: node9 addto <claude|gemini|cursor|codex>"));
|
|
9997
10156
|
return;
|
|
9998
10157
|
}
|
|
9999
10158
|
console.log(chalk11.bold("Detected agents:"));
|
|
@@ -10006,6 +10165,7 @@ function registerInitCommand(program2) {
|
|
|
10006
10165
|
if (agent === "claude") await setupClaude();
|
|
10007
10166
|
else if (agent === "gemini") await setupGemini();
|
|
10008
10167
|
else if (agent === "cursor") await setupCursor();
|
|
10168
|
+
else if (agent === "codex") await setupCodex();
|
|
10009
10169
|
console.log("");
|
|
10010
10170
|
}
|
|
10011
10171
|
{
|
|
@@ -10627,6 +10787,20 @@ var TOOLS = [
|
|
|
10627
10787
|
required: ["service"]
|
|
10628
10788
|
}
|
|
10629
10789
|
},
|
|
10790
|
+
{
|
|
10791
|
+
name: "node9_shield_disable",
|
|
10792
|
+
description: "Disable a node9 shield. Use node9_shield_list to see currently active shields.",
|
|
10793
|
+
inputSchema: {
|
|
10794
|
+
type: "object",
|
|
10795
|
+
properties: {
|
|
10796
|
+
service: {
|
|
10797
|
+
type: "string",
|
|
10798
|
+
description: 'Shield name to disable (e.g. "postgres", "aws", "github", "filesystem").'
|
|
10799
|
+
}
|
|
10800
|
+
},
|
|
10801
|
+
required: ["service"]
|
|
10802
|
+
}
|
|
10803
|
+
},
|
|
10630
10804
|
{
|
|
10631
10805
|
name: "node9_approver_list",
|
|
10632
10806
|
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).",
|
|
@@ -10756,6 +10930,24 @@ function handleShieldEnable(args) {
|
|
|
10756
10930
|
const shield = getShield(name);
|
|
10757
10931
|
return `Shield "${name}" enabled \u2014 ${shield.smartRules.length} smart rule${shield.smartRules.length === 1 ? "" : "s"} now active.`;
|
|
10758
10932
|
}
|
|
10933
|
+
function handleShieldDisable(args) {
|
|
10934
|
+
const service = args.service;
|
|
10935
|
+
if (typeof service !== "string" || !service) {
|
|
10936
|
+
throw new Error("service is required");
|
|
10937
|
+
}
|
|
10938
|
+
const name = resolveShieldName(service);
|
|
10939
|
+
if (!name) {
|
|
10940
|
+
throw new Error(
|
|
10941
|
+
`Unknown shield: "${service}". Run node9_shield_list to see available shields.`
|
|
10942
|
+
);
|
|
10943
|
+
}
|
|
10944
|
+
const active = readActiveShields();
|
|
10945
|
+
if (!active.includes(name)) {
|
|
10946
|
+
return `Shield "${name}" is not active.`;
|
|
10947
|
+
}
|
|
10948
|
+
writeActiveShields(active.filter((s) => s !== name));
|
|
10949
|
+
return `Shield "${name}" disabled.`;
|
|
10950
|
+
}
|
|
10759
10951
|
var GLOBAL_CONFIG_PATH2 = path27.join(os20.homedir(), ".node9", "config.json");
|
|
10760
10952
|
var APPROVER_CHANNELS = ["native", "browser", "cloud", "terminal"];
|
|
10761
10953
|
function readGlobalConfigRaw() {
|
|
@@ -10884,6 +11076,8 @@ function runMcpServer() {
|
|
|
10884
11076
|
text = handleShieldList();
|
|
10885
11077
|
} else if (toolName === "node9_shield_enable") {
|
|
10886
11078
|
text = handleShieldEnable(toolArgs);
|
|
11079
|
+
} else if (toolName === "node9_shield_disable") {
|
|
11080
|
+
text = handleShieldDisable(toolArgs);
|
|
10887
11081
|
} else if (toolName === "node9_approver_list") {
|
|
10888
11082
|
text = handleApproverList();
|
|
10889
11083
|
} else if (toolName === "node9_approver_set") {
|