@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/index.js
CHANGED
|
@@ -280,173 +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
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
filesystem: {
|
|
415
|
-
name: "filesystem",
|
|
416
|
-
description: "Protects the local filesystem from dangerous AI operations",
|
|
417
|
-
aliases: ["fs"],
|
|
418
|
-
smartRules: [
|
|
419
|
-
{
|
|
420
|
-
name: "shield:filesystem:review-chmod-777",
|
|
421
|
-
tool: "bash",
|
|
422
|
-
conditions: [
|
|
423
|
-
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
424
|
-
],
|
|
425
|
-
verdict: "review",
|
|
426
|
-
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
427
|
-
},
|
|
428
|
-
{
|
|
429
|
-
name: "shield:filesystem:review-write-etc",
|
|
430
|
-
tool: "bash",
|
|
431
|
-
conditions: [
|
|
432
|
-
{
|
|
433
|
-
field: "command",
|
|
434
|
-
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
435
|
-
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
436
|
-
op: "matches",
|
|
437
|
-
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
438
|
-
}
|
|
439
|
-
],
|
|
440
|
-
verdict: "review",
|
|
441
|
-
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
442
|
-
}
|
|
443
|
-
],
|
|
444
|
-
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
445
|
-
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
446
|
-
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
447
|
-
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;
|
|
448
290
|
}
|
|
449
|
-
|
|
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();
|
|
450
350
|
function resolveShieldName(input) {
|
|
451
351
|
const lower = input.toLowerCase();
|
|
452
352
|
if (SHIELDS[lower]) return lower;
|
|
@@ -2070,7 +1970,7 @@ function isDaemonRunning() {
|
|
|
2070
1970
|
return false;
|
|
2071
1971
|
}
|
|
2072
1972
|
}
|
|
2073
|
-
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) {
|
|
2074
1974
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2075
1975
|
const ctrl = new AbortController();
|
|
2076
1976
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -2091,7 +1991,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2091
1991
|
...cwd && { cwd },
|
|
2092
1992
|
...recoveryCommand && { recoveryCommand },
|
|
2093
1993
|
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
2094
|
-
...viewOnly && { viewOnly: true }
|
|
1994
|
+
...viewOnly && { viewOnly: true },
|
|
1995
|
+
...localSmartRuleMatched && { localSmartRuleMatched: true }
|
|
2095
1996
|
}),
|
|
2096
1997
|
signal: ctrl.signal
|
|
2097
1998
|
});
|
|
@@ -2759,6 +2660,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2759
2660
|
let policyMatchedWord;
|
|
2760
2661
|
let riskMetadata;
|
|
2761
2662
|
let statefulRecoveryCommand;
|
|
2663
|
+
let localSmartRuleMatched = false;
|
|
2762
2664
|
let taintWarning = null;
|
|
2763
2665
|
if (isNetworkTool(toolName, args)) {
|
|
2764
2666
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -2902,6 +2804,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2902
2804
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
2903
2805
|
policyMatchedField = policyResult.matchedField;
|
|
2904
2806
|
policyMatchedWord = policyResult.matchedWord;
|
|
2807
|
+
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
2905
2808
|
riskMetadata = computeRiskMetadata(
|
|
2906
2809
|
args,
|
|
2907
2810
|
policyResult.tier ?? 6,
|
|
@@ -2944,22 +2847,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2944
2847
|
}
|
|
2945
2848
|
let cloudRequestId = null;
|
|
2946
2849
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
2947
|
-
if (cloudEnforced) {
|
|
2850
|
+
if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
2948
2851
|
try {
|
|
2949
2852
|
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
2950
2853
|
if (!initResult.pending) {
|
|
2951
2854
|
if (initResult.shadowMode) {
|
|
2952
2855
|
return { approved: true, checkedBy: "cloud" };
|
|
2953
2856
|
}
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
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;
|
|
2961
2869
|
}
|
|
2962
|
-
cloudRequestId = initResult.requestId || null;
|
|
2963
2870
|
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
2964
2871
|
} catch {
|
|
2965
2872
|
}
|
|
@@ -3005,7 +2912,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3005
2912
|
riskMetadata,
|
|
3006
2913
|
options?.activityId,
|
|
3007
2914
|
options?.cwd,
|
|
3008
|
-
statefulRecoveryCommand
|
|
2915
|
+
statefulRecoveryCommand,
|
|
2916
|
+
void 0,
|
|
2917
|
+
void 0,
|
|
2918
|
+
localSmartRuleMatched || options?.localSmartRuleMatched
|
|
3009
2919
|
);
|
|
3010
2920
|
daemonEntryId = entry.id;
|
|
3011
2921
|
daemonAllowCount = entry.allowCount;
|
|
@@ -3013,7 +2923,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3013
2923
|
}
|
|
3014
2924
|
}
|
|
3015
2925
|
}
|
|
3016
|
-
if (cloudEnforced && cloudRequestId) {
|
|
2926
|
+
if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
3017
2927
|
racePromises.push(
|
|
3018
2928
|
(async () => {
|
|
3019
2929
|
try {
|
package/dist/index.mjs
CHANGED
|
@@ -250,173 +250,73 @@ ${lines.join("\n")}`
|
|
|
250
250
|
import fs2 from "fs";
|
|
251
251
|
import path2 from "path";
|
|
252
252
|
import os2 from "os";
|
|
253
|
-
var
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
name: "shield:postgres:block-drop-table",
|
|
261
|
-
tool: "*",
|
|
262
|
-
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
263
|
-
verdict: "block",
|
|
264
|
-
reason: "DROP TABLE is irreversible \u2014 blocked by Postgres shield"
|
|
265
|
-
},
|
|
266
|
-
{
|
|
267
|
-
name: "shield:postgres:block-truncate",
|
|
268
|
-
tool: "*",
|
|
269
|
-
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
270
|
-
verdict: "block",
|
|
271
|
-
reason: "TRUNCATE is irreversible \u2014 blocked by Postgres shield"
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
name: "shield:postgres:block-drop-column",
|
|
275
|
-
tool: "*",
|
|
276
|
-
conditions: [
|
|
277
|
-
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
278
|
-
],
|
|
279
|
-
verdict: "block",
|
|
280
|
-
reason: "DROP COLUMN is irreversible \u2014 blocked by Postgres shield"
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
name: "shield:postgres:review-grant-revoke",
|
|
284
|
-
tool: "*",
|
|
285
|
-
conditions: [{ field: "sql", op: "matches", value: "\\b(GRANT|REVOKE)\\b", flags: "i" }],
|
|
286
|
-
verdict: "review",
|
|
287
|
-
reason: "Permission changes require human approval (Postgres shield)"
|
|
288
|
-
}
|
|
289
|
-
],
|
|
290
|
-
dangerousWords: ["dropdb", "pg_dropcluster"]
|
|
291
|
-
},
|
|
292
|
-
github: {
|
|
293
|
-
name: "github",
|
|
294
|
-
description: "Protects GitHub repositories from destructive AI operations",
|
|
295
|
-
aliases: ["git"],
|
|
296
|
-
smartRules: [
|
|
297
|
-
{
|
|
298
|
-
// Note: git branch -d/-D is already caught by the built-in review-git-destructive rule.
|
|
299
|
-
// This rule adds coverage for `git push --delete` which the built-in does not match.
|
|
300
|
-
name: "shield:github:review-delete-branch-remote",
|
|
301
|
-
tool: "bash",
|
|
302
|
-
conditions: [
|
|
303
|
-
{
|
|
304
|
-
field: "command",
|
|
305
|
-
op: "matches",
|
|
306
|
-
value: "git\\s+push\\s+.*--delete",
|
|
307
|
-
flags: "i"
|
|
308
|
-
}
|
|
309
|
-
],
|
|
310
|
-
verdict: "review",
|
|
311
|
-
reason: "Remote branch deletion requires human approval (GitHub shield)"
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
name: "shield:github:block-delete-repo",
|
|
315
|
-
tool: "*",
|
|
316
|
-
conditions: [
|
|
317
|
-
{ field: "command", op: "matches", value: "gh\\s+repo\\s+delete", flags: "i" }
|
|
318
|
-
],
|
|
319
|
-
verdict: "block",
|
|
320
|
-
reason: "Repository deletion is irreversible \u2014 blocked by GitHub shield"
|
|
321
|
-
}
|
|
322
|
-
],
|
|
323
|
-
dangerousWords: []
|
|
324
|
-
},
|
|
325
|
-
aws: {
|
|
326
|
-
name: "aws",
|
|
327
|
-
description: "Protects AWS infrastructure from destructive AI operations",
|
|
328
|
-
aliases: ["amazon"],
|
|
329
|
-
smartRules: [
|
|
330
|
-
{
|
|
331
|
-
name: "shield:aws:block-delete-s3-bucket",
|
|
332
|
-
tool: "*",
|
|
333
|
-
conditions: [
|
|
334
|
-
{
|
|
335
|
-
field: "command",
|
|
336
|
-
op: "matches",
|
|
337
|
-
value: "aws\\s+s3.*rb\\s|aws\\s+s3api\\s+delete-bucket",
|
|
338
|
-
flags: "i"
|
|
339
|
-
}
|
|
340
|
-
],
|
|
341
|
-
verdict: "block",
|
|
342
|
-
reason: "S3 bucket deletion is irreversible \u2014 blocked by AWS shield"
|
|
343
|
-
},
|
|
344
|
-
{
|
|
345
|
-
name: "shield:aws:review-iam-changes",
|
|
346
|
-
tool: "*",
|
|
347
|
-
conditions: [
|
|
348
|
-
{
|
|
349
|
-
field: "command",
|
|
350
|
-
op: "matches",
|
|
351
|
-
value: "aws\\s+iam\\s+(create|delete|attach|detach|put|remove)",
|
|
352
|
-
flags: "i"
|
|
353
|
-
}
|
|
354
|
-
],
|
|
355
|
-
verdict: "review",
|
|
356
|
-
reason: "IAM changes require human approval (AWS shield)"
|
|
357
|
-
},
|
|
358
|
-
{
|
|
359
|
-
name: "shield:aws:block-ec2-terminate",
|
|
360
|
-
tool: "*",
|
|
361
|
-
conditions: [
|
|
362
|
-
{
|
|
363
|
-
field: "command",
|
|
364
|
-
op: "matches",
|
|
365
|
-
value: "aws\\s+ec2\\s+terminate-instances",
|
|
366
|
-
flags: "i"
|
|
367
|
-
}
|
|
368
|
-
],
|
|
369
|
-
verdict: "block",
|
|
370
|
-
reason: "EC2 instance termination is irreversible \u2014 blocked by AWS shield"
|
|
371
|
-
},
|
|
372
|
-
{
|
|
373
|
-
name: "shield:aws:review-rds-delete",
|
|
374
|
-
tool: "*",
|
|
375
|
-
conditions: [
|
|
376
|
-
{ field: "command", op: "matches", value: "aws\\s+rds\\s+delete-", flags: "i" }
|
|
377
|
-
],
|
|
378
|
-
verdict: "review",
|
|
379
|
-
reason: "RDS deletion requires human approval (AWS shield)"
|
|
380
|
-
}
|
|
381
|
-
],
|
|
382
|
-
dangerousWords: []
|
|
383
|
-
},
|
|
384
|
-
filesystem: {
|
|
385
|
-
name: "filesystem",
|
|
386
|
-
description: "Protects the local filesystem from dangerous AI operations",
|
|
387
|
-
aliases: ["fs"],
|
|
388
|
-
smartRules: [
|
|
389
|
-
{
|
|
390
|
-
name: "shield:filesystem:review-chmod-777",
|
|
391
|
-
tool: "bash",
|
|
392
|
-
conditions: [
|
|
393
|
-
{ field: "command", op: "matches", value: "chmod\\s+(777|a\\+rwx)", flags: "i" }
|
|
394
|
-
],
|
|
395
|
-
verdict: "review",
|
|
396
|
-
reason: "chmod 777 requires human approval (filesystem shield)"
|
|
397
|
-
},
|
|
398
|
-
{
|
|
399
|
-
name: "shield:filesystem:review-write-etc",
|
|
400
|
-
tool: "bash",
|
|
401
|
-
conditions: [
|
|
402
|
-
{
|
|
403
|
-
field: "command",
|
|
404
|
-
// Narrow to write-indicative operations to avoid approval fatigue on reads.
|
|
405
|
-
// Matches: tee /etc/*, cp .../etc/*, mv .../etc/*, > /etc/*, install .../etc/*
|
|
406
|
-
op: "matches",
|
|
407
|
-
value: "(tee|\\bcp\\b|\\bmv\\b|install|>+)\\s+.*\\/etc\\/"
|
|
408
|
-
}
|
|
409
|
-
],
|
|
410
|
-
verdict: "review",
|
|
411
|
-
reason: "Writing to /etc requires human approval (filesystem shield)"
|
|
412
|
-
}
|
|
413
|
-
],
|
|
414
|
-
// dd removed: too common as a legitimate tool (disk imaging, file ops).
|
|
415
|
-
// mkfs removed: already in the built-in DANGEROUS_WORDS baseline.
|
|
416
|
-
// wipefs retained: rarely legitimate in an agent context and not in built-ins.
|
|
417
|
-
dangerousWords: ["wipefs"]
|
|
253
|
+
var BUILTIN_DIR = path2.join(__dirname, "shields", "builtin");
|
|
254
|
+
var USER_SHIELDS_DIR = path2.join(os2.homedir(), ".node9", "shields");
|
|
255
|
+
function validateShieldDefinition(raw, filePath) {
|
|
256
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
257
|
+
process.stderr.write(`[node9] Shield file is not an object: ${filePath}
|
|
258
|
+
`);
|
|
259
|
+
return null;
|
|
418
260
|
}
|
|
419
|
-
|
|
261
|
+
const r = raw;
|
|
262
|
+
if (typeof r.name !== "string" || !r.name) {
|
|
263
|
+
process.stderr.write(`[node9] Shield file missing 'name': ${filePath}
|
|
264
|
+
`);
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
if (typeof r.description !== "string") {
|
|
268
|
+
process.stderr.write(`[node9] Shield file missing 'description': ${filePath}
|
|
269
|
+
`);
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
if (!Array.isArray(r.aliases)) {
|
|
273
|
+
process.stderr.write(`[node9] Shield file missing 'aliases' array: ${filePath}
|
|
274
|
+
`);
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
if (!Array.isArray(r.smartRules)) {
|
|
278
|
+
process.stderr.write(`[node9] Shield file missing 'smartRules' array: ${filePath}
|
|
279
|
+
`);
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
if (!Array.isArray(r.dangerousWords)) {
|
|
283
|
+
process.stderr.write(`[node9] Shield file missing 'dangerousWords' array: ${filePath}
|
|
284
|
+
`);
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
return r;
|
|
288
|
+
}
|
|
289
|
+
function loadShieldsFromDir(dir, label) {
|
|
290
|
+
const result = {};
|
|
291
|
+
let entries;
|
|
292
|
+
try {
|
|
293
|
+
entries = fs2.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
294
|
+
} catch (err) {
|
|
295
|
+
if (err.code !== "ENOENT") {
|
|
296
|
+
process.stderr.write(`[node9] Could not read ${label} shields dir ${dir}: ${String(err)}
|
|
297
|
+
`);
|
|
298
|
+
}
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
for (const file of entries) {
|
|
302
|
+
const filePath = path2.join(dir, file);
|
|
303
|
+
try {
|
|
304
|
+
const raw = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
|
|
305
|
+
const shield = validateShieldDefinition(raw, filePath);
|
|
306
|
+
if (shield) result[shield.name] = shield;
|
|
307
|
+
} catch (err) {
|
|
308
|
+
process.stderr.write(`[node9] Failed to load ${label} shield ${file}: ${String(err)}
|
|
309
|
+
`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
function buildSHIELDS() {
|
|
315
|
+
const builtins = loadShieldsFromDir(BUILTIN_DIR, "builtin");
|
|
316
|
+
const userShields = loadShieldsFromDir(USER_SHIELDS_DIR, "user");
|
|
317
|
+
return { ...builtins, ...userShields };
|
|
318
|
+
}
|
|
319
|
+
var SHIELDS = buildSHIELDS();
|
|
420
320
|
function resolveShieldName(input) {
|
|
421
321
|
const lower = input.toLowerCase();
|
|
422
322
|
if (SHIELDS[lower]) return lower;
|
|
@@ -2040,7 +1940,7 @@ function isDaemonRunning() {
|
|
|
2040
1940
|
return false;
|
|
2041
1941
|
}
|
|
2042
1942
|
}
|
|
2043
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
|
|
1943
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly, localSmartRuleMatched) {
|
|
2044
1944
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2045
1945
|
const ctrl = new AbortController();
|
|
2046
1946
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -2061,7 +1961,8 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2061
1961
|
...cwd && { cwd },
|
|
2062
1962
|
...recoveryCommand && { recoveryCommand },
|
|
2063
1963
|
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
2064
|
-
...viewOnly && { viewOnly: true }
|
|
1964
|
+
...viewOnly && { viewOnly: true },
|
|
1965
|
+
...localSmartRuleMatched && { localSmartRuleMatched: true }
|
|
2065
1966
|
}),
|
|
2066
1967
|
signal: ctrl.signal
|
|
2067
1968
|
});
|
|
@@ -2729,6 +2630,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2729
2630
|
let policyMatchedWord;
|
|
2730
2631
|
let riskMetadata;
|
|
2731
2632
|
let statefulRecoveryCommand;
|
|
2633
|
+
let localSmartRuleMatched = false;
|
|
2732
2634
|
let taintWarning = null;
|
|
2733
2635
|
if (isNetworkTool(toolName, args)) {
|
|
2734
2636
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -2872,6 +2774,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2872
2774
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
2873
2775
|
policyMatchedField = policyResult.matchedField;
|
|
2874
2776
|
policyMatchedWord = policyResult.matchedWord;
|
|
2777
|
+
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
2875
2778
|
riskMetadata = computeRiskMetadata(
|
|
2876
2779
|
args,
|
|
2877
2780
|
policyResult.tier ?? 6,
|
|
@@ -2914,22 +2817,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2914
2817
|
}
|
|
2915
2818
|
let cloudRequestId = null;
|
|
2916
2819
|
const cloudEnforced = approvers.cloud && !!creds?.apiKey;
|
|
2917
|
-
if (cloudEnforced) {
|
|
2820
|
+
if (cloudEnforced && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
2918
2821
|
try {
|
|
2919
2822
|
const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
|
|
2920
2823
|
if (!initResult.pending) {
|
|
2921
2824
|
if (initResult.shadowMode) {
|
|
2922
2825
|
return { approved: true, checkedBy: "cloud" };
|
|
2923
2826
|
}
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2827
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
2828
|
+
return {
|
|
2829
|
+
approved: !!initResult.approved,
|
|
2830
|
+
reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
|
|
2831
|
+
checkedBy: initResult.approved ? "cloud" : void 0,
|
|
2832
|
+
blockedBy: initResult.approved ? void 0 : "team-policy",
|
|
2833
|
+
blockedByLabel: "Organization Policy (SaaS)"
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
if (!localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
2838
|
+
cloudRequestId = initResult.requestId || null;
|
|
2931
2839
|
}
|
|
2932
|
-
cloudRequestId = initResult.requestId || null;
|
|
2933
2840
|
if (!taintWarning) explainableLabel = "Organization Policy (SaaS)";
|
|
2934
2841
|
} catch {
|
|
2935
2842
|
}
|
|
@@ -2975,7 +2882,10 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2975
2882
|
riskMetadata,
|
|
2976
2883
|
options?.activityId,
|
|
2977
2884
|
options?.cwd,
|
|
2978
|
-
statefulRecoveryCommand
|
|
2885
|
+
statefulRecoveryCommand,
|
|
2886
|
+
void 0,
|
|
2887
|
+
void 0,
|
|
2888
|
+
localSmartRuleMatched || options?.localSmartRuleMatched
|
|
2979
2889
|
);
|
|
2980
2890
|
daemonEntryId = entry.id;
|
|
2981
2891
|
daemonAllowCount = entry.allowCount;
|
|
@@ -2983,7 +2893,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2983
2893
|
}
|
|
2984
2894
|
}
|
|
2985
2895
|
}
|
|
2986
|
-
if (cloudEnforced && cloudRequestId) {
|
|
2896
|
+
if (cloudEnforced && cloudRequestId && !localSmartRuleMatched && !options?.localSmartRuleMatched) {
|
|
2987
2897
|
racePromises.push(
|
|
2988
2898
|
(async () => {
|
|
2989
2899
|
try {
|