@node9/proxy 1.29.0 → 1.30.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/index.mjs CHANGED
@@ -51,11 +51,17 @@ __export(audit_exports, {
51
51
  appendHookDebug: () => appendHookDebug,
52
52
  appendLocalAudit: () => appendLocalAudit,
53
53
  appendToLog: () => appendToLog,
54
+ buildArgsPreview: () => buildArgsPreview,
55
+ generateEventId: () => generateEventId,
54
56
  redactSecrets: () => redactSecrets
55
57
  });
56
58
  import fs from "fs";
57
59
  import path from "path";
58
60
  import os from "os";
61
+ import crypto from "crypto";
62
+ function generateEventId() {
63
+ return `${Date.now().toString(36)}-${crypto.randomBytes(6).toString("hex")}`;
64
+ }
59
65
  function isTestCall(toolName, args) {
60
66
  if (toolName !== "Bash" && toolName !== "bash") return false;
61
67
  const cmd = args?.command;
@@ -74,6 +80,17 @@ function redactSecrets(text) {
74
80
  );
75
81
  return redacted;
76
82
  }
83
+ function buildArgsPreview(args) {
84
+ try {
85
+ const o = args && typeof args === "object" ? args : null;
86
+ const primary = o && (o.command ?? o.file_path ?? o.path ?? o.url ?? o.query);
87
+ const text = typeof primary === "string" ? primary : args ? JSON.stringify(args) : "";
88
+ if (!text) return void 0;
89
+ return redactSecrets(text).slice(0, 120);
90
+ } catch {
91
+ return void 0;
92
+ }
93
+ }
77
94
  function appendToLog(logPath, entry) {
78
95
  try {
79
96
  const dir = path.dirname(logPath);
@@ -96,11 +113,18 @@ function appendHookDebug(toolName, args, meta, auditHashArgsEnabled) {
96
113
  });
97
114
  }
98
115
  function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashArgsEnabled) {
99
- const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
116
+ const isDlpRow = checkedBy.toLowerCase().includes("dlp") || Boolean(meta?.dlpPattern);
117
+ const preview = auditHashArgsEnabled && !isDlpRow ? buildArgsPreview(args) : void 0;
118
+ const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args), ...preview ? { argsPreview: preview } : {} } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
100
119
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
101
120
  const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
102
121
  const agentToolNameField = meta?.agentToolName ? { agentToolName: meta.agentToolName } : {};
