@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.mjs
CHANGED
|
@@ -56,6 +56,11 @@ __export(audit_exports, {
|
|
|
56
56
|
import fs from "fs";
|
|
57
57
|
import path from "path";
|
|
58
58
|
import os from "os";
|
|
59
|
+
function isTestCall(toolName, args) {
|
|
60
|
+
if (toolName !== "Bash" && toolName !== "bash") return false;
|
|
61
|
+
const cmd = args?.command;
|
|
62
|
+
return typeof cmd === "string" && TEST_COMMAND_RE.test(cmd);
|
|
63
|
+
}
|
|
59
64
|
function redactSecrets(text) {
|
|
60
65
|
if (!text) return text;
|
|
61
66
|
let redacted = text;
|
|
@@ -91,12 +96,14 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
|
91
96
|
}
|
|
92
97
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
93
98
|
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
99
|
+
const testRun = isTestCall(toolName, args) ? { testRun: true } : {};
|
|
94
100
|
appendToLog(LOCAL_AUDIT_LOG, {
|
|
95
101
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
96
102
|
tool: toolName,
|
|
97
103
|
...argsField,
|
|
98
104
|
decision,
|
|
99
105
|
checkedBy,
|
|
106
|
+
...testRun,
|
|
100
107
|
agent: meta?.agent,
|
|
101
108
|
mcpServer: meta?.mcpServer,
|
|
102
109
|
hostname: os.hostname()
|
|
@@ -109,13 +116,14 @@ function appendConfigAudit(entry) {
|
|
|
109
116
|
hostname: os.hostname()
|
|
110
117
|
});
|
|
111
118
|
}
|
|
112
|
-
var LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG;
|
|
119
|
+
var LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, TEST_COMMAND_RE;
|
|
113
120
|
var init_audit = __esm({
|
|
114
121
|
"src/audit/index.ts"() {
|
|
115
122
|
"use strict";
|
|
116
123
|
init_hasher();
|
|
117
124
|
LOCAL_AUDIT_LOG = path.join(os.homedir(), ".node9", "audit.log");
|
|
118
125
|
HOOK_DEBUG_LOG = path.join(os.homedir(), ".node9", "hook-debug.log");
|
|
126
|
+
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;
|
|
119
127
|
}
|
|
120
128
|
});
|
|
121
129
|
|
|
@@ -169,6 +177,7 @@ var SmartRuleSchema = z.object({
|
|
|
169
177
|
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
170
178
|
}),
|
|
171
179
|
reason: z.string().optional(),
|
|
180
|
+
description: z.string().optional(),
|
|
172
181
|
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
173
182
|
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
174
183
|
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
@@ -215,6 +224,11 @@ var ConfigFileSchema = z.object({
|
|
|
215
224
|
dlp: z.object({
|
|
216
225
|
enabled: z.boolean().optional(),
|
|
217
226
|
scanIgnoredTools: z.boolean().optional()
|
|
227
|
+
}).optional(),
|
|
228
|
+
loopDetection: z.object({
|
|
229
|
+
enabled: z.boolean().optional(),
|
|
230
|
+
threshold: z.number().min(2).optional(),
|
|
231
|
+
windowSeconds: z.number().min(10).optional()
|
|
218
232
|
}).optional()
|
|
219
233
|
}).optional(),
|
|
220
234
|
environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
|
|
@@ -236,8 +250,8 @@ function sanitizeConfig(raw) {
|
|
|
236
250
|
}
|
|
237
251
|
}
|
|
238
252
|
const lines = result.error.issues.map((issue) => {
|
|
239
|
-
const
|
|
240
|
-
return ` \u2022 ${
|
|
253
|
+
const path15 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
254
|
+
return ` \u2022 ${path15}: ${issue.message}`;
|
|
241
255
|
});
|
|
242
256
|
return {
|
|
243
257
|
sanitized,
|
|
@@ -462,7 +476,8 @@ var DEFAULT_CONFIG = {
|
|
|
462
476
|
}
|
|
463
477
|
],
|
|
464
478
|
verdict: "block",
|
|
465
|
-
reason: "Recursive delete of home directory is irreversible"
|
|
479
|
+
reason: "Recursive delete of home directory is irreversible",
|
|
480
|
+
description: "The AI wants to recursively delete your home directory. This will permanently destroy all your personal files and cannot be undone."
|
|
466
481
|
},
|
|
467
482
|
// ── SQL safety ────────────────────────────────────────────────────────
|
|
468
483
|
{
|
|
@@ -474,7 +489,8 @@ var DEFAULT_CONFIG = {
|
|
|
474
489
|
],
|
|
475
490
|
conditionMode: "all",
|
|
476
491
|
verdict: "review",
|
|
477
|
-
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
492
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table",
|
|
493
|
+
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."
|
|
478
494
|
},
|
|
479
495
|
{
|
|
480
496
|
name: "review-drop-truncate-shell",
|
|
@@ -489,7 +505,8 @@ var DEFAULT_CONFIG = {
|
|
|
489
505
|
],
|
|
490
506
|
conditionMode: "all",
|
|
491
507
|
verdict: "review",
|
|
492
|
-
reason: "SQL DDL destructive statement inside a shell command"
|
|
508
|
+
reason: "SQL DDL destructive statement inside a shell command",
|
|
509
|
+
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
493
510
|
},
|
|
494
511
|
// ── Git safety ────────────────────────────────────────────────────────
|
|
495
512
|
{
|
|
@@ -505,7 +522,8 @@ var DEFAULT_CONFIG = {
|
|
|
505
522
|
],
|
|
506
523
|
conditionMode: "all",
|
|
507
524
|
verdict: "block",
|
|
508
|
-
reason: "Force push overwrites remote history and cannot be undone"
|
|
525
|
+
reason: "Force push overwrites remote history and cannot be undone",
|
|
526
|
+
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."
|
|
509
527
|
},
|
|
510
528
|
{
|
|
511
529
|
name: "review-git-push",
|
|
@@ -520,7 +538,8 @@ var DEFAULT_CONFIG = {
|
|
|
520
538
|
],
|
|
521
539
|
conditionMode: "all",
|
|
522
540
|
verdict: "review",
|
|
523
|
-
reason: "git push sends changes to a shared remote"
|
|
541
|
+
reason: "git push sends changes to a shared remote",
|
|
542
|
+
description: "The AI wants to push commits to a remote repository. Once pushed, those changes are visible to everyone with access."
|
|
524
543
|
},
|
|
525
544
|
{
|
|
526
545
|
name: "review-git-destructive",
|
|
@@ -535,7 +554,8 @@ var DEFAULT_CONFIG = {
|
|
|
535
554
|
],
|
|
536
555
|
conditionMode: "all",
|
|
537
556
|
verdict: "review",
|
|
538
|
-
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
557
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes",
|
|
558
|
+
description: "The AI wants to run a destructive git operation (reset, rebase, clean, or branch delete) that can permanently discard commits or uncommitted work."
|
|
539
559
|
},
|
|
540
560
|
// ── Shell safety ──────────────────────────────────────────────────────
|
|
541
561
|
{
|
|
@@ -544,7 +564,8 @@ var DEFAULT_CONFIG = {
|
|
|
544
564
|
conditions: [{ field: "command", op: "matches", value: "\\bsudo\\s", flags: "i" }],
|
|
545
565
|
conditionMode: "all",
|
|
546
566
|
verdict: "review",
|
|
547
|
-
reason: "Command requires elevated privileges"
|
|
567
|
+
reason: "Command requires elevated privileges",
|
|
568
|
+
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."
|
|
548
569
|
},
|
|
549
570
|
{
|
|
550
571
|
name: "review-curl-pipe-shell",
|
|
@@ -559,10 +580,12 @@ var DEFAULT_CONFIG = {
|
|
|
559
580
|
],
|
|
560
581
|
conditionMode: "all",
|
|
561
582
|
verdict: "block",
|
|
562
|
-
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
583
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector",
|
|
584
|
+
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."
|
|
563
585
|
}
|
|
564
586
|
],
|
|
565
|
-
dlp: { enabled: true, scanIgnoredTools: true }
|
|
587
|
+
dlp: { enabled: true, scanIgnoredTools: true },
|
|
588
|
+
loopDetection: { enabled: true, threshold: 5, windowSeconds: 120 }
|
|
566
589
|
},
|
|
567
590
|
environments: {}
|
|
568
591
|
};
|
|
@@ -592,7 +615,8 @@ var ADVISORY_SMART_RULES = [
|
|
|
592
615
|
tool: "*",
|
|
593
616
|
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
594
617
|
verdict: "review",
|
|
595
|
-
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
618
|
+
reason: "rm can permanently delete files \u2014 confirm the target path",
|
|
619
|
+
description: "The AI wants to delete files. Unlike moving to trash, rm is permanent \u2014 the files cannot be recovered without a backup."
|
|
596
620
|
},
|
|
597
621
|
// ── SQL safety (Safe by Default) ──────────────────────────────────────────
|
|
598
622
|
// These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
|
|
@@ -604,14 +628,16 @@ var ADVISORY_SMART_RULES = [
|
|
|
604
628
|
tool: "*",
|
|
605
629
|
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
606
630
|
verdict: "review",
|
|
607
|
-
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead"
|
|
631
|
+
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead",
|
|
632
|
+
description: "The AI wants to drop a database table. This permanently deletes the table and all its data \u2014 there is no undo."
|
|
608
633
|
},
|
|
609
634
|
{
|
|
610
635
|
name: "review-truncate-sql",
|
|
611
636
|
tool: "*",
|
|
612
637
|
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
613
638
|
verdict: "review",
|
|
614
|
-
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead"
|
|
639
|
+
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead",
|
|
640
|
+
description: "The AI wants to truncate a database table, which instantly deletes every row. The table structure remains but all data is gone."
|
|
615
641
|
},
|
|
616
642
|
{
|
|
617
643
|
name: "review-drop-column-sql",
|
|
@@ -620,7 +646,8 @@ var ADVISORY_SMART_RULES = [
|
|
|
620
646
|
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
621
647
|
],
|
|
622
648
|
verdict: "review",
|
|
623
|
-
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead"
|
|
649
|
+
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead",
|
|
650
|
+
description: "The AI wants to drop a column from a database table. This permanently removes the column and all its data from every row."
|
|
624
651
|
}
|
|
625
652
|
];
|
|
626
653
|
var cachedConfig = null;
|
|
@@ -680,7 +707,8 @@ function getConfig(cwd) {
|
|
|
680
707
|
onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
|
|
681
708
|
ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
|
|
682
709
|
},
|
|
683
|
-
dlp: { ...DEFAULT_CONFIG.policy.dlp }
|
|
710
|
+
dlp: { ...DEFAULT_CONFIG.policy.dlp },
|
|
711
|
+
loopDetection: { ...DEFAULT_CONFIG.policy.loopDetection }
|
|
684
712
|
};
|
|
685
713
|
const mergedEnvironments = { ...DEFAULT_CONFIG.environments };
|
|
686
714
|
const applyLayer = (source) => {
|
|
@@ -719,6 +747,13 @@ function getConfig(cwd) {
|
|
|
719
747
|
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
720
748
|
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
721
749
|
}
|
|
750
|
+
if (p.loopDetection) {
|
|
751
|
+
const ld = p.loopDetection;
|
|
752
|
+
if (ld.enabled !== void 0) mergedPolicy.loopDetection.enabled = ld.enabled;
|
|
753
|
+
if (ld.threshold !== void 0) mergedPolicy.loopDetection.threshold = ld.threshold;
|
|
754
|
+
if (ld.windowSeconds !== void 0)
|
|
755
|
+
mergedPolicy.loopDetection.windowSeconds = ld.windowSeconds;
|
|
756
|
+
}
|
|
722
757
|
const envs = source.environments || {};
|
|
723
758
|
for (const [envName, envConfig] of Object.entries(envs)) {
|
|
724
759
|
if (envConfig && typeof envConfig === "object") {
|
|
@@ -1493,9 +1528,9 @@ function matchesPattern(text, patterns) {
|
|
|
1493
1528
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1494
1529
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1495
1530
|
}
|
|
1496
|
-
function getNestedValue(obj,
|
|
1531
|
+
function getNestedValue(obj, path15) {
|
|
1497
1532
|
if (!obj || typeof obj !== "object") return null;
|
|
1498
|
-
return
|
|
1533
|
+
return path15.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1499
1534
|
}
|
|
1500
1535
|
function evaluateSmartConditions(args, rule) {
|
|
1501
1536
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -1635,6 +1670,9 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1635
1670
|
reason: matchedRule.reason,
|
|
1636
1671
|
tier: 2,
|
|
1637
1672
|
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1673
|
+
...(matchedRule.description ?? matchedRule.reason) && {
|
|
1674
|
+
ruleDescription: matchedRule.description ?? matchedRule.reason
|
|
1675
|
+
},
|
|
1638
1676
|
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1639
1677
|
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1640
1678
|
},
|
|
@@ -2528,6 +2566,52 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
|
|
|
2528
2566
|
}
|
|
2529
2567
|
}
|
|
2530
2568
|
|
|
2569
|
+
// src/loop-detector.ts
|
|
2570
|
+
import fs10 from "fs";
|
|
2571
|
+
import path14 from "path";
|
|
2572
|
+
import os9 from "os";
|
|
2573
|
+
import crypto from "crypto";
|
|
2574
|
+
function loopStateFile() {
|
|
2575
|
+
return path14.join(os9.homedir(), ".node9", "loop-state.json");
|
|
2576
|
+
}
|
|
2577
|
+
var MAX_RECORDS = 500;
|
|
2578
|
+
function computeArgsHash(args) {
|
|
2579
|
+
const str = JSON.stringify(args ?? "");
|
|
2580
|
+
return crypto.createHash("sha256").update(str).digest("hex").slice(0, 16);
|
|
2581
|
+
}
|
|
2582
|
+
function readState() {
|
|
2583
|
+
try {
|
|
2584
|
+
if (!fs10.existsSync(loopStateFile())) return [];
|
|
2585
|
+
const raw = fs10.readFileSync(loopStateFile(), "utf-8");
|
|
2586
|
+
const parsed = JSON.parse(raw);
|
|
2587
|
+
if (!Array.isArray(parsed)) return [];
|
|
2588
|
+
return parsed;
|
|
2589
|
+
} catch {
|
|
2590
|
+
return [];
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
function writeState(records) {
|
|
2594
|
+
const dir = path14.dirname(loopStateFile());
|
|
2595
|
+
if (!fs10.existsSync(dir)) fs10.mkdirSync(dir, { recursive: true });
|
|
2596
|
+
const tmpPath = `${loopStateFile()}.${os9.hostname()}.${process.pid}.tmp`;
|
|
2597
|
+
fs10.writeFileSync(tmpPath, JSON.stringify(records));
|
|
2598
|
+
fs10.renameSync(tmpPath, loopStateFile());
|
|
2599
|
+
}
|
|
2600
|
+
function recordAndCheck(tool, args, threshold = 3, windowMs = 12e4) {
|
|
2601
|
+
try {
|
|
2602
|
+
const hash = computeArgsHash(args);
|
|
2603
|
+
const now = Date.now();
|
|
2604
|
+
const cutoff = now - windowMs;
|
|
2605
|
+
const records = readState().filter((r) => r.ts >= cutoff);
|
|
2606
|
+
records.push({ t: tool, h: hash, ts: now });
|
|
2607
|
+
const count = records.filter((r) => r.t === tool && r.h === hash).length;
|
|
2608
|
+
writeState(records.slice(-MAX_RECORDS));
|
|
2609
|
+
return { looping: count >= threshold, count };
|
|
2610
|
+
} catch {
|
|
2611
|
+
return { looping: false, count: 0 };
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2531
2615
|
// src/auth/orchestrator.ts
|
|
2532
2616
|
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
2533
2617
|
"write",
|
|
@@ -2626,6 +2710,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2626
2710
|
let explainableLabel = "Local Config";
|
|
2627
2711
|
let policyMatchedField;
|
|
2628
2712
|
let policyMatchedWord;
|
|
2713
|
+
let policyRuleDescription;
|
|
2629
2714
|
let riskMetadata;
|
|
2630
2715
|
let statefulRecoveryCommand;
|
|
2631
2716
|
let localSmartRuleMatched = false;
|
|
@@ -2719,6 +2804,21 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2719
2804
|
return { approved: true, checkedBy: "audit" };
|
|
2720
2805
|
}
|
|
2721
2806
|
if (!taintWarning && !isIgnoredTool(toolName)) {
|
|
2807
|
+
const ld = config.policy.loopDetection;
|
|
2808
|
+
if (ld.enabled) {
|
|
2809
|
+
const loopResult = recordAndCheck(toolName, args, ld.threshold, ld.windowSeconds * 1e3);
|
|
2810
|
+
if (loopResult.looping) {
|
|
2811
|
+
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?`;
|
|
2812
|
+
if (!isManual)
|
|
2813
|
+
appendLocalAudit(toolName, args, "deny", "loop-detected", meta, hashAuditArgs);
|
|
2814
|
+
return {
|
|
2815
|
+
approved: false,
|
|
2816
|
+
reason,
|
|
2817
|
+
blockedBy: "loop-detection",
|
|
2818
|
+
blockedByLabel: "\u{1F504} Loop Detected"
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2722
2822
|
if (getActiveTrustSession(toolName)) {
|
|
2723
2823
|
if (approvers.cloud && creds?.apiKey)
|
|
2724
2824
|
await auditLocalAllow(toolName, args, "trust", creds, meta);
|
|
@@ -2765,7 +2865,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2765
2865
|
blockedBy: "local-config",
|
|
2766
2866
|
blockedByLabel: policyResult.blockedByLabel,
|
|
2767
2867
|
ruleHit: policyResult.ruleName,
|
|
2768
|
-
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
|
|
2868
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
|
|
2869
|
+
...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
|
|
2769
2870
|
};
|
|
2770
2871
|
}
|
|
2771
2872
|
}
|
|
@@ -2773,6 +2874,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2773
2874
|
policyMatchedField = policyResult.matchedField;
|
|
2774
2875
|
policyMatchedWord = policyResult.matchedWord;
|
|
2775
2876
|
if (policyResult.ruleName) localSmartRuleMatched = true;
|
|
2877
|
+
if (policyResult.ruleDescription) policyRuleDescription = policyResult.ruleDescription;
|
|
2878
|
+
else if (policyResult.reason) policyRuleDescription = policyResult.reason;
|
|
2776
2879
|
riskMetadata = computeRiskMetadata(
|
|
2777
2880
|
args,
|
|
2778
2881
|
policyResult.tier ?? 6,
|
|
@@ -3036,7 +3139,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
|
|
|
3036
3139
|
hashAuditArgs
|
|
3037
3140
|
);
|
|
3038
3141
|
}
|
|
3039
|
-
|
|
3142
|
+
const enrichedResult = !finalResult.approved && policyRuleDescription && !finalResult.ruleDescription ? { ...finalResult, ruleDescription: policyRuleDescription } : finalResult;
|
|
3143
|
+
return enrichedResult;
|
|
3040
3144
|
}
|
|
3041
3145
|
async function authorizeAction(toolName, args) {
|
|
3042
3146
|
const result = await authorizeHeadless(toolName, args);
|