@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.mjs
CHANGED
|
@@ -168,12 +168,21 @@ var SmartRuleSchema = z.object({
|
|
|
168
168
|
verdict: z.enum(["allow", "review", "block"], {
|
|
169
169
|
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
170
170
|
}),
|
|
171
|
-
reason: z.string().optional()
|
|
171
|
+
reason: z.string().optional(),
|
|
172
|
+
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
173
|
+
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
174
|
+
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
175
|
+
dependsOnState: z.array(z.string()).transform(
|
|
176
|
+
(arr) => arr.filter(
|
|
177
|
+
(p) => p === "no_test_passed_since_last_edit"
|
|
178
|
+
)
|
|
179
|
+
).optional(),
|
|
180
|
+
recoveryCommand: z.string().optional()
|
|
172
181
|
});
|
|
173
182
|
var ConfigFileSchema = z.object({
|
|
174
183
|
version: z.string().optional(),
|
|
175
184
|
settings: z.object({
|
|
176
|
-
mode: z.enum(["standard", "strict", "audit"]).optional(),
|
|
185
|
+
mode: z.enum(["standard", "strict", "audit", "observe"]).optional(),
|
|
177
186
|
autoStartDaemon: z.boolean().optional(),
|
|
178
187
|
enableUndo: z.boolean().optional(),
|
|
179
188
|
enableHookLogDebug: z.boolean().optional(),
|
|
@@ -542,7 +551,9 @@ var DEFAULT_CONFIG = {
|
|
|
542
551
|
{
|
|
543
552
|
field: "command",
|
|
544
553
|
op: "matches",
|
|
545
|
-
|
|
554
|
+
// Require the recursive flag to be preceded by whitespace so that
|
|
555
|
+
// filenames containing "-r" (e.g. "ai-review.yml") don't false-positive.
|
|
556
|
+
value: "rm\\b.*\\s(-[rRfF]*[rR][rRfF]*|--recursive)(\\s|$)"
|
|
546
557
|
},
|
|
547
558
|
{
|
|
548
559
|
field: "command",
|
|
@@ -786,12 +797,17 @@ function getConfig(cwd) {
|
|
|
786
797
|
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
787
798
|
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
788
799
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
800
|
+
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
789
801
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
790
802
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
791
803
|
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
792
804
|
if (p.toolInspection)
|
|
793
805
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
794
|
-
if (p.smartRules)
|
|
806
|
+
if (p.smartRules) {
|
|
807
|
+
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
808
|
+
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
809
|
+
mergedPolicy.smartRules = [...defaultBlocks, ...p.smartRules, ...defaultNonBlocks];
|
|
810
|
+
}
|
|
795
811
|
if (p.snapshot) {
|
|
796
812
|
const s2 = p.snapshot;
|
|
797
813
|
if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
|
|
@@ -1718,7 +1734,13 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1718
1734
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
1719
1735
|
reason: matchedRule.reason,
|
|
1720
1736
|
tier: 2,
|
|
1721
|
-
ruleName: matchedRule.name ?? matchedRule.tool
|
|
1737
|
+
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1738
|
+
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1739
|
+
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1740
|
+
},
|
|
1741
|
+
...matchedRule.verdict === "block" && matchedRule.recoveryCommand && {
|
|
1742
|
+
recoveryCommand: matchedRule.recoveryCommand
|
|
1743
|
+
}
|
|
1722
1744
|
};
|
|
1723
1745
|
}
|
|
1724
1746
|
}
|
|
@@ -1950,9 +1972,39 @@ function getPersistentDecision(toolName) {
|
|
|
1950
1972
|
|
|
1951
1973
|
// src/auth/daemon.ts
|
|
1952
1974
|
import fs8 from "fs";
|
|
1975
|
+
import net from "net";
|
|
1953
1976
|
import path10 from "path";
|
|
1954
1977
|
import os7 from "os";
|
|
1955
1978
|
import { spawnSync } from "child_process";
|
|
1979
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path10.join(os7.tmpdir(), "node9-activity.sock");
|
|
1980
|
+
function notifyActivitySocket(data) {
|
|
1981
|
+
return new Promise((resolve) => {
|
|
1982
|
+
try {
|
|
1983
|
+
const payload = JSON.stringify(data);
|
|
1984
|
+
const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
|
|
1985
|
+
sock.on("connect", () => {
|
|
1986
|
+
sock.on("close", resolve);
|
|
1987
|
+
sock.end(payload);
|
|
1988
|
+
});
|
|
1989
|
+
sock.on("error", resolve);
|
|
1990
|
+
} catch {
|
|
1991
|
+
resolve();
|
|
1992
|
+
}
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
async function checkStatePredicates(predicates) {
|
|
1996
|
+
if (predicates.length === 0) return {};
|
|
1997
|
+
try {
|
|
1998
|
+
const qs = predicates.map(encodeURIComponent).join(",");
|
|
1999
|
+
const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/state/check?predicates=${qs}`, {
|
|
2000
|
+
signal: AbortSignal.timeout(100)
|
|
2001
|
+
});
|
|
2002
|
+
if (!res.ok) return null;
|
|
2003
|
+
return await res.json();
|
|
2004
|
+
} catch {
|
|
2005
|
+
return null;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
1956
2008
|
var DAEMON_PORT = 7391;
|
|
1957
2009
|
var DAEMON_HOST = "127.0.0.1";
|
|
1958
2010
|
function getInternalToken() {
|
|
@@ -1988,7 +2040,7 @@ function isDaemonRunning() {
|
|
|
1988
2040
|
return false;
|
|
1989
2041
|
}
|
|
1990
2042
|
}
|
|
1991
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
|
|
2043
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
|
|
1992
2044
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1993
2045
|
const ctrl = new AbortController();
|
|
1994
2046
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -2006,7 +2058,10 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2006
2058
|
// activity-result as the CLI used for the pending activity event.
|
|
2007
2059
|
activityId,
|
|
2008
2060
|
...riskMetadata && { riskMetadata },
|
|
2009
|
-
...cwd && { cwd }
|
|
2061
|
+
...cwd && { cwd },
|
|
2062
|
+
...recoveryCommand && { recoveryCommand },
|
|
2063
|
+
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
2064
|
+
...viewOnly && { viewOnly: true }
|
|
2010
2065
|
}),
|
|
2011
2066
|
signal: ctrl.signal
|
|
2012
2067
|
});
|
|
@@ -2026,10 +2081,10 @@ async function waitForDaemonDecision(id, signal) {
|
|
|
2026
2081
|
try {
|
|
2027
2082
|
const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
|
|
2028
2083
|
if (!waitRes.ok) return { decision: "deny" };
|
|
2029
|
-
const { decision, source } = await waitRes.json();
|
|
2084
|
+
const { decision, source, reason } = await waitRes.json();
|
|
2030
2085
|
if (decision === "allow") return { decision: "allow", source };
|
|
2031
2086
|
if (decision === "abandoned") return { decision: "abandoned", source };
|
|
2032
|
-
return { decision: "deny", source };
|
|
2087
|
+
return { decision: "deny", source, reason };
|
|
2033
2088
|
} finally {
|
|
2034
2089
|
clearTimeout(waitTimer);
|
|
2035
2090
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
@@ -2104,9 +2159,6 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
2104
2159
|
}
|
|
2105
2160
|
|
|
2106
2161
|
// src/auth/orchestrator.ts
|
|
2107
|
-
import net from "net";
|
|
2108
|
-
import path13 from "path";
|
|
2109
|
-
import os9 from "os";
|
|
2110
2162
|
import { randomUUID } from "crypto";
|
|
2111
2163
|
|
|
2112
2164
|
// src/ui/native.ts
|
|
@@ -2436,6 +2488,7 @@ init_audit();
|
|
|
2436
2488
|
init_audit();
|
|
2437
2489
|
import fs9 from "fs";
|
|
2438
2490
|
import os8 from "os";
|
|
2491
|
+
import path13 from "path";
|
|
2439
2492
|
function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
2440
2493
|
return fetch(`${creds.apiUrl}/audit`, {
|
|
2441
2494
|
method: "POST",
|
|
@@ -2460,6 +2513,33 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
|
|
|
2460
2513
|
async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
2461
2514
|
const controller = new AbortController();
|
|
2462
2515
|
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
2516
|
+
if (!creds.apiKey) throw new Error("Node9 API Key is missing");
|
|
2517
|
+
let ciContext;
|
|
2518
|
+
if (process.env.CI) {
|
|
2519
|
+
try {
|
|
2520
|
+
const ciContextPath = path13.join(os8.homedir(), ".node9", "ci-context.json");
|
|
2521
|
+
const stats = fs9.statSync(ciContextPath);
|
|
2522
|
+
if (stats.size > 1e4) throw new Error("ci-context.json exceeds 10 KB");
|
|
2523
|
+
const raw = fs9.readFileSync(ciContextPath, "utf8");
|
|
2524
|
+
const parsed = JSON.parse(raw);
|
|
2525
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2526
|
+
throw new Error("ci-context.json is not a plain object");
|
|
2527
|
+
}
|
|
2528
|
+
const p = parsed;
|
|
2529
|
+
ciContext = {
|
|
2530
|
+
tests_after: p["tests_after"],
|
|
2531
|
+
files_changed: p["files_changed"],
|
|
2532
|
+
issues_found: p["issues_found"],
|
|
2533
|
+
issues_fixed: p["issues_fixed"],
|
|
2534
|
+
github_repository: p["github_repository"],
|
|
2535
|
+
github_head_ref: p["github_head_ref"],
|
|
2536
|
+
iteration: p["iteration"],
|
|
2537
|
+
draft_pr_number: p["draft_pr_number"],
|
|
2538
|
+
draft_pr_url: p["draft_pr_url"]
|
|
2539
|
+
};
|
|
2540
|
+
} catch {
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2463
2543
|
try {
|
|
2464
2544
|
const response = await fetch(creds.apiUrl, {
|
|
2465
2545
|
method: "POST",
|
|
@@ -2474,7 +2554,8 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
|
|
|
2474
2554
|
cwd: process.cwd(),
|
|
2475
2555
|
platform: os8.platform()
|
|
2476
2556
|
},
|
|
2477
|
-
...riskMetadata && { riskMetadata }
|
|
2557
|
+
...riskMetadata && { riskMetadata },
|
|
2558
|
+
...ciContext && { ciContext }
|
|
2478
2559
|
}),
|
|
2479
2560
|
signal: controller.signal
|
|
2480
2561
|
});
|
|
@@ -2500,12 +2581,17 @@ async function pollNode9SaaS(requestId, creds, signal) {
|
|
|
2500
2581
|
});
|
|
2501
2582
|
clearTimeout(pollTimer);
|
|
2502
2583
|
if (!statusRes.ok) continue;
|
|
2503
|
-
const
|
|
2584
|
+
const statusBody = await statusRes.json();
|
|
2585
|
+
const { status } = statusBody;
|
|
2504
2586
|
if (status === "APPROVED") {
|
|
2505
|
-
return { approved: true, reason };
|
|
2587
|
+
return { approved: true, reason: statusBody.reason };
|
|
2506
2588
|
}
|
|
2507
2589
|
if (status === "DENIED" || status === "AUTO_BLOCKED" || status === "TIMED_OUT") {
|
|
2508
|
-
return { approved: false, reason };
|
|
2590
|
+
return { approved: false, reason: statusBody.reason };
|
|
2591
|
+
}
|
|
2592
|
+
if (status === "FIX") {
|
|
2593
|
+
const feedbackText = statusBody.feedbackText ?? statusBody.reason ?? "Run again with feedback.";
|
|
2594
|
+
return { approved: false, reason: feedbackText };
|
|
2509
2595
|
}
|
|
2510
2596
|
} catch {
|
|
2511
2597
|
}
|
|
@@ -2589,21 +2675,8 @@ function isNetworkTool(toolName, args) {
|
|
|
2589
2675
|
}
|
|
2590
2676
|
return false;
|
|
2591
2677
|
}
|
|
2592
|
-
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os9.tmpdir(), "node9-activity.sock");
|
|
2593
2678
|
function notifyActivity(data) {
|
|
2594
|
-
return
|
|
2595
|
-
try {
|
|
2596
|
-
const payload = JSON.stringify(data);
|
|
2597
|
-
const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
|
|
2598
|
-
sock.on("connect", () => {
|
|
2599
|
-
sock.on("close", resolve);
|
|
2600
|
-
sock.end(payload);
|
|
2601
|
-
});
|
|
2602
|
-
sock.on("error", resolve);
|
|
2603
|
-
} catch {
|
|
2604
|
-
resolve();
|
|
2605
|
-
}
|
|
2606
|
-
});
|
|
2679
|
+
return notifyActivitySocket(data);
|
|
2607
2680
|
}
|
|
2608
2681
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
2609
2682
|
if (!options?.calledFromDaemon) {
|
|
@@ -2620,7 +2693,9 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
2620
2693
|
tool: toolName,
|
|
2621
2694
|
ts: actTs,
|
|
2622
2695
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
2623
|
-
label: result.blockedByLabel
|
|
2696
|
+
label: result.blockedByLabel,
|
|
2697
|
+
ruleHit: result.ruleHit,
|
|
2698
|
+
observeWouldBlock: result.observeWouldBlock
|
|
2624
2699
|
});
|
|
2625
2700
|
}
|
|
2626
2701
|
return result;
|
|
@@ -2647,10 +2722,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2647
2722
|
appendHookDebug(toolName, args, meta, hashAuditArgs);
|
|
2648
2723
|
}
|
|
2649
2724
|
const isManual = meta?.agent === "Terminal";
|
|
2725
|
+
const isObserveMode = config.settings.mode === "observe";
|
|
2650
2726
|
let explainableLabel = "Local Config";
|
|
2651
2727
|
let policyMatchedField;
|
|
2652
2728
|
let policyMatchedWord;
|
|
2653
2729
|
let riskMetadata;
|
|
2730
|
+
let statefulRecoveryCommand;
|
|
2654
2731
|
let taintWarning = null;
|
|
2655
2732
|
if (isNetworkTool(toolName, args)) {
|
|
2656
2733
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -2671,10 +2748,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2671
2748
|
if (dlpMatch) {
|
|
2672
2749
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
2673
2750
|
if (dlpMatch.severity === "block") {
|
|
2674
|
-
if (!isManual)
|
|
2751
|
+
if (!isManual)
|
|
2752
|
+
appendLocalAudit(
|
|
2753
|
+
toolName,
|
|
2754
|
+
args,
|
|
2755
|
+
"deny",
|
|
2756
|
+
isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
|
|
2757
|
+
meta,
|
|
2758
|
+
true
|
|
2759
|
+
);
|
|
2675
2760
|
if (isWriteTool(toolName) && filePath) {
|
|
2676
2761
|
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
|
|
2677
2762
|
}
|
|
2763
|
+
if (isObserveMode) {
|
|
2764
|
+
return {
|
|
2765
|
+
approved: true,
|
|
2766
|
+
checkedBy: "audit",
|
|
2767
|
+
observeWouldBlock: true,
|
|
2768
|
+
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2678
2771
|
return {
|
|
2679
2772
|
approved: false,
|
|
2680
2773
|
reason: dlpReason,
|
|
@@ -2687,6 +2780,31 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2687
2780
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
2688
2781
|
}
|
|
2689
2782
|
}
|
|
2783
|
+
if (isObserveMode) {
|
|
2784
|
+
if (!isIgnoredTool(toolName)) {
|
|
2785
|
+
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
2786
|
+
const wouldBlock = policyResult.decision === "block";
|
|
2787
|
+
if (!isManual)
|
|
2788
|
+
appendLocalAudit(
|
|
2789
|
+
toolName,
|
|
2790
|
+
args,
|
|
2791
|
+
"allow",
|
|
2792
|
+
wouldBlock ? "observe-mode-would-block" : "observe-mode",
|
|
2793
|
+
meta,
|
|
2794
|
+
hashAuditArgs
|
|
2795
|
+
);
|
|
2796
|
+
return {
|
|
2797
|
+
approved: true,
|
|
2798
|
+
checkedBy: "audit",
|
|
2799
|
+
...wouldBlock && {
|
|
2800
|
+
observeWouldBlock: true,
|
|
2801
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
2802
|
+
ruleHit: policyResult.ruleName
|
|
2803
|
+
}
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
return { approved: true, checkedBy: "audit" };
|
|
2807
|
+
}
|
|
2690
2808
|
if (config.settings.mode === "audit") {
|
|
2691
2809
|
if (!isIgnoredTool(toolName)) {
|
|
2692
2810
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
@@ -2709,19 +2827,46 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2709
2827
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
|
|
2710
2828
|
if (policyResult.decision === "allow") {
|
|
2711
2829
|
if (approvers.cloud && creds?.apiKey)
|
|
2712
|
-
auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
2830
|
+
await auditLocalAllow(toolName, args, "local-policy", creds, meta);
|
|
2713
2831
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
|
|
2714
2832
|
return { approved: true, checkedBy: "local-policy" };
|
|
2715
2833
|
}
|
|
2716
2834
|
if (policyResult.decision === "block") {
|
|
2717
|
-
if (
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2835
|
+
if (policyResult.dependsOnStatePredicates?.length) {
|
|
2836
|
+
const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
|
|
2837
|
+
const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
|
|
2838
|
+
if (stateResults === null && !isManual) {
|
|
2839
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
2840
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2841
|
+
event: "state-check-fail-open",
|
|
2842
|
+
tool: toolName,
|
|
2843
|
+
rule: policyResult.ruleName,
|
|
2844
|
+
predicates: policyResult.dependsOnStatePredicates,
|
|
2845
|
+
reason: "daemon unreachable or /state/check timed out \u2014 block rule downgraded to review"
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
if (predicatesMet && policyResult.recoveryCommand) {
|
|
2849
|
+
statefulRecoveryCommand = policyResult.recoveryCommand;
|
|
2850
|
+
}
|
|
2851
|
+
} else if (isDaemonRunning() && !isTestEnv2) {
|
|
2852
|
+
if (!isManual)
|
|
2853
|
+
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
2854
|
+
if (approvers.cloud && creds?.apiKey)
|
|
2855
|
+
auditLocalAllow(toolName, args, "smart-rule-block", creds, meta);
|
|
2856
|
+
} else {
|
|
2857
|
+
if (!isManual)
|
|
2858
|
+
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
2859
|
+
if (approvers.cloud && creds?.apiKey)
|
|
2860
|
+
auditLocalAllow(toolName, args, "smart-rule-block", creds, meta);
|
|
2861
|
+
return {
|
|
2862
|
+
approved: false,
|
|
2863
|
+
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
2864
|
+
blockedBy: "local-config",
|
|
2865
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
2866
|
+
ruleHit: policyResult.ruleName,
|
|
2867
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2725
2870
|
}
|
|
2726
2871
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
2727
2872
|
policyMatchedField = policyResult.matchedField;
|
|
@@ -2828,7 +2973,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2828
2973
|
meta,
|
|
2829
2974
|
riskMetadata,
|
|
2830
2975
|
options?.activityId,
|
|
2831
|
-
options?.cwd
|
|
2976
|
+
options?.cwd,
|
|
2977
|
+
statefulRecoveryCommand
|
|
2832
2978
|
);
|
|
2833
2979
|
daemonEntryId = entry.id;
|
|
2834
2980
|
daemonAllowCount = entry.allowCount;
|
|
@@ -2889,20 +3035,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2889
3035
|
if (daemonEntryId && (approvers.browser || approvers.terminal)) {
|
|
2890
3036
|
racePromises.push(
|
|
2891
3037
|
(async () => {
|
|
2892
|
-
const {
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
3038
|
+
const {
|
|
3039
|
+
decision: daemonDecision,
|
|
3040
|
+
source: decisionSource,
|
|
3041
|
+
reason: daemonReason
|
|
3042
|
+
} = await waitForDaemonDecision(daemonEntryId, signal);
|
|
2896
3043
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
2897
3044
|
const isApproved = daemonDecision === "allow";
|
|
2898
|
-
const
|
|
3045
|
+
const isRedirect = decisionSource === "terminal-redirect";
|
|
3046
|
+
const src = decisionSource === "terminal" || decisionSource === "terminal-redirect" || decisionSource === "browser" ? decisionSource === "browser" ? "browser" : "terminal" : approvers.browser ? "browser" : "terminal";
|
|
2899
3047
|
const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
|
|
2900
3048
|
return {
|
|
2901
3049
|
approved: isApproved,
|
|
2902
|
-
reason: isApproved ? void 0 :
|
|
3050
|
+
reason: isApproved ? void 0 : (
|
|
3051
|
+
// Use the redirect reason from the tail when choice [2] was selected;
|
|
3052
|
+
// otherwise fall back to the generic rejection message.
|
|
3053
|
+
isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`
|
|
3054
|
+
),
|
|
2903
3055
|
checkedBy: isApproved ? "daemon" : void 0,
|
|
2904
3056
|
blockedBy: isApproved ? void 0 : "local-decision",
|
|
2905
|
-
blockedByLabel: `User Decision (${via})`,
|
|
3057
|
+
blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
|
|
2906
3058
|
decisionSource: src
|
|
2907
3059
|
};
|
|
2908
3060
|
})()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
4
4
|
"description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -58,12 +58,12 @@
|
|
|
58
58
|
"format:check": "prettier --check .",
|
|
59
59
|
"fix": "npm run format && npm run lint:fix",
|
|
60
60
|
"validate": "npm run format && npm run lint && npm run typecheck && npm run test && npm run test:e2e && npm run build",
|
|
61
|
-
"test:e2e": "NODE9_TESTING=1 bash scripts/e2e.sh",
|
|
61
|
+
"test:e2e": "cross-env NODE9_TESTING=1 bash scripts/e2e.sh",
|
|
62
|
+
"test": "cross-env NODE9_TESTING=1 vitest --run",
|
|
63
|
+
"test:coverage": "cross-env NODE9_TESTING=1 vitest --run --coverage",
|
|
64
|
+
"test:watch": "cross-env NODE9_TESTING=1 vitest",
|
|
62
65
|
"preuninstall": "node9 uninstall || echo 'node9 uninstall failed — remove hooks manually from ~/.claude/settings.json'",
|
|
63
66
|
"prepublishOnly": "npm run validate",
|
|
64
|
-
"test": "vitest --run",
|
|
65
|
-
"test:coverage": "vitest --run --coverage",
|
|
66
|
-
"test:watch": "vitest",
|
|
67
67
|
"test:ui": "vitest --ui",
|
|
68
68
|
"dev:tail": "node -e \"try{const d=JSON.parse(require('fs').readFileSync(require('os').homedir()+'/.node9/daemon.pid','utf8'));const pid=d.pid;if(Number.isInteger(pid)&&pid>0&&pid<4194304)process.kill(pid)}catch(e){if(e.code!=='ESRCH'&&e.code!=='ENOENT')process.stderr.write(e.message+'\\n')}\" && npm run build && node dist/cli.js tail"
|
|
69
69
|
},
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
"@types/node": "^25.3.1",
|
|
89
89
|
"@types/picomatch": "^4.0.2",
|
|
90
90
|
"@vitest/coverage-v8": "4.1.2",
|
|
91
|
+
"cross-env": "^10.1.0",
|
|
91
92
|
"prettier": "^3.4.2",
|
|
92
93
|
"semantic-release": "^25.0.3",
|
|
93
94
|
"tsup": "^8.5.1",
|