@node9/proxy 1.5.2 → 1.5.3
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 +60 -7
- package/dist/cli.js +945 -190
- package/dist/cli.mjs +942 -187
- package/dist/index.js +156 -47
- package/dist/index.mjs +156 -47
- package/package.json +1 -1
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(),
|
|
@@ -227,8 +236,8 @@ function sanitizeConfig(raw) {
|
|
|
227
236
|
}
|
|
228
237
|
}
|
|
229
238
|
const lines = result.error.issues.map((issue) => {
|
|
230
|
-
const
|
|
231
|
-
return ` \u2022 ${
|
|
239
|
+
const path13 = issue.path.length > 0 ? issue.path.join(".") : "root";
|
|
240
|
+
return ` \u2022 ${path13}: ${issue.message}`;
|
|
232
241
|
});
|
|
233
242
|
return {
|
|
234
243
|
sanitized,
|
|
@@ -786,12 +795,17 @@ function getConfig(cwd) {
|
|
|
786
795
|
if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
|
|
787
796
|
mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
|
|
788
797
|
if (s.environment !== void 0) mergedSettings.environment = s.environment;
|
|
798
|
+
if (s.hud !== void 0) mergedSettings.hud = { ...mergedSettings.hud, ...s.hud };
|
|
789
799
|
if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
|
|
790
800
|
if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
|
|
791
801
|
if (p.dangerousWords) mergedPolicy.dangerousWords = [...p.dangerousWords];
|
|
792
802
|
if (p.toolInspection)
|
|
793
803
|
mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
|
|
794
|
-
if (p.smartRules)
|
|
804
|
+
if (p.smartRules) {
|
|
805
|
+
const defaultBlocks = mergedPolicy.smartRules.filter((r) => r.verdict === "block");
|
|
806
|
+
const defaultNonBlocks = mergedPolicy.smartRules.filter((r) => r.verdict !== "block");
|
|
807
|
+
mergedPolicy.smartRules = [...defaultBlocks, ...p.smartRules, ...defaultNonBlocks];
|
|
808
|
+
}
|
|
795
809
|
if (p.snapshot) {
|
|
796
810
|
const s2 = p.snapshot;
|
|
797
811
|
if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
|
|
@@ -1577,9 +1591,9 @@ function matchesPattern(text, patterns) {
|
|
|
1577
1591
|
const withoutDotSlash = text.replace(/^\.\//, "");
|
|
1578
1592
|
return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
|
|
1579
1593
|
}
|
|
1580
|
-
function getNestedValue(obj,
|
|
1594
|
+
function getNestedValue(obj, path13) {
|
|
1581
1595
|
if (!obj || typeof obj !== "object") return null;
|
|
1582
|
-
return
|
|
1596
|
+
return path13.split(".").reduce((prev, curr) => prev?.[curr], obj);
|
|
1583
1597
|
}
|
|
1584
1598
|
function evaluateSmartConditions(args, rule) {
|
|
1585
1599
|
if (!rule.conditions || rule.conditions.length === 0) return true;
|
|
@@ -1718,7 +1732,13 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1718
1732
|
blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
|
|
1719
1733
|
reason: matchedRule.reason,
|
|
1720
1734
|
tier: 2,
|
|
1721
|
-
ruleName: matchedRule.name ?? matchedRule.tool
|
|
1735
|
+
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1736
|
+
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1737
|
+
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1738
|
+
},
|
|
1739
|
+
...matchedRule.verdict === "block" && matchedRule.recoveryCommand && {
|
|
1740
|
+
recoveryCommand: matchedRule.recoveryCommand
|
|
1741
|
+
}
|
|
1722
1742
|
};
|
|
1723
1743
|
}
|
|
1724
1744
|
}
|
|
@@ -1950,9 +1970,39 @@ function getPersistentDecision(toolName) {
|
|
|
1950
1970
|
|
|
1951
1971
|
// src/auth/daemon.ts
|
|
1952
1972
|
import fs8 from "fs";
|
|
1973
|
+
import net from "net";
|
|
1953
1974
|
import path10 from "path";
|
|
1954
1975
|
import os7 from "os";
|
|
1955
1976
|
import { spawnSync } from "child_process";
|
|
1977
|
+
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path10.join(os7.tmpdir(), "node9-activity.sock");
|
|
1978
|
+
function notifyActivitySocket(data) {
|
|
1979
|
+
return new Promise((resolve) => {
|
|
1980
|
+
try {
|
|
1981
|
+
const payload = JSON.stringify(data);
|
|
1982
|
+
const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
|
|
1983
|
+
sock.on("connect", () => {
|
|
1984
|
+
sock.on("close", resolve);
|
|
1985
|
+
sock.end(payload);
|
|
1986
|
+
});
|
|
1987
|
+
sock.on("error", resolve);
|
|
1988
|
+
} catch {
|
|
1989
|
+
resolve();
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
async function checkStatePredicates(predicates) {
|
|
1994
|
+
if (predicates.length === 0) return {};
|
|
1995
|
+
try {
|
|
1996
|
+
const qs = predicates.map(encodeURIComponent).join(",");
|
|
1997
|
+
const res = await fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/state/check?predicates=${qs}`, {
|
|
1998
|
+
signal: AbortSignal.timeout(100)
|
|
1999
|
+
});
|
|
2000
|
+
if (!res.ok) return null;
|
|
2001
|
+
return await res.json();
|
|
2002
|
+
} catch {
|
|
2003
|
+
return null;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
1956
2006
|
var DAEMON_PORT = 7391;
|
|
1957
2007
|
var DAEMON_HOST = "127.0.0.1";
|
|
1958
2008
|
function getInternalToken() {
|
|
@@ -1988,7 +2038,7 @@ function isDaemonRunning() {
|
|
|
1988
2038
|
return false;
|
|
1989
2039
|
}
|
|
1990
2040
|
}
|
|
1991
|
-
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd) {
|
|
2041
|
+
async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityId, cwd, recoveryCommand, skipBackgroundAuth, viewOnly) {
|
|
1992
2042
|
const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
1993
2043
|
const ctrl = new AbortController();
|
|
1994
2044
|
const timer = setTimeout(() => ctrl.abort(), 5e3);
|
|
@@ -2006,7 +2056,10 @@ async function registerDaemonEntry(toolName, args, meta, riskMetadata, activityI
|
|
|
2006
2056
|
// activity-result as the CLI used for the pending activity event.
|
|
2007
2057
|
activityId,
|
|
2008
2058
|
...riskMetadata && { riskMetadata },
|
|
2009
|
-
...cwd && { cwd }
|
|
2059
|
+
...cwd && { cwd },
|
|
2060
|
+
...recoveryCommand && { recoveryCommand },
|
|
2061
|
+
...skipBackgroundAuth && { skipBackgroundAuth: true },
|
|
2062
|
+
...viewOnly && { viewOnly: true }
|
|
2010
2063
|
}),
|
|
2011
2064
|
signal: ctrl.signal
|
|
2012
2065
|
});
|
|
@@ -2026,10 +2079,10 @@ async function waitForDaemonDecision(id, signal) {
|
|
|
2026
2079
|
try {
|
|
2027
2080
|
const waitRes = await fetch(`${base}/wait/${id}`, { signal: waitCtrl.signal });
|
|
2028
2081
|
if (!waitRes.ok) return { decision: "deny" };
|
|
2029
|
-
const { decision, source } = await waitRes.json();
|
|
2082
|
+
const { decision, source, reason } = await waitRes.json();
|
|
2030
2083
|
if (decision === "allow") return { decision: "allow", source };
|
|
2031
2084
|
if (decision === "abandoned") return { decision: "abandoned", source };
|
|
2032
|
-
return { decision: "deny", source };
|
|
2085
|
+
return { decision: "deny", source, reason };
|
|
2033
2086
|
} finally {
|
|
2034
2087
|
clearTimeout(waitTimer);
|
|
2035
2088
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
@@ -2104,9 +2157,6 @@ async function resolveViaDaemon(id, decision, internalToken, source) {
|
|
|
2104
2157
|
}
|
|
2105
2158
|
|
|
2106
2159
|
// src/auth/orchestrator.ts
|
|
2107
|
-
import net from "net";
|
|
2108
|
-
import path13 from "path";
|
|
2109
|
-
import os9 from "os";
|
|
2110
2160
|
import { randomUUID } from "crypto";
|
|
2111
2161
|
|
|
2112
2162
|
// src/ui/native.ts
|
|
@@ -2589,21 +2639,8 @@ function isNetworkTool(toolName, args) {
|
|
|
2589
2639
|
}
|
|
2590
2640
|
return false;
|
|
2591
2641
|
}
|
|
2592
|
-
var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path13.join(os9.tmpdir(), "node9-activity.sock");
|
|
2593
2642
|
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
|
-
});
|
|
2643
|
+
return notifyActivitySocket(data);
|
|
2607
2644
|
}
|
|
2608
2645
|
async function authorizeHeadless(toolName, args, meta, options) {
|
|
2609
2646
|
if (!options?.calledFromDaemon) {
|
|
@@ -2620,7 +2657,9 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
2620
2657
|
tool: toolName,
|
|
2621
2658
|
ts: actTs,
|
|
2622
2659
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
2623
|
-
label: result.blockedByLabel
|
|
2660
|
+
label: result.blockedByLabel,
|
|
2661
|
+
ruleHit: result.ruleHit,
|
|
2662
|
+
observeWouldBlock: result.observeWouldBlock
|
|
2624
2663
|
});
|
|
2625
2664
|
}
|
|
2626
2665
|
return result;
|
|
@@ -2647,10 +2686,12 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2647
2686
|
appendHookDebug(toolName, args, meta, hashAuditArgs);
|
|
2648
2687
|
}
|
|
2649
2688
|
const isManual = meta?.agent === "Terminal";
|
|
2689
|
+
const isObserveMode = config.settings.mode === "observe";
|
|
2650
2690
|
let explainableLabel = "Local Config";
|
|
2651
2691
|
let policyMatchedField;
|
|
2652
2692
|
let policyMatchedWord;
|
|
2653
2693
|
let riskMetadata;
|
|
2694
|
+
let statefulRecoveryCommand;
|
|
2654
2695
|
let taintWarning = null;
|
|
2655
2696
|
if (isNetworkTool(toolName, args)) {
|
|
2656
2697
|
const filePaths = extractFilePaths(toolName, args);
|
|
@@ -2671,10 +2712,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2671
2712
|
if (dlpMatch) {
|
|
2672
2713
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
2673
2714
|
if (dlpMatch.severity === "block") {
|
|
2674
|
-
if (!isManual)
|
|
2715
|
+
if (!isManual)
|
|
2716
|
+
appendLocalAudit(
|
|
2717
|
+
toolName,
|
|
2718
|
+
args,
|
|
2719
|
+
"deny",
|
|
2720
|
+
isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
|
|
2721
|
+
meta,
|
|
2722
|
+
true
|
|
2723
|
+
);
|
|
2675
2724
|
if (isWriteTool(toolName) && filePath) {
|
|
2676
2725
|
await notifyTaint(filePath, `DLP:${dlpMatch.patternName}`);
|
|
2677
2726
|
}
|
|
2727
|
+
if (isObserveMode) {
|
|
2728
|
+
return {
|
|
2729
|
+
approved: true,
|
|
2730
|
+
checkedBy: "audit",
|
|
2731
|
+
observeWouldBlock: true,
|
|
2732
|
+
blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2678
2735
|
return {
|
|
2679
2736
|
approved: false,
|
|
2680
2737
|
reason: dlpReason,
|
|
@@ -2687,6 +2744,31 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2687
2744
|
explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
|
|
2688
2745
|
}
|
|
2689
2746
|
}
|
|
2747
|
+
if (isObserveMode) {
|
|
2748
|
+
if (!isIgnoredTool(toolName)) {
|
|
2749
|
+
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
2750
|
+
const wouldBlock = policyResult.decision === "block";
|
|
2751
|
+
if (!isManual)
|
|
2752
|
+
appendLocalAudit(
|
|
2753
|
+
toolName,
|
|
2754
|
+
args,
|
|
2755
|
+
"allow",
|
|
2756
|
+
wouldBlock ? "observe-mode-would-block" : "observe-mode",
|
|
2757
|
+
meta,
|
|
2758
|
+
hashAuditArgs
|
|
2759
|
+
);
|
|
2760
|
+
return {
|
|
2761
|
+
approved: true,
|
|
2762
|
+
checkedBy: "audit",
|
|
2763
|
+
...wouldBlock && {
|
|
2764
|
+
observeWouldBlock: true,
|
|
2765
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
2766
|
+
ruleHit: policyResult.ruleName
|
|
2767
|
+
}
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2770
|
+
return { approved: true, checkedBy: "audit" };
|
|
2771
|
+
}
|
|
2690
2772
|
if (config.settings.mode === "audit") {
|
|
2691
2773
|
if (!isIgnoredTool(toolName)) {
|
|
2692
2774
|
const policyResult = await evaluatePolicy(toolName, args, meta?.agent, options?.cwd);
|
|
@@ -2714,14 +2796,34 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2714
2796
|
return { approved: true, checkedBy: "local-policy" };
|
|
2715
2797
|
}
|
|
2716
2798
|
if (policyResult.decision === "block") {
|
|
2717
|
-
if (
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2799
|
+
if (policyResult.dependsOnStatePredicates?.length) {
|
|
2800
|
+
const stateResults = await checkStatePredicates(policyResult.dependsOnStatePredicates);
|
|
2801
|
+
const predicatesMet = stateResults !== null && policyResult.dependsOnStatePredicates.every((p) => stateResults[p] === true);
|
|
2802
|
+
if (stateResults === null && !isManual) {
|
|
2803
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
2804
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2805
|
+
event: "state-check-fail-open",
|
|
2806
|
+
tool: toolName,
|
|
2807
|
+
rule: policyResult.ruleName,
|
|
2808
|
+
predicates: policyResult.dependsOnStatePredicates,
|
|
2809
|
+
reason: "daemon unreachable or /state/check timed out \u2014 block rule downgraded to review"
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
if (predicatesMet && policyResult.recoveryCommand) {
|
|
2813
|
+
statefulRecoveryCommand = policyResult.recoveryCommand;
|
|
2814
|
+
}
|
|
2815
|
+
} else {
|
|
2816
|
+
if (!isManual)
|
|
2817
|
+
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
2818
|
+
return {
|
|
2819
|
+
approved: false,
|
|
2820
|
+
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
2821
|
+
blockedBy: "local-config",
|
|
2822
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
2823
|
+
ruleHit: policyResult.ruleName,
|
|
2824
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
|
|
2825
|
+
};
|
|
2826
|
+
}
|
|
2725
2827
|
}
|
|
2726
2828
|
explainableLabel = policyResult.blockedByLabel || "Local Config";
|
|
2727
2829
|
policyMatchedField = policyResult.matchedField;
|
|
@@ -2828,7 +2930,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2828
2930
|
meta,
|
|
2829
2931
|
riskMetadata,
|
|
2830
2932
|
options?.activityId,
|
|
2831
|
-
options?.cwd
|
|
2933
|
+
options?.cwd,
|
|
2934
|
+
statefulRecoveryCommand
|
|
2832
2935
|
);
|
|
2833
2936
|
daemonEntryId = entry.id;
|
|
2834
2937
|
daemonAllowCount = entry.allowCount;
|
|
@@ -2889,20 +2992,26 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2889
2992
|
if (daemonEntryId && (approvers.browser || approvers.terminal)) {
|
|
2890
2993
|
racePromises.push(
|
|
2891
2994
|
(async () => {
|
|
2892
|
-
const {
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2995
|
+
const {
|
|
2996
|
+
decision: daemonDecision,
|
|
2997
|
+
source: decisionSource,
|
|
2998
|
+
reason: daemonReason
|
|
2999
|
+
} = await waitForDaemonDecision(daemonEntryId, signal);
|
|
2896
3000
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
2897
3001
|
const isApproved = daemonDecision === "allow";
|
|
2898
|
-
const
|
|
3002
|
+
const isRedirect = decisionSource === "terminal-redirect";
|
|
3003
|
+
const src = decisionSource === "terminal" || decisionSource === "terminal-redirect" || decisionSource === "browser" ? decisionSource === "browser" ? "browser" : "terminal" : approvers.browser ? "browser" : "terminal";
|
|
2899
3004
|
const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
|
|
2900
3005
|
return {
|
|
2901
3006
|
approved: isApproved,
|
|
2902
|
-
reason: isApproved ? void 0 :
|
|
3007
|
+
reason: isApproved ? void 0 : (
|
|
3008
|
+
// Use the redirect reason from the tail when choice [2] was selected;
|
|
3009
|
+
// otherwise fall back to the generic rejection message.
|
|
3010
|
+
isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`
|
|
3011
|
+
),
|
|
2903
3012
|
checkedBy: isApproved ? "daemon" : void 0,
|
|
2904
3013
|
blockedBy: isApproved ? void 0 : "local-decision",
|
|
2905
|
-
blockedByLabel: `User Decision (${via})`,
|
|
3014
|
+
blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
|
|
2906
3015
|
decisionSource: src
|
|
2907
3016
|
};
|
|
2908
3017
|
})()
|