122
+ const dlpFields = meta?.dlpPattern ? { dlpPattern: meta.dlpPattern, dlpSample: meta.dlpSample } : {};
123
+ const cloudLinkField = meta?.cloudRequestId ? { cloudRequestId: meta.cloudRequestId } : {};
103
124
  appendToLog(LOCAL_AUDIT_LOG, {
125
+ // eid first: the outbox shipper dedups on it, and a fixed leading field
126
+ // makes the JSONL easy to eyeball.
127
+ eid: generateEventId(),
104
128
  ts: (/* @__PURE__ */ new Date()).toISOString(),
105
129
  tool: toolName,
106
130
  ...agentToolNameField,
@@ -108,6 +132,8 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
108
132
  decision,
109
133
  checkedBy,
110
134
  ...ruleNameField,
135
+ ...dlpFields,
136
+ ...cloudLinkField,
111
137
  ...testRun,
112
138
  agent: meta?.agent,
113
139
  mcpServer: meta?.mcpServer,
@@ -216,7 +242,13 @@ var ConfigFileSchema = z.object({
216
242
  allowGlobalPause: z.boolean().optional(),
217
243
  auditHashArgs: z.boolean().optional(),
218
244
  agentPolicy: z.enum(["require_approval", "block_on_rules"]).optional(),
219
- cloudSyncIntervalHours: z.number().positive().optional()
245
+ cloudSyncIntervalHours: z.number().positive().optional(),
246
+ // Outbox shipper (audit.log → SaaS batch ingest). enabled defaults
247
+ // to true; set false to fall back to local-only auditing.
248
+ shipper: z.object({
249
+ enabled: z.boolean().optional(),
250
+ intervalSeconds: z.number().min(5).optional()
251
+ }).optional()
220
252
  }).optional(),
221
253
  policy: z.object({
222
254
  sandboxPaths: z.array(z.string()).optional(),
@@ -284,7 +316,7 @@ import mvdanSh from "mvdan-sh";
284
316
  import pm from "picomatch";
285
317
  import safeRegex2 from "safe-regex2";
286
318
  import safeRegex3 from "safe-regex2";
287
- import crypto from "crypto";
319
+ import crypto2 from "crypto";
288
320
  var ASSIGNMENT_CONTEXT_RE = /\b(?:password|passwd|secret|token|api[_-]?key|auth(?:_key|_token)?|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\s*[=:]\s*/i;
289
321
  function isAssignmentContext(text) {
290
322
  return ASSIGNMENT_CONTEXT_RE.test(text);
@@ -2817,7 +2849,7 @@ assertBuiltinShieldRegexesAreSafe();
2817
2849
  var LOOP_MAX_RECORDS = 500;
2818
2850
  function computeArgsHash(args) {
2819
2851
  const str = JSON.stringify(args ?? "");
2820
- return crypto.createHash("sha256").update(str).digest("hex").slice(0, 16);
2852
+ return crypto2.createHash("sha256").update(str).digest("hex").slice(0, 16);
2821
2853
  }
2822
2854
  function evaluateLoopWindow(records, tool, args, threshold, windowMs, now) {
2823
2855
  const hash = computeArgsHash(args);
@@ -2930,7 +2962,8 @@ var DEFAULT_CONFIG = {
2930
2962
  flightRecorder: true,
2931
2963
  auditHashArgs: true,
2932
2964
  approvers: { native: true, browser: false, cloud: false, terminal: true },
2933
- cloudSyncIntervalHours: 5
2965
+ cloudSyncIntervalHours: 5,
2966
+ shipper: { enabled: true, intervalSeconds: 20 }
2934
2967
  },
2935
2968
  policy: {
2936
2969
  sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
@@ -3225,7 +3258,8 @@ function getConfig(cwd) {
3225
3258
  const projectConfig = tryLoadConfig(projectPath);
3226
3259
  const mergedSettings = {
3227
3260
  ...DEFAULT_CONFIG.settings,
3228
- approvers: { ...DEFAULT_CONFIG.settings.approvers }
3261
+ approvers: { ...DEFAULT_CONFIG.settings.approvers },
3262
+ shipper: { ...DEFAULT_CONFIG.settings.shipper }
3229
3263
  };
3230
3264
  const mergedPolicy = {
3231
3265
  sandboxPaths: [...DEFAULT_CONFIG.policy.sandboxPaths],
@@ -3256,6 +3290,7 @@ function getConfig(cwd) {
3256
3290
  if (s.enableHookLogDebug !== void 0)
3257
3291
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
3258
3292
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
3293
+ if (s.shipper) mergedSettings.shipper = { ...mergedSettings.shipper, ...s.shipper };
3259
3294
  if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
3260
3295
  if (s.approvalTimeoutSeconds !== void 0 && s.approvalTimeoutMs === void 0)
3261
3296
  mergedSettings.approvalTimeoutMs = s.approvalTimeoutSeconds * 1e3;
@@ -4251,91 +4286,6 @@ init_audit();
4251
4286
  import fs9 from "fs";
4252
4287
  import os8 from "os";
4253
4288
  import path11 from "path";
4254
- var DLP_SAMPLE_MAX_LEN = 200;
4255
- var DLP_PATTERN_MAX_LEN = 100;
4256
- var KNOWN_CHECKED_BY = /* @__PURE__ */ new Set([
4257
- "dlp-block",
4258
- "observe-mode-dlp-would-block",
4259
- "dlp-review-flagged",
4260
- "loop-detected",
4261
- "audit-mode",
4262
- "local-policy",
4263
- "smart-rule-block",
4264
- // Smart-rule block was downgraded to review because the daemon was
4265
- // running and we're not in CI. The block attempt is still recorded;
4266
- // the user got a popup. Distinct from 'smart-rule-block' so the
4267
- // dashboard can show "block rule overridden" separately from a hard
4268
- // block that fired with no human in the loop.
4269
- "smart-rule-block-override",
4270
- "persistent",
4271
- "trust",
4272
- "observe-mode",
4273
- "observe-mode-would-block"
4274
- ]);
4275
- function validateApiUrl(raw) {
4276
- let u;
4277
- try {
4278
- u = new URL(raw);
4279
- } catch {
4280
- return null;
4281
- }
4282
- if (u.username || u.password) return null;
4283
- if (u.protocol === "https:") return u;
4284
- if (u.protocol === "http:") {
4285
- const h = u.hostname;
4286
- if (h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]") return u;
4287
- }
4288
- return null;
4289
- }
4290
- function auditLocalAllow(toolName, args, checkedBy, creds, meta, dlpInfo, containsSensitiveArgs = false, riskMetadata) {
4291
- const validated = validateApiUrl(creds.apiUrl);
4292
- if (!validated) {
4293
- try {
4294
- fs9.appendFileSync(
4295
- HOOK_DEBUG_LOG,
4296
- `[audit] refused to send: invalid apiUrl scheme/host (got "${String(creds.apiUrl).slice(0, 200)}")
4297
- `
4298
- );
4299
- } catch {
4300
- }
4301
- return Promise.resolve();
4302
- }
4303
- const safeArgs = containsSensitiveArgs ? { tool: toolName, redacted: true } : args;
4304
- const dlpSample = dlpInfo && typeof dlpInfo.redactedSample === "string" ? dlpInfo.redactedSample.slice(0, DLP_SAMPLE_MAX_LEN) : void 0;
4305
- const dlpPattern = dlpInfo && typeof dlpInfo.pattern === "string" ? dlpInfo.pattern.slice(0, DLP_PATTERN_MAX_LEN) : void 0;
4306
- const safeCheckedBy = KNOWN_CHECKED_BY.has(checkedBy) ? checkedBy : "unknown";
4307
- const cleanedRiskMetadata = riskMetadata ? Object.fromEntries(
4308
- Object.entries(riskMetadata).filter(([, v]) => typeof v === "string" && v.length > 0)
4309
- ) : void 0;
4310
- const hasRiskMetadata = cleanedRiskMetadata && Object.keys(cleanedRiskMetadata).length > 0;
4311
- return fetch(`${validated.toString().replace(/\/$/, "")}/audit`, {
4312
- method: "POST",
4313
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
4314
- body: JSON.stringify({
4315
- toolName,
4316
- args: safeArgs,
4317
- checkedBy: safeCheckedBy,
4318
- ...dlpInfo && { dlpPattern, dlpSample },
4319
- ...hasRiskMetadata && { riskMetadata: cleanedRiskMetadata },
4320
- // session_id (Claude Code + Gemini CLI) groups all audit rows from one
4321
- // agent run; transcript_path is the authoritative pointer to the
4322
- // session log (survives Gemini resume drift). Both optional —
4323
- // unsupported agents (MCP-mediated) leave them undefined.
4324
- ...meta?.sessionId && { runId: meta.sessionId },
4325
- ...meta?.transcriptPath && { transcriptPath: meta.transcriptPath },
4326
- context: {
4327
- agent: meta?.agent,
4328
- mcpServer: meta?.mcpServer,
4329
- hostname: os8.hostname(),
4330
- cwd: process.cwd(),
4331
- platform: os8.platform()
4332
- }
4333
- }),
4334
- signal: AbortSignal.timeout(5e3)
4335
- }).then(() => {
4336
- }).catch(() => {
4337
- });
4338
- }
4339
4289
  async function initNode9SaaS(toolName, args, creds, meta, riskMetadata, agentPolicy, forceReview) {
4340
4290
  const controller = new AbortController();
4341
4291
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -4641,17 +4591,11 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4641
4591
  args,
4642
4592
  "deny",
4643
4593
  isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
4644
- meta,
4645
- true
4646
- );
4647
- if (approvers.cloud && creds?.apiKey)
4648
- auditLocalAllow(
4649
- toolName,
4650
- args,
4651
- isObserveMode ? "observe-mode-dlp-would-block" : "dlp-block",
4652
- creds,
4653
- meta,
4654
- { pattern: dlpMatch.patternName, redactedSample: dlpMatch.redactedSample },
4594
+ {
4595
+ ...meta,
4596
+ dlpPattern: dlpMatch.patternName,
4597
+ dlpSample: dlpMatch.redactedSample
4598
+ },
4655
4599
  true
4656
4600
  );
4657
4601
  if (isWriteTool(toolName) && filePath) {
@@ -4707,9 +4651,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4707
4651
  const policyResult = await evaluatePolicy2(toolName, args, meta?.agent, options?.cwd);
4708
4652
  if (policyResult.decision === "review") {
4709
4653
  appendLocalAudit(toolName, args, "allow", "audit-mode", meta, hashAuditArgs);
4710
- if (approvers.cloud && creds?.apiKey) {
4711
- await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
4712
- }
4713
4654
  }
4714
4655
  }
4715
4656
  return { approved: true, checkedBy: "audit" };
@@ -4722,8 +4663,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4722
4663
  const reason = `It looks like you've called "${toolName}" ${loopResult.count} times with identical arguments in the last ${ld.windowSeconds}s. Are you stuck? Step back and reconsider your approach \u2014 what are you actually trying to accomplish, and is there a different way to get there?`;
4723
4664
  if (!isManual)
4724
4665
  appendLocalAudit(toolName, args, "deny", "loop-detected", meta, hashAuditArgs);
4725
- if (approvers.cloud && creds?.apiKey)
4726
- auditLocalAllow(toolName, args, "loop-detected", creds, meta, void 0, true);
4727
4666
  return {
4728
4667
  approved: false,
4729
4668
  reason,
@@ -4742,15 +4681,15 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4742
4681
  };
4743
4682
  }
4744
4683
  if (policyResult.decision === "allow") {
4745
- if (approvers.cloud && creds?.apiKey)
4746
- await auditLocalAllow(toolName, args, "local-policy", creds, meta, void 0, false, {
4747
- ruleName: policyResult.ruleName,
4748
- ruleDescription: policyResult.ruleDescription,
4749
- blockedByLabel: policyResult.blockedByLabel,
4750
- matchedField: policyResult.matchedField,
4751
- matchedWord: policyResult.matchedWord
4752
- });
4753
- if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta, hashAuditArgs);
4684
+ if (!isManual)
4685
+ appendLocalAudit(
4686
+ toolName,
4687
+ args,
4688
+ "allow",
4689
+ "local-policy",
4690
+ { ...meta, ruleName: policyResult.ruleName },
4691
+ hashAuditArgs
4692
+ );
4754
4693
  return { approved: true, checkedBy: "local-policy" };
4755
4694
  }
4756
4695
  if (policyResult.decision === "block") {
@@ -4783,23 +4722,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4783
4722
  { ...meta, ruleName: policyResult.ruleName },
4784
4723
  hashAuditArgs
4785
4724
  );
4786
- if (approvers.cloud && creds?.apiKey)
4787
- auditLocalAllow(
4788
- toolName,
4789
- args,
4790
- "smart-rule-block-override",
4791
- creds,
4792
- meta,
4793
- void 0,
4794
- false,
4795
- {
4796
- ruleName: policyResult.ruleName,
4797
- ruleDescription: policyResult.ruleDescription,
4798
- blockedByLabel: policyResult.blockedByLabel,
4799
- matchedField: policyResult.matchedField,
4800
- matchedWord: policyResult.matchedWord
4801
- }
4802
- );
4803
4725
  const baseLabel = policyResult.blockedByLabel || "Smart Rule";
4804
4726
  const OVERRIDE_PREFIX = "\u26A0\uFE0F Override block rule: ";
4805
4727
  if (!baseLabel.startsWith(OVERRIDE_PREFIX)) {
@@ -4824,14 +4746,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4824
4746
  { ...meta, ruleName: policyResult.ruleName },
4825
4747
  hashAuditArgs
4826
4748
  );
4827
- if (approvers.cloud && creds?.apiKey)
4828
- auditLocalAllow(toolName, args, "smart-rule-block", creds, meta, void 0, false, {
4829
- ruleName: policyResult.ruleName,
4830
- ruleDescription: policyResult.ruleDescription,
4831
- blockedByLabel: policyResult.blockedByLabel,
4832
- matchedField: policyResult.matchedField,
4833
- matchedWord: policyResult.matchedWord
4834
- });
4835
4749
  return {
4836
4750
  approved: false,
4837
4751
  reason: policyResult.reason ?? "Action explicitly blocked by Smart Policy.",
@@ -4860,8 +4774,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4860
4774
  if (policyRuleDescription) riskMetadata.ruleDescription = policyRuleDescription.slice(0, 200);
4861
4775
  const persistent = policyResult.ruleName ? null : getPersistentDecision(toolName);
4862
4776
  if (persistent === "allow") {
4863
- if (approvers.cloud && creds?.apiKey)
4864
- await auditLocalAllow(toolName, args, "persistent", creds, meta);
4865
4777
  if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta, hashAuditArgs);
4866
4778
  return { approved: true, checkedBy: "persistent" };
4867
4779
  }
@@ -4894,8 +4806,6 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
4894
4806
  }
4895
4807
  }
4896
4808
  if (!taintWarning && getActiveTrustSession(toolName, args)) {
4897
- if (approvers.cloud && creds?.apiKey)
4898
- await auditLocalAllow(toolName, args, "trust", creds, meta);
4899
4809
  if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta, hashAuditArgs);
4900
4810
  return { approved: true, checkedBy: "trust" };
4901
4811
  }
@@ -5140,7 +5050,12 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
5140
5050
  args,
5141
5051
  finalResult.approved ? "allow" : "deny",
5142
5052
  finalResult.checkedBy || finalResult.blockedBy || "unknown",
5143
- meta,
5053
+ // cloudRequestId links this row to the BE-origin AuditLog row the
5054
+ // /intercept handshake created — the shipper hands it to the SaaS so
5055
+ // the BE enriches that row instead of inserting a duplicate. Matters
5056
+ // for EVERY racer outcome, not just cloud wins: a native-popup
5057
+ // decision on a cloud-pending request would otherwise count twice.
5058
+ cloudRequestId ? { ...meta, cloudRequestId } : meta,
5144
5059
  hashAuditArgs
5145
5060
  );
5146
5061
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.29.0",
3
+ "version": "1.30.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",