@node9/proxy 1.34.0 → 1.35.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.
Files changed (3) hide show
  1. package/dist/cli.js +144 -1
  2. package/dist/cli.mjs +144 -0
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5889,6 +5889,57 @@ function validateApiUrl(raw) {
5889
5889
  }
5890
5890
  return null;
5891
5891
  }
5892
+ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false, riskMetadata) {
5893
+ const validated = validateApiUrl(creds.apiUrl);
5894
+ if (!validated) {
5895
+ try {
5896
+ import_fs10.default.appendFileSync(
5897
+ HOOK_DEBUG_LOG,
5898
+ `[audit] refused to send: invalid apiUrl scheme/host (got "${String(creds.apiUrl).slice(0, 200)}")
5899
+ `
5900
+ );
5901
+ } catch {
5902
+ }
5903
+ return Promise.resolve();
5904
+ }
5905
+ const safeArgs = containsSensitiveArgs ? { tool: toolName, redacted: true } : args;
5906
+ const dlpSample = dlpInfo && typeof dlpInfo.redactedSample === "string" ? dlpInfo.redactedSample.slice(0, DLP_SAMPLE_MAX_LEN) : void 0;
5907
+ const dlpPattern = dlpInfo && typeof dlpInfo.pattern === "string" ? dlpInfo.pattern.slice(0, DLP_PATTERN_MAX_LEN) : void 0;
5908
+ const safeCheckedBy = KNOWN_CHECKED_BY.has(checkedBy) ? checkedBy : "unknown";
5909
+ const cleanedRiskMetadata = riskMetadata ? Object.fromEntries(
5910
+ Object.entries(riskMetadata).filter(
5911
+ ([, v]) => typeof v === "string" && v.length > 0 || typeof v === "number" && Number.isFinite(v)
5912
+ )
5913
+ ) : void 0;
5914
+ const hasRiskMetadata = cleanedRiskMetadata && Object.keys(cleanedRiskMetadata).length > 0;
5915
+ return fetch(`${validated.toString().replace(/\/$/, "")}/audit`, {
5916
+ method: "POST",
5917
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
5918
+ body: JSON.stringify({
5919
+ toolName,
5920
+ args: safeArgs,
5921
+ checkedBy: safeCheckedBy,
5922
+ ...dlpInfo && { dlpPattern, dlpSample },
5923
+ ...hasRiskMetadata && { riskMetadata: cleanedRiskMetadata },
5924
+ // session_id (Claude Code + Gemini CLI) groups all audit rows from one
5925
+ // agent run; transcript_path is the authoritative pointer to the
5926
+ // session log (survives Gemini resume drift). Both optional —
5927
+ // unsupported agents (MCP-mediated) leave them undefined.
5928
+ ...meta?.sessionId && { runId: meta.sessionId },
5929
+ ...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
5930
+ context: {
5931
+ agent: meta?.agent,
5932
+ mcpServer: meta?.mcpServer,
5933
+ hostname: import_os9.default.hostname(),
5934
+ cwd: process.cwd(),
5935
+ platform: import_os9.default.platform()
5936
+ }
5937
+ }),
5938
+ signal: AbortSignal.timeout(5e3)
5939
+ }).then(() => {
5940
+ }).catch(() => {
5941
+ });
5942
+ }
5892
5943
  async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
5893
5944
  const controller = new AbortController();
5894
5945
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -6012,7 +6063,7 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
6012
6063
  );
6013
6064
  }
6014
6065
  }
