@node9/proxy 1.9.2 → 1.10.0
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 +56 -7
- package/dist/cli.js +1154 -623
- package/dist/cli.mjs +1136 -605
- package/dist/index.js +127 -23
- package/dist/index.mjs +125 -21
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -76,6 +76,11 @@ __export(audit_exports, {
|
|
|
76
76
|
appendToLog: () => appendToLog,
|
|
77
77
|
redactSecrets: () => redactSecrets
|
|
78
78
|
});
|
|
79
|
+
function isTestCall(toolName, args) {
|
|
80
|
+
if (toolName !== "Bash" && toolName !== "bash") return false;
|
|
81
|
+
const cmd = args?.command;
|
|
82
|
+
return typeof cmd === "string" && TEST_COMMAND_RE.test(cmd);
|
|
83
|
+
}
|
|
79
84
|
function redactSecrets(text) {
|
|
80
85
|
if (!text) return text;
|
|
81
86
|
let redacted = text;
|
|
@@ -111,12 +116,14 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
|
111
116
|
}
|
|
112
117
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
113
118
|
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
119
|
+
const testRun = isTestCall(toolName, args) ? { testRun: true } : {};
|
|
114
120
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
115
121
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
116
122
|
tool: toolName,
|
|
117
123
|
...argsField,
|
|
118
124
|
decision,
|
|
119
125
|
checkedBy,
|
|
126
|
+
...testRun,
|
|
120
127
|
agent: meta?.agent,
|
|
121
128
|
mcpServer: meta?.mcpServer,
|
|
122
129
|
hostname: import_os.default.hostname()
|
|
@@ -129,7 +136,7 @@ function appendConfigAudit(entry) {
|
|
|
129
136
|
hostname: import_os.default.hostname()
|
|
130
137
|
});
|
|
131
138
|
}
|
|
132
|
-
var import_fs, import_path, import_os, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG;
|
|
139
|
+
var import_fs, import_path, import_os, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, TEST_COMMAND_RE;
|
|
133
140
|
var init_audit = __esm({
|
|
134
141
|
"src/audit/index.ts"() {
|
|
135
142
|
"use strict";
|
|
@@ -139,6 +146,7 @@ var init_audit = __esm({
|
|
|
139
146
|
init_hasher();
|
|
140
147
|
LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
|
|
141
148
|
HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
|
|
149
|
+
TEST_COMMAND_RE = /(?:^|\s)(npm\s+(?:run\s+)?test|npx\s+(?:vitest|jest|mocha)|yarn\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|vitest|jest|mocha|pytest|py\.test|cargo\s+test|go\s+test|bundle\s+exec\s+rspec|rspec|phpunit|dotnet\s+test)\b/i;
|
|
142
150
|
}
|
|
143
151
|
});
|
|
144
152
|
|
|
@@ -199,6 +207,7 @@ var SmartRuleSchema = import_zod.z.object({
|
|
|
199
207
|
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
200
208
|
}),
|
|
201
209
|
reason: import_zod.z.string().optional(),
|
|
210
|
+
description: import_zod.z.string().optional(),
|
|
202
211
|
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
203
212
|
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
204
213
|
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
@@ -245,6 +254,11 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
245
254
|
dlp: import_zod.z.object({
|
|
246
255
|
enabled: import_zod.z.boolean().optional(),
|
|
247
256
|
scanIgnoredTools: import_zod.z.boolean().optional()
|
|
257
|
+
}).optional(),
|
|
258
|
+
loopDetection: import_zod.z.object({
|
|
259
|
+
enabled: import_zod.z.boolean().optional(),
|
|
260
|
+
threshold: import_zod.z.number().min(2).optional(),
|
|
261
|
+
windowSeconds: import_zod.z.number().min(10).optional()
|
|
248
262
|
}).optional()
|
|
249
263
|
}).optional(),
|
|
250
264
|
environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
|
|
@@ -266,8 +280,8 @@ function sanitizeConfig(raw) {
|
|
|
266
280
|
}
|
|
267
281
|
}
|
|
268
282
|
const lines = result.error.issues.map((issue) => {
|
|
269
|
-
const
|
|
270
|
-
return ` \u2022 ${
|
|
283
|
+
const path15 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
284
|
+
return ` \u2022 ${path15}: ${issue.message}`;
|
|
271
285
|
});
|
|
272
286
|
return {
|
|
273
287
|
sanitized,
|
|
@@ -492,7 +506,8 @@ var DEFAULT_CONFIG = {
|
|
|
492
506
|
}
|
|
493
507
|
],
|
|
494
508
|
verdict: "block",
|
|
495
|
-
reason: "Recursive delete of home directory is irreversible"
|
|
509
|
+
reason: "Recursive delete of home directory is irreversible",
|
|
510
|
+
description: "The AI wants to recursively delete your home directory. This will permanently destroy all your personal files and cannot be undone."
|
|
496
511
|
},
|
|
497
512
|
// ── SQL safety ────────────────────────────────────────────────────────
|
|
498
513
|
{
|
|
@@ -504,7 +519,8 @@ var DEFAULT_CONFIG = {
|
|
|
504
519
|
],
|
|
505
520
|
conditionMode: "all",
|
|
506
521
|
verdict: "review",
|
|
507
|
-
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
522
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table",
|
|
523
|
+
description: "The AI is running a SQL statement that will modify every row in the table \u2014 no WHERE filter was found. This could wipe or corrupt all your data."
|
|
508
524
|
},
|
|
509
525
|
{
|
|
510
526
|
name: "review-drop-truncate-shell",
|
|
@@ -519,7 +535,8 @@ var DEFAULT_CONFIG = {
|
|
|
519
535
|
],
|
|
520
536
|
conditionMode: "all",
|
|
521
537
|
verdict: "review",
|
|
522
|
-
reason: "SQL DDL destructive statement inside a shell command"
|
|
538
|
+
reason: "SQL DDL destructive statement inside a shell command",
|
|
539
|
+
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
523
540
|
},
|
|
524
541
|
// ── Git safety ────────────────────────────────────────────────────────
|
|
525
542
|
{
|
|
@@ -535,7 +552,8 @@ var DEFAULT_CONFIG = {
|
|
|
535
552
|
],
|
|
536
553
|
conditionMode: "all",
|
|
537
554
|
verdict: "block",
|
|
538
|
-
reason: "Force push overwrites remote history and cannot be undone"
|
|
555
|
+
reason: "Force push overwrites remote history and cannot be undone",
|
|
556
|
+
description: "The AI wants to force push to a remote git branch. This rewrites shared history and can permanently destroy commits that teammates have already pulled."
|
|
539
557
|
},
|
|
540
558
|
{
|
|
541
559
|
name: "review-git-push",
|
|
@@ -550,7 +568,8 @@ var DEFAULT_CONFIG = {
|
|
|
550
568
|
],
|
|
551
569
|
conditionMode: "all",
|
|
552
570
|
verdict: "review",
|
|
553
|
-
reason: "git push sends changes to a shared remote"
|
|
571
|
+
reason: "git push sends changes to a shared remote",
|
|
572
|
+
description: "The AI wants to push commits to a remote repository. Once pushed, those changes are visible to everyone with access."
|
|
554
573
|
},
|
|
555
574
|
{
|
|
556
575
|
name: "review-git-destructive",
|
|
@@ -565,7 +584,8 @@ var DEFAULT_CONFIG = {
|
|
|
565
584
|
],
|
|
566
585
|
conditionMode: "all",
|
|
567
586
|
verdict: "review",
|
|
568
|
-
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
587
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes",
|
|
588
|
+
description: "The AI wants to run a destructive git operation (reset, rebase, clean, or branch delete) that can permanently discard commits or uncommitted work."
|
|
569
589
|
},
|
|
570
590
|
// ── Shell safety ──────────────────────────────────────────────────────
|
|
571
591
|
{
|
|
@@ -574,7 +594,8 @@ var DEFAULT_CONFIG = {
|
|
|
574
594
|
conditions: [{ field: "command", op: "matches", value: "\\bsudo\\s", flags: "i" }],
|
|
575
595
|
conditionMode: "all",
|
|
576
596
|
verdict: "review",
|
|
577
|
-
reason: "Command requires elevated privileges"
|
|
597
|
+
reason: "Command requires elevated privileges",
|
|
598
|
+
description: "The AI wants to run a command as root (sudo). Commands with root access can modify system files, install software, or change security settings."
|
|
578
599
|
},
|
|
579
600
|
{
|
|
580
601
|
name: "review-curl-pipe-shell",
|
|
@@ -589,10 +610,12 @@ var DEFAULT_CONFIG = {
|
|
|
589
610
|
],
|
|
590
611
|
conditionMode: "all",
|
|
591
612
|
verdict: "block",
|
|
592
|
-
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
613
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector",
|
|
614
|
+
description: "The AI wants to download a script from the internet and run it immediately, without you seeing what it contains. This is one of the most common ways malware gets installed."
|
|
593
615
|
}
|
|
594
616
|
],
|
|
595
|
-
dlp: { enabled: true, scanIgnoredTools: true }
|
|
617
|
+
dlp: { enabled: true, scanIgnoredTools: true },
|
|
618
|
+
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 }
|
|
596
619
|
},
|
|
597
620
|
environments: {}
|
|
598
621
|
};
|
|
@@ -622,7 +645,8 @@ var ADVISORY_SMART_RULES = [
|
|
|
622
645
|
tool: "*",
|
|
623
646
|
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
624
647
|
verdict: "review",
|
|
625
|
-
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
648
|
+
reason: "rm can permanently delete files \u2014 confirm the target path",
|
|
649
|
+
description: "The AI wants to delete files. Unlike moving to trash, rm is permanent \u2014 the files cannot be recovered without a backup."
|
|
626
650
|
},
|
|
627
651
|
// ── SQL safety (Safe by Default) ──────────────────────────────────────────
|
|
628
652
|
// These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
|
|
@@ -634,14 +658,16 @@ var ADVISORY_SMART_RULES = [
|
|
|
634
658
|
tool: "*",
|
|
635
659
|
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
636
660
|
verdict: "review",
|
|
637
|
-
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead"
|
|
661
|
+
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead",
|
|
662
|
+
description: "The AI wants to drop a database table. This permanently deletes the table and all its data \u2014 there is no undo."
|
|
638
663
|
},
|
|
639
664
|
{
|
|
640
665
|
name: "review-truncate-sql",
|
|
641
666
|
tool: "*",
|
|
642
667
|
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
643
668
|
verdict: "review",
|
|
644
|
-
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead"
|
|
669
|
+
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead",
|
|
670
|
+
description: "The AI wants to truncate a database table, which instantly deletes every row. The table structure remains but all data is gone."
|
|
645
671
|
},
|
|
646
672
|
{
|
|
647
673
|
name: "review-drop-column-sql",
|
|
@@ -650,7 +676,8 @@ var ADVISORY_SMART_RULES = [
|
|
|
650
676
|
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
651
677
|
],
|
|
652
678
|
verdict: "review",
|
|
653
|
-
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead"
|
|
679
|
+
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead",
|
|
680
|
+
description: "The AI wants to drop a column from a database table. This permanently removes the column and all its data from every row."
|
|
654
681
|
}
|
|
655
682
|
];
|
|
656
683
|
var cachedConfig = null;
|
|
@@ -710,7 +737,8 @@ function getConfig(cwd) {
|
|
|
710
737
|
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
711
738
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
712
739
|
},
|
|
713
|
-
dlp: { ...DEFAULT_CONFIG.policy.dlp }
|
|
740
|
+
dlp: { ...DEFAULT_CONFIG.policy.dlp },
|
|
741
|
+
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection }
|
|
714
742
|
};
|
|
715
743
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
716
744
|
const applyLayer = (source) => {
|
|
@@ -749,6 +777,13 @@ function getConfig(cwd) {
|
|
|
749
777
|
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
750
778
|
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
751
779
|
}
|
|
780
|
+
if (p.loopDetection) {
|
|
781
|
+
const ld = p.loopDetection;
|
|
782
|
+
if (ld.enabled !== void 0) mergedPolicy.loopDetection.enabled = ld.enabled;
|
|
783
|
+
if (ld.threshold !== void 0) mergedPolicy.loopDetection.threshold = ld.threshold;
|
|
784
|
+
if (ld.windowSeconds !== void 0)
|
|
785
|
+
mergedPolicy.loopDetection.windowSeconds = ld.windowSeconds;
|
|
786
|
+
}
|
|
752
787
|
const envs = source.environments || {};
|
|
753
788
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
754
789
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -1523,9 +1558,9 @@ function matchesPattern(text, patterns) {
|
|
|
1523
1558
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1524
1559
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1525
1560
|
}
|
|
1526
|
-
function getNestedValue(obj,
|
|
1561
|
+
function getNestedValue(obj, path15) {
|
|
1527
1562
|
if (!obj || typeof obj !== "object") return null;
|
|
1528
|
-
return
|
|
1563
|
+
return path15.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1529
1564
|
}
|
|
1530
1565
|
function evaluateSmartConditions(args, rule) {
|
|
1531
1566
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -1665,6 +1700,9 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1665
1700
|
reason: matchedRule.reason,
|
|
1666
1701
|
tier: 2,
|
|
1667
1702
|
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1703
|
+
...(matchedRule.description ?? matchedRule.reason) && {
|
|
1704
|
+
ruleDescription: matchedRule.description ?? matchedRule.reason
|
|
1705
|
+
},
|
|
1668
1706
|
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1669
1707
|
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1670
1708
|
},
|
|
@@ -2090,7 +2128,7 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
2090
2128
|
}
|
|
2091
2129
|
|
|
2092
2130
|
// src/auth/orchestrator.ts
|
|
2093
|
-
var
|
|
2131
|
+
var import_crypto3 = require("crypto");
|
|
2094
2132
|
|
|
2095
2133
|
// src/ui/native.ts
|
|
2096
2134
|
var import_child_process2 = require("child_process");
|
|
@@ -2558,6 +2596,52 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
2558
2596
|
}
|
|
2559
2597
|
}
|
|
2560
2598
|
|
|
2599
|
+
// src/loop-detector.ts
|
|
2600
|
+
var import_fs10 = __toESM(require("fs"));
|
|
2601
|
+
var import_path14 = __toESM(require("path"));
|
|
2602
|
+
var import_os9 = __toESM(require("os"));
|
|
2603
|
+
var import_crypto2 = __toESM(require("crypto"));
|
|
2604
|
+
function loopStateFile() {
|
|
2605
|
+
return import_path14.default.join(import_os9.default.homedir(), ".node9", "loop-state.json");
|
|
2606
|
+
}
|
|
2607
|
+
var MAX_RECORDS = 500;
|
|
2608
|
+
function computeArgsHash(args) {
|
|
2609
|
+
const str = JSON.stringify(args ?? "");
|
|
2610
|
+
return import_crypto2.default.createHash("sha256").update(str).digest("hex").slice(0, 16);
|
|
2611
|
+
}
|
|
2612
|
+
function readState() {
|
|
2613
|
+
try {
|
|
2614
|
+
if (!import_fs10.default.existsSync(loopStateFile())) return [];
|
|
2615
|
+
const raw = import_fs10.default.readFileSync(loopStateFile(), "utf-8");
|
|
2616
|
+
const parsed = JSON.parse(raw);
|
|
2617
|
+
if (!Array.isArray(parsed)) return [];
|
|
2618
|
+
return parsed;
|
|
2619
|
+
} catch {
|
|
2620
|
+
return [];
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
function writeState(records) {
|
|
2624
|
+
const dir = import_path14.default.dirname(loopStateFile());
|
|
2625
|
+
if (!import_fs10.default.existsSync(dir)) import_fs10.default.mkdirSync(dir, { recursive: true });
|
|
2626
|
+
const tmpPath = `${loopStateFile()}.${import_os9.default.hostname()}.${process.pid}.tmp`;
|
|
2627
|
+
import_fs10.default.writeFileSync(tmpPath, JSON.stringify(records));
|
|
2628
|
+
import_fs10.default.renameSync(tmpPath, loopStateFile());
|
|
2629
|
+
}
|
|
2630
|
+
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
2631
|
+
try {
|
|
2632
|
+
const hash = computeArgsHash(args);
|
|
2633
|
+
const now = Date.now();
|
|
2634
|
+
const cutoff = now - windowMs;
|
|
2635
|
+
const records = readState().filter((r) => r.ts >= cutoff);
|
|
2636
|
+
records.push({ t: tool, h: hash, ts: now });
|
|
2637
|
+
const count = records.filter((r) => r.t === tool && r.h === hash).length;
|
|
2638
|
+
writeState(records.slice(-MAX_RECORDS));
|
|
2639
|
+
return { looping: count >= threshold, count };
|
|
2640
|
+
} catch {
|
|
2641
|
+
return { looping: false, count: 0 };
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2561
2645
|
// src/auth/orchestrator.ts
|
|
2562
2646
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
2563
2647
|
"write",
|
|
@@ -2609,7 +2693,7 @@ function notifyActivity(data) {
|
|
|
2609
2693
|
}
|
|
2610
2694
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
2611
2695
|
if (!options?.calledFromDaemon) {
|
|
2612
|
-
const actId = (0,
|
|
2696
|
+
const actId = (0, import_crypto3.randomUUID)();
|
|
2613
2697
|
const actTs = Date.now();
|
|
2614
2698
|
await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
|
|
2615
2699
|
const result = await _authorizeHeadlessCore(toolName, args, meta, {
|
|
@@ -2656,6 +2740,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2656
2740
|
let explainableLabel = "Local Config";
|
|
2657
2741
|
let policyMatchedField;
|
|
2658
2742
|
let policyMatchedWord;
|
|
2743
|
+
let policyRuleDescription;
|
|
2659
2744
|
let riskMetadata;
|
|
2660
2745
|
let statefulRecoveryCommand;
|
|
2661
2746
|
let localSmartRuleMatched = false;
|
|
@@ -2749,6 +2834,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2749
2834
|
return { approved: true, checkedBy: "audit" };
|
|
2750
2835
|
}
|
|
2751
2836
|
if (!taintWarning && !isIgnoredTool(toolName)) {
|
|
2837
|
+
const ld = config.policy.loopDetection;
|
|
2838
|
+
if (ld.enabled) {
|
|
2839
|
+
const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
|
|
2840
|
+
if (loopResult.looping) {
|
|
2841
|
+
const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
|
|
2842
|
+
if (!isManual)
|
|
2843
|
+
appendLocalAudit(toolName, args, "deny", "loop-detected", meta, hashAuditArgs);
|
|
2844
|
+
return {
|
|
2845
|
+
approved: false,
|
|
2846
|
+
reason,
|
|
2847
|
+
blockedBy: "loop-detection",
|
|
2848
|
+
blockedByLabel: "\u{1F504} Loop Detected"
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2752
2852
|
if (getActiveTrustSession(toolName)) {
|
|
2753
2853
|
if (approvers.cloud && creds?.apiKey)
|
|
2754
2854
|
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
@@ -2795,7 +2895,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2795
2895
|
blockedBy: "local-config",
|
|
2796
2896
|
blockedByLabel: policyResult.blockedByLabel,
|
|
2797
2897
|
ruleHit: policyResult.ruleName,
|
|
2798
|
-
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
|
|
2898
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
|
|
2899
|
+
...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
|
|
2799
2900
|
};
|
|
2800
2901
|
}
|
|
2801
2902
|
}
|
|
@@ -2803,6 +2904,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2803
2904
|
policyMatchedField = policyResult.matchedField;
|
|
2804
2905
|
policyMatchedWord = policyResult.matchedWord;
|
|
2805
2906
|
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
2907
|
+
if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
|
|
2908
|
+
else if (policyResult.reason) policyRuleDescription = policyResult.reason;
|
|
2806
2909
|
riskMetadata = computeRiskMetadata(
|
|
2807
2910
|
args,
|
|
2808
2911
|
policyResult.tier ?? 6,
|
|
@@ -3066,7 +3169,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3066
3169
|
hashAuditArgs
|
|
3067
3170
|
);
|
|
3068
3171
|
}
|
|
3069
|
-
|
|
3172
|
+
const enrichedResult = !finalResult.approved && policyRuleDescription && !finalResult.ruleDescription ? { ...finalResult, ruleDescription: policyRuleDescription } : finalResult;
|
|
3173
|
+
return enrichedResult;
|
|
3070
3174
|
}
|
|
3071
3175
|
async function authorizeAction(toolName, args) {
|
|
3072
3176
|
const result = await authorizeHeadless(toolName, args);
|