@node9/proxy 1.7.1 → 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/dist/index.js CHANGED
@@ -280,251 +280,73 @@ ${lines.join("\n")}`
280
280
  var import_fs2 = __toESM(require("fs"));
281
281
  var import_path2 = __toESM(require("path"));
282
282
  var import_os2 = __toESM(require("os"));
283
- var SHIELDS = {
284
- postgres: {
285
- name: "postgres",
286
- description: "Protects PostgreSQL databases from destructive AI operations",
287
- aliases: ["pg", "postgresql"],
288
- smartRules: [
289
- {
290
- name: "shield:postgres:block-drop-table",
291
- tool: "*",
292
- conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
293
- verdict: "block",
294
- reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
295
- },
296
- {
297
- name: "shield:postgres:block-truncate",
298
- tool: "*",
299
- conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
300
- verdict: "block",
301
- reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
302
- },
303
- {
304
- name: "shield:postgres:block-drop-column",
305
- tool: "*",
306
- conditions: [
307
- { field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
308
- ],
309
- verdict: "block",
310
- reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
311
- },
312
- {
313
- name: "shield:postgres:review-grant-revoke",
314
- tool: "*",
315
- conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
316
- verdict: "review",
317
- reason: "Permission changes require human approval (Postgres shield)"
318
- }
319
- ],
320
- dangerousWords: ["dropdb", "pg_dropcluster"]
321
- },
322
- github: {
323
- name: "github",
324
- description: "Protects GitHub repositories from destructive AI operations",
325
- aliases: ["git"],
326
- smartRules: [
327
- {
328
- // Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
329
- // This rule adds coverage for `git push --delete` which the built-in does not match.
330
- name: "shield:github:review-delete-branch-remote",
331
- tool: "bash",
332
- conditions: [
333
- {
334
- field: "command",
335
- op: "matches",
336
- value: "git\\s+push\\s+.*--delete",
337
- flags: "i"
338
- }
339
- ],
340
- verdict: "review",
341
- reason: "Remote branch deletion requires human approval (GitHub shield)"
342
- },
343
- {
344
- name: "shield:github:block-delete-repo",
345
- tool: "*",
346
- conditions: [
347
- { field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
348
- ],
349
- verdict: "block",
350
- reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
351
- }
352
- ],
353
- dangerousWords: []
354
- },
355
- aws: {
356
- name: "aws",
357
- description: "Protects AWS infrastructure from destructive AI operations",
358
- aliases: ["amazon"],
359
- smartRules: [
360
- {
361
- name: "shield:aws:block-delete-s3-bucket",
362
- tool: "*",
363
- conditions: [
364
- {
365
- field: "command",
366
- op: "matches",
367
- value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
368
- flags: "i"
369
- }
370
- ],
371
- verdict: "block",
372
- reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
373
- },
374
- {
375
- name: "shield:aws:review-iam-changes",
376
- tool: "*",
377
- conditions: [
378
- {
379
- field: "command",
380
- op: "matches",
381
- value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
382
- flags: "i"
383
- }
384
- ],
385
- verdict: "review",
386
- reason: "IAM changes require human approval (AWS shield)"
387
- },
388
- {
389
- name: "shield:aws:block-ec2-terminate",
390
- tool: "*",
391
- conditions: [
392
- {
393
- field: "command",
394
- op: "matches",
395
- value: "aws\\s+ec2\\s+terminate-instances",
396
- flags: "i"
397
- }
398
- ],
399
- verdict: "block",
400
- reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
401
- },
402
- {
403
- name: "shield:aws:review-rds-delete",
404
- tool: "*",
405
- conditions: [
406
- { field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
407
- ],
408
- verdict: "review",
409
- reason: "RDS deletion requires human approval (AWS shield)"
410
- }
411
- ],
412
- dangerousWords: []
413
- },
414
- "bash-safe": {
415
- name: "bash-safe",
416
- description: "Blocks high-risk bash patterns: pipe-to-shell, rm -rf /, disk overwrites, eval",
417
- aliases: ["bash", "shell"],
418
- smartRules: [
419
- {
420
- name: "shield:bash-safe:block-pipe-to-shell",
421
- tool: "bash",
422
- conditions: [
423
- {
424
- field: "command",
425
- op: "matches",
426
- value: "(curl|wget)\\s+[^|]*\\|\\s*(bash|sh|zsh|fish|python3?|ruby|perl|node)",
427
- flags: "i"
428
- }
429
- ],
430
- verdict: "block",
431
- reason: "Pipe-to-shell is a common supply-chain attack vector \u2014 blocked by bash-safe shield"
432
- },
433
- {
434
- name: "shield:bash-safe:block-obfuscated-exec",
435
- tool: "bash",
436
- conditions: [
437
- {
438
- field: "command",
439
- op: "matches",
440
- value: "base64\\s+(-d|--decode).*\\|\\s*(bash|sh|zsh)",
441
- flags: "i"
442
- }
443
- ],
444
- verdict: "block",
445
- reason: "Obfuscated execution via base64 decode \u2014 blocked by bash-safe shield"
446
- },
447
- {
448
- name: "shield:bash-safe:block-rm-root",
449
- tool: "bash",
450
- conditions: [
451
- {
452
- field: "command",
453
- op: "matches",
454
- 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*$",
455
- flags: "i"
456
- }
457
- ],
458
- verdict: "block",
459
- reason: "rm -rf of root or home directory is catastrophic \u2014 blocked by bash-safe shield"
460
- },
461
- {
462
- name: "shield:bash-safe:block-disk-overwrite",
463
- tool: "bash",
464
- conditions: [
465
- {
466
- field: "command",
467
- op: "matches",
468
- value: "dd\\s+.*of=\\/dev\\/(sd|nvme|hd|vd|xvd)",
469
- flags: "i"
470
- }
471
- ],
472
- verdict: "block",
473
- reason: "Writing directly to a block device is irreversible \u2014 blocked by bash-safe shield"
474
- },
475
- {
476
- name: "shield:bash-safe:review-eval",
477
- tool: "bash",
478
- conditions: [
479
- {
480
- field: "command",
481
- op: "matches",
482
- value: '\\beval\\s+[\\$`("]',
483
- flags: "i"
484
- }
485
- ],
486
- verdict: "review",
487
- reason: "eval of dynamic content requires human approval (bash-safe shield)"
488
- }
489
- ],
490
- dangerousWords: []
491
- },
492
- filesystem: {
493
- name: "filesystem",
494
- description: "Protects the local filesystem from dangerous AI operations",
495
- aliases: ["fs"],
496
- smartRules: [
497
- {
498
- name: "shield:filesystem:review-chmod-777",
499
- tool: "bash",
500
- conditions: [
501
- { field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
502
- ],
503
- verdict: "review",
504
- reason: "chmod 777 requires human approval (filesystem shield)"
505
- },
506
- {
507
- name: "shield:filesystem:review-write-etc",
508
- tool: "bash",
509
- conditions: [
510
- {
511
- field: "command",
512
- // Narrow to write-indicative operations to avoid approval fatigue on reads.
513
- // Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
514
- op: "matches",
515
- value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
516
- }
517
- ],
518
- verdict: "review",
519
- reason: "Writing to /etc requires human approval (filesystem shield)"
520
- }
521
- ],
522
- // dd removed: too common as a legitimate tool (disk imaging, file ops).
523
- // mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
524
- // wipefs retained: rarely legitimate in an agent context and not in built-ins.
525
- dangerousWords: ["wipefs"]
283
+ var BUILTIN_DIR = import_path2.default.join(__dirname, "shields", "builtin");
284
+ var USER_SHIELDS_DIR = import_path2.default.join(import_os2.default.homedir(), ".node9", "shields");
285
+ function validateShieldDefinition(raw, filePath) {
286
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
287
+ process.stderr.write(`[node9] Shield file is not an object: ${filePath}
288
+ `);
289
+ return null;
526
290
  }
527
- };
291
+ const r = raw;
292
+ if (typeof r.name !== "string" || !r.name) {
293
+ process.stderr.write(`[node9] Shield file missing 'name': ${filePath}
294
+ `);
295
+ return null;
296
+ }
297
+ if (typeof r.description !== "string") {
298
+ process.stderr.write(`[node9] Shield file missing 'description': ${filePath}
299
+ `);
300
+ return null;
301
+ }
302
+ if (!Array.isArray(r.aliases)) {
303
+ process.stderr.write(`[node9] Shield file missing 'aliases' array: ${filePath}
304
+ `);
305
+ return null;
306
+ }
307
+ if (!Array.isArray(r.smartRules)) {
308
+ process.stderr.write(`[node9] Shield file missing 'smartRules' array: ${filePath}
309
+ `);
310
+ return null;
311
+ }
312
+ if (!Array.isArray(r.dangerousWords)) {
313
+ process.stderr.write(`[node9] Shield file missing 'dangerousWords' array: ${filePath}
314
+ `);
315
+ return null;
316
+ }
317
+ return r;
318
+ }
319
+ function loadShieldsFromDir(dir, label) {
320
+ const result = {};
321
+ let entries;
322
+ try {
323
+ entries = import_fs2.default.readdirSync(dir).filter((f) => f.endsWith(".json"));
324
+ } catch (err) {
325
+ if (err.code !== "ENOENT") {
326
+ process.stderr.write(`[node9] Could not read ${label} shields dir ${dir}: ${String(err)}
327
+ `);
328
+ }
329
+ return result;
330
+ }
331
+ for (const file of entries) {
332
+ const filePath = import_path2.default.join(dir, file);
333
+ try {
334
+ const raw = JSON.parse(import_fs2.default.readFileSync(filePath, "utf-8"));
335
+ const shield = validateShieldDefinition(raw, filePath);
336
+ if (shield) result[shield.name] = shield;
337
+ } catch (err) {
338
+ process.stderr.write(`[node9] Failed to load ${label} shield ${file}: ${String(err)}
339
+ `);
340
+ }
341
+ }
342
+ return result;
343
+ }
344
+ function buildSHIELDS() {
345
+ const builtins = loadShieldsFromDir(BUILTIN_DIR, "builtin");
346
+ const userShields = loadShieldsFromDir(USER_SHIELDS_DIR, "user");
347
+ return { ...builtins, ...userShields };
348
+ }
349
+ var SHIELDS = buildSHIELDS();
528
350
  function resolveShieldName(input) {
529
351
  const lower = input.toLowerCase();
530
352
  if (SHIELDS[lower]) return lower;
@@ -2148,7 +1970,7 @@ function isDaemonRunning() {
2148
1970
  return false;
2149
1971
  }
2150
1972
  }