6015
- var import_fs10, import_os9, import_path12;
6066
+ var import_fs10, import_os9, import_path12, DLP_SAMPLE_MAX_LEN, DLP_PATTERN_MAX_LEN, KNOWN_CHECKED_BY;
6016
6067
  var init_cloud = __esm({
6017
6068
  "src/auth/cloud.ts"() {
6018
6069
  "use strict";
@@ -6020,6 +6071,39 @@ var init_cloud = __esm({
6020
6071
  import_os9 = __toESM(require("os"));
6021
6072
  import_path12 = __toESM(require("path"));
6022
6073
  init_audit();
6074
+ DLP_SAMPLE_MAX_LEN = 200;
6075
+ DLP_PATTERN_MAX_LEN = 100;
6076
+ KNOWN_CHECKED_BY = /* @__PURE__ */ new Set([
6077
+ "dlp-block",
6078
+ "observe-mode-dlp-would-block",
6079
+ "dlp-review-flagged",
6080
+ "loop-detected",
6081
+ "audit-mode",
6082
+ "local-policy",
6083
+ "smart-rule-block",
6084
+ // Smart-rule block was downgraded to review because the daemon was
6085
+ // running and we're not in CI. The block attempt is still recorded;
6086
+ // the user got a popup. Distinct from 'smart-rule-block' so the
6087
+ // dashboard can show "block rule overridden" separately from a hard
6088
+ // block that fired with no human in the loop.
6089
+ "smart-rule-block-override",
6090
+ "persistent",
6091
+ "trust",
6092
+ "observe-mode",
6093
+ "observe-mode-would-block",
6094
+ // MCP supply-chain: the gateway pinned a server's tool definitions and they
6095
+ // changed since (possible tool poisoning / rug pull). Emitted as a synthetic
6096
+ // audit row so the SaaS surfaces it as a blocked event. The firewall maps
6097
+ // this checkedBy to AUTO_BLOCKED. See doc/roadmap/active/saas-value-first.md
6098
+ // (workstream B-Tier2).
6099
+ "mcp-pin-mismatch",
6100
+ // MCP visibility (B-Tier2, informational — NOT blocks): the gateway
6101
+ // discovered a server's tool inventory (mcp-discovered) or saw an oversized
6102
+ // tool response that bloats the context window (mcp-large-response). Stored
6103
+ // AUTO_ALLOWED; carries mcpToolCount / mcpResponseBytes in riskMetadata.
6104
+ "mcp-discovered",
6105
+ "mcp-large-response"
6106
+ ]);
6023
6107
  }
6024
6108
  });
6025
6109
 
@@ -22047,6 +22131,8 @@ var import_chalk19 = __toESM(require("chalk"));
22047
22131
  var import_child_process10 = require("child_process");
22048
22132
  var import_execa3 = require("execa");
22049
22133
  init_orchestrator();
22134
+ init_cloud();
22135
+ init_config();
22050
22136
  init_provenance();
22051
22137
  init_mcp_pin();
22052
22138
  init_mcp_tools();
@@ -22075,6 +22161,60 @@ function normalizeClientName(name) {
22075
22161
  const sanitized = sanitize4(name).slice(0, 40);
22076
22162
  return sanitized.length > 0 ? sanitized : void 0;
22077
22163
  }
22164
+ function reportPinMismatchToCloud(serverKey, agent) {
22165
+ try {
22166
+ const creds = getCredentials();
22167
+ if (!creds) return;
22168
+ void auditLocalAllow(
22169
+ `mcp-server:${serverKey}`,
22170
+ { serverKey, reason: "tool-pin-mismatch" },
22171
+ "mcp-pin-mismatch",
22172
+ creds,
22173
+ { mcpServer: serverKey, agent },
22174
+ void 0,
22175
+ false,
22176
+ {
22177
+ ruleName: "MCP tool definitions changed (possible rug pull)",
22178
+ ruleDescription: `The MCP server "${serverKey}" changed its tool definitions since they were pinned. This can indicate a supply-chain attack (tool poisoning). The session was quarantined. Review with: node9 mcp pin update ${serverKey}`
22179
+ }
22180
+ );
22181
+ } catch {
22182
+ }
22183
+ }
22184
+ function reportInventoryToCloud(serverKey, toolCount, agent) {
22185
+ try {
22186
+ const creds = getCredentials();
22187
+ if (!creds) return;
22188
+ void auditLocalAllow(
22189
+ `mcp-server:${serverKey}`,
22190
+ { serverKey, toolCount },
22191
+ "mcp-discovered",
22192
+ creds,
22193
+ { mcpServer: serverKey, agent },
22194
+ void 0,
22195
+ false,
22196
+ { mcpToolCount: toolCount }
22197
+ );
22198
+ } catch {
22199
+ }
22200
+ }
22201
+ function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
22202
+ try {
22203
+ const creds = getCredentials();
22204
+ if (!creds) return;
22205
+ void auditLocalAllow(
22206
+ `mcp-server:${serverKey}`,
22207
+ { serverKey, responseBytes },
22208
+ "mcp-large-response",
22209
+ creds,
22210
+ { mcpServer: serverKey, agent },
22211
+ void 0,
22212
+ false,
22213
+ { mcpResponseBytes: responseBytes }
22214
+ );
22215
+ } catch {
22216
+ }
22217
+ }
22078
22218
  function tokenize4(cmd) {
22079
22219
  const tokens = [];
22080
22220
  let current = "";
@@ -22335,6 +22475,7 @@ async function runMcpGateway(upstreamCommand) {
22335
22475
  const currentHash = hashToolDefinitions(tools);
22336
22476
  const pinStatus = checkPin(serverKey, currentHash, gatewayCwd);
22337
22477
  const token = getInternalToken();
22478
+ reportInventoryToCloud(serverKey, tools.length, clientName);
22338
22479
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
22339
22480
  const toolSummary = tools.map((t) => ({ name: t.name, description: t.description }));
22340
22481
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/discovered`, {
@@ -22399,6 +22540,7 @@ async function runMcpGateway(upstreamCommand) {
22399
22540
  console.error(import_chalk19.default.red(" Session quarantined \u2014 all tool calls blocked."));
22400
22541
  console.error(import_chalk19.default.yellow(` Run: node9 mcp pin update ${serverKey}
22401
22542
  `));
22543
+ reportPinMismatchToCloud(serverKey, clientName);
22402
22544
  const errorResponse = {
22403
22545
  jsonrpc: "2.0",
22404
22546
  id: parsed.id,
@@ -22444,6 +22586,7 @@ async function runMcpGateway(upstreamCommand) {
22444
22586
  `\u26A1 Node9: Large MCP response from '${toolName}' (${(line.length / 1024).toFixed(0)}KB) \u2014 context window enlarged`
22445
22587
  )
22446
22588
  );
22589
+ reportLargeResponseToCloud(serverKey, line.length, clientName);
22447
22590
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
22448
22591
  const token = getInternalToken();
22449
22592
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/large-response`, {
package/dist/cli.mjs CHANGED
@@ -5869,6 +5869,57 @@ function validateApiUrl(raw) {
5869
5869
  }
5870
5870
  return null;
5871
5871
  }
5872
+ function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false, riskMetadata) {
5873
+ const validated = validateApiUrl(creds.apiUrl);
5874
+ if (!validated) {
5875
+ try {
5876
+ fs10.appendFileSync(
5877
+ HOOK_DEBUG_LOG,
5878
+ `[audit] refused to send: invalid apiUrl scheme/host (got "${String(creds.apiUrl).slice(0, 200)}")
5879
+ `
5880
+ );
5881
+ } catch {
5882
+ }
5883
+ return Promise.resolve();
5884
+ }
5885
+ const safeArgs = containsSensitiveArgs ? { tool: toolName, redacted: true } : args;
5886
+ const dlpSample = dlpInfo && typeof dlpInfo.redactedSample === "string" ? dlpInfo.redactedSample.slice(0, DLP_SAMPLE_MAX_LEN) : void 0;
5887
+ const dlpPattern = dlpInfo && typeof dlpInfo.pattern === "string" ? dlpInfo.pattern.slice(0, DLP_PATTERN_MAX_LEN) : void 0;
5888
+ const safeCheckedBy = KNOWN_CHECKED_BY.has(checkedBy) ? checkedBy : "unknown";
5889
+ const cleanedRiskMetadata = riskMetadata ? Object.fromEntries(
5890
+ Object.entries(riskMetadata).filter(
5891
+ ([, v]) => typeof v === "string" && v.length > 0 || typeof v === "number" && Number.isFinite(v)
5892
+ )
5893
+ ) : void 0;
5894
+ const hasRiskMetadata = cleanedRiskMetadata && Object.keys(cleanedRiskMetadata).length > 0;
5895
+ return fetch(`${validated.toString().replace(/\/$/, "")}/audit`, {
5896
+ method: "POST",
5897
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
5898
+ body: JSON.stringify({
5899
+ toolName,
5900
+ args: safeArgs,
5901
+ checkedBy: safeCheckedBy,
5902
+ ...dlpInfo && { dlpPattern, dlpSample },
5903
+ ...hasRiskMetadata && { riskMetadata: cleanedRiskMetadata },
5904
+ // session_id (Claude Code + Gemini CLI) groups all audit rows from one
5905
+ // agent run; transcript_path is the authoritative pointer to the
5906
+ // session log (survives Gemini resume drift). Both optional —
5907
+ // unsupported agents (MCP-mediated) leave them undefined.
5908
+ ...meta?.sessionId && { runId: meta.sessionId },
5909
+ ...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
5910
+ context: {
5911
+ agent: meta?.agent,
5912
+ mcpServer: meta?.mcpServer,
5913
+ hostname: os9.hostname(),
5914
+ cwd: process.cwd(),
5915
+ platform: os9.platform()
5916
+ }
5917
+ }),
5918
+ signal: AbortSignal.timeout(5e3)
5919
+ }).then(() => {
5920
+ }).catch(() => {
5921
+ });
5922
+ }
5872
5923
  async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
5873
5924
  const controller = new AbortController();
5874
5925
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -5992,10 +6043,44 @@ async function resolveNode9SaaS(requestId, creds, approved, decidedBy) {
5992
6043
  );
5993
6044
  }
5994
6045
  }
