@node9/proxy 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -39
- package/dist/cli.js +3801 -6608
- package/dist/cli.mjs +3751 -6557
- package/dist/index.js +99 -29
- package/dist/index.mjs +99 -29
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -110,6 +110,7 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
|
110
110
|
...argsField,
|
|
111
111
|
agent: meta?.agent,
|
|
112
112
|
mcpServer: meta?.mcpServer,
|
|
113
|
+
sessionId: meta?.sessionId,
|
|
113
114
|
hostname: import_os.default.hostname(),
|
|
114
115
|
cwd: process.cwd()
|
|
115
116
|
});
|
|
@@ -2088,13 +2089,6 @@ var k8s_default = {
|
|
|
2088
2089
|
],
|
|
2089
2090
|
dangerousWords: []
|
|
2090
2091
|
};
|
|
2091
|
-
var mcp_tool_gating_default = {
|
|
2092
|
-
name: "mcp-tool-gating",
|
|
2093
|
-
description: "Intercept MCP tool lists and require user approval before the agent can use any tools from a new server",
|
|
2094
|
-
aliases: ["mcp-gating", "mcp-tools"],
|
|
2095
|
-
smartRules: [],
|
|
2096
|
-
dangerousWords: []
|
|
2097
|
-
};
|
|
2098
2092
|
var mongodb_default = {
|
|
2099
2093
|
name: "mongodb",
|
|
2100
2094
|
description: "Protects MongoDB databases from destructive AI operations",
|
|
@@ -2400,7 +2394,6 @@ var BUILTIN_SHIELDS = {
|
|
|
2400
2394
|
[filesystem_default.name]: filesystem_default,
|
|
2401
2395
|
[github_default.name]: github_default,
|
|
2402
2396
|
[k8s_default.name]: k8s_default,
|
|
2403
|
-
[mcp_tool_gating_default.name]: mcp_tool_gating_default,
|
|
2404
2397
|
[mongodb_default.name]: mongodb_default,
|
|
2405
2398
|
[postgres_default.name]: postgres_default,
|
|
2406
2399
|
[project_jail_default.name]: project_jail_default,
|
|
@@ -2929,6 +2922,12 @@ function getConfig(cwd) {
|
|
|
2929
2922
|
if (Array.isArray(raw.rules) && raw.rules.length > 0) {
|
|
2930
2923
|
applyLayer({ policy: { smartRules: raw.rules } });
|
|
2931
2924
|
}
|
|
2925
|
+
if (raw.panicMode === true) {
|
|
2926
|
+
mergedSettings.panicMode = true;
|
|
2927
|
+
}
|
|
2928
|
+
if (raw.shadowMode === true) {
|
|
2929
|
+
mergedSettings.mode = "observe";
|
|
2930
|
+
}
|
|
2932
2931
|
} catch {
|
|
2933
2932
|
}
|
|
2934
2933
|
}
|
|
@@ -3866,6 +3865,12 @@ var KNOWN_CHECKED_BY = /* @__PURE__ */ new Set([
|
|
|
3866
3865
|
"audit-mode",
|
|
3867
3866
|
"local-policy",
|
|
3868
3867
|
"smart-rule-block",
|
|
3868
|
+
// Smart-rule block was downgraded to review because the daemon was
|
|
3869
|
+
// running and we're not in CI. The block attempt is still recorded;
|
|
3870
|
+
// the user got a popup. Distinct from 'smart-rule-block' so the
|
|
3871
|
+
// dashboard can show "block rule overridden" separately from a hard
|
|
3872
|
+
// block that fired with no human in the loop.
|
|
3873
|
+
"smart-rule-block-override",
|
|
3869
3874
|
"persistent",
|
|
3870
3875
|
"trust",
|
|
3871
3876
|
"observe-mode",
|
|
@@ -3886,7 +3891,7 @@ function validateApiUrl(raw) {
|
|
|
3886
3891
|
}
|
|
3887
3892
|
return null;
|
|
3888
3893
|
}
|
|
3889
|
-
function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false) {
|
|
3894
|
+
function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false, riskMetadata) {
|
|
3890
3895
|
const validated = validateApiUrl(creds.apiUrl);
|
|
3891
3896
|
if (!validated) {
|
|
3892
3897
|
try {
|
|
@@ -3903,6 +3908,10 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, contai
|
|
|
3903
3908
|
const dlpSample = dlpInfo && typeof dlpInfo.redactedSample === "string" ? dlpInfo.redactedSample.slice(0, DLP_SAMPLE_MAX_LEN) : void 0;
|
|
3904
3909
|
const dlpPattern = dlpInfo && typeof dlpInfo.pattern === "string" ? dlpInfo.pattern.slice(0, DLP_PATTERN_MAX_LEN) : void 0;
|
|
3905
3910
|
const safeCheckedBy = KNOWN_CHECKED_BY.has(checkedBy) ? checkedBy : "unknown";
|
|
3911
|
+
const cleanedRiskMetadata = riskMetadata ? Object.fromEntries(
|
|
3912
|
+
Object.entries(riskMetadata).filter(([, v]) => typeof v === "string" && v.length > 0)
|
|
3913
|
+
) : void 0;
|
|
3914
|
+
const hasRiskMetadata = cleanedRiskMetadata && Object.keys(cleanedRiskMetadata).length > 0;
|
|
3906
3915
|
return fetch(`${validated.toString().replace(/\/$/, "")}/audit`, {
|
|
3907
3916
|
method: "POST",
|
|
3908
3917
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
@@ -3911,6 +3920,13 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, contai
|
|
|
3911
3920
|
args: safeArgs,
|
|
3912
3921
|
checkedBy: safeCheckedBy,
|
|
3913
3922
|
...dlpInfo && { dlpPattern, dlpSample },
|
|
3923
|
+
...hasRiskMetadata && { riskMetadata: cleanedRiskMetadata },
|
|
3924
|
+
// session_id (Claude Code + Gemini CLI) groups all audit rows from one
|
|
3925
|
+
// agent run; transcript_path is the authoritative pointer to the
|
|
3926
|
+
// session log (survives Gemini resume drift). Both optional —
|
|
3927
|
+
// unsupported agents (MCP-mediated) leave them undefined.
|
|
3928
|
+
...meta?.sessionId && { runId: meta.sessionId },
|
|
3929
|
+
...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
|
|
3914
3930
|
context: {
|
|
3915
3931
|
agent: meta?.agent,
|
|
3916
3932
|
mcpServer: meta?.mcpServer,
|
|
@@ -3961,6 +3977,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
3961
3977
|
body: JSON.stringify({
|
|
3962
3978
|
toolName,
|
|
3963
3979
|
args,
|
|
3980
|
+
// See auditLocalAllow above for the rationale on these two fields.
|
|
3981
|
+
...meta?.sessionId && { runId: meta.sessionId },
|
|
3982
|
+
...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
|
|
3964
3983
|
context: {
|
|
3965
3984
|
agent: meta?.agent,
|
|
3966
3985
|
mcpServer: meta?.mcpServer,
|
|
@@ -4133,15 +4152,18 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
4133
4152
|
if (!options?.calledFromDaemon) {
|
|
4134
4153
|
const actId = (0, import_crypto3.randomUUID)();
|
|
4135
4154
|
const actTs = Date.now();
|
|
4155
|
+
const stripAnsi = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
|
|
4156
|
+
const sanitizedAgent = meta?.agent ? stripAnsi(meta.agent).slice(0, 80) : void 0;
|
|
4157
|
+
const sanitizedMcpServer = meta?.mcpServer ? stripAnsi(meta.mcpServer).slice(0, 40) : void 0;
|
|
4136
4158
|
const socketOk = await notifyActivity({
|
|
4137
4159
|
id: actId,
|
|
4138
4160
|
ts: actTs,
|
|
4139
4161
|
tool: toolName,
|
|
4140
4162
|
args,
|
|
4141
4163
|
status: "pending",
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4164
|
+
agent: sanitizedAgent,
|
|
4165
|
+
mcpServer: sanitizedMcpServer,
|
|
4166
|
+
sessionId: meta?.sessionId
|
|
4145
4167
|
});
|
|
4146
4168
|
const result = await _authorizeHeadlessCore(toolName, args, meta, {
|
|
4147
4169
|
...options,
|
|
@@ -4159,7 +4181,10 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
4159
4181
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
4160
4182
|
label: result.blockedByLabel,
|
|
4161
4183
|
ruleHit: result.ruleHit,
|
|
4162
|
-
observeWouldBlock: result.observeWouldBlock
|
|
4184
|
+
observeWouldBlock: result.observeWouldBlock,
|
|
4185
|
+
agent: sanitizedAgent,
|
|
4186
|
+
mcpServer: sanitizedMcpServer,
|
|
4187
|
+
sessionId: meta?.sessionId
|
|
4163
4188
|
});
|
|
4164
4189
|
}
|
|
4165
4190
|
return result;
|
|
@@ -4179,9 +4204,9 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4179
4204
|
};
|
|
4180
4205
|
if (isTestEnv2) {
|
|
4181
4206
|
approvers.native = false;
|
|
4182
|
-
approvers.browser = false;
|
|
4183
4207
|
approvers.terminal = false;
|
|
4184
4208
|
}
|
|
4209
|
+
approvers.browser = false;
|
|
4185
4210
|
if (config.settings.enableHookLogDebug && !isTestEnv2) {
|
|
4186
4211
|
appendHookDebug(toolName, args, meta, hashAuditArgs);
|
|
4187
4212
|
}
|
|
@@ -4311,10 +4336,24 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4311
4336
|
};
|
|
4312
4337
|
}
|
|
4313
4338
|
}
|
|
4314
|
-
|
|
4339
|
+
let policyResult = await evaluatePolicy2(toolName, args, meta?.agent);
|
|
4340
|
+
if (config.settings.panicMode && policyResult.decision === "review") {
|
|
4341
|
+
policyResult = {
|
|
4342
|
+
...policyResult,
|
|
4343
|
+
decision: "block",
|
|
4344
|
+
blockedByLabel: "\u{1F6A8} Panic mode (org policy)",
|
|
4345
|
+
reason: "Workspace is in panic mode \u2014 all review-verdict actions are blocked. Contact your admin to disable panic mode in the Node9 dashboard."
|
|
4346
|
+
};
|
|
4347
|
+
}
|
|
4315
4348
|
if (policyResult.decision === "allow") {
|
|
4316
4349
|
if (approvers.cloud && creds?.apiKey)
|
|
4317
|
-
await auditLocalAllow(toolName, args, "local-policy", creds, meta
|
|
4350
|
+
await auditLocalAllow(toolName, args, "local-policy", creds, meta, void 0, false, {
|
|
4351
|
+
ruleName: policyResult.ruleName,
|
|
4352
|
+
ruleDescription: policyResult.ruleDescription,
|
|
4353
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
4354
|
+
matchedField: policyResult.matchedField,
|
|
4355
|
+
matchedWord: policyResult.matchedWord
|
|
4356
|
+
});
|
|
4318
4357
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
|
|
4319
4358
|
return { approved: true, checkedBy: "local-policy" };
|
|
4320
4359
|
}
|
|
@@ -4337,14 +4376,50 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4337
4376
|
}
|
|
4338
4377
|
} else if (isDaemonRunning() && !isTestEnv2) {
|
|
4339
4378
|
if (!isManual)
|
|
4340
|
-
appendLocalAudit(
|
|
4379
|
+
appendLocalAudit(
|
|
4380
|
+
toolName,
|
|
4381
|
+
args,
|
|
4382
|
+
"deny",
|
|
4383
|
+
"smart-rule-block-override",
|
|
4384
|
+
meta,
|
|
4385
|
+
hashAuditArgs
|
|
4386
|
+
);
|
|
4341
4387
|
if (approvers.cloud && creds?.apiKey)
|
|
4342
|
-
auditLocalAllow(
|
|
4388
|
+
auditLocalAllow(
|
|
4389
|
+
toolName,
|
|
4390
|
+
args,
|
|
4391
|
+
"smart-rule-block-override",
|
|
4392
|
+
creds,
|
|
4393
|
+
meta,
|
|
4394
|
+
void 0,
|
|
4395
|
+
false,
|
|
4396
|
+
{
|
|
4397
|
+
ruleName: policyResult.ruleName,
|
|
4398
|
+
ruleDescription: policyResult.ruleDescription,
|
|
4399
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
4400
|
+
matchedField: policyResult.matchedField,
|
|
4401
|
+
matchedWord: policyResult.matchedWord
|
|
4402
|
+
}
|
|
4403
|
+
);
|
|
4404
|
+
const baseLabel = policyResult.blockedByLabel || "Smart Rule";
|
|
4405
|
+
const OVERRIDE_PREFIX = "\u26A0\uFE0F Override block rule: ";
|
|
4406
|
+
if (!baseLabel.startsWith(OVERRIDE_PREFIX)) {
|
|
4407
|
+
policyResult = {
|
|
4408
|
+
...policyResult,
|
|
4409
|
+
blockedByLabel: `${OVERRIDE_PREFIX}${baseLabel}`
|
|
4410
|
+
};
|
|
4411
|
+
}
|
|
4343
4412
|
} else {
|
|
4344
4413
|
if (!isManual)
|
|
4345
4414
|
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
4346
4415
|
if (approvers.cloud && creds?.apiKey)
|
|
4347
|
-
auditLocalAllow(toolName, args, "smart-rule-block", creds, meta
|
|
4416
|
+
auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
|
|
4417
|
+
ruleName: policyResult.ruleName,
|
|
4418
|
+
ruleDescription: policyResult.ruleDescription,
|
|
4419
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
4420
|
+
matchedField: policyResult.matchedField,
|
|
4421
|
+
matchedWord: policyResult.matchedWord
|
|
4422
|
+
});
|
|
4348
4423
|
return {
|
|
4349
4424
|
approved: false,
|
|
4350
4425
|
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
@@ -4482,7 +4557,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4482
4557
|
const internalToken = getInternalToken();
|
|
4483
4558
|
let daemonEntryId = null;
|
|
4484
4559
|
let daemonAllowCount = 1;
|
|
4485
|
-
if (
|
|
4560
|
+
if (approvers.terminal && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
4486
4561
|
if (cloudEnforced && cloudRequestId) {
|
|
4487
4562
|
const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
|
|
4488
4563
|
viewerId = viewer?.id ?? null;
|
|
@@ -4560,7 +4635,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4560
4635
|
})()
|
|
4561
4636
|
);
|
|
4562
4637
|
}
|
|
4563
|
-
if (daemonEntryId &&
|
|
4638
|
+
if (daemonEntryId && approvers.terminal) {
|
|
4564
4639
|
racePromises.push(
|
|
4565
4640
|
(async () => {
|
|
4566
4641
|
const {
|
|
@@ -4571,19 +4646,14 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4571
4646
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
4572
4647
|
const isApproved = daemonDecision === "allow";
|
|
4573
4648
|
const isRedirect = decisionSource === "terminal-redirect";
|
|
4574
|
-
const
|
|
4575
|
-
const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
|
|
4649
|
+
const via = "Terminal (node9 tail)";
|
|
4576
4650
|
return {
|
|
4577
4651
|
approved: isApproved,
|
|
4578
|
-
reason: isApproved ? void 0 :
|
|
4579
|
-
// Use the redirect reason from the tail when choice [2] was selected;
|
|
4580
|
-
// otherwise fall back to the generic rejection message.
|
|
4581
|
-
isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`
|
|
4582
|
-
),
|
|
4652
|
+
reason: isApproved ? void 0 : isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`,
|
|
4583
4653
|
checkedBy: isApproved ? "daemon" : void 0,
|
|
4584
4654
|
blockedBy: isApproved ? void 0 : "local-decision",
|
|
4585
4655
|
blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
|
|
4586
|
-
decisionSource:
|
|
4656
|
+
decisionSource: "terminal"
|
|
4587
4657
|
};
|
|
4588
4658
|
})()
|
|
4589
4659
|
);
|
package/dist/index.mjs
CHANGED
|
@@ -90,6 +90,7 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
|
|
|
90
90
|
...argsField,
|
|
91
91
|
agent: meta?.agent,
|
|
92
92
|
mcpServer: meta?.mcpServer,
|
|
93
|
+
sessionId: meta?.sessionId,
|
|
93
94
|
hostname: os.hostname(),
|
|
94
95
|
cwd: process.cwd()
|
|
95
96
|
});
|
|
@@ -2058,13 +2059,6 @@ var k8s_default = {
|
|
|
2058
2059
|
],
|
|
2059
2060
|
dangerousWords: []
|
|
2060
2061
|
};
|
|
2061
|
-
var mcp_tool_gating_default = {
|
|
2062
|
-
name: "mcp-tool-gating",
|
|
2063
|
-
description: "Intercept MCP tool lists and require user approval before the agent can use any tools from a new server",
|
|
2064
|
-
aliases: ["mcp-gating", "mcp-tools"],
|
|
2065
|
-
smartRules: [],
|
|
2066
|
-
dangerousWords: []
|
|
2067
|
-
};
|
|
2068
2062
|
var mongodb_default = {
|
|
2069
2063
|
name: "mongodb",
|
|
2070
2064
|
description: "Protects MongoDB databases from destructive AI operations",
|
|
@@ -2370,7 +2364,6 @@ var BUILTIN_SHIELDS = {
|
|
|
2370
2364
|
[filesystem_default.name]: filesystem_default,
|
|
2371
2365
|
[github_default.name]: github_default,
|
|
2372
2366
|
[k8s_default.name]: k8s_default,
|
|
2373
|
-
[mcp_tool_gating_default.name]: mcp_tool_gating_default,
|
|
2374
2367
|
[mongodb_default.name]: mongodb_default,
|
|
2375
2368
|
[postgres_default.name]: postgres_default,
|
|
2376
2369
|
[project_jail_default.name]: project_jail_default,
|
|
@@ -2899,6 +2892,12 @@ function getConfig(cwd) {
|
|
|
2899
2892
|
if (Array.isArray(raw.rules) && raw.rules.length > 0) {
|
|
2900
2893
|
applyLayer({ policy: { smartRules: raw.rules } });
|
|
2901
2894
|
}
|
|
2895
|
+
if (raw.panicMode === true) {
|
|
2896
|
+
mergedSettings.panicMode = true;
|
|
2897
|
+
}
|
|
2898
|
+
if (raw.shadowMode === true) {
|
|
2899
|
+
mergedSettings.mode = "observe";
|
|
2900
|
+
}
|
|
2902
2901
|
} catch {
|
|
2903
2902
|
}
|
|
2904
2903
|
}
|
|
@@ -3836,6 +3835,12 @@ var KNOWN_CHECKED_BY = /* @__PURE__ */ new Set([
|
|
|
3836
3835
|
"audit-mode",
|
|
3837
3836
|
"local-policy",
|
|
3838
3837
|
"smart-rule-block",
|
|
3838
|
+
// Smart-rule block was downgraded to review because the daemon was
|
|
3839
|
+
// running and we're not in CI. The block attempt is still recorded;
|
|
3840
|
+
// the user got a popup. Distinct from 'smart-rule-block' so the
|
|
3841
|
+
// dashboard can show "block rule overridden" separately from a hard
|
|
3842
|
+
// block that fired with no human in the loop.
|
|
3843
|
+
"smart-rule-block-override",
|
|
3839
3844
|
"persistent",
|
|
3840
3845
|
"trust",
|
|
3841
3846
|
"observe-mode",
|
|
@@ -3856,7 +3861,7 @@ function validateApiUrl(raw) {
|
|
|
3856
3861
|
}
|
|
3857
3862
|
return null;
|
|
3858
3863
|
}
|
|
3859
|
-
function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false) {
|
|
3864
|
+
function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false, riskMetadata) {
|
|
3860
3865
|
const validated = validateApiUrl(creds.apiUrl);
|
|
3861
3866
|
if (!validated) {
|
|
3862
3867
|
try {
|
|
@@ -3873,6 +3878,10 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, contai
|
|
|
3873
3878
|
const dlpSample = dlpInfo && typeof dlpInfo.redactedSample === "string" ? dlpInfo.redactedSample.slice(0, DLP_SAMPLE_MAX_LEN) : void 0;
|
|
3874
3879
|
const dlpPattern = dlpInfo && typeof dlpInfo.pattern === "string" ? dlpInfo.pattern.slice(0, DLP_PATTERN_MAX_LEN) : void 0;
|
|
3875
3880
|
const safeCheckedBy = KNOWN_CHECKED_BY.has(checkedBy) ? checkedBy : "unknown";
|
|
3881
|
+
const cleanedRiskMetadata = riskMetadata ? Object.fromEntries(
|
|
3882
|
+
Object.entries(riskMetadata).filter(([, v]) => typeof v === "string" && v.length > 0)
|
|
3883
|
+
) : void 0;
|
|
3884
|
+
const hasRiskMetadata = cleanedRiskMetadata && Object.keys(cleanedRiskMetadata).length > 0;
|
|
3876
3885
|
return fetch(`${validated.toString().replace(/\/$/, "")}/audit`, {
|
|
3877
3886
|
method: "POST",
|
|
3878
3887
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
|
|
@@ -3881,6 +3890,13 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, contai
|
|
|
3881
3890
|
args: safeArgs,
|
|
3882
3891
|
checkedBy: safeCheckedBy,
|
|
3883
3892
|
...dlpInfo && { dlpPattern, dlpSample },
|
|
3893
|
+
...hasRiskMetadata && { riskMetadata: cleanedRiskMetadata },
|
|
3894
|
+
// session_id (Claude Code + Gemini CLI) groups all audit rows from one
|
|
3895
|
+
// agent run; transcript_path is the authoritative pointer to the
|
|
3896
|
+
// session log (survives Gemini resume drift). Both optional —
|
|
3897
|
+
// unsupported agents (MCP-mediated) leave them undefined.
|
|
3898
|
+
...meta?.sessionId && { runId: meta.sessionId },
|
|
3899
|
+
...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
|
|
3884
3900
|
context: {
|
|
3885
3901
|
agent: meta?.agent,
|
|
3886
3902
|
mcpServer: meta?.mcpServer,
|
|
@@ -3931,6 +3947,9 @@ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPol
|
|
|
3931
3947
|
body: JSON.stringify({
|
|
3932
3948
|
toolName,
|
|
3933
3949
|
args,
|
|
3950
|
+
// See auditLocalAllow above for the rationale on these two fields.
|
|
3951
|
+
...meta?.sessionId && { runId: meta.sessionId },
|
|
3952
|
+
...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
|
|
3934
3953
|
context: {
|
|
3935
3954
|
agent: meta?.agent,
|
|
3936
3955
|
mcpServer: meta?.mcpServer,
|
|
@@ -4103,15 +4122,18 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
4103
4122
|
if (!options?.calledFromDaemon) {
|
|
4104
4123
|
const actId = randomUUID();
|
|
4105
4124
|
const actTs = Date.now();
|
|
4125
|
+
const stripAnsi = (s) => s.replace(/\x1b(?:\[[0-9;?]*[a-zA-Z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-_])/g, "");
|
|
4126
|
+
const sanitizedAgent = meta?.agent ? stripAnsi(meta.agent).slice(0, 80) : void 0;
|
|
4127
|
+
const sanitizedMcpServer = meta?.mcpServer ? stripAnsi(meta.mcpServer).slice(0, 40) : void 0;
|
|
4106
4128
|
const socketOk = await notifyActivity({
|
|
4107
4129
|
id: actId,
|
|
4108
4130
|
ts: actTs,
|
|
4109
4131
|
tool: toolName,
|
|
4110
4132
|
args,
|
|
4111
4133
|
status: "pending",
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4134
|
+
agent: sanitizedAgent,
|
|
4135
|
+
mcpServer: sanitizedMcpServer,
|
|
4136
|
+
sessionId: meta?.sessionId
|
|
4115
4137
|
});
|
|
4116
4138
|
const result = await _authorizeHeadlessCore(toolName, args, meta, {
|
|
4117
4139
|
...options,
|
|
@@ -4129,7 +4151,10 @@ async function authorizeHeadless(toolName, args, meta, options) {
|
|
|
4129
4151
|
status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : result.blockedByLabel?.includes("Taint") ? "taint" : "block",
|
|
4130
4152
|
label: result.blockedByLabel,
|
|
4131
4153
|
ruleHit: result.ruleHit,
|
|
4132
|
-
observeWouldBlock: result.observeWouldBlock
|
|
4154
|
+
observeWouldBlock: result.observeWouldBlock,
|
|
4155
|
+
agent: sanitizedAgent,
|
|
4156
|
+
mcpServer: sanitizedMcpServer,
|
|
4157
|
+
sessionId: meta?.sessionId
|
|
4133
4158
|
});
|
|
4134
4159
|
}
|
|
4135
4160
|
return result;
|
|
@@ -4149,9 +4174,9 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4149
4174
|
};
|
|
4150
4175
|
if (isTestEnv2) {
|
|
4151
4176
|
approvers.native = false;
|
|
4152
|
-
approvers.browser = false;
|
|
4153
4177
|
approvers.terminal = false;
|
|
4154
4178
|
}
|
|
4179
|
+
approvers.browser = false;
|
|
4155
4180
|
if (config.settings.enableHookLogDebug && !isTestEnv2) {
|
|
4156
4181
|
appendHookDebug(toolName, args, meta, hashAuditArgs);
|
|
4157
4182
|
}
|
|
@@ -4281,10 +4306,24 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4281
4306
|
};
|
|
4282
4307
|
}
|
|
4283
4308
|
}
|
|
4284
|
-
|
|
4309
|
+
let policyResult = await evaluatePolicy2(toolName, args, meta?.agent);
|
|
4310
|
+
if (config.settings.panicMode && policyResult.decision === "review") {
|
|
4311
|
+
policyResult = {
|
|
4312
|
+
...policyResult,
|
|
4313
|
+
decision: "block",
|
|
4314
|
+
blockedByLabel: "\u{1F6A8} Panic mode (org policy)",
|
|
4315
|
+
reason: "Workspace is in panic mode \u2014 all review-verdict actions are blocked. Contact your admin to disable panic mode in the Node9 dashboard."
|
|
4316
|
+
};
|
|
4317
|
+
}
|
|
4285
4318
|
if (policyResult.decision === "allow") {
|
|
4286
4319
|
if (approvers.cloud && creds?.apiKey)
|
|
4287
|
-
await auditLocalAllow(toolName, args, "local-policy", creds, meta
|
|
4320
|
+
await auditLocalAllow(toolName, args, "local-policy", creds, meta, void 0, false, {
|
|
4321
|
+
ruleName: policyResult.ruleName,
|
|
4322
|
+
ruleDescription: policyResult.ruleDescription,
|
|
4323
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
4324
|
+
matchedField: policyResult.matchedField,
|
|
4325
|
+
matchedWord: policyResult.matchedWord
|
|
4326
|
+
});
|
|
4288
4327
|
if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
|
|
4289
4328
|
return { approved: true, checkedBy: "local-policy" };
|
|
4290
4329
|
}
|
|
@@ -4307,14 +4346,50 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4307
4346
|
}
|
|
4308
4347
|
} else if (isDaemonRunning() && !isTestEnv2) {
|
|
4309
4348
|
if (!isManual)
|
|
4310
|
-
appendLocalAudit(
|
|
4349
|
+
appendLocalAudit(
|
|
4350
|
+
toolName,
|
|
4351
|
+
args,
|
|
4352
|
+
"deny",
|
|
4353
|
+
"smart-rule-block-override",
|
|
4354
|
+
meta,
|
|
4355
|
+
hashAuditArgs
|
|
4356
|
+
);
|
|
4311
4357
|
if (approvers.cloud && creds?.apiKey)
|
|
4312
|
-
auditLocalAllow(
|
|
4358
|
+
auditLocalAllow(
|
|
4359
|
+
toolName,
|
|
4360
|
+
args,
|
|
4361
|
+
"smart-rule-block-override",
|
|
4362
|
+
creds,
|
|
4363
|
+
meta,
|
|
4364
|
+
void 0,
|
|
4365
|
+
false,
|
|
4366
|
+
{
|
|
4367
|
+
ruleName: policyResult.ruleName,
|
|
4368
|
+
ruleDescription: policyResult.ruleDescription,
|
|
4369
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
4370
|
+
matchedField: policyResult.matchedField,
|
|
4371
|
+
matchedWord: policyResult.matchedWord
|
|
4372
|
+
}
|
|
4373
|
+
);
|
|
4374
|
+
const baseLabel = policyResult.blockedByLabel || "Smart Rule";
|
|
4375
|
+
const OVERRIDE_PREFIX = "\u26A0\uFE0F Override block rule: ";
|
|
4376
|
+
if (!baseLabel.startsWith(OVERRIDE_PREFIX)) {
|
|
4377
|
+
policyResult = {
|
|
4378
|
+
...policyResult,
|
|
4379
|
+
blockedByLabel: `${OVERRIDE_PREFIX}${baseLabel}`
|
|
4380
|
+
};
|
|
4381
|
+
}
|
|
4313
4382
|
} else {
|
|
4314
4383
|
if (!isManual)
|
|
4315
4384
|
appendLocalAudit(toolName, args, "deny", "smart-rule-block", meta, hashAuditArgs);
|
|
4316
4385
|
if (approvers.cloud && creds?.apiKey)
|
|
4317
|
-
auditLocalAllow(toolName, args, "smart-rule-block", creds, meta
|
|
4386
|
+
auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
|
|
4387
|
+
ruleName: policyResult.ruleName,
|
|
4388
|
+
ruleDescription: policyResult.ruleDescription,
|
|
4389
|
+
blockedByLabel: policyResult.blockedByLabel,
|
|
4390
|
+
matchedField: policyResult.matchedField,
|
|
4391
|
+
matchedWord: policyResult.matchedWord
|
|
4392
|
+
});
|
|
4318
4393
|
return {
|
|
4319
4394
|
approved: false,
|
|
4320
4395
|
reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
|
|
@@ -4452,7 +4527,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4452
4527
|
const internalToken = getInternalToken();
|
|
4453
4528
|
let daemonEntryId = null;
|
|
4454
4529
|
let daemonAllowCount = 1;
|
|
4455
|
-
if (
|
|
4530
|
+
if (approvers.terminal && isDaemonRunning() && !options?.calledFromDaemon) {
|
|
4456
4531
|
if (cloudEnforced && cloudRequestId) {
|
|
4457
4532
|
const viewer = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(() => null);
|
|
4458
4533
|
viewerId = viewer?.id ?? null;
|
|
@@ -4530,7 +4605,7 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4530
4605
|
})()
|
|
4531
4606
|
);
|
|
4532
4607
|
}
|
|
4533
|
-
if (daemonEntryId &&
|
|
4608
|
+
if (daemonEntryId && approvers.terminal) {
|
|
4534
4609
|
racePromises.push(
|
|
4535
4610
|
(async () => {
|
|
4536
4611
|
const {
|
|
@@ -4541,19 +4616,14 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
4541
4616
|
if (daemonDecision === "abandoned") throw new Error("Abandoned");
|
|
4542
4617
|
const isApproved = daemonDecision === "allow";
|
|
4543
4618
|
const isRedirect = decisionSource === "terminal-redirect";
|
|
4544
|
-
const
|
|
4545
|
-
const via = src === "terminal" ? "Terminal (node9 tail)" : "Browser Dashboard";
|
|
4619
|
+
const via = "Terminal (node9 tail)";
|
|
4546
4620
|
return {
|
|
4547
4621
|
approved: isApproved,
|
|
4548
|
-
reason: isApproved ? void 0 :
|
|
4549
|
-
// Use the redirect reason from the tail when choice [2] was selected;
|
|
4550
|
-
// otherwise fall back to the generic rejection message.
|
|
4551
|
-
isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`
|
|
4552
|
-
),
|
|
4622
|
+
reason: isApproved ? void 0 : isRedirect && daemonReason || `The human user rejected this action via the Node9 ${via}.`,
|
|
4553
4623
|
checkedBy: isApproved ? "daemon" : void 0,
|
|
4554
4624
|
blockedBy: isApproved ? void 0 : "local-decision",
|
|
4555
4625
|
blockedByLabel: isRedirect ? "Steered Redirect (Terminal)" : `User Decision (${via})`,
|
|
4556
|
-
decisionSource:
|
|
4626
|
+
decisionSource: "terminal"
|
|
4557
4627
|
};
|
|
4558
4628
|
})()
|
|
4559
4629
|
);
|