2151
- async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
1973
+ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched) {
2152
1974
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
2153
1975
  const ctrl = new AbortController();
2154
1976
  const timer = setTimeout(() => ctrl.abort(), 5e3);
@@ -2169,7 +1991,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
2169
1991
  ...cwd && { cwd },
2170
1992
  ...recoveryCommand && { recoveryCommand },
2171
1993
  ...skipBackgroundAuth && { skipBackgroundAuth: true },
2172
- ...viewOnly && { viewOnly: true }
1994
+ ...viewOnly && { viewOnly: true },
1995
+ ...localSmartRuleMatched && { localSmartRuleMatched: true }
2173
1996
  }),
2174
1997
  signal: ctrl.signal
2175
1998
  });
@@ -2837,6 +2660,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2837
2660
  let policyMatchedWord;
2838
2661
  let riskMetadata;
2839
2662
  let statefulRecoveryCommand;
2663
+ let localSmartRuleMatched = false;
2840
2664
  let taintWarning = null;
2841
2665
  if (isNetworkTool(toolName, args)) {
2842
2666
  const filePaths = extractFilePaths(toolName, args);
@@ -2980,6 +2804,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
2980
2804
  explainableLabel = policyResult.blockedByLabel || "Local Config";
2981
2805
  policyMatchedField = policyResult.matchedField;
2982
2806
  policyMatchedWord = policyResult.matchedWord;
2807
+ if (policyResult.ruleName) localSmartRuleMatched = true;
2983
2808
  riskMetadata = computeRiskMetadata(
2984
2809
  args,
2985
2810
  policyResult.tier ?? 6,
@@ -3022,22 +2847,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3022
2847
  }
3023
2848
  let cloudRequestId = null;
3024
2849
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
3025
- if (cloudEnforced) {
2850
+ if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3026
2851
  try {
3027
2852
  const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
3028
2853
  if (!initResult.pending) {
3029
2854
  if (initResult.shadowMode) {
3030
2855
  return { approved: true, checkedBy: "cloud" };
3031
2856
  }
3032
- return {
3033
- approved: !!initResult.approved,
3034
- reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
3035
- checkedBy: initResult.approved ? "cloud" : void 0,
3036
- blockedBy: initResult.approved ? void 0 : "team-policy",
3037
- blockedByLabel: "Organization Policy (SaaS)"
3038
- };
2857
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
2858
+ return {
2859
+ approved: !!initResult.approved,
2860
+ reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
2861
+ checkedBy: initResult.approved ? "cloud" : void 0,
2862
+ blockedBy: initResult.approved ? void 0 : "team-policy",
2863
+ blockedByLabel: "Organization Policy (SaaS)"
2864
+ };
2865
+ }
2866
+ }
2867
+ if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
2868
+ cloudRequestId = initResult.requestId || null;
3039
2869
  }
3040
- cloudRequestId = initResult.requestId || null;
3041
2870
  if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
3042
2871
  } catch {
3043
2872
  }
@@ -3083,7 +2912,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3083
2912
  riskMetadata,
3084
2913
  options?.activityId,
3085
2914
  options?.cwd,
3086
- statefulRecoveryCommand
2915
+ statefulRecoveryCommand,
2916
+ void 0,
2917
+ void 0,
2918
+ localSmartRuleMatched || options?.localSmartRuleMatched
3087
2919
  );
3088
2920
  daemonEntryId = entry.id;
3089
2921
  daemonAllowCount = entry.allowCount;
@@ -3091,7 +2923,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
3091
2923
  }
3092
2924
  }
3093
2925
  }
3094
- if (cloudEnforced && cloudRequestId) {
2926
+ if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
3095
2927
  racePromises.push(
3096
2928
  (async () => {
3097
2929
  try {