6046
+ var DLP_SAMPLE_MAX_LEN, DLP_PATTERN_MAX_LEN, KNOWN_CHECKED_BY;
5995
6047
  var init_cloud = __esm({
5996
6048
  "src/auth/cloud.ts"() {
5997
6049
  "use strict";
5998
6050
  init_audit();
6051
+ DLP_SAMPLE_MAX_LEN = 200;
6052
+ DLP_PATTERN_MAX_LEN = 100;
6053
+ KNOWN_CHECKED_BY = /* @__PURE__ */ new Set([
6054
+ "dlp-block",
6055
+ "observe-mode-dlp-would-block",
6056
+ "dlp-review-flagged",
6057
+ "loop-detected",
6058
+ "audit-mode",
6059
+ "local-policy",
6060
+ "smart-rule-block",
6061
+ // Smart-rule block was downgraded to review because the daemon was
6062
+ // running and we're not in CI. The block attempt is still recorded;
6063
+ // the user got a popup. Distinct from 'smart-rule-block' so the
6064
+ // dashboard can show "block rule overridden" separately from a hard
6065
+ // block that fired with no human in the loop.
6066
+ "smart-rule-block-override",
6067
+ "persistent",
6068
+ "trust",
6069
+ "observe-mode",
6070
+ "observe-mode-would-block",
6071
+ // MCP supply-chain: the gateway pinned a server's tool definitions and they
6072
+ // changed since (possible tool poisoning / rug pull). Emitted as a synthetic
6073
+ // audit row so the SaaS surfaces it as a blocked event. The firewall maps
6074
+ // this checkedBy to AUTO_BLOCKED. See doc/roadmap/active/saas-value-first.md
6075
+ // (workstream B-Tier2).
6076
+ "mcp-pin-mismatch",
6077
+ // MCP visibility (B-Tier2, informational — NOT blocks): the gateway
6078
+ // discovered a server's tool inventory (mcp-discovered) or saw an oversized
6079
+ // tool response that bloats the context window (mcp-large-response). Stored
6080
+ // AUTO_ALLOWED; carries mcpToolCount / mcpResponseBytes in riskMetadata.
6081
+ "mcp-discovered",
6082
+ "mcp-large-response"
6083
+ ]);
5999
6084
  }
6000
6085
  });
