@node9/proxy 1.5.2 → 1.5.4
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 +115 -20
- package/dist/cli.js +1408 -345
- package/dist/cli.mjs +1406 -343
- package/dist/index.js +201 -49
- package/dist/index.mjs +201 -49
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -198,12 +198,21 @@ var SmartRuleSchema = import_zod.z.object({
|
|
|
198
198
|
verdict: import_zod.z.enum(["allow", "review", "block"], {
|
|
199
199
|
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
200
200
|
}),
|
|
201
|
-
reason: import_zod.z.string().optional()
|
|
201
|
+
reason: import_zod.z.string().optional(),
|
|
202
|
+
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
203
|
+
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
204
|
+
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
205
|
+
dependsOnState: import_zod.z.array(import_zod.z.string()).transform(
|
|
206
|
+
(arr) => arr.filter(
|
|
207
|
+
(p) => p === "no_test_passed_since_last_edit"
|
|
208
|
+
)
|
|
209
|
+
).optional(),
|
|
210
|
+
recoveryCommand: import_zod.z.string().optional()
|
|
202
211
|
});
|
|
203
212
|
var ConfigFileSchema = import_zod.z.object({
|
|
204
213
|
version: import_zod.z.string().optional(),
|
|
205
214
|
settings: import_zod.z.object({
|
|
206
|
-
mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
|
|
215
|
+
mode: import_zod.z.enum(["standard", "strict", "audit", "observe"]).optional(),
|
|
207
216
|
autoStartDaemon: import_zod.z.boolean().optional(),
|
|
208
217
|
enableUndo: import_zod.z.boolean().optional(),
|
|
209
218
|
enableHookLogDebug: import_zod.z.boolean().optional(),
|
|
@@ -572,7 +581,9 @@ var DEFAULT_CONFIG = {
|
|
|
572
581
|
{
|
|
573
582
|
field: "command",
|
|
574
583
|
op: "matches",
|
|
575
|
-
|
|
584
|
+
// Require the recursive flag to be preceded by whitespace so that
|
|
585
|
+
// filenames containing "-r" (e.g. "ai-review.yml") don't false-positive.
|
|
586
|
+
value: "rm\\b.*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
|
|
576
587
|
},
|
|
577
588
|
{
|
|
578
589
|
field: "command",
|
|
@@ -816,12 +827,17 @@ function getConfig(cwd) {
|
|
|
816
827
|
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
817
828
|
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
818
829
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
830
|
+
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
819
831
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
820
832
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
821
833
|
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
822
834
|
if (p.toolInspection)
|
|
823
835
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
824
|
-
if (p.smartRules)
|
|
836
|
+
if (p.smartRules) {
|
|
837
|
+
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
838
|
+
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
839
|
+
mergedPolicy.smartRules = [...defaultBlocks, ...p.smartRules, ...defaultNonBlocks];
|
|
840
|
+
}
|
|
825
841
|
if (p.snapshot) {
|
|
826
842
|
const s2 = p.snapshot;
|
|
827
843
|
if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
|
|
@@ -1748,7 +1764,13 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1748
1764
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
1749
1765
|
reason: matchedRule.reason,
|
|
1750
1766
|
tier: 2,
|
|
1751
|
-
ruleName: matchedRule.name ?? matchedRule.tool
|
|
1767
|
+
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1768
|
+
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1769
|
+
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1770
|
+
},
|
|
1771
|
+
...matchedRule.verdict === "block" && matchedRule.recoveryCommand && {
|
|
1772
|
+
recoveryCommand: matchedRule.recoveryCommand
|
|
1773
|
+
}
|
|
1752
1774
|
};
|
|
1753
1775
|
}
|
|
1754
1776
|
}
|
|
@@ -1980,9 +2002,39 @@ function getPersistentDecision(toolName) {
|
|
|
1980
2002
|
|
|
1981
2003
|
// src/auth/daemon.ts
|
|
1982
2004
|
var import_fs8 = __toESM(require("fs"));
|
|
2005
|
+
var import_net = __toESM(require("net"));
|
|
1983
2006
|
var import_path10 = __toESM(require("path"));
|
|
1984
2007
|
var import_os7 = __toESM(require("os"));
|
|
1985
2008
|
var import_child_process = require("child_process");
|
|
2009
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path10.default.join(import_os7.default.tmpdir(), "node9-activity.sock");
|
|
2010
|
+
function notifyActivitySocket(data) {
|
|
2011
|
+
return new Promise((resolve) => {
|
|
2012
|
+
try {
|
|
2013
|
+
const payload = JSON.stringify(data);
|
|
2014
|
+
const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
|
|
2015
|
+
sock.on("connect", () => {
|
|
2016
|
+
sock.on("close", resolve);
|
|
2017
|
+
sock.end(payload);
|
|
2018
|
+
});
|
|
2019
|
+
sock.on("error", resolve);
|
|
2020
|
+
} catch {
|
|
2021
|
+
resolve();
|
|
2022
|
+
}
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
async function checkStatePredicates(predicates) {
|
|
2026
|
+
if (predicates.length === 0) return {};
|
|
2027
|
+
try {
|
|
2028
|
+
const qs = predicates.map(encodeURIComponent).join(",");
|
|
2029
|
+
const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/state/check?predicates=${qs}`, {
|
|
2030
|
+
signal: AbortSignal.timeout(100)
|
|
2031
|
+
});
|
|
2032
|
+
if (!res.ok) return null;
|
|
2033
|
+
return await res.json();
|
|
2034
|
+
} catch {
|
|
2035
|
+
return null;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
1986
2038
|
var DAEMON_PORT = 7391;
|
|
1987
2039
|
var DAEMON_HOST = "127.0.0.1";
|
|
1988
2040
|
function getInternalToken() {
|
|
@@ -2018,7 +2070,7 @@ function isDaemonRunning() {
|
|
|
2018
2070
|
return false;
|
|
2019
2071
|
}
|
|
2020
2072
|
}
|
|
2021
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
|
|
2073
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
|
|
2022
2074
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
2023
2075
|
const ctrl = new AbortController();
|
|
2024
2076
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -2036,7 +2088,10 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2036
2088
|
// activity-result as the CLI used for the pending activity event.
|
|
2037
2089
|
activityId,
|
|
2038
2090
|
...riskMetadata && { riskMetadata },
|
|
2039
|
-
...cwd && { cwd }
|
|
2091
|
+
...cwd && { cwd },
|
|
2092
|
+
...recoveryCommand && { recoveryCommand },
|
|
2093
|
+
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
2094
|
+
...viewOnly && { viewOnly: true }
|
|
2040
2095
|
}),
|
|
2041
2096
|
signal: ctrl.signal
|
|
2042
2097
|
});
|
|
@@ -2056,10 +2111,10 @@ async function waitForDaemonDecision(id, signal) {
|
|
|
2056
2111
|
try {
|
|
2057
2112
|
const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
|
|
2058
2113
|
if (!waitRes.ok) return { decision: "deny" };
|
|
2059
|
-
const { decision, source } = await waitRes.json();
|
|
2114
|
+
const { decision, source, reason } = await waitRes.json();
|
|
2060
2115
|
if (decision === "allow") return { decision: "allow", source };
|
|
2061
2116
|
if (decision === "abandoned") return { decision: "abandoned", source };
|
|
2062
|
-
return { decision: "deny", source };
|
|
2117
|
+
return { decision: "deny", source, reason };
|
|
2063
2118
|
} finally {
|
|
2064
2119
|
clearTimeout(waitTimer);
|
|
2065
2120
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
@@ -2134,9 +2189,6 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
2134
2189
|
}
|
|
2135
2190
|
|
|
2136
2191
|
// src/auth/orchestrator.ts
|
|
2137
|
-
var import_net = __toESM(require("net"));
|
|
2138
|
-
var import_path13 = __toESM(require("path"));
|
|
2139
|
-
var import_os9 = __toESM(require("os"));
|
|
2140
2192
|
var import_crypto2 = require("crypto");
|
|
2141
2193
|
|
|
2142
2194
|
// src/ui/native.ts
|
|
@@ -2465,6 +2517,7 @@ init_audit();
|
|
|
2465
2517
|
// src/auth/cloud.ts
|
|
2466
2518
|
var import_fs9 = __toESM(require("fs"));
|
|
2467
2519
|
var import_os8 = __toESM(require("os"));
|
|
2520
|
+
var import_path13 = __toESM(require("path"));
|
|
2468
2521
|
init_audit();
|
|
2469
2522
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
2470
2523
|
return fetch(`${creds.apiUrl}/audit`, {
|
|
@@ -2490,6 +2543,33 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
2490
2543
|
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
2491
2544
|
const controller = new AbortController();
|
|
2492
2545
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
2546
|
+
if (!creds.apiKey) throw new Error("Node9 API Key is missing");
|
|
2547
|
+
let ciContext;
|
|
2548
|
+
if (process.env.CI) {
|
|
2549
|
+
try {
|
|
2550
|
+
const ciContextPath = import_path13.default.join(import_os8.default.homedir(), ".node9", "ci-context.json");
|
|
2551
|
+
const stats = import_fs9.default.statSync(ciContextPath);
|
|
2552
|
+
if (stats.size > 1e4) throw new Error("ci-context.json exceeds 10 KB");
|
|
2553
|
+
const raw = import_fs9.default.readFileSync(ciContextPath, "utf8");
|
|
2554
|
+
const parsed = JSON.parse(raw);
|
|
2555
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2556
|
+
throw new Error("ci-context.json is not a plain object");
|
|
2557
|
+
}
|
|
2558
|
+
const p = parsed;
|
|
2559
|
+
ciContext = {
|
|
2560
|
+
tests_after: p["tests_after"],
|
|
2561
|
+
files_changed: p["files_changed"],
|
|
2562
|
+
issues_found: p["issues_found"],
|
|
2563
|
+
issues_fixed: p["issues_fixed"],
|
|
2564
|
+
github_repository: p["github_repository"],
|
|
2565
|
+
github_head_ref: p["github_head_ref"],
|
|
2566
|
+
iteration: p["iteration"],
|
|
2567
|
+
draft_pr_number: p["draft_pr_number"],
|
|
2568
|
+
draft_pr_url: p["draft_pr_url"]
|
|
2569
|
+
};
|
|
2570
|
+
} catch {
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2493
2573
|
try {
|
|
2494
2574
|
const response = await fetch(creds.apiUrl, {
|
|
2495
2575
|
method: "POST",
|
|
@@ -2504,7 +2584,8 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
2504
2584
|
cwd: process.cwd(),
|
|
2505
2585
|
platform: import_os8.default.platform()
|
|
2506
2586
|
},
|
|
2507
|
-
...riskMetadata && { riskMetadata }
|
|
2587
|
+
...riskMetadata && { riskMetadata },
|
|
2588
|
+
...ciContext && { ciContext }
|
|
2508
2589
|
}),
|
|
2509
2590
|
signal: controller.signal
|
|
2510
2591
|
});
|
|
@@ -2530,12 +2611,17 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
2530
2611
|
});
|
|
2531
2612
|
clearTimeout(pollTimer);
|
|
2532
2613
|
if (!statusRes.ok) continue;
|
|
2533
|
-
const
|
|
2614
|
+
const statusBody = await statusRes.json();
|
|
2615
|
+
const { status } = statusBody;
|
|
2534
2616
|
if (status === "APPROVED") {
|
|
2535
|
-
return { approved: true, reason };
|
|
2617
|
+
return { approved: true, reason: statusBody.reason };
|
|
2536
2618
|
}
|
|
2537
2619
|
if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
|
|
2538
|
-
return { approved: false, reason };
|
|
2620
|
+
return { approved: false, reason: statusBody.reason };
|
|
2621
|
+
}
|
|
2622
|
+
if (status === "FIX") {
|
|
2623
|
+
const feedbackText = statusBody.feedbackText ?? statusBody.reason ?? "Run again with feedback.";
|
|
2624
|
+
return { approved: false, reason: feedbackText };
|
|
2539
2625
|
}
|
|
2540
2626
|
} catch {
|
|
2541
2627
|
}
|
|
@@ -2619,21 +2705,8 @@ function isNetworkTool(toolName, args) {
|
|
|
2619
2705
|
}
|
|
2620
2706
|
return false;
|
|
2621
2707
|
}
|
|
2622
|
-
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path13.default.join(import_os9.default.tmpdir(), "node9-activity.sock");
|
|
2623
2708
|
function notifyActivity(data) {
|
|
2624
|
-
return
|
|
2625
|
-
try {
|
|
2626
|
-
const payload = JSON.stringify(data);
|
|
2627
|
-
const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
|
|
2628
|
-
sock.on("connect", () => {
|
|
2629
|
-
sock.on("close", resolve);
|
|
2630
|
-
sock.end(payload);
|
|
2631
|
-
});
|
|
2632
|
-
sock.on("error", resolve);
|
|
2633
|
-
} catch {
|
|
2634
|
-
resolve();
|
|
2635
|
-
}
|
|
2636
|
-
});
|
|
2709
|
+
return notifyActivitySocket(data);
|
|
2637
2710
|
}
|
|
2638
2711
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
2639
2712
|
if (!options?.calledFromDaemon) {
|
|
@@ -2650,7 +2723,9 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
2650
2723
|
tool: toolName,
|
|
2651
2724
|
ts: actTs,
|
|
2652
2725
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
2653
|
-
label: result.blockedByLabel
|
|
2726
|
+
label: result.blockedByLabel,
|
|
2727
|
+
ruleHit: result.ruleHit,
|
|
2728
|
+
observeWouldBlock: result.observeWouldBlock
|
|
2654
2729
|
});
|
|
2655
2730
|
}
|
|
2656
2731
|
return result;
|
|
@@ -2677,10 +2752,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2677
2752
|
appendHookDebug(toolName, args, meta, hashAuditArgs);
|
|
2678
2753
|
}
|
|
2679
2754
|
const isManual = meta?.agent === "Terminal";
|
|
2755
|
+
const isObserveMode = config.settings.mode === "observe";
|
|
2680
2756
|
let explainableLabel = "Local Config";
|
|
2681
2757
|
let policyMatchedField;
|
|
2682
2758
|
let policyMatchedWord;
|
|
2683
2759
|
let riskMetadata;
|
|
2760
|
+
let statefulRecoveryCommand;
|
|
2684
2761
|
let taintWarning = null;
|
|
2685
2762
|
if (isNetworkTool(toolName, args)) {
|
|
2686
2763
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -2701,10 +2778,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2701
2778
|
if (dlpMatch) {
|
|
2702
2779
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
2703
2780
|
if (dlpMatch.severity === "block") {
|
|
2704
|
-
if (!isManual)
|
|
2781
|
+
if (!isManual)
|
|
2782
|
+
appendLocalAudit(
|
|
2783
|
+
toolName,
|
|
2784
|
+
args,
|
|
2785
|
+
"deny",
|
|
2786
|
+
isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
|
|
2787
|
+
meta,
|
|
2788
|
+
true
|
|
2789
|
+
);
|
|
2705
2790
|
if (isWriteTool(toolName) && filePath) {
|
|
2706
2791
|
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
|
|
2707
2792
|
}
|
|
2793
|
+
if (isObserveMode) {
|
|
2794
|
+
return {
|
|
2795
|
+
approved: true,
|
|
2796
|
+
checkedBy: "audit",
|
|
2797
|
+
observeWouldBlock: true,
|
|
2798
|
+
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2708
2801
|
return {
|
|
2709
2802
|
approved: false,
|
|
2710
2803
|
reason: dlpReason,
|
|
@@ -2717,6 +2810,31 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2717
2810
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
2718
2811
|
}
|
|
2719
2812
|
}
|
|
2813
|
+
if (isObserveMode) {
|
|
2814
|
+
if (!isIgnoredTool(toolName)) {
|
|
2815
|
+
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
2816
|
+
const wouldBlock = policyResult.decision === "block";
|
|
2817
|
+
if (!isManual)
|
|
2818
|
+
appendLocalAudit(
|
|
2819
|
+
toolName,
|
|
2820
|
+
args,
|
|
2821
|
+
"allow",
|
|
2822
|
+
wouldBlock ? "observe-mode-would-block" : "observe-mode",
|
|
2823
|
+
meta,
|
|
2824
|
+
hashAuditArgs
|
|
2825
|
+
);
|
|
2826
|
+
return {
|
|
2827
|
+
approved: true,
|
|
2828
|
+
checkedBy: "audit",
|
|
2829
|
+
...wouldBlock && {
|
|
2830
|
+
observeWouldBlock: true,
|
|
2831
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
2832
|
+
ruleHit: policyResult.ruleName
|
|
2833
|
+
}
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
return { approved: true, checkedBy: "audit" };
|
|
2837
|
+
}
|
|
2720
2838
|
if (config.settings.mode === "audit") {
|
|
2721
2839
|
if (!isIgnoredTool(toolName)) {
|
|
2722
2840
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
@@ -2739,19 +2857,46 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2739
2857
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
2740
2858
|
if (policyResult.decision === "allow") {
|
|
2741
2859
|
if (approvers.cloud && creds?.apiKey)
|
|
2742
|
-
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
2860
|
+
await auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
2743
2861
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
|
|
2744
2862
|
return { approved: true, checkedBy: "local-policy" };
|
|
2745
2863
|
}
|
|
2746
2864
|
if (policyResult.decision === "block") {
|
|
2747
|
-
if (
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2865
|
+
if (policyResult.dependsOnStatePredicates?.length) {
|
|
2866
|
+
const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
|
|
2867
|
+
const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
|
|
2868
|
+
if (stateResults === null && !isManual) {
|
|
2869
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
2870
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2871
|
+
event: "state-check-fail-open",
|
|
2872
|
+
tool: toolName,
|
|
2873
|
+
rule: policyResult.ruleName,
|
|
2874
|
+
predicates: policyResult.dependsOnStatePredicates,
|
|
2875
|
+
reason: "daemon unreachable or /state/check timed out \u2014 block rule downgraded to review"
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
if (predicatesMet && policyResult.recoveryCommand) {
|
|
2879
|
+
statefulRecoveryCommand = policyResult.recoveryCommand;
|
|
2880
|
+
}
|
|
2881
|
+
} else if (isDaemonRunning() && !isTestEnv2) {
|
|
2882
|
+
if (!isManual)
|
|
2883
|
+
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
2884
|
+
if (approvers.cloud && creds?.apiKey)
|
|
2885
|
+
auditLocalAllow(toolName, args, "smart-rule-block", creds, meta);
|
|
2886
|
+
} else {
|
|
2887
|
+
if (!isManual)
|
|
2888
|
+
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
2889
|
+
if (approvers.cloud && creds?.apiKey)
|
|
2890
|
+
auditLocalAllow(toolName, args, "smart-rule-block", creds, meta);
|
|
2891
|
+
return {
|
|
2892
|
+
approved: false,
|
|
2893
|
+
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
2894
|
+
blockedBy: "local-config",
|
|
2895
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
2896
|
+
ruleHit: policyResult.ruleName,
|
|
2897
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2755
2900
|
}
|
|
2756
2901
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
2757
2902
|
policyMatchedField = policyResult.matchedField;
|
|
@@ -2858,7 +3003,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2858
3003
|
meta,
|
|
2859
3004
|
riskMetadata,
|
|
2860
3005
|
options?.activityId,
|
|
2861
|
-
options?.cwd
|
|
3006
|
+
options?.cwd,
|
|
3007
|
+
statefulRecoveryCommand
|
|
2862
3008
|
);
|
|
2863
3009
|
daemonEntryId = entry.id;
|
|
2864
3010
|
daemonAllowCount = entry.allowCount;
|
|
@@ -2919,20 +3065,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2919
3065
|
if (daemonEntryId && (approvers.browser || approvers.terminal)) {
|
|
2920
3066
|
racePromises.push(
|
|
2921
3067
|
(async () => {
|
|
2922
|
-
const {
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
3068
|
+
const {
|
|
3069
|
+
decision: daemonDecision,
|
|
3070
|
+
source: decisionSource,
|
|
3071
|
+
reason: daemonReason
|
|
3072
|
+
} = await waitForDaemonDecision(daemonEntryId, signal);
|
|
2926
3073
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
2927
3074
|
const isApproved = daemonDecision === "allow";
|
|
2928
|
-
const
|
|
3075
|
+
const isRedirect = decisionSource === "terminal-redirect";
|
|
3076
|
+
const src = decisionSource === "terminal" || decisionSource === "terminal-redirect" || decisionSource === "browser" ? decisionSource === "browser" ? "browser" : "terminal" : approvers.browser ? "browser" : "terminal";
|
|
2929
3077
|
const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
|
|
2930
3078
|
return {
|
|
2931
3079
|
approved: isApproved,
|
|
2932
|
-
reason: isApproved ? void 0 :
|
|
3080
|
+
reason: isApproved ? void 0 : (
|
|
3081
|
+
// Use the redirect reason from the tail when choice [2] was selected;
|
|
3082
|
+
// otherwise fall back to the generic rejection message.
|
|
3083
|
+
isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`
|
|
3084
|
+
),
|
|
2933
3085
|
checkedBy: isApproved ? "daemon" : void 0,
|
|
2934
3086
|
blockedBy: isApproved ? void 0 : "local-decision",
|
|
2935
|
-
blockedByLabel: `User Decision (${via})`,
|
|
3087
|
+
blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
|
|
2936
3088
|
decisionSource: src
|
|
2937
3089
|
};
|
|
2938
3090
|
})()
|