@node9/proxy 1.65.0 → 1.66.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/dist/cli.js +124 -35
- package/dist/cli.mjs +124 -35
- package/dist/dashboard.mjs +34 -21
- package/dist/index.js +31 -11
- package/dist/index.mjs +31 -11
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -94,7 +94,7 @@ function redactSecrets(text) {
|
|
|
94
94
|
if (!text) return text;
|
|
95
95
|
let redacted = text;
|
|
96
96
|
redacted = redacted.replace(
|
|
97
|
-
/(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._
|
|
97
|
+
/(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/=]+/gi,
|
|
98
98
|
"$1********"
|
|
99
99
|
);
|
|
100
100
|
redacted = redacted.replace(
|
|
@@ -155,7 +155,7 @@ function filePathFromArgs(args) {
|
|
|
155
155
|
return void 0;
|
|
156
156
|
}
|
|
157
157
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
158
|
-
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
|
|
158
|
+
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern) || /dlp|taint/i.test(String(meta?.ruleName ?? ""));
|
|
159
159
|
const preview2 = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
|
|
160
160
|
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args), ...preview2 ? { argsPreview: preview2 } : {} } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
161
161
|
const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
|
|
@@ -332,8 +332,10 @@ var init_config_schema = __esm({
|
|
|
332
332
|
agentPolicy: import_zod.z.enum(["require_approval", "block_on_rules"]).optional(),
|
|
333
333
|
// Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
|
|
334
334
|
// inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
|
|
335
|
-
// node9's own approver (terminal/native/cloud). Unset →
|
|
336
|
-
//
|
|
335
|
+
// node9's own approver (terminal/native/cloud). Unset → default ASK for
|
|
336
|
+
// ask-capable agents (v2: cloud no longer disables inline — the outcome
|
|
337
|
+
// ships to the dashboard as audit; admins force routing via managed
|
|
338
|
+
// reviewChannel, which outranks the local --ask flag).
|
|
337
339
|
reviewChannel: import_zod.z.enum(["ask", "approver"]).optional(),
|
|
338
340
|
// When true, agents may call WEAKENING node9 MCP tools (shield_disable,
|
|
339
341
|
// approver_set). Default (unset/false): those tools refuse over MCP — a human
|
|
@@ -371,7 +373,8 @@ var init_config_schema = __esm({
|
|
|
371
373
|
dlp: import_zod.z.object({
|
|
372
374
|
enabled: import_zod.z.boolean().optional(),
|
|
373
375
|
scanIgnoredTools: import_zod.z.boolean().optional(),
|
|
374
|
-
pii: import_zod.z.enum(["off", "block"]).optional()
|
|
376
|
+
pii: import_zod.z.enum(["off", "block"]).optional(),
|
|
377
|
+
reviewAction: import_zod.z.enum(["review", "block"]).optional()
|
|
375
378
|
}).optional(),
|
|
376
379
|
egress: import_zod.z.object({
|
|
377
380
|
enabled: import_zod.z.boolean().optional(),
|
|
@@ -1581,7 +1584,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1581
1584
|
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
1582
1585
|
if (dlpMatch) {
|
|
1583
1586
|
return {
|
|
1584
|
-
|
|
1587
|
+
// reviewAction:'block' (inline-ask v2): the admin upgraded
|
|
1588
|
+
// review-severity matches to a hard block — every evaluatePolicy
|
|
1589
|
+
// caller (orchestrator, explain, gateway) must agree with the gate.
|
|
1590
|
+
decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
|
|
1585
1591
|
blockedByLabel: `DLP: ${dlpMatch.patternName}`,
|
|
1586
1592
|
reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
|
|
1587
1593
|
};
|
|
@@ -4366,6 +4372,14 @@ function applyManagedDlp(local, managed, locked) {
|
|
|
4366
4372
|
locked.includes("dlpPii")
|
|
4367
4373
|
);
|
|
4368
4374
|
}
|
|
4375
|
+
if (typeof managed.reviewAction === "string") {
|
|
4376
|
+
next.reviewAction = resolveByOrder(
|
|
4377
|
+
DLP_REVIEW_ACTION_ORDER,
|
|
4378
|
+
local.reviewAction ?? "review",
|
|
4379
|
+
managed.reviewAction,
|
|
4380
|
+
locked.includes("dlpReviewAction")
|
|
4381
|
+
);
|
|
4382
|
+
}
|
|
4369
4383
|
return next;
|
|
4370
4384
|
}
|
|
4371
4385
|
function applyManagedApprovers(local, managed) {
|
|
@@ -4377,13 +4391,14 @@ function applyManagedApprovers(local, managed) {
|
|
|
4377
4391
|
terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
|
|
4378
4392
|
};
|
|
4379
4393
|
}
|
|
4380
|
-
var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER;
|
|
4394
|
+
var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER;
|
|
4381
4395
|
var init_managed = __esm({
|
|
4382
4396
|
"src/config/managed.ts"() {
|
|
4383
4397
|
"use strict";
|
|
4384
4398
|
MODE_ORDER = ["observe", "audit", "standard", "strict"];
|
|
4385
4399
|
EGRESS_MODE_ORDER = ["off", "review", "block"];
|
|
4386
4400
|
DLP_PII_ORDER = ["off", "block"];
|
|
4401
|
+
DLP_REVIEW_ACTION_ORDER = ["review", "block"];
|
|
4387
4402
|
}
|
|
4388
4403
|
});
|
|
4389
4404
|
|
|
@@ -4758,6 +4773,7 @@ function getConfig(cwd) {
|
|
|
4758
4773
|
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
4759
4774
|
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
4760
4775
|
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
4776
|
+
if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
|
|
4761
4777
|
}
|
|
4762
4778
|
if (p.egress) {
|
|
4763
4779
|
const e = p.egress;
|
|
@@ -4854,7 +4870,8 @@ function getConfig(cwd) {
|
|
|
4854
4870
|
mergedPolicy.dlp,
|
|
4855
4871
|
{
|
|
4856
4872
|
enabled: typeof mc.dlp.enabled === "boolean" ? mc.dlp.enabled : void 0,
|
|
4857
|
-
pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0
|
|
4873
|
+
pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0,
|
|
4874
|
+
reviewAction: mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block" ? mc.dlp.reviewAction : void 0
|
|
4858
4875
|
},
|
|
4859
4876
|
locked
|
|
4860
4877
|
);
|
|
@@ -4870,6 +4887,7 @@ function getConfig(cwd) {
|
|
|
4870
4887
|
}
|
|
4871
4888
|
if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
|
|
4872
4889
|
mergedSettings.reviewChannel = mc.reviewChannel;
|
|
4890
|
+
mergedSettings.reviewChannelManaged = true;
|
|
4873
4891
|
}
|
|
4874
4892
|
if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
|
|
4875
4893
|
mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
|
|
@@ -5614,13 +5632,14 @@ async function deriveExplainTrace(toolName, args) {
|
|
|
5614
5632
|
const filePathE = String(argsObjE.file_path ?? argsObjE.path ?? argsObjE.filename ?? "");
|
|
5615
5633
|
const dlpMatch = (filePathE ? scanFilePath(filePathE) : null) ?? (args !== void 0 ? scanArgs(args) : null);
|
|
5616
5634
|
if (dlpMatch) {
|
|
5635
|
+
const dlpBlocks = dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block";
|
|
5617
5636
|
steps.push({
|
|
5618
5637
|
name: "DLP Content Scanner",
|
|
5619
|
-
outcome:
|
|
5638
|
+
outcome: dlpBlocks ? "block" : "review",
|
|
5620
5639
|
detail: `\u{1F6A8} ${dlpMatch.patternName} detected in ${dlpMatch.fieldPath} \u2014 sample: ${dlpMatch.redactedSample}`,
|
|
5621
|
-
isFinal:
|
|
5640
|
+
isFinal: dlpBlocks
|
|
5622
5641
|
});
|
|
5623
|
-
if (
|
|
5642
|
+
if (dlpBlocks) {
|
|
5624
5643
|
return { tool: toolName, args, waterfall, steps, decision: "block" };
|
|
5625
5644
|
}
|
|
5626
5645
|
} else {
|
|
@@ -7119,7 +7138,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
7119
7138
|
const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
|
|
7120
7139
|
if (dlpMatch) {
|
|
7121
7140
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
7122
|
-
if (dlpMatch.severity === "block") {
|
|
7141
|
+
if (dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block") {
|
|
7123
7142
|
let dlpEid;
|
|
7124
7143
|
if (!isManual)
|
|
7125
7144
|
dlpEid = appendLocalAudit(
|
|
@@ -7457,12 +7476,14 @@ ${appPermReview}`
|
|
|
7457
7476
|
);
|
|
7458
7477
|
}
|
|
7459
7478
|
}
|
|
7460
|
-
|
|
7461
|
-
if (options?.deferReview && !taintWarning && !appPermReview && !cloudEnforcedForDefer) {
|
|
7479
|
+
if (options?.deferReview && !hardBlockDowngraded) {
|
|
7462
7480
|
return {
|
|
7463
7481
|
approved: false,
|
|
7464
7482
|
review: true,
|
|
7465
|
-
|
|
7483
|
+
// The prompt must say WHY: the taint sentence beats the bare label so
|
|
7484
|
+
// the dev sees the actual risk context inline. (No appPermReview term:
|
|
7485
|
+
// deferReview and serverKey never co-occur in production — see above.)
|
|
7486
|
+
reason: taintWarning || explainableLabel || "Node9 flagged this action for review.",
|
|
7466
7487
|
ruleDescription: policyRuleDescription,
|
|
7467
7488
|
blockedByLabel: explainableLabel
|
|
7468
7489
|
};
|
|
@@ -8404,7 +8425,7 @@ function printInlineAskNotice() {
|
|
|
8404
8425
|
);
|
|
8405
8426
|
console.log(
|
|
8406
8427
|
import_chalk.default.gray(
|
|
8407
|
-
' Prefer node9\u2019s own approver? Set "reviewChannel": "approver" in config, or add --no-ask to the hook.\n (Inline
|
|
8428
|
+
' Prefer node9\u2019s own approver? Set "reviewChannel": "approver" in config, or add --no-ask to the hook.\n (Inline is the default for every review; with a cloud workspace the outcome ships to the dashboard as audit.)'
|
|
8408
8429
|
)
|
|
8409
8430
|
);
|
|
8410
8431
|
}
|
|
@@ -18353,7 +18374,9 @@ function extractManagedConfig(body) {
|
|
|
18353
18374
|
const d = {};
|
|
18354
18375
|
if (typeof mc.dlp.enabled === "boolean") d.enabled = mc.dlp.enabled;
|
|
18355
18376
|
if (typeof mc.dlp.pii === "string") d.pii = mc.dlp.pii;
|
|
18356
|
-
if (
|
|
18377
|
+
if (mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block")
|
|
18378
|
+
d.reviewAction = mc.dlp.reviewAction;
|
|
18379
|
+
if (d.enabled !== void 0 || d.pii !== void 0 || d.reviewAction !== void 0) out.dlp = d;
|
|
18357
18380
|
}
|
|
18358
18381
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
18359
18382
|
const a = {};
|
|
@@ -19049,12 +19072,23 @@ function classifyDecision(a, b) {
|
|
|
19049
19072
|
function decisionTag(view) {
|
|
19050
19073
|
return `[${view.label}]`.padEnd(14);
|
|
19051
19074
|
}
|
|
19052
|
-
var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
|
|
19075
|
+
var HUMAN_SOURCES, TIMEOUT_SOURCES, NON_DECISION_SOURCES, has;
|
|
19053
19076
|
var init_decision = __esm({
|
|
19054
19077
|
"src/audit/decision.ts"() {
|
|
19055
19078
|
"use strict";
|
|
19056
|
-
HUMAN_SOURCES = /* @__PURE__ */ new Set([
|
|
19079
|
+
HUMAN_SOURCES = /* @__PURE__ */ new Set([
|
|
19080
|
+
"daemon",
|
|
19081
|
+
"cloud",
|
|
19082
|
+
"local-decision",
|
|
19083
|
+
"inline-review-approved",
|
|
19084
|
+
"inline-review"
|
|
19085
|
+
]);
|
|
19057
19086
|
TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
|
|
19087
|
+
NON_DECISION_SOURCES = /* @__PURE__ */ new Set([
|
|
19088
|
+
"post-hook",
|
|
19089
|
+
"inline-review-approved",
|
|
19090
|
+
"response-dlp"
|
|
19091
|
+
]);
|
|
19058
19092
|
has = (s, needle) => s.includes(needle);
|
|
19059
19093
|
}
|
|
19060
19094
|
});
|
|
@@ -19630,7 +19664,7 @@ function buildDaemonReport(allEntries, period, now) {
|
|
|
19630
19664
|
else if (period === "30d") start.setDate(start.getDate() - 29);
|
|
19631
19665
|
else if (period === "month") start = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
19632
19666
|
const entries = allEntries.filter((e) => {
|
|
19633
|
-
if (e.source === "
|
|
19667
|
+
if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
|
|
19634
19668
|
return new Date(e.ts) >= start;
|
|
19635
19669
|
});
|
|
19636
19670
|
const isBlocked = (e) => classifyDecision(e).outcome === "deny";
|
|
@@ -47379,8 +47413,8 @@ INSTRUCTIONS:
|
|
|
47379
47413
|
- Do NOT retry this exact command or attempt to bypass the rule.${recovery}
|
|
47380
47414
|
- Inform the user which security rule was triggered and ask how to proceed.`;
|
|
47381
47415
|
}
|
|
47382
|
-
function buildReviewMessage(blockedByLabel, ruleDescription) {
|
|
47383
|
-
const why = ruleDescription || blockedByLabel || "this action needs your review";
|
|
47416
|
+
function buildReviewMessage(blockedByLabel, ruleDescription, reason) {
|
|
47417
|
+
const why = ruleDescription || (reason && reason !== blockedByLabel ? reason : void 0) || blockedByLabel || "this action needs your review";
|
|
47384
47418
|
return `Node9 flagged this for your review: ${why}. Approve to proceed, or deny to cancel.`;
|
|
47385
47419
|
}
|
|
47386
47420
|
|
|
@@ -48121,6 +48155,7 @@ init_hasher();
|
|
|
48121
48155
|
function storePath() {
|
|
48122
48156
|
return process.env.NODE9_PENDING_STORE || import_path45.default.join(import_os42.default.homedir(), ".node9", "pending-reviews.json");
|
|
48123
48157
|
}
|
|
48158
|
+
var TTL_TUID_MS = 72 * 60 * 60 * 1e3;
|
|
48124
48159
|
var TTL_MS2 = 6 * 60 * 60 * 1e3;
|
|
48125
48160
|
var MAX_ENTRIES = 500;
|
|
48126
48161
|
function reviewCorrelationKey(payload) {
|
|
@@ -48154,7 +48189,9 @@ function write(store) {
|
|
|
48154
48189
|
}
|
|
48155
48190
|
}
|
|
48156
48191
|
function prune(entries, now) {
|
|
48157
|
-
const fresh = entries.filter(
|
|
48192
|
+
const fresh = entries.filter(
|
|
48193
|
+
(e) => now - e.ts < (e.key.startsWith("tuid:") ? TTL_TUID_MS : TTL_MS2)
|
|
48194
|
+
);
|
|
48158
48195
|
return fresh.length > MAX_ENTRIES ? fresh.slice(fresh.length - MAX_ENTRIES) : fresh;
|
|
48159
48196
|
}
|
|
48160
48197
|
function recordPendingReview(entry) {
|
|
@@ -48169,19 +48206,30 @@ function recordPendingReview(entry) {
|
|
|
48169
48206
|
function resolvePendingReview(key, now = Date.now()) {
|
|
48170
48207
|
try {
|
|
48171
48208
|
const store = read();
|
|
48172
|
-
const
|
|
48209
|
+
const live = prune(store.entries, now);
|
|
48210
|
+
const idx = live.findIndex((e) => e.key === key);
|
|
48173
48211
|
if (idx === -1) {
|
|
48174
|
-
|
|
48175
|
-
if (pruned.length !== store.entries.length) write({ entries: pruned });
|
|
48212
|
+
if (live.length !== store.entries.length) write({ entries: live });
|
|
48176
48213
|
return null;
|
|
48177
48214
|
}
|
|
48178
|
-
const [match] =
|
|
48179
|
-
write({ entries:
|
|
48215
|
+
const [match] = live.splice(idx, 1);
|
|
48216
|
+
write({ entries: live });
|
|
48180
48217
|
return match;
|
|
48181
48218
|
} catch {
|
|
48182
48219
|
return null;
|
|
48183
48220
|
}
|
|
48184
48221
|
}
|
|
48222
|
+
function discardPendingReview(key, now = Date.now()) {
|
|
48223
|
+
try {
|
|
48224
|
+
const store = read();
|
|
48225
|
+
const kept = prune(
|
|
48226
|
+
store.entries.filter((e) => e.key !== key),
|
|
48227
|
+
now
|
|
48228
|
+
);
|
|
48229
|
+
if (kept.length !== store.entries.length) write({ entries: kept });
|
|
48230
|
+
} catch {
|
|
48231
|
+
}
|
|
48232
|
+
}
|
|
48185
48233
|
|
|
48186
48234
|
// src/cli/commands/check.ts
|
|
48187
48235
|
init_hook_payload();
|
|
@@ -48237,7 +48285,10 @@ function agentSupportsAsk(agent) {
|
|
|
48237
48285
|
}
|
|
48238
48286
|
function resolveAskMode(agent, opts, config) {
|
|
48239
48287
|
if (!agentSupportsAsk(agent)) return false;
|
|
48240
|
-
if (config.settings.
|
|
48288
|
+
if (config.settings.reviewChannelManaged === true) {
|
|
48289
|
+
if (config.settings.reviewChannel === "ask") return true;
|
|
48290
|
+
if (config.settings.reviewChannel === "approver") return false;
|
|
48291
|
+
}
|
|
48241
48292
|
if (opts.ask === true) return true;
|
|
48242
48293
|
if (opts.ask === false) return false;
|
|
48243
48294
|
if (config.settings.reviewChannel === "ask") return true;
|
|
@@ -48476,7 +48527,11 @@ RAW: ${raw}
|
|
|
48476
48527
|
process.exit(2);
|
|
48477
48528
|
};
|
|
48478
48529
|
const sendAsk = (result2) => {
|
|
48479
|
-
const msg = buildReviewMessage(
|
|
48530
|
+
const msg = buildReviewMessage(
|
|
48531
|
+
result2.blockedByLabel,
|
|
48532
|
+
result2.ruleDescription,
|
|
48533
|
+
result2.reason
|
|
48534
|
+
);
|
|
48480
48535
|
try {
|
|
48481
48536
|
const key = reviewCorrelationKey(payload);
|
|
48482
48537
|
if (key) {
|
|
@@ -48659,6 +48714,13 @@ RAW: ${raw}
|
|
|
48659
48714
|
cwd: safeCwdForAuth,
|
|
48660
48715
|
deferReview: askMode
|
|
48661
48716
|
});
|
|
48717
|
+
if (!result.review) {
|
|
48718
|
+
try {
|
|
48719
|
+
const key = reviewCorrelationKey(payload);
|
|
48720
|
+
if (key) discardPendingReview(key);
|
|
48721
|
+
} catch {
|
|
48722
|
+
}
|
|
48723
|
+
}
|
|
48662
48724
|
if (result.approved) {
|
|
48663
48725
|
if (result.checkedBy && process.env.NODE9_DEBUG === "1")
|
|
48664
48726
|
process.stderr.write(`\u2713 node9 [${result.checkedBy}]: "${toolName}" allowed
|
|
@@ -48835,9 +48897,11 @@ function registerLogCommand(program2) {
|
|
|
48835
48897
|
})();
|
|
48836
48898
|
const agent = agentOverride !== void 0 ? agentOverride : metaTag !== void 0 ? metaTag : payload.turn_id !== void 0 ? "Codex" : payload.toolCall !== void 0 || payload.conversationId !== void 0 ? "Antigravity" : payload.hook_event_name === "pre_tool_call" || payload.hook_event_name === "post_tool_call" ? "Hermes" : payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0 ? "Claude Code" : payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0 ? "Gemini CLI" : process.env.HERMES_SESSION_ID || process.env.HERMES_HOME || process.env.HERMES_INTERACTIVE ? "Hermes" : process.env.ANTIGRAVITY_CONVERSATION_ID ? "Antigravity" : void 0;
|
|
48837
48899
|
let reviewApproved = false;
|
|
48900
|
+
let resolvedReview = null;
|
|
48838
48901
|
try {
|
|
48839
48902
|
const key = reviewCorrelationKey(payload);
|
|
48840
|
-
if (key
|
|
48903
|
+
if (key) resolvedReview = resolvePendingReview(key);
|
|
48904
|
+
if (resolvedReview) reviewApproved = true;
|
|
48841
48905
|
} catch {
|
|
48842
48906
|
}
|
|
48843
48907
|
const entry = {
|
|
@@ -48890,6 +48954,33 @@ function registerLogCommand(program2) {
|
|
|
48890
48954
|
const payloadCwd = typeof payload.cwd === "string" ? payload.cwd : Array.isArray(payload.workspacePaths) && typeof payload.workspacePaths[0] === "string" ? payload.workspacePaths[0] : void 0;
|
|
48891
48955
|
const safeCwd = typeof payloadCwd === "string" && import_path47.default.isAbsolute(payloadCwd) ? payloadCwd : void 0;
|
|
48892
48956
|
const config = getConfig(safeCwd);
|
|
48957
|
+
if (resolvedReview) {
|
|
48958
|
+
try {
|
|
48959
|
+
const reviewLabel = resolvedReview.label || "inline-review";
|
|
48960
|
+
const sensitiveReview = /dlp|taint/i.test(reviewLabel);
|
|
48961
|
+
appendLocalAudit(
|
|
48962
|
+
tool,
|
|
48963
|
+
rawInput,
|
|
48964
|
+
"allow",
|
|
48965
|
+
"inline-review",
|
|
48966
|
+
{
|
|
48967
|
+
agent,
|
|
48968
|
+
ruleName: reviewLabel,
|
|
48969
|
+
...rawToolName !== tool ? { agentToolName: rawToolName } : {},
|
|
48970
|
+
...typeof payloadSessionId === "string" ? { sessionId: payloadSessionId } : {},
|
|
48971
|
+
...safeCwd ? { workingDir: safeCwd } : {}
|
|
48972
|
+
},
|
|
48973
|
+
sensitiveReview || config.settings.auditHashArgs === true
|
|
48974
|
+
);
|
|
48975
|
+
} catch (err2) {
|
|
48976
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
48977
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
48978
|
+
event: "inline-review-ship-row-fail",
|
|
48979
|
+
tool,
|
|
48980
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
48981
|
+
});
|
|
48982
|
+
}
|
|
48983
|
+
}
|
|
48893
48984
|
{
|
|
48894
48985
|
const toolOutput = payload.tool_response?.output;
|
|
48895
48986
|
const inj = config.policy.injectionScan;
|
|
@@ -50358,8 +50449,7 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
50358
50449
|
const priorEnd = new Date(start.getTime() - 1);
|
|
50359
50450
|
const priorStart = new Date(start.getTime() - periodMs);
|
|
50360
50451
|
const priorEntries = allEntries.filter((e) => {
|
|
50361
|
-
if (e.source === "
|
|
50362
|
-
if (e.source === "response-dlp") return false;
|
|
50452
|
+
if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
|
|
50363
50453
|
if (typeof e.decision !== "string") return false;
|
|
50364
50454
|
const ts = new Date(e.ts);
|
|
50365
50455
|
return ts >= priorStart && ts <= priorEnd;
|
|
@@ -50370,8 +50460,7 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
50370
50460
|
const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
|
|
50371
50461
|
let excludedTests = 0;
|
|
50372
50462
|
const entries = allEntries.filter((e) => {
|
|
50373
|
-
if (e.source === "
|
|
50374
|
-
if (e.source === "response-dlp") return false;
|
|
50463
|
+
if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
|
|
50375
50464
|
if (typeof e.decision !== "string") return false;
|
|
50376
50465
|
const ts = new Date(e.ts);
|
|
50377
50466
|
if (ts < start || ts > end) return false;
|
package/dist/cli.mjs
CHANGED
|
@@ -102,7 +102,7 @@ function redactSecrets(text) {
|
|
|
102
102
|
if (!text) return text;
|
|
103
103
|
let redacted = text;
|
|
104
104
|
redacted = redacted.replace(
|
|
105
|
-
/(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._
|
|
105
|
+
/(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/=]+/gi,
|
|
106
106
|
"$1********"
|
|
107
107
|
);
|
|
108
108
|
redacted = redacted.replace(
|
|
@@ -163,7 +163,7 @@ function filePathFromArgs(args) {
|
|
|
163
163
|
return void 0;
|
|
164
164
|
}
|
|
165
165
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
166
|
-
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
|
|
166
|
+
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern) || /dlp|taint/i.test(String(meta?.ruleName ?? ""));
|
|
167
167
|
const preview2 = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
|
|
168
168
|
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args), ...preview2 ? { argsPreview: preview2 } : {} } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
169
169
|
const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
|
|
@@ -336,8 +336,10 @@ var init_config_schema = __esm({
|
|
|
336
336
|
agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
|
|
337
337
|
// Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
|
|
338
338
|
// inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
|
|
339
|
-
// node9's own approver (terminal/native/cloud). Unset →
|
|
340
|
-
//
|
|
339
|
+
// node9's own approver (terminal/native/cloud). Unset → default ASK for
|
|
340
|
+
// ask-capable agents (v2: cloud no longer disables inline — the outcome
|
|
341
|
+
// ships to the dashboard as audit; admins force routing via managed
|
|
342
|
+
// reviewChannel, which outranks the local --ask flag).
|
|
341
343
|
reviewChannel: z.enum(["ask", "approver"]).optional(),
|
|
342
344
|
// When true, agents may call WEAKENING node9 MCP tools (shield_disable,
|
|
343
345
|
// approver_set). Default (unset/false): those tools refuse over MCP — a human
|
|
@@ -375,7 +377,8 @@ var init_config_schema = __esm({
|
|
|
375
377
|
dlp: z.object({
|
|
376
378
|
enabled: z.boolean().optional(),
|
|
377
379
|
scanIgnoredTools: z.boolean().optional(),
|
|
378
|
-
pii: z.enum(["off", "block"]).optional()
|
|
380
|
+
pii: z.enum(["off", "block"]).optional(),
|
|
381
|
+
reviewAction: z.enum(["review", "block"]).optional()
|
|
379
382
|
}).optional(),
|
|
380
383
|
egress: z.object({
|
|
381
384
|
enabled: z.boolean().optional(),
|
|
@@ -1591,7 +1594,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
1591
1594
|
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
1592
1595
|
if (dlpMatch) {
|
|
1593
1596
|
return {
|
|
1594
|
-
|
|
1597
|
+
// reviewAction:'block' (inline-ask v2): the admin upgraded
|
|
1598
|
+
// review-severity matches to a hard block — every evaluatePolicy
|
|
1599
|
+
// caller (orchestrator, explain, gateway) must agree with the gate.
|
|
1600
|
+
decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
|
|
1595
1601
|
blockedByLabel: `DLP: ${dlpMatch.patternName}`,
|
|
1596
1602
|
reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
|
|
1597
1603
|
};
|
|
@@ -4370,6 +4376,14 @@ function applyManagedDlp(local, managed, locked) {
|
|
|
4370
4376
|
locked.includes("dlpPii")
|
|
4371
4377
|
);
|
|
4372
4378
|
}
|
|
4379
|
+
if (typeof managed.reviewAction === "string") {
|
|
4380
|
+
next.reviewAction = resolveByOrder(
|
|
4381
|
+
DLP_REVIEW_ACTION_ORDER,
|
|
4382
|
+
local.reviewAction ?? "review",
|
|
4383
|
+
managed.reviewAction,
|
|
4384
|
+
locked.includes("dlpReviewAction")
|
|
4385
|
+
);
|
|
4386
|
+
}
|
|
4373
4387
|
return next;
|
|
4374
4388
|
}
|
|
4375
4389
|
function applyManagedApprovers(local, managed) {
|
|
@@ -4381,13 +4395,14 @@ function applyManagedApprovers(local, managed) {
|
|
|
4381
4395
|
terminal: typeof managed.terminal === "boolean" ? managed.terminal : local.terminal
|
|
4382
4396
|
};
|
|
4383
4397
|
}
|
|
4384
|
-
var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER;
|
|
4398
|
+
var MODE_ORDER, EGRESS_MODE_ORDER, DLP_PII_ORDER, DLP_REVIEW_ACTION_ORDER;
|
|
4385
4399
|
var init_managed = __esm({
|
|
4386
4400
|
"src/config/managed.ts"() {
|
|
4387
4401
|
"use strict";
|
|
4388
4402
|
MODE_ORDER = ["observe", "audit", "standard", "strict"];
|
|
4389
4403
|
EGRESS_MODE_ORDER = ["off", "review", "block"];
|
|
4390
4404
|
DLP_PII_ORDER = ["off", "block"];
|
|
4405
|
+
DLP_REVIEW_ACTION_ORDER = ["review", "block"];
|
|
4391
4406
|
}
|
|
4392
4407
|
});
|
|
4393
4408
|
|
|
@@ -4765,6 +4780,7 @@ function getConfig(cwd) {
|
|
|
4765
4780
|
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
4766
4781
|
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
4767
4782
|
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
4783
|
+
if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
|
|
4768
4784
|
}
|
|
4769
4785
|
if (p.egress) {
|
|
4770
4786
|
const e = p.egress;
|
|
@@ -4861,7 +4877,8 @@ function getConfig(cwd) {
|
|
|
4861
4877
|
mergedPolicy.dlp,
|
|
4862
4878
|
{
|
|
4863
4879
|
enabled: typeof mc.dlp.enabled === "boolean" ? mc.dlp.enabled : void 0,
|
|
4864
|
-
pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0
|
|
4880
|
+
pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0,
|
|
4881
|
+
reviewAction: mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block" ? mc.dlp.reviewAction : void 0
|
|
4865
4882
|
},
|
|
4866
4883
|
locked
|
|
4867
4884
|
);
|
|
@@ -4877,6 +4894,7 @@ function getConfig(cwd) {
|
|
|
4877
4894
|
}
|
|
4878
4895
|
if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
|
|
4879
4896
|
mergedSettings.reviewChannel = mc.reviewChannel;
|
|
4897
|
+
mergedSettings.reviewChannelManaged = true;
|
|
4880
4898
|
}
|
|
4881
4899
|
if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
|
|
4882
4900
|
mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
|
|
@@ -5621,13 +5639,14 @@ async function deriveExplainTrace(toolName, args) {
|
|
|
5621
5639
|
const filePathE = String(argsObjE.file_path ?? argsObjE.path ?? argsObjE.filename ?? "");
|
|
5622
5640
|
const dlpMatch = (filePathE ? scanFilePath(filePathE) : null) ?? (args !== void 0 ? scanArgs(args) : null);
|
|
5623
5641
|
if (dlpMatch) {
|
|
5642
|
+
const dlpBlocks = dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block";
|
|
5624
5643
|
steps.push({
|
|
5625
5644
|
name: "DLP Content Scanner",
|
|
5626
|
-
outcome:
|
|
5645
|
+
outcome: dlpBlocks ? "block" : "review",
|
|
5627
5646
|
detail: `\u{1F6A8} ${dlpMatch.patternName} detected in ${dlpMatch.fieldPath} \u2014 sample: ${dlpMatch.redactedSample}`,
|
|
5628
|
-
isFinal:
|
|
5647
|
+
isFinal: dlpBlocks
|
|
5629
5648
|
});
|
|
5630
|
-
if (
|
|
5649
|
+
if (dlpBlocks) {
|
|
5631
5650
|
return { tool: toolName, args, waterfall, steps, decision: "block" };
|
|
5632
5651
|
}
|
|
5633
5652
|
} else {
|
|
@@ -7122,7 +7141,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
7122
7141
|
const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
|
|
7123
7142
|
if (dlpMatch) {
|
|
7124
7143
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
7125
|
-
if (dlpMatch.severity === "block") {
|
|
7144
|
+
if (dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block") {
|
|
7126
7145
|
let dlpEid;
|
|
7127
7146
|
if (!isManual)
|
|
7128
7147
|
dlpEid = appendLocalAudit(
|
|
@@ -7460,12 +7479,14 @@ ${appPermReview}`
|
|
|
7460
7479
|
);
|
|
7461
7480
|
}
|
|
7462
7481
|
}
|
|
7463
|
-
|
|
7464
|
-
if (options?.deferReview && !taintWarning && !appPermReview && !cloudEnforcedForDefer) {
|
|
7482
|
+
if (options?.deferReview && !hardBlockDowngraded) {
|
|
7465
7483
|
return {
|
|
7466
7484
|
approved: false,
|
|
7467
7485
|
review: true,
|
|
7468
|
-
|
|
7486
|
+
// The prompt must say WHY: the taint sentence beats the bare label so
|
|
7487
|
+
// the dev sees the actual risk context inline. (No appPermReview term:
|
|
7488
|
+
// deferReview and serverKey never co-occur in production — see above.)
|
|
7489
|
+
reason: taintWarning || explainableLabel || "Node9 flagged this action for review.",
|
|
7469
7490
|
ruleDescription: policyRuleDescription,
|
|
7470
7491
|
blockedByLabel: explainableLabel
|
|
7471
7492
|
};
|
|
@@ -8412,7 +8433,7 @@ function printInlineAskNotice() {
|
|
|
8412
8433
|
);
|
|
8413
8434
|
console.log(
|
|
8414
8435
|
chalk.gray(
|
|
8415
|
-
' Prefer node9\u2019s own approver? Set "reviewChannel": "approver" in config, or add --no-ask to the hook.\n (Inline
|
|
8436
|
+
' Prefer node9\u2019s own approver? Set "reviewChannel": "approver" in config, or add --no-ask to the hook.\n (Inline is the default for every review; with a cloud workspace the outcome ships to the dashboard as audit.)'
|
|
8416
8437
|
)
|
|
8417
8438
|
);
|
|
8418
8439
|
}
|
|
@@ -18351,7 +18372,9 @@ function extractManagedConfig(body) {
|
|
|
18351
18372
|
const d = {};
|
|
18352
18373
|
if (typeof mc.dlp.enabled === "boolean") d.enabled = mc.dlp.enabled;
|
|
18353
18374
|
if (typeof mc.dlp.pii === "string") d.pii = mc.dlp.pii;
|
|
18354
|
-
if (
|
|
18375
|
+
if (mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block")
|
|
18376
|
+
d.reviewAction = mc.dlp.reviewAction;
|
|
18377
|
+
if (d.enabled !== void 0 || d.pii !== void 0 || d.reviewAction !== void 0) out.dlp = d;
|
|
18355
18378
|
}
|
|
18356
18379
|
if (mc.approvers && typeof mc.approvers === "object") {
|
|
18357
18380
|
const a = {};
|
|
@@ -19043,12 +19066,23 @@ function classifyDecision(a, b) {
|
|
|
19043
19066
|
function decisionTag(view) {
|
|
19044
19067
|
return `[${view.label}]`.padEnd(14);
|
|
19045
19068
|
}
|
|
19046
|
-
var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
|
|
19069
|
+
var HUMAN_SOURCES, TIMEOUT_SOURCES, NON_DECISION_SOURCES, has;
|
|
19047
19070
|
var init_decision = __esm({
|
|
19048
19071
|
"src/audit/decision.ts"() {
|
|
19049
19072
|
"use strict";
|
|
19050
|
-
HUMAN_SOURCES = /* @__PURE__ */ new Set([
|
|
19073
|
+
HUMAN_SOURCES = /* @__PURE__ */ new Set([
|
|
19074
|
+
"daemon",
|
|
19075
|
+
"cloud",
|
|
19076
|
+
"local-decision",
|
|
19077
|
+
"inline-review-approved",
|
|
19078
|
+
"inline-review"
|
|
19079
|
+
]);
|
|
19051
19080
|
TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
|
|
19081
|
+
NON_DECISION_SOURCES = /* @__PURE__ */ new Set([
|
|
19082
|
+
"post-hook",
|
|
19083
|
+
"inline-review-approved",
|
|
19084
|
+
"response-dlp"
|
|
19085
|
+
]);
|
|
19052
19086
|
has = (s, needle) => s.includes(needle);
|
|
19053
19087
|
}
|
|
19054
19088
|
});
|
|
@@ -19631,7 +19665,7 @@ function buildDaemonReport(allEntries, period, now) {
|
|
|
19631
19665
|
else if (period === "30d") start.setDate(start.getDate() - 29);
|
|
19632
19666
|
else if (period === "month") start = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
19633
19667
|
const entries = allEntries.filter((e) => {
|
|
19634
|
-
if (e.source === "
|
|
19668
|
+
if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
|
|
19635
19669
|
return new Date(e.ts) >= start;
|
|
19636
19670
|
});
|
|
19637
19671
|
const isBlocked = (e) => classifyDecision(e).outcome === "deny";
|
|
@@ -47372,8 +47406,8 @@ INSTRUCTIONS:
|
|
|
47372
47406
|
- Do NOT retry this exact command or attempt to bypass the rule.${recovery}
|
|
47373
47407
|
- Inform the user which security rule was triggered and ask how to proceed.`;
|
|
47374
47408
|
}
|
|
47375
|
-
function buildReviewMessage(blockedByLabel, ruleDescription) {
|
|
47376
|
-
const why = ruleDescription || blockedByLabel || "this action needs your review";
|
|
47409
|
+
function buildReviewMessage(blockedByLabel, ruleDescription, reason) {
|
|
47410
|
+
const why = ruleDescription || (reason && reason !== blockedByLabel ? reason : void 0) || blockedByLabel || "this action needs your review";
|
|
47377
47411
|
return `Node9 flagged this for your review: ${why}. Approve to proceed, or deny to cancel.`;
|
|
47378
47412
|
}
|
|
47379
47413
|
|
|
@@ -48114,6 +48148,7 @@ import path46 from "path";
|
|
|
48114
48148
|
function storePath() {
|
|
48115
48149
|
return process.env.NODE9_PENDING_STORE || path46.join(os43.homedir(), ".node9", "pending-reviews.json");
|
|
48116
48150
|
}
|
|
48151
|
+
var TTL_TUID_MS = 72 * 60 * 60 * 1e3;
|
|
48117
48152
|
var TTL_MS2 = 6 * 60 * 60 * 1e3;
|
|
48118
48153
|
var MAX_ENTRIES = 500;
|
|
48119
48154
|
function reviewCorrelationKey(payload) {
|
|
@@ -48147,7 +48182,9 @@ function write(store) {
|
|
|
48147
48182
|
}
|
|
48148
48183
|
}
|
|
48149
48184
|
function prune(entries, now) {
|
|
48150
|
-
const fresh = entries.filter(
|
|
48185
|
+
const fresh = entries.filter(
|
|
48186
|
+
(e) => now - e.ts < (e.key.startsWith("tuid:") ? TTL_TUID_MS : TTL_MS2)
|
|
48187
|
+
);
|
|
48151
48188
|
return fresh.length > MAX_ENTRIES ? fresh.slice(fresh.length - MAX_ENTRIES) : fresh;
|
|
48152
48189
|
}
|
|
48153
48190
|
function recordPendingReview(entry) {
|
|
@@ -48162,19 +48199,30 @@ function recordPendingReview(entry) {
|
|
|
48162
48199
|
function resolvePendingReview(key, now = Date.now()) {
|
|
48163
48200
|
try {
|
|
48164
48201
|
const store = read();
|
|
48165
|
-
const
|
|
48202
|
+
const live = prune(store.entries, now);
|
|
48203
|
+
const idx = live.findIndex((e) => e.key === key);
|
|
48166
48204
|
if (idx === -1) {
|
|
48167
|
-
|
|
48168
|
-
if (pruned.length !== store.entries.length) write({ entries: pruned });
|
|
48205
|
+
if (live.length !== store.entries.length) write({ entries: live });
|
|
48169
48206
|
return null;
|
|
48170
48207
|
}
|
|
48171
|
-
const [match] =
|
|
48172
|
-
write({ entries:
|
|
48208
|
+
const [match] = live.splice(idx, 1);
|
|
48209
|
+
write({ entries: live });
|
|
48173
48210
|
return match;
|
|
48174
48211
|
} catch {
|
|
48175
48212
|
return null;
|
|
48176
48213
|
}
|
|
48177
48214
|
}
|
|
48215
|
+
function discardPendingReview(key, now = Date.now()) {
|
|
48216
|
+
try {
|
|
48217
|
+
const store = read();
|
|
48218
|
+
const kept = prune(
|
|
48219
|
+
store.entries.filter((e) => e.key !== key),
|
|
48220
|
+
now
|
|
48221
|
+
);
|
|
48222
|
+
if (kept.length !== store.entries.length) write({ entries: kept });
|
|
48223
|
+
} catch {
|
|
48224
|
+
}
|
|
48225
|
+
}
|
|
48178
48226
|
|
|
48179
48227
|
// src/cli/commands/check.ts
|
|
48180
48228
|
init_hook_payload();
|
|
@@ -48230,7 +48278,10 @@ function agentSupportsAsk(agent) {
|
|
|
48230
48278
|
}
|
|
48231
48279
|
function resolveAskMode(agent, opts, config) {
|
|
48232
48280
|
if (!agentSupportsAsk(agent)) return false;
|
|
48233
|
-
if (config.settings.
|
|
48281
|
+
if (config.settings.reviewChannelManaged === true) {
|
|
48282
|
+
if (config.settings.reviewChannel === "ask") return true;
|
|
48283
|
+
if (config.settings.reviewChannel === "approver") return false;
|
|
48284
|
+
}
|
|
48234
48285
|
if (opts.ask === true) return true;
|
|
48235
48286
|
if (opts.ask === false) return false;
|
|
48236
48287
|
if (config.settings.reviewChannel === "ask") return true;
|
|
@@ -48469,7 +48520,11 @@ RAW: ${raw}
|
|
|
48469
48520
|
process.exit(2);
|
|
48470
48521
|
};
|
|
48471
48522
|
const sendAsk = (result2) => {
|
|
48472
|
-
const msg = buildReviewMessage(
|
|
48523
|
+
const msg = buildReviewMessage(
|
|
48524
|
+
result2.blockedByLabel,
|
|
48525
|
+
result2.ruleDescription,
|
|
48526
|
+
result2.reason
|
|
48527
|
+
);
|
|
48473
48528
|
try {
|
|
48474
48529
|
const key = reviewCorrelationKey(payload);
|
|
48475
48530
|
if (key) {
|
|
@@ -48652,6 +48707,13 @@ RAW: ${raw}
|
|
|
48652
48707
|
cwd: safeCwdForAuth,
|
|
48653
48708
|
deferReview: askMode
|
|
48654
48709
|
});
|
|
48710
|
+
if (!result.review) {
|
|
48711
|
+
try {
|
|
48712
|
+
const key = reviewCorrelationKey(payload);
|
|
48713
|
+
if (key) discardPendingReview(key);
|
|
48714
|
+
} catch {
|
|
48715
|
+
}
|
|
48716
|
+
}
|
|
48655
48717
|
if (result.approved) {
|
|
48656
48718
|
if (result.checkedBy && process.env.NODE9_DEBUG === "1")
|
|
48657
48719
|
process.stderr.write(`\u2713 node9 [${result.checkedBy}]: "${toolName}" allowed
|
|
@@ -48828,9 +48890,11 @@ function registerLogCommand(program2) {
|
|
|
48828
48890
|
})();
|
|
48829
48891
|
const agent = agentOverride !== void 0 ? agentOverride : metaTag !== void 0 ? metaTag : payload.turn_id !== void 0 ? "Codex" : payload.toolCall !== void 0 || payload.conversationId !== void 0 ? "Antigravity" : payload.hook_event_name === "pre_tool_call" || payload.hook_event_name === "post_tool_call" ? "Hermes" : payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0 ? "Claude Code" : payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0 ? "Gemini CLI" : process.env.HERMES_SESSION_ID || process.env.HERMES_HOME || process.env.HERMES_INTERACTIVE ? "Hermes" : process.env.ANTIGRAVITY_CONVERSATION_ID ? "Antigravity" : void 0;
|
|
48830
48892
|
let reviewApproved = false;
|
|
48893
|
+
let resolvedReview = null;
|
|
48831
48894
|
try {
|
|
48832
48895
|
const key = reviewCorrelationKey(payload);
|
|
48833
|
-
if (key
|
|
48896
|
+
if (key) resolvedReview = resolvePendingReview(key);
|
|
48897
|
+
if (resolvedReview) reviewApproved = true;
|
|
48834
48898
|
} catch {
|
|
48835
48899
|
}
|
|
48836
48900
|
const entry = {
|
|
@@ -48883,6 +48947,33 @@ function registerLogCommand(program2) {
|
|
|
48883
48947
|
const payloadCwd = typeof payload.cwd === "string" ? payload.cwd : Array.isArray(payload.workspacePaths) && typeof payload.workspacePaths[0] === "string" ? payload.workspacePaths[0] : void 0;
|
|
48884
48948
|
const safeCwd = typeof payloadCwd === "string" && path48.isAbsolute(payloadCwd) ? payloadCwd : void 0;
|
|
48885
48949
|
const config = getConfig(safeCwd);
|
|
48950
|
+
if (resolvedReview) {
|
|
48951
|
+
try {
|
|
48952
|
+
const reviewLabel = resolvedReview.label || "inline-review";
|
|
48953
|
+
const sensitiveReview = /dlp|taint/i.test(reviewLabel);
|
|
48954
|
+
appendLocalAudit(
|
|
48955
|
+
tool,
|
|
48956
|
+
rawInput,
|
|
48957
|
+
"allow",
|
|
48958
|
+
"inline-review",
|
|
48959
|
+
{
|
|
48960
|
+
agent,
|
|
48961
|
+
ruleName: reviewLabel,
|
|
48962
|
+
...rawToolName !== tool ? { agentToolName: rawToolName } : {},
|
|
48963
|
+
...typeof payloadSessionId === "string" ? { sessionId: payloadSessionId } : {},
|
|
48964
|
+
...safeCwd ? { workingDir: safeCwd } : {}
|
|
48965
|
+
},
|
|
48966
|
+
sensitiveReview || config.settings.auditHashArgs === true
|
|
48967
|
+
);
|
|
48968
|
+
} catch (err2) {
|
|
48969
|
+
appendToLog(HOOK_DEBUG_LOG, {
|
|
48970
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
48971
|
+
event: "inline-review-ship-row-fail",
|
|
48972
|
+
tool,
|
|
48973
|
+
error: err2 instanceof Error ? err2.message : String(err2)
|
|
48974
|
+
});
|
|
48975
|
+
}
|
|
48976
|
+
}
|
|
48886
48977
|
{
|
|
48887
48978
|
const toolOutput = payload.tool_response?.output;
|
|
48888
48979
|
const inj = config.policy.injectionScan;
|
|
@@ -50351,8 +50442,7 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
50351
50442
|
const priorEnd = new Date(start.getTime() - 1);
|
|
50352
50443
|
const priorStart = new Date(start.getTime() - periodMs);
|
|
50353
50444
|
const priorEntries = allEntries.filter((e) => {
|
|
50354
|
-
if (e.source === "
|
|
50355
|
-
if (e.source === "response-dlp") return false;
|
|
50445
|
+
if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
|
|
50356
50446
|
if (typeof e.decision !== "string") return false;
|
|
50357
50447
|
const ts = new Date(e.ts);
|
|
50358
50448
|
return ts >= priorStart && ts <= priorEnd;
|
|
@@ -50363,8 +50453,7 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
50363
50453
|
const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
|
|
50364
50454
|
let excludedTests = 0;
|
|
50365
50455
|
const entries = allEntries.filter((e) => {
|
|
50366
|
-
if (e.source === "
|
|
50367
|
-
if (e.source === "response-dlp") return false;
|
|
50456
|
+
if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
|
|
50368
50457
|
if (typeof e.decision !== "string") return false;
|
|
50369
50458
|
const ts = new Date(e.ts);
|
|
50370
50459
|
if (ts < start || ts > end) return false;
|
package/dist/dashboard.mjs
CHANGED
|
@@ -2292,8 +2292,10 @@ var init_config_schema = __esm({
|
|
|
2292
2292
|
agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
|
|
2293
2293
|
// Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
|
|
2294
2294
|
// inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
|
|
2295
|
-
// node9's own approver (terminal/native/cloud). Unset →
|
|
2296
|
-
//
|
|
2295
|
+
// node9's own approver (terminal/native/cloud). Unset → default ASK for
|
|
2296
|
+
// ask-capable agents (v2: cloud no longer disables inline — the outcome
|
|
2297
|
+
// ships to the dashboard as audit; admins force routing via managed
|
|
2298
|
+
// reviewChannel, which outranks the local --ask flag).
|
|
2297
2299
|
reviewChannel: z.enum(["ask", "approver"]).optional(),
|
|
2298
2300
|
// When true, agents may call WEAKENING node9 MCP tools (shield_disable,
|
|
2299
2301
|
// approver_set). Default (unset/false): those tools refuse over MCP — a human
|
|
@@ -2331,7 +2333,8 @@ var init_config_schema = __esm({
|
|
|
2331
2333
|
dlp: z.object({
|
|
2332
2334
|
enabled: z.boolean().optional(),
|
|
2333
2335
|
scanIgnoredTools: z.boolean().optional(),
|
|
2334
|
-
pii: z.enum(["off", "block"]).optional()
|
|
2336
|
+
pii: z.enum(["off", "block"]).optional(),
|
|
2337
|
+
reviewAction: z.enum(["review", "block"]).optional()
|
|
2335
2338
|
}).optional(),
|
|
2336
2339
|
egress: z.object({
|
|
2337
2340
|
enabled: z.boolean().optional(),
|
|
@@ -2960,17 +2963,6 @@ var init_costSync = __esm({
|
|
|
2960
2963
|
}
|
|
2961
2964
|
});
|
|
2962
2965
|
|
|
2963
|
-
// src/daemon/scan-watermark.ts
|
|
2964
|
-
var MAX_LINE_BYTES;
|
|
2965
|
-
var init_scan_watermark = __esm({
|
|
2966
|
-
"src/daemon/scan-watermark.ts"() {
|
|
2967
|
-
"use strict";
|
|
2968
|
-
init_dlp();
|
|
2969
|
-
init_dist();
|
|
2970
|
-
MAX_LINE_BYTES = 2 * 1024 * 1024;
|
|
2971
|
-
}
|
|
2972
|
-
});
|
|
2973
|
-
|
|
2974
2966
|
// src/audit/decision.ts
|
|
2975
2967
|
function classifyDecision(a, b) {
|
|
2976
2968
|
const isRow = !!a && typeof a === "object";
|
|
@@ -3005,16 +2997,38 @@ function classifyDecision(a, b) {
|
|
|
3005
2997
|
}
|
|
3006
2998
|
return { outcome: "unknown", label: raw ? `? ${raw}` : "? (none)", raw };
|
|
3007
2999
|
}
|
|
3008
|
-
var HUMAN_SOURCES, TIMEOUT_SOURCES, has;
|
|
3000
|
+
var HUMAN_SOURCES, TIMEOUT_SOURCES, NON_DECISION_SOURCES, has;
|
|
3009
3001
|
var init_decision = __esm({
|
|
3010
3002
|
"src/audit/decision.ts"() {
|
|
3011
3003
|
"use strict";
|
|
3012
|
-
HUMAN_SOURCES = /* @__PURE__ */ new Set([
|
|
3004
|
+
HUMAN_SOURCES = /* @__PURE__ */ new Set([
|
|
3005
|
+
"daemon",
|
|
3006
|
+
"cloud",
|
|
3007
|
+
"local-decision",
|
|
3008
|
+
"inline-review-approved",
|
|
3009
|
+
"inline-review"
|
|
3010
|
+
]);
|
|
3013
3011
|
TIMEOUT_SOURCES = /* @__PURE__ */ new Set(["timeout"]);
|
|
3012
|
+
NON_DECISION_SOURCES = /* @__PURE__ */ new Set([
|
|
3013
|
+
"post-hook",
|
|
3014
|
+
"inline-review-approved",
|
|
3015
|
+
"response-dlp"
|
|
3016
|
+
]);
|
|
3014
3017
|
has = (s, needle) => s.includes(needle);
|
|
3015
3018
|
}
|
|
3016
3019
|
});
|
|
3017
3020
|
|
|
3021
|
+
// src/daemon/scan-watermark.ts
|
|
3022
|
+
var MAX_LINE_BYTES;
|
|
3023
|
+
var init_scan_watermark = __esm({
|
|
3024
|
+
"src/daemon/scan-watermark.ts"() {
|
|
3025
|
+
"use strict";
|
|
3026
|
+
init_dlp();
|
|
3027
|
+
init_dist();
|
|
3028
|
+
MAX_LINE_BYTES = 2 * 1024 * 1024;
|
|
3029
|
+
}
|
|
3030
|
+
});
|
|
3031
|
+
|
|
3018
3032
|
// src/cli/aggregate/report-audit.ts
|
|
3019
3033
|
import fs5 from "fs";
|
|
3020
3034
|
import os6 from "os";
|
|
@@ -3574,8 +3588,7 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
3574
3588
|
const priorEnd = new Date(start.getTime() - 1);
|
|
3575
3589
|
const priorStart = new Date(start.getTime() - periodMs);
|
|
3576
3590
|
const priorEntries = allEntries.filter((e) => {
|
|
3577
|
-
if (e.source === "
|
|
3578
|
-
if (e.source === "response-dlp") return false;
|
|
3591
|
+
if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
|
|
3579
3592
|
if (typeof e.decision !== "string") return false;
|
|
3580
3593
|
const ts = new Date(e.ts);
|
|
3581
3594
|
return ts >= priorStart && ts <= priorEnd;
|
|
@@ -3586,8 +3599,7 @@ function aggregateReportFromAudit(period, opts = {}) {
|
|
|
3586
3599
|
const testTs = excludeTests ? buildTestTimestamps(allEntries) : /* @__PURE__ */ new Set();
|
|
3587
3600
|
let excludedTests = 0;
|
|
3588
3601
|
const entries = allEntries.filter((e) => {
|
|
3589
|
-
if (e.source === "
|
|
3590
|
-
if (e.source === "response-dlp") return false;
|
|
3602
|
+
if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
|
|
3591
3603
|
if (typeof e.decision !== "string") return false;
|
|
3592
3604
|
const ts = new Date(e.ts);
|
|
3593
3605
|
if (ts < start || ts > end) return false;
|
|
@@ -4915,7 +4927,7 @@ function readAuditEntriesAsync(chunkSize = 1e3, customPath) {
|
|
|
4915
4927
|
}
|
|
4916
4928
|
function aggregateAudit(entries, startMs, endMs = Date.now()) {
|
|
4917
4929
|
const inWindow = entries.filter((e) => {
|
|
4918
|
-
if (e.source === "
|
|
4930
|
+
if (typeof e.source === "string" && NON_DECISION_SOURCES.has(e.source)) return false;
|
|
4919
4931
|
const t = Date.parse(e.ts);
|
|
4920
4932
|
return t >= startMs && t <= endMs;
|
|
4921
4933
|
});
|
|
@@ -5361,6 +5373,7 @@ var init_data = __esm({
|
|
|
5361
5373
|
init_daemon();
|
|
5362
5374
|
init_costSync();
|
|
5363
5375
|
init_shields();
|
|
5376
|
+
init_decision();
|
|
5364
5377
|
init_scan_watermark();
|
|
5365
5378
|
init_report_audit();
|
|
5366
5379
|
init_litellm();
|
package/dist/index.js
CHANGED
|
@@ -91,7 +91,7 @@ function redactSecrets(text) {
|
|
|
91
91
|
if (!text) return text;
|
|
92
92
|
let redacted = text;
|
|
93
93
|
redacted = redacted.replace(
|
|
94
|
-
/(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._
|
|
94
|
+
/(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/=]+/gi,
|
|
95
95
|
"$1********"
|
|
96
96
|
);
|
|
97
97
|
redacted = redacted.replace(
|
|
@@ -152,7 +152,7 @@ function filePathFromArgs(args) {
|
|
|
152
152
|
return void 0;
|
|
153
153
|
}
|
|
154
154
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
155
|
-
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
|
|
155
|
+
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern) || /dlp|taint/i.test(String(meta?.ruleName ?? ""));
|
|
156
156
|
const preview = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
|
|
157
157
|
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args), ...preview ? { argsPreview: preview } : {} } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
158
158
|
const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
|
|
@@ -314,8 +314,10 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
314
314
|
agentPolicy: import_zod.z.enum(["require_approval", "block_on_rules"]).optional(),
|
|
315
315
|
// Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
|
|
316
316
|
// inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
|
|
317
|
-
// node9's own approver (terminal/native/cloud). Unset →
|
|
318
|
-
//
|
|
317
|
+
// node9's own approver (terminal/native/cloud). Unset → default ASK for
|
|
318
|
+
// ask-capable agents (v2: cloud no longer disables inline — the outcome
|
|
319
|
+
// ships to the dashboard as audit; admins force routing via managed
|
|
320
|
+
// reviewChannel, which outranks the local --ask flag).
|
|
319
321
|
reviewChannel: import_zod.z.enum(["ask", "approver"]).optional(),
|
|
320
322
|
// When true, agents may call WEAKENING node9 MCP tools (shield_disable,
|
|
321
323
|
// approver_set). Default (unset/false): those tools refuse over MCP — a human
|
|
@@ -353,7 +355,8 @@ var ConfigFileSchema = import_zod.z.object({
|
|
|
353
355
|
dlp: import_zod.z.object({
|
|
354
356
|
enabled: import_zod.z.boolean().optional(),
|
|
355
357
|
scanIgnoredTools: import_zod.z.boolean().optional(),
|
|
356
|
-
pii: import_zod.z.enum(["off", "block"]).optional()
|
|
358
|
+
pii: import_zod.z.enum(["off", "block"]).optional(),
|
|
359
|
+
reviewAction: import_zod.z.enum(["review", "block"]).optional()
|
|
357
360
|
}).optional(),
|
|
358
361
|
egress: import_zod.z.object({
|
|
359
362
|
enabled: import_zod.z.boolean().optional(),
|
|
@@ -2442,7 +2445,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2442
2445
|
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
2443
2446
|
if (dlpMatch) {
|
|
2444
2447
|
return {
|
|
2445
|
-
|
|
2448
|
+
// reviewAction:'block' (inline-ask v2): the admin upgraded
|
|
2449
|
+
// review-severity matches to a hard block — every evaluatePolicy
|
|
2450
|
+
// caller (orchestrator, explain, gateway) must agree with the gate.
|
|
2451
|
+
decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
|
|
2446
2452
|
blockedByLabel: `DLP: ${dlpMatch.patternName}`,
|
|
2447
2453
|
reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
|
|
2448
2454
|
};
|
|
@@ -3638,6 +3644,7 @@ function applyManagedEgress(local, managed, locked) {
|
|
|
3638
3644
|
return next;
|
|
3639
3645
|
}
|
|
3640
3646
|
var DLP_PII_ORDER = ["off", "block"];
|
|
3647
|
+
var DLP_REVIEW_ACTION_ORDER = ["review", "block"];
|
|
3641
3648
|
function applyManagedDlp(local, managed, locked) {
|
|
3642
3649
|
const next = { ...local };
|
|
3643
3650
|
if (typeof managed.enabled === "boolean") {
|
|
@@ -3651,6 +3658,14 @@ function applyManagedDlp(local, managed, locked) {
|
|
|
3651
3658
|
locked.includes("dlpPii")
|
|
3652
3659
|
);
|
|
3653
3660
|
}
|
|
3661
|
+
if (typeof managed.reviewAction === "string") {
|
|
3662
|
+
next.reviewAction = resolveByOrder(
|
|
3663
|
+
DLP_REVIEW_ACTION_ORDER,
|
|
3664
|
+
local.reviewAction ?? "review",
|
|
3665
|
+
managed.reviewAction,
|
|
3666
|
+
locked.includes("dlpReviewAction")
|
|
3667
|
+
);
|
|
3668
|
+
}
|
|
3654
3669
|
return next;
|
|
3655
3670
|
}
|
|
3656
3671
|
function applyManagedApprovers(local, managed) {
|
|
@@ -4221,6 +4236,7 @@ function getConfig(cwd) {
|
|
|
4221
4236
|
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
4222
4237
|
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
4223
4238
|
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
4239
|
+
if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
|
|
4224
4240
|
}
|
|
4225
4241
|
if (p.egress) {
|
|
4226
4242
|
const e = p.egress;
|
|
@@ -4317,7 +4333,8 @@ function getConfig(cwd) {
|
|
|
4317
4333
|
mergedPolicy.dlp,
|
|
4318
4334
|
{
|
|
4319
4335
|
enabled: typeof mc.dlp.enabled === "boolean" ? mc.dlp.enabled : void 0,
|
|
4320
|
-
pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0
|
|
4336
|
+
pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0,
|
|
4337
|
+
reviewAction: mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block" ? mc.dlp.reviewAction : void 0
|
|
4321
4338
|
},
|
|
4322
4339
|
locked
|
|
4323
4340
|
);
|
|
@@ -4333,6 +4350,7 @@ function getConfig(cwd) {
|
|
|
4333
4350
|
}
|
|
4334
4351
|
if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
|
|
4335
4352
|
mergedSettings.reviewChannel = mc.reviewChannel;
|
|
4353
|
+
mergedSettings.reviewChannelManaged = true;
|
|
4336
4354
|
}
|
|
4337
4355
|
if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
|
|
4338
4356
|
mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
|
|
@@ -5691,7 +5709,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5691
5709
|
const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
|
|
5692
5710
|
if (dlpMatch) {
|
|
5693
5711
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
5694
|
-
if (dlpMatch.severity === "block") {
|
|
5712
|
+
if (dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block") {
|
|
5695
5713
|
let dlpEid;
|
|
5696
5714
|
if (!isManual)
|
|
5697
5715
|
dlpEid = appendLocalAudit(
|
|
@@ -6029,12 +6047,14 @@ ${appPermReview}`
|
|
|
6029
6047
|
);
|
|
6030
6048
|
}
|
|
6031
6049
|
}
|
|
6032
|
-
|
|
6033
|
-
if (options?.deferReview && !taintWarning && !appPermReview && !cloudEnforcedForDefer) {
|
|
6050
|
+
if (options?.deferReview && !hardBlockDowngraded) {
|
|
6034
6051
|
return {
|
|
6035
6052
|
approved: false,
|
|
6036
6053
|
review: true,
|
|
6037
|
-
|
|
6054
|
+
// The prompt must say WHY: the taint sentence beats the bare label so
|
|
6055
|
+
// the dev sees the actual risk context inline. (No appPermReview term:
|
|
6056
|
+
// deferReview and serverKey never co-occur in production — see above.)
|
|
6057
|
+
reason: taintWarning || explainableLabel || "Node9 flagged this action for review.",
|
|
6038
6058
|
ruleDescription: policyRuleDescription,
|
|
6039
6059
|
blockedByLabel: explainableLabel
|
|
6040
6060
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -72,7 +72,7 @@ function redactSecrets(text) {
|
|
|
72
72
|
if (!text) return text;
|
|
73
73
|
let redacted = text;
|
|
74
74
|
redacted = redacted.replace(
|
|
75
|
-
/(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._
|
|
75
|
+
/(authorization:\s*(?:bearer|basic)\s+)[a-zA-Z0-9._\-\/=]+/gi,
|
|
76
76
|
"$1********"
|
|
77
77
|
);
|
|
78
78
|
redacted = redacted.replace(
|
|
@@ -133,7 +133,7 @@ function filePathFromArgs(args) {
|
|
|
133
133
|
return void 0;
|
|
134
134
|
}
|
|
135
135
|
function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
|
|
136
|
-
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
|
|
136
|
+
const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern) || /dlp|taint/i.test(String(meta?.ruleName ?? ""));
|
|
137
137
|
const preview = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
|
|
138
138
|
const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args), ...preview ? { argsPreview: preview } : {} } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
|
|
139
139
|
const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
|
|
@@ -284,8 +284,10 @@ var ConfigFileSchema = z.object({
|
|
|
284
284
|
agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
|
|
285
285
|
// Where a `review` verdict's prompt is rendered: 'ask' = the agent's own
|
|
286
286
|
// inline approve/deny prompt (Claude Code / GitHub Copilot); 'approver' =
|
|
287
|
-
// node9's own approver (terminal/native/cloud). Unset →
|
|
288
|
-
//
|
|
287
|
+
// node9's own approver (terminal/native/cloud). Unset → default ASK for
|
|
288
|
+
// ask-capable agents (v2: cloud no longer disables inline — the outcome
|
|
289
|
+
// ships to the dashboard as audit; admins force routing via managed
|
|
290
|
+
// reviewChannel, which outranks the local --ask flag).
|
|
289
291
|
reviewChannel: z.enum(["ask", "approver"]).optional(),
|
|
290
292
|
// When true, agents may call WEAKENING node9 MCP tools (shield_disable,
|
|
291
293
|
// approver_set). Default (unset/false): those tools refuse over MCP — a human
|
|
@@ -323,7 +325,8 @@ var ConfigFileSchema = z.object({
|
|
|
323
325
|
dlp: z.object({
|
|
324
326
|
enabled: z.boolean().optional(),
|
|
325
327
|
scanIgnoredTools: z.boolean().optional(),
|
|
326
|
-
pii: z.enum(["off", "block"]).optional()
|
|
328
|
+
pii: z.enum(["off", "block"]).optional(),
|
|
329
|
+
reviewAction: z.enum(["review", "block"]).optional()
|
|
327
330
|
}).optional(),
|
|
328
331
|
egress: z.object({
|
|
329
332
|
enabled: z.boolean().optional(),
|
|
@@ -2412,7 +2415,10 @@ async function evaluatePolicy(config, toolName, args, context = {}, hooks = {})
|
|
|
2412
2415
|
const dlpMatch = args !== void 0 ? scanArgs(args) : null;
|
|
2413
2416
|
if (dlpMatch) {
|
|
2414
2417
|
return {
|
|
2415
|
-
|
|
2418
|
+
// reviewAction:'block' (inline-ask v2): the admin upgraded
|
|
2419
|
+
// review-severity matches to a hard block — every evaluatePolicy
|
|
2420
|
+
// caller (orchestrator, explain, gateway) must agree with the gate.
|
|
2421
|
+
decision: dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block" ? "block" : "review",
|
|
2416
2422
|
blockedByLabel: `DLP: ${dlpMatch.patternName}`,
|
|
2417
2423
|
reason: `${dlpMatch.patternName} detected in ${dlpMatch.fieldPath}`
|
|
2418
2424
|
};
|
|
@@ -3608,6 +3614,7 @@ function applyManagedEgress(local, managed, locked) {
|
|
|
3608
3614
|
return next;
|
|
3609
3615
|
}
|
|
3610
3616
|
var DLP_PII_ORDER = ["off", "block"];
|
|
3617
|
+
var DLP_REVIEW_ACTION_ORDER = ["review", "block"];
|
|
3611
3618
|
function applyManagedDlp(local, managed, locked) {
|
|
3612
3619
|
const next = { ...local };
|
|
3613
3620
|
if (typeof managed.enabled === "boolean") {
|
|
@@ -3621,6 +3628,14 @@ function applyManagedDlp(local, managed, locked) {
|
|
|
3621
3628
|
locked.includes("dlpPii")
|
|
3622
3629
|
);
|
|
3623
3630
|
}
|
|
3631
|
+
if (typeof managed.reviewAction === "string") {
|
|
3632
|
+
next.reviewAction = resolveByOrder(
|
|
3633
|
+
DLP_REVIEW_ACTION_ORDER,
|
|
3634
|
+
local.reviewAction ?? "review",
|
|
3635
|
+
managed.reviewAction,
|
|
3636
|
+
locked.includes("dlpReviewAction")
|
|
3637
|
+
);
|
|
3638
|
+
}
|
|
3624
3639
|
return next;
|
|
3625
3640
|
}
|
|
3626
3641
|
function applyManagedApprovers(local, managed) {
|
|
@@ -4191,6 +4206,7 @@ function getConfig(cwd) {
|
|
|
4191
4206
|
if (d.enabled !== void 0) mergedPolicy.dlp.enabled = d.enabled;
|
|
4192
4207
|
if (d.scanIgnoredTools !== void 0) mergedPolicy.dlp.scanIgnoredTools = d.scanIgnoredTools;
|
|
4193
4208
|
if (d.pii !== void 0) mergedPolicy.dlp.pii = d.pii;
|
|
4209
|
+
if (d.reviewAction !== void 0) mergedPolicy.dlp.reviewAction = d.reviewAction;
|
|
4194
4210
|
}
|
|
4195
4211
|
if (p.egress) {
|
|
4196
4212
|
const e = p.egress;
|
|
@@ -4287,7 +4303,8 @@ function getConfig(cwd) {
|
|
|
4287
4303
|
mergedPolicy.dlp,
|
|
4288
4304
|
{
|
|
4289
4305
|
enabled: typeof mc.dlp.enabled === "boolean" ? mc.dlp.enabled : void 0,
|
|
4290
|
-
pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0
|
|
4306
|
+
pii: typeof mc.dlp.pii === "string" ? mc.dlp.pii : void 0,
|
|
4307
|
+
reviewAction: mc.dlp.reviewAction === "review" || mc.dlp.reviewAction === "block" ? mc.dlp.reviewAction : void 0
|
|
4291
4308
|
},
|
|
4292
4309
|
locked
|
|
4293
4310
|
);
|
|
@@ -4303,6 +4320,7 @@ function getConfig(cwd) {
|
|
|
4303
4320
|
}
|
|
4304
4321
|
if (mc.reviewChannel === "ask" || mc.reviewChannel === "approver") {
|
|
4305
4322
|
mergedSettings.reviewChannel = mc.reviewChannel;
|
|
4323
|
+
mergedSettings.reviewChannelManaged = true;
|
|
4306
4324
|
}
|
|
4307
4325
|
if (typeof mc.approvalTimeoutMs === "number" && mc.approvalTimeoutMs > 0) {
|
|
4308
4326
|
mergedSettings.approvalTimeoutMs = mc.approvalTimeoutMs;
|
|
@@ -5661,7 +5679,7 @@ async function _authorizeHeadlessCore(toolName, args, metaArg, options) {
|
|
|
5661
5679
|
const dlpMatch = (filePath ? scanFilePath(filePath) : null) ?? scanArgs(args);
|
|
5662
5680
|
if (dlpMatch) {
|
|
5663
5681
|
const dlpReason = `\u{1F6A8} DATA LOSS PREVENTION: ${dlpMatch.patternName} detected in field "${dlpMatch.fieldPath}" (${dlpMatch.redactedSample})`;
|
|
5664
|
-
if (dlpMatch.severity === "block") {
|
|
5682
|
+
if (dlpMatch.severity === "block" || config.policy.dlp.reviewAction === "block") {
|
|
5665
5683
|
let dlpEid;
|
|
5666
5684
|
if (!isManual)
|
|
5667
5685
|
dlpEid = appendLocalAudit(
|
|
@@ -5999,12 +6017,14 @@ ${appPermReview}`
|
|
|
5999
6017
|
);
|
|
6000
6018
|
}
|
|
6001
6019
|
}
|
|
6002
|
-
|
|
6003
|
-
if (options?.deferReview && !taintWarning && !appPermReview && !cloudEnforcedForDefer) {
|
|
6020
|
+
if (options?.deferReview && !hardBlockDowngraded) {
|
|
6004
6021
|
return {
|
|
6005
6022
|
approved: false,
|
|
6006
6023
|
review: true,
|
|
6007
|
-
|
|
6024
|
+
// The prompt must say WHY: the taint sentence beats the bare label so
|
|
6025
|
+
// the dev sees the actual risk context inline. (No appPermReview term:
|
|
6026
|
+
// deferReview and serverKey never co-occur in production — see above.)
|
|
6027
|
+
reason: taintWarning || explainableLabel || "Node9 flagged this action for review.",
|
|
6008
6028
|
ruleDescription: policyRuleDescription,
|
|
6009
6029
|
blockedByLabel: explainableLabel
|
|
6010
6030
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node9/proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.66.0",
|
|
4
4
|
"description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|