6001
6086
 
@@ -22014,6 +22099,8 @@ function registerUndoCommand(program2) {
22014
22099
 
22015
22100
  // src/mcp-gateway/index.ts
22016
22101
  init_orchestrator();
22102
+ init_cloud();
22103
+ init_config();
22017
22104
  import readline4 from "readline";
22018
22105
  import chalk19 from "chalk";
22019
22106
  import { spawn as spawn7 } from "child_process";
@@ -22046,6 +22133,60 @@ function normalizeClientName(name) {
22046
22133
  const sanitized = sanitize4(name).slice(0, 40);
22047
22134
  return sanitized.length > 0 ? sanitized : void 0;
22048
22135
  }
22136
+ function reportPinMismatchToCloud(serverKey, agent) {
22137
+ try {
22138
+ const creds = getCredentials();
22139
+ if (!creds) return;
22140
+ void auditLocalAllow(
22141
+ `mcp-server:${serverKey}`,
22142
+ { serverKey, reason: "tool-pin-mismatch" },
22143
+ "mcp-pin-mismatch",
22144
+ creds,
22145
+ { mcpServer: serverKey, agent },
22146
+ void 0,
22147
+ false,
22148
+ {
22149
+ ruleName: "MCP tool definitions changed (possible rug pull)",
22150
+ ruleDescription: `The MCP server "${serverKey}" changed its tool definitions since they were pinned. This can indicate a supply-chain attack (tool poisoning). The session was quarantined. Review with: node9 mcp pin update ${serverKey}`
22151
+ }
22152
+ );
22153
+ } catch {
22154
+ }
22155
+ }
22156
+ function reportInventoryToCloud(serverKey, toolCount, agent) {
22157
+ try {
22158
+ const creds = getCredentials();
22159
+ if (!creds) return;
22160
+ void auditLocalAllow(
22161
+ `mcp-server:${serverKey}`,
22162
+ { serverKey, toolCount },
22163
+ "mcp-discovered",
22164
+ creds,
22165
+ { mcpServer: serverKey, agent },
22166
+ void 0,
22167
+ false,
22168
+ { mcpToolCount: toolCount }
22169
+ );
22170
+ } catch {
22171
+ }
22172
+ }
22173
+ function reportLargeResponseToCloud(serverKey, responseBytes, agent) {
22174
+ try {
22175
+ const creds = getCredentials();
22176
+ if (!creds) return;
22177
+ void auditLocalAllow(
22178
+ `mcp-server:${serverKey}`,
22179
+ { serverKey, responseBytes },
22180
+ "mcp-large-response",
22181
+ creds,
22182
+ { mcpServer: serverKey, agent },
22183
+ void 0,
22184
+ false,
22185
+ { mcpResponseBytes: responseBytes }
22186
+ );
22187
+ } catch {
22188
+ }
22189
+ }
22049
22190
  function tokenize4(cmd) {
22050
22191
  const tokens = [];
22051
22192
  let current = "";
@@ -22306,6 +22447,7 @@ async function runMcpGateway(upstreamCommand) {
22306
22447
  const currentHash = hashToolDefinitions(tools);
22307
22448
  const pinStatus = checkPin(serverKey, currentHash, gatewayCwd);
22308
22449
  const token = getInternalToken();
22450
+ reportInventoryToCloud(serverKey, tools.length, clientName);
22309
22451
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
22310
22452
  const toolSummary = tools.map((t) => ({ name: t.name, description: t.description }));
22311
22453
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/discovered`, {
@@ -22370,6 +22512,7 @@ async function runMcpGateway(upstreamCommand) {
22370
22512
  console.error(chalk19.red(" Session quarantined \u2014 all tool calls blocked."));
22371
22513
  console.error(chalk19.yellow(` Run: node9 mcp pin update ${serverKey}
22372
22514
  `));
22515
+ reportPinMismatchToCloud(serverKey, clientName);
22373
22516
  const errorResponse = {
22374
22517
  jsonrpc: "2.0",
22375
22518
  id: parsed.id,
@@ -22415,6 +22558,7 @@ async function runMcpGateway(upstreamCommand) {
22415
22558
  `\u26A1 Node9: Large MCP response from '${toolName}' (${(line.length / 1024).toFixed(0)}KB) \u2014 context window enlarged`
22416
22559
  )
22417
22560
  );
22561
+ reportLargeResponseToCloud(serverKey, line.length, clientName);
22418
22562
  if (isDaemonRunning() && process.env.NODE9_TESTING !== "1") {
22419
22563
  const token = getInternalToken();
22420
22564
  fetch(`http://${DAEMON_HOST}:${DAEMON_PORT}/mcp/large-response`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.34.0",
3
+ "version": "1.35.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",