@node9/proxy 2.9.1 → 2.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -235,30 +235,86 @@ var init_audit = __esm({
235
235
  });
236
236
 
237
237
  // src/config-schema.ts
238
+ function formatIssues(issues) {
239
+ const lines = issues.map((issue) => {
240
+ const path77 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
241
+ return ` \u2022 ${path77}: ${issue.message}`;
242
+ });
243
+ return `Invalid config:
244
+ ${lines.join("\n")}`;
245
+ }
246
+ function prunePaths(root, paths) {
247
+ let removed = false;
248
+ const ordered = [...paths].sort((x, y) => {
249
+ if (y.length !== x.length) return y.length - x.length;
250
+ const xi = x[x.length - 1];
251
+ const yi = y[y.length - 1];
252
+ return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
253
+ });
254
+ for (const path77 of ordered) {
255
+ let cur = root;
256
+ for (const key of path77.slice(0, -1)) {
257
+ if (cur === null || typeof cur !== "object") {
258
+ cur = void 0;
259
+ break;
260
+ }
261
+ cur = cur[key];
262
+ }
263
+ if (cur === null || typeof cur !== "object") continue;
264
+ const last = path77[path77.length - 1];
265
+ if (Array.isArray(cur)) {
266
+ const i = Number(last);
267
+ if (Number.isInteger(i) && i >= 0 && i < cur.length) {
268
+ cur.splice(i, 1);
269
+ removed = true;
270
+ }
271
+ continue;
272
+ }
273
+ const obj = cur;
274
+ if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
275
+ delete obj[String(last)];
276
+ removed = true;
277
+ }
278
+ }
279
+ return removed;
280
+ }
238
281
  function sanitizeConfig(raw) {
239
282
  const result = ConfigFileSchema.safeParse(raw);
240
283
  if (result.success) {
241
284
  return { sanitized: result.data, error: null };
242
285
  }
243
- const invalidTopLevelKeys = new Set(
244
- result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
245
- );
246
- const sanitized = {};
247
- if (typeof raw === "object" && raw !== null) {
248
- for (const [key, value] of Object.entries(raw)) {
249
- if (!invalidTopLevelKeys.has(key)) {
250
- sanitized[key] = value;
251
- }
286
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
287
+ return { sanitized: {}, error: formatIssues(result.error.issues) };
288
+ }
289
+ const working = structuredClone(raw);
290
+ for (let level = 0; level < 6; level++) {
291
+ for (let pass = 0; pass < 5; pass++) {
292
+ const attempt = ConfigFileSchema.safeParse(working);
293
+ if (attempt.success) break;
294
+ const paths = attempt.error.issues.flatMap((issue) => {
295
+ const at = issue.path;
296
+ if (issue.code === "unrecognized_keys") {
297
+ return issue.keys.map((k) => [...at, k]);
298
+ }
299
+ return [at.slice(0, -level || void 0)];
300
+ }).filter((path77) => path77.length > 0);
301
+ if (paths.length === 0 || !prunePaths(working, paths)) break;
252
302
  }
303
+ if (ConfigFileSchema.safeParse(working).success) break;
304
+ }
305
+ const after = ConfigFileSchema.safeParse(working);
306
+ const sanitized = {};
307
+ const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
308
+ after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
309
+ );
310
+ for (const [key, value] of Object.entries(working)) {
311
+ if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
253
312
  }
254
- const lines = result.error.issues.map((issue) => {
255
- const path77 = issue.path.length > 0 ? issue.path.join(".") : "root";
256
- return ` \u2022 ${path77}: ${issue.message}`;
257
- });
258
313
  return {
259
314
  sanitized,
260
- error: `Invalid config:
261
- ${lines.join("\n")}`
315
+ // The message names what the USER should fix, so it is built from the
316
+ // original parse, not from whatever survived the prune.
317
+ error: formatIssues(result.error.issues)
262
318
  };
263
319
  }
264
320
  var import_zod, noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
@@ -283,9 +339,9 @@ var init_config_schema = __esm({
283
339
  "notMatchesGlob"
284
340
  ],
285
341
  {
286
- errorMap: () => ({
287
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
288
- })
342
+ // zod 4 replaced errorMap with `error`. The wording is kept verbatim:
343
+ // it is what a user sees when their config is rejected.
344
+ error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
289
345
  }
290
346
  ),
291
347
  value: import_zod.z.string().optional(),
@@ -303,7 +359,7 @@ var init_config_schema = __esm({
303
359
  conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
304
360
  conditionMode: import_zod.z.enum(["all", "any"]).optional(),
305
361
  verdict: import_zod.z.enum(["allow", "review", "block"], {
306
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
362
+ error: () => "verdict must be one of: allow, review, block"
307
363
  }),
308
364
  reason: import_zod.z.string().optional(),
309
365
  description: import_zod.z.string().optional(),
@@ -376,7 +432,7 @@ var init_config_schema = __esm({
376
432
  sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
377
433
  dangerousWords: import_zod.z.array(noNewlines).optional(),
378
434
  ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
379
- toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
435
+ toolInspection: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
380
436
  smartRules: import_zod.z.array(SmartRuleSchema).optional(),
381
437
  dlp: import_zod.z.object({
382
438
  enabled: import_zod.z.boolean().optional(),
@@ -421,8 +477,8 @@ var init_config_schema = __esm({
421
477
  roots: import_zod.z.array(import_zod.z.string()).optional()
422
478
  }).optional()
423
479
  }).optional(),
424
- environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
425
- }).strict({ message: "Config contains unknown top-level keys" });
480
+ environments: import_zod.z.record(import_zod.z.string(), import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
481
+ }).strict();
426
482
  }
427
483
  });
428
484
 
@@ -2007,7 +2063,7 @@ function classifySsrf(host) {
2007
2063
  if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
2008
2064
  const o = v4Octets(ip);
2009
2065
  if (o) {
2010
- if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", false);
2066
+ if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
2011
2067
  if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
2012
2068
  if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
2013
2069
  if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
@@ -2019,7 +2075,7 @@ function classifySsrf(host) {
2019
2075
  }
2020
2076
  const g = expandIpv6(ip);
2021
2077
  if (!g) return null;
2022
- if (g.every((x) => x === 0)) return hit("unspecified", false);
2078
+ if (g.every((x) => x === 0)) return hit("unspecified", true);
2023
2079
  if ((g[0] & 65472) === 65152) return hit("link-local", false);
2024
2080
  if ((g[0] & 65280) === 65280) return hit("multicast", false);
2025
2081
  if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
@@ -2035,7 +2091,7 @@ function ssrfFloor(tokens, opts = {}) {
2035
2091
  for (const { token, binary } of tokens) {
2036
2092
  const m = classifySsrf(token);
2037
2093
  if (!m) continue;
2038
- if (m.tier === "private" && !opts.ssrfStrict) continue;
2094
+ if (STRICT_TIERS.has(m.tier) && !opts.ssrfStrict) continue;
2039
2095
  if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
2040
2096
  return {
2041
2097
  ...m,
@@ -3149,7 +3205,7 @@ function* stringValues(obj, depth = 0) {
3149
3205
  }
3150
3206
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
3151
3207
  }
3152
- var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, METADATA_HOSTNAMES, v4Octets, TIER_REASON, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
3208
+ var import_safe_regex2, import_crypto3, import_mvdan_sh, import_picomatch, import_safe_regex22, import_safe_regex23, import_crypto4, IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
3153
3209
  var init_dist = __esm({
3154
3210
  "packages/policy-engine/dist/index.mjs"() {
3155
3211
  "use strict";
@@ -4304,9 +4360,15 @@ var init_dist = __esm({
4304
4360
  // AWS ECS task role
4305
4361
  "168.63.129.16",
4306
4362
  // Azure WireServer
4307
- "fd00:ec2::254"
4363
+ "fd00:ec2::254",
4308
4364
  // AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
4365
+ // Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
4366
+ // classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
4367
+ // live credential endpoint. It has to be named here, above the range check,
4368
+ // and it is the reason relaxing cgnat is safe.
4369
+ "100.100.100.200"
4309
4370
  ]);
4371
+ STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
4310
4372
  METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
4311
4373
  v4Octets = (a) => {
4312
4374
  const p = a.split(".");
@@ -4316,7 +4378,7 @@ var init_dist = __esm({
4316
4378
  metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
4317
4379
  "link-local": "a link-local address",
4318
4380
  multicast: "a multicast address",
4319
- unspecified: "the unspecified address",
4381
+ unspecified: "the unspecified address, which reaches this host",
4320
4382
  cgnat: "a carrier-grade NAT address",
4321
4383
  private: "a loopback or private address"
4322
4384
  };
@@ -54427,12 +54489,7 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
54427
54489
  `${num2(d.approvals.approved)} approved \xB7 ${num2(d.approvals.denied)} denied \xB7 ${num2(d.approvals.timedOut)} timed-out\u2192deny`
54428
54490
  );
54429
54491
  dimRow("\u{1F4C1}", "Files", d.files.blocked > 0, `${num2(d.files.blocked)} jail-path reads blocked`);
54430
- dimRow(
54431
- "\u{1F6E0}",
54432
- "Tool rules",
54433
- d.toolRules.blocked > 0,
54434
- `${num2(d.toolRules.blocked)} shields/rules`
54435
- );
54492
+ dimRow("\u{1F6E0}", "Tool rules", d.toolRules.blocked > 0, `${num2(d.toolRules.blocked)} shields/rules`);
54436
54493
  dimRow("\u{1F9E9}", "Apps (MCP)", d.apps.blocked > 0, `${num2(d.apps.blocked)} app-permission blocks`);
54437
54494
  dimRow("\u{1F4B0}", "Cost", d.cost.totalUSD > 0, `${fmtCost2(d.cost.totalUSD)} this period`);
54438
54495
  }
package/dist/cli.mjs CHANGED
@@ -240,30 +240,86 @@ var init_audit = __esm({
240
240
 
241
241
  // src/config-schema.ts
242
242
  import { z } from "zod";
243
+ function formatIssues(issues) {
244
+ const lines = issues.map((issue) => {
245
+ const path77 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
246
+ return ` \u2022 ${path77}: ${issue.message}`;
247
+ });
248
+ return `Invalid config:
249
+ ${lines.join("\n")}`;
250
+ }
251
+ function prunePaths(root, paths) {
252
+ let removed = false;
253
+ const ordered = [...paths].sort((x, y) => {
254
+ if (y.length !== x.length) return y.length - x.length;
255
+ const xi = x[x.length - 1];
256
+ const yi = y[y.length - 1];
257
+ return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
258
+ });
259
+ for (const path77 of ordered) {
260
+ let cur = root;
261
+ for (const key of path77.slice(0, -1)) {
262
+ if (cur === null || typeof cur !== "object") {
263
+ cur = void 0;
264
+ break;
265
+ }
266
+ cur = cur[key];
267
+ }
268
+ if (cur === null || typeof cur !== "object") continue;
269
+ const last = path77[path77.length - 1];
270
+ if (Array.isArray(cur)) {
271
+ const i = Number(last);
272
+ if (Number.isInteger(i) && i >= 0 && i < cur.length) {
273
+ cur.splice(i, 1);
274
+ removed = true;
275
+ }
276
+ continue;
277
+ }
278
+ const obj = cur;
279
+ if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
280
+ delete obj[String(last)];
281
+ removed = true;
282
+ }
283
+ }
284
+ return removed;
285
+ }
243
286
  function sanitizeConfig(raw) {
244
287
  const result = ConfigFileSchema.safeParse(raw);
245
288
  if (result.success) {
246
289
  return { sanitized: result.data, error: null };
247
290
  }
248
- const invalidTopLevelKeys = new Set(
249
- result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
250
- );
251
- const sanitized = {};
252
- if (typeof raw === "object" && raw !== null) {
253
- for (const [key, value] of Object.entries(raw)) {
254
- if (!invalidTopLevelKeys.has(key)) {
255
- sanitized[key] = value;
256
- }
291
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
292
+ return { sanitized: {}, error: formatIssues(result.error.issues) };
293
+ }
294
+ const working = structuredClone(raw);
295
+ for (let level = 0; level < 6; level++) {
296
+ for (let pass = 0; pass < 5; pass++) {
297
+ const attempt = ConfigFileSchema.safeParse(working);
298
+ if (attempt.success) break;
299
+ const paths = attempt.error.issues.flatMap((issue) => {
300
+ const at = issue.path;
301
+ if (issue.code === "unrecognized_keys") {
302
+ return issue.keys.map((k) => [...at, k]);
303
+ }
304
+ return [at.slice(0, -level || void 0)];
305
+ }).filter((path77) => path77.length > 0);
306
+ if (paths.length === 0 || !prunePaths(working, paths)) break;
257
307
  }
308
+ if (ConfigFileSchema.safeParse(working).success) break;
309
+ }
310
+ const after = ConfigFileSchema.safeParse(working);
311
+ const sanitized = {};
312
+ const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
313
+ after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
314
+ );
315
+ for (const [key, value] of Object.entries(working)) {
316
+ if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
258
317
  }
259
- const lines = result.error.issues.map((issue) => {
260
- const path77 = issue.path.length > 0 ? issue.path.join(".") : "root";
261
- return ` \u2022 ${path77}: ${issue.message}`;
262
- });
263
318
  return {
264
319
  sanitized,
265
- error: `Invalid config:
266
- ${lines.join("\n")}`
320
+ // The message names what the USER should fix, so it is built from the
321
+ // original parse, not from whatever survived the prune.
322
+ error: formatIssues(result.error.issues)
267
323
  };
268
324
  }
269
325
  var noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
@@ -287,9 +343,9 @@ var init_config_schema = __esm({
287
343
  "notMatchesGlob"
288
344
  ],
289
345
  {
290
- errorMap: () => ({
291
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
292
- })
346
+ // zod 4 replaced errorMap with `error`. The wording is kept verbatim:
347
+ // it is what a user sees when their config is rejected.
348
+ error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
293
349
  }
294
350
  ),
295
351
  value: z.string().optional(),
@@ -307,7 +363,7 @@ var init_config_schema = __esm({
307
363
  conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
308
364
  conditionMode: z.enum(["all", "any"]).optional(),
309
365
  verdict: z.enum(["allow", "review", "block"], {
310
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
366
+ error: () => "verdict must be one of: allow, review, block"
311
367
  }),
312
368
  reason: z.string().optional(),
313
369
  description: z.string().optional(),
@@ -380,7 +436,7 @@ var init_config_schema = __esm({
380
436
  sandboxPaths: z.array(z.string()).optional(),
381
437
  dangerousWords: z.array(noNewlines).optional(),
382
438
  ignoredTools: z.array(z.string()).optional(),
383
- toolInspection: z.record(z.string()).optional(),
439
+ toolInspection: z.record(z.string(), z.string()).optional(),
384
440
  smartRules: z.array(SmartRuleSchema).optional(),
385
441
  dlp: z.object({
386
442
  enabled: z.boolean().optional(),
@@ -425,8 +481,8 @@ var init_config_schema = __esm({
425
481
  roots: z.array(z.string()).optional()
426
482
  }).optional()
427
483
  }).optional(),
428
- environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
429
- }).strict({ message: "Config contains unknown top-level keys" });
484
+ environments: z.record(z.string(), z.object({ requireApproval: z.boolean().optional() })).optional()
485
+ }).strict();
430
486
  }
431
487
  });
432
488
 
@@ -2018,7 +2074,7 @@ function classifySsrf(host) {
2018
2074
  if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
2019
2075
  const o = v4Octets(ip);
2020
2076
  if (o) {
2021
- if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", false);
2077
+ if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
2022
2078
  if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
2023
2079
  if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
2024
2080
  if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
@@ -2030,7 +2086,7 @@ function classifySsrf(host) {
2030
2086
  }
2031
2087
  const g = expandIpv6(ip);
2032
2088
  if (!g) return null;
2033
- if (g.every((x) => x === 0)) return hit("unspecified", false);
2089
+ if (g.every((x) => x === 0)) return hit("unspecified", true);
2034
2090
  if ((g[0] & 65472) === 65152) return hit("link-local", false);
2035
2091
  if ((g[0] & 65280) === 65280) return hit("multicast", false);
2036
2092
  if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
@@ -2046,7 +2102,7 @@ function ssrfFloor(tokens, opts = {}) {
2046
2102
  for (const { token, binary } of tokens) {
2047
2103
  const m = classifySsrf(token);
2048
2104
  if (!m) continue;
2049
- if (m.tier === "private" && !opts.ssrfStrict) continue;
2105
+ if (STRICT_TIERS.has(m.tier) && !opts.ssrfStrict) continue;
2050
2106
  if (m.overridable && m.normalized && exempt2.has(m.normalized)) continue;
2051
2107
  return {
2052
2108
  ...m,
@@ -3160,7 +3216,7 @@ function* stringValues(obj, depth = 0) {
3160
3216
  }
3161
3217
  for (const v of Object.values(obj)) yield* stringValues(v, depth + 1);
3162
3218
  }
3163
- var IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, METADATA_HOSTNAMES, v4Octets, TIER_REASON, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
3219
+ var IBAN_LENGTH, B58, B58_INDEX, XPRV_VERSIONS, MAX, UNTRUSTED_TOOLS, SIGNALS, ASSIGNMENT_CONTEXT_RE, DLP_STOPWORDS, DLP_PATTERNS, DLP_PATTERNS_GLOBAL, SENSITIVE_PATH_PATTERNS, MAX_DEPTH, MAX_STRING_BYTES, MAX_JSON_PARSE_BYTES, DLP_SCAN_LIMITS, MAX_REGEX_LENGTH, REGEX_CACHE_MAX, regexCache, FORBIDDEN_PATH_SEGMENTS, syntax, sharedParser, MESSAGE_FLAGS, SHELL_INTERPRETERS, DOWNLOAD_CMDS, NORMALIZE_CACHE_MAX, normalizeCache, AST_CACHE_MAX, astCache, PARSE_FAIL, FS_READ_TOOLS, FS_OP_PRESCREEN_RE, HOME_CACHE_ALLOWLIST, SENSITIVE_PATH_RULES, BASH_TOOL_NAMES, AST_FS_REGEX_RULES, SQL_DB_CLIS, SQL_DDL_RE, CHMOD_OPEN_PERM_TOKENS, COMMAND_WRAPPERS, INLINE_INTERPRETER, RUNNER_WRAPPERS, _redirStdinOps, _listOps, WRAPPER_TAKES_TARGET, INTERP_LEADING_TARGET, NET_BINARIES, VALUE_FLAGS, FS_OP_CACHE_MAX, fsOpCache, stripDotSlash, REDIR_TRUNCATE_OPS, REDIR_HEREDOC_OPS, DEFAULT_EGRESS_ALLOWLIST, SOURCE_COMMANDS, SINK_COMMANDS, OBFUSCATORS, SENSITIVE_PATTERNS, FLAGS_WITH_VALUES, SSRF_MAX_HOST, METADATA_ADDRESSES, STRICT_TIERS, METADATA_HOSTNAMES, v4Octets, TIER_REASON, VERDICT_RANK, SQL_DML_KEYWORDS, aws_default, bash_safe_default, docker_default, filesystem_default, github_default, k8s_default, mongodb_default, postgres_default, project_jail_default, redis_default, BUILTIN_SHIELDS, LOOP_MAX_RECORDS, FINDING_TO_SIGNAL, SCAN_SIGNAL_WEIGHTS, LOOP_THRESHOLD_FOR_WASTE, DESTRUCTIVE_OP_RE, SENSITIVE_PATH_RE, FILE_TOOLS, PII_EMAIL_RE, PII_SSN_RE, PII_PHONE_RE, PII_CC16_RE, PII_CC15_RE, PII_IBAN_RE, REALTIME_PII_PATTERNS, MAX_PII_SCAN_BYTES, CANARY_MIN_LENGTH, MAX_TEXT, MAX_DEPTH2, MAX_JSON_PARSE, URL_DEPTH, B64_DEPTH, MIN_SEGMENT, SEPARATORS, stripSeparators, looksText, CANARY_DECODERS, VIEWS, LONG_OUTPUT_THRESHOLD_BYTES, CANONICAL_EXTRACTOR_VERSION, DEDUPE_PREVIEW_LEN, TERMINAL_ESCAPE_RE, ENGINE_VERSION;
3164
3220
  var init_dist = __esm({
3165
3221
  "packages/policy-engine/dist/index.mjs"() {
3166
3222
  "use strict";
@@ -4308,9 +4364,15 @@ var init_dist = __esm({
4308
4364
  // AWS ECS task role
4309
4365
  "168.63.129.16",
4310
4366
  // Azure WireServer
4311
- "fd00:ec2::254"
4367
+ "fd00:ec2::254",
4312
4368
  // AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
4369
+ // Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
4370
+ // classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
4371
+ // live credential endpoint. It has to be named here, above the range check,
4372
+ // and it is the reason relaxing cgnat is safe.
4373
+ "100.100.100.200"
4313
4374
  ]);
4375
+ STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
4314
4376
  METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
4315
4377
  v4Octets = (a) => {
4316
4378
  const p = a.split(".");
@@ -4320,7 +4382,7 @@ var init_dist = __esm({
4320
4382
  metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
4321
4383
  "link-local": "a link-local address",
4322
4384
  multicast: "a multicast address",
4323
- unspecified: "the unspecified address",
4385
+ unspecified: "the unspecified address, which reaches this host",
4324
4386
  cgnat: "a carrier-grade NAT address",
4325
4387
  private: "a loopback or private address"
4326
4388
  };
@@ -54419,12 +54481,7 @@ function renderTerminalReport(data, responseDlpEntries, excludeTests) {
54419
54481
  `${num2(d.approvals.approved)} approved \xB7 ${num2(d.approvals.denied)} denied \xB7 ${num2(d.approvals.timedOut)} timed-out\u2192deny`
54420
54482
  );
54421
54483
  dimRow("\u{1F4C1}", "Files", d.files.blocked > 0, `${num2(d.files.blocked)} jail-path reads blocked`);
54422
- dimRow(
54423
- "\u{1F6E0}",
54424
- "Tool rules",
54425
- d.toolRules.blocked > 0,
54426
- `${num2(d.toolRules.blocked)} shields/rules`
54427
- );
54484
+ dimRow("\u{1F6E0}", "Tool rules", d.toolRules.blocked > 0, `${num2(d.toolRules.blocked)} shields/rules`);
54428
54485
  dimRow("\u{1F9E9}", "Apps (MCP)", d.apps.blocked > 0, `${num2(d.apps.blocked)} app-permission blocks`);
54429
54486
  dimRow("\u{1F4B0}", "Cost", d.cost.totalUSD > 0, `${fmtCost2(d.cost.totalUSD)} this period`);
54430
54487
  }
@@ -856,7 +856,7 @@ function classifySsrf(host) {
856
856
  if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
857
857
  const o = v4Octets(ip);
858
858
  if (o) {
859
- if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", false);
859
+ if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
860
860
  if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
861
861
  if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
862
862
  if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
@@ -868,7 +868,7 @@ function classifySsrf(host) {
868
868
  }
869
869
  const g = expandIpv6(ip);
870
870
  if (!g) return null;
871
- if (g.every((x) => x === 0)) return hit("unspecified", false);
871
+ if (g.every((x) => x === 0)) return hit("unspecified", true);
872
872
  if ((g[0] & 65472) === 65152) return hit("link-local", false);
873
873
  if ((g[0] & 65280) === 65280) return hit("multicast", false);
874
874
  if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
@@ -1759,8 +1759,13 @@ var init_dist = __esm({
1759
1759
  // AWS ECS task role
1760
1760
  "168.63.129.16",
1761
1761
  // Azure WireServer
1762
- "fd00:ec2::254"
1762
+ "fd00:ec2::254",
1763
1763
  // AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
1764
+ // Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
1765
+ // classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
1766
+ // live credential endpoint. It has to be named here, above the range check,
1767
+ // and it is the reason relaxing cgnat is safe.
1768
+ "100.100.100.200"
1764
1769
  ]);
1765
1770
  METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
1766
1771
  v4Octets = (a) => {
@@ -2737,30 +2742,86 @@ var init_daemon = __esm({
2737
2742
 
2738
2743
  // src/config-schema.ts
2739
2744
  import { z } from "zod";
2745
+ function formatIssues(issues) {
2746
+ const lines = issues.map((issue) => {
2747
+ const path14 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
2748
+ return ` \u2022 ${path14}: ${issue.message}`;
2749
+ });
2750
+ return `Invalid config:
2751
+ ${lines.join("\n")}`;
2752
+ }
2753
+ function prunePaths(root, paths) {
2754
+ let removed = false;
2755
+ const ordered = [...paths].sort((x, y) => {
2756
+ if (y.length !== x.length) return y.length - x.length;
2757
+ const xi = x[x.length - 1];
2758
+ const yi = y[y.length - 1];
2759
+ return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
2760
+ });
2761
+ for (const path14 of ordered) {
2762
+ let cur = root;
2763
+ for (const key of path14.slice(0, -1)) {
2764
+ if (cur === null || typeof cur !== "object") {
2765
+ cur = void 0;
2766
+ break;
2767
+ }
2768
+ cur = cur[key];
2769
+ }
2770
+ if (cur === null || typeof cur !== "object") continue;
2771
+ const last = path14[path14.length - 1];
2772
+ if (Array.isArray(cur)) {
2773
+ const i = Number(last);
2774
+ if (Number.isInteger(i) && i >= 0 && i < cur.length) {
2775
+ cur.splice(i, 1);
2776
+ removed = true;
2777
+ }
2778
+ continue;
2779
+ }
2780
+ const obj = cur;
2781
+ if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
2782
+ delete obj[String(last)];
2783
+ removed = true;
2784
+ }
2785
+ }
2786
+ return removed;
2787
+ }
2740
2788
  function sanitizeConfig(raw) {
2741
2789
  const result = ConfigFileSchema.safeParse(raw);
2742
2790
  if (result.success) {
2743
2791
  return { sanitized: result.data, error: null };
2744
2792
  }
2745
- const invalidTopLevelKeys = new Set(
2746
- result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
2747
- );
2748
- const sanitized = {};
2749
- if (typeof raw === "object" && raw !== null) {
2750
- for (const [key, value] of Object.entries(raw)) {
2751
- if (!invalidTopLevelKeys.has(key)) {
2752
- sanitized[key] = value;
2753
- }
2793
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
2794
+ return { sanitized: {}, error: formatIssues(result.error.issues) };
2795
+ }
2796
+ const working = structuredClone(raw);
2797
+ for (let level = 0; level < 6; level++) {
2798
+ for (let pass = 0; pass < 5; pass++) {
2799
+ const attempt = ConfigFileSchema.safeParse(working);
2800
+ if (attempt.success) break;
2801
+ const paths = attempt.error.issues.flatMap((issue) => {
2802
+ const at = issue.path;
2803
+ if (issue.code === "unrecognized_keys") {
2804
+ return issue.keys.map((k) => [...at, k]);
2805
+ }
2806
+ return [at.slice(0, -level || void 0)];
2807
+ }).filter((path14) => path14.length > 0);
2808
+ if (paths.length === 0 || !prunePaths(working, paths)) break;
2754
2809
  }
2810
+ if (ConfigFileSchema.safeParse(working).success) break;
2811
+ }
2812
+ const after = ConfigFileSchema.safeParse(working);
2813
+ const sanitized = {};
2814
+ const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
2815
+ after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
2816
+ );
2817
+ for (const [key, value] of Object.entries(working)) {
2818
+ if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
2755
2819
  }
2756
- const lines = result.error.issues.map((issue) => {
2757
- const path14 = issue.path.length > 0 ? issue.path.join(".") : "root";
2758
- return ` \u2022 ${path14}: ${issue.message}`;
2759
- });
2760
2820
  return {
2761
2821
  sanitized,
2762
- error: `Invalid config:
2763
- ${lines.join("\n")}`
2822
+ // The message names what the USER should fix, so it is built from the
2823
+ // original parse, not from whatever survived the prune.
2824
+ error: formatIssues(result.error.issues)
2764
2825
  };
2765
2826
  }
2766
2827
  var noNewlines, SmartConditionSchema, SmartRuleSchema, ConfigFileSchema;
@@ -2784,9 +2845,9 @@ var init_config_schema = __esm({
2784
2845
  "notMatchesGlob"
2785
2846
  ],
2786
2847
  {
2787
- errorMap: () => ({
2788
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
2789
- })
2848
+ // zod 4 replaced errorMap with `error`. The wording is kept verbatim:
2849
+ // it is what a user sees when their config is rejected.
2850
+ error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
2790
2851
  }
2791
2852
  ),
2792
2853
  value: z.string().optional(),
@@ -2804,7 +2865,7 @@ var init_config_schema = __esm({
2804
2865
  conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
2805
2866
  conditionMode: z.enum(["all", "any"]).optional(),
2806
2867
  verdict: z.enum(["allow", "review", "block"], {
2807
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
2868
+ error: () => "verdict must be one of: allow, review, block"
2808
2869
  }),
2809
2870
  reason: z.string().optional(),
2810
2871
  description: z.string().optional(),
@@ -2877,7 +2938,7 @@ var init_config_schema = __esm({
2877
2938
  sandboxPaths: z.array(z.string()).optional(),
2878
2939
  dangerousWords: z.array(noNewlines).optional(),
2879
2940
  ignoredTools: z.array(z.string()).optional(),
2880
- toolInspection: z.record(z.string()).optional(),
2941
+ toolInspection: z.record(z.string(), z.string()).optional(),
2881
2942
  smartRules: z.array(SmartRuleSchema).optional(),
2882
2943
  dlp: z.object({
2883
2944
  enabled: z.boolean().optional(),
@@ -2922,8 +2983,8 @@ var init_config_schema = __esm({
2922
2983
  roots: z.array(z.string()).optional()
2923
2984
  }).optional()
2924
2985
  }).optional(),
2925
- environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
2926
- }).strict({ message: "Config contains unknown top-level keys" });
2986
+ environments: z.record(z.string(), z.object({ requireApproval: z.boolean().optional() })).optional()
2987
+ }).strict();
2927
2988
  }
2928
2989
  });
2929
2990
 
package/dist/index.js CHANGED
@@ -265,9 +265,9 @@ var SmartConditionSchema = import_zod.z.object({
265
265
  "notMatchesGlob"
266
266
  ],
267
267
  {
268
- errorMap: () => ({
269
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
270
- })
268
+ // zod 4 replaced errorMap with `error`. The wording is kept verbatim:
269
+ // it is what a user sees when their config is rejected.
270
+ error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
271
271
  }
272
272
  ),
273
273
  value: import_zod.z.string().optional(),
@@ -285,7 +285,7 @@ var SmartRuleSchema = import_zod.z.object({
285
285
  conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
286
286
  conditionMode: import_zod.z.enum(["all", "any"]).optional(),
287
287
  verdict: import_zod.z.enum(["allow", "review", "block"], {
288
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
288
+ error: () => "verdict must be one of: allow, review, block"
289
289
  }),
290
290
  reason: import_zod.z.string().optional(),
291
291
  description: import_zod.z.string().optional(),
@@ -358,7 +358,7 @@ var ConfigFileSchema = import_zod.z.object({
358
358
  sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
359
359
  dangerousWords: import_zod.z.array(noNewlines).optional(),
360
360
  ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
361
- toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
361
+ toolInspection: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
362
362
  smartRules: import_zod.z.array(SmartRuleSchema).optional(),
363
363
  dlp: import_zod.z.object({
364
364
  enabled: import_zod.z.boolean().optional(),
@@ -403,32 +403,88 @@ var ConfigFileSchema = import_zod.z.object({
403
403
  roots: import_zod.z.array(import_zod.z.string()).optional()
404
404
  }).optional()
405
405
  }).optional(),
406
- environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
407
- }).strict({ message: "Config contains unknown top-level keys" });
406
+ environments: import_zod.z.record(import_zod.z.string(), import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
407
+ }).strict();
408
+ function formatIssues(issues) {
409
+ const lines = issues.map((issue) => {
410
+ const path15 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
411
+ return ` \u2022 ${path15}: ${issue.message}`;
412
+ });
413
+ return `Invalid config:
414
+ ${lines.join("\n")}`;
415
+ }
416
+ function prunePaths(root, paths) {
417
+ let removed = false;
418
+ const ordered = [...paths].sort((x, y) => {
419
+ if (y.length !== x.length) return y.length - x.length;
420
+ const xi = x[x.length - 1];
421
+ const yi = y[y.length - 1];
422
+ return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
423
+ });
424
+ for (const path15 of ordered) {
425
+ let cur = root;
426
+ for (const key of path15.slice(0, -1)) {
427
+ if (cur === null || typeof cur !== "object") {
428
+ cur = void 0;
429
+ break;
430
+ }
431
+ cur = cur[key];
432
+ }
433
+ if (cur === null || typeof cur !== "object") continue;
434
+ const last = path15[path15.length - 1];
435
+ if (Array.isArray(cur)) {
436
+ const i = Number(last);
437
+ if (Number.isInteger(i) && i >= 0 && i < cur.length) {
438
+ cur.splice(i, 1);
439
+ removed = true;
440
+ }
441
+ continue;
442
+ }
443
+ const obj = cur;
444
+ if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
445
+ delete obj[String(last)];
446
+ removed = true;
447
+ }
448
+ }
449
+ return removed;
450
+ }
408
451
  function sanitizeConfig(raw) {
409
452
  const result = ConfigFileSchema.safeParse(raw);
410
453
  if (result.success) {
411
454
  return { sanitized: result.data, error: null };
412
455
  }
413
- const invalidTopLevelKeys = new Set(
414
- result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
415
- );
416
- const sanitized = {};
417
- if (typeof raw === "object" && raw !== null) {
418
- for (const [key, value] of Object.entries(raw)) {
419
- if (!invalidTopLevelKeys.has(key)) {
420
- sanitized[key] = value;
421
- }
456
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
457
+ return { sanitized: {}, error: formatIssues(result.error.issues) };
458
+ }
459
+ const working = structuredClone(raw);
460
+ for (let level = 0; level < 6; level++) {
461
+ for (let pass = 0; pass < 5; pass++) {
462
+ const attempt = ConfigFileSchema.safeParse(working);
463
+ if (attempt.success) break;
464
+ const paths = attempt.error.issues.flatMap((issue) => {
465
+ const at = issue.path;
466
+ if (issue.code === "unrecognized_keys") {
467
+ return issue.keys.map((k) => [...at, k]);
468
+ }
469
+ return [at.slice(0, -level || void 0)];
470
+ }).filter((path15) => path15.length > 0);
471
+ if (paths.length === 0 || !prunePaths(working, paths)) break;
422
472
  }
473
+ if (ConfigFileSchema.safeParse(working).success) break;
474
+ }
475
+ const after = ConfigFileSchema.safeParse(working);
476
+ const sanitized = {};
477
+ const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
478
+ after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
479
+ );
480
+ for (const [key, value] of Object.entries(working)) {
481
+ if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
423
482
  }
424
- const lines = result.error.issues.map((issue) => {
425
- const path15 = issue.path.length > 0 ? issue.path.join(".") : "root";
426
- return ` \u2022 ${path15}: ${issue.message}`;
427
- });
428
483
  return {
429
484
  sanitized,
430
- error: `Invalid config:
431
- ${lines.join("\n")}`
485
+ // The message names what the USER should fix, so it is built from the
486
+ // original parse, not from whatever survived the prune.
487
+ error: formatIssues(result.error.issues)
432
488
  };
433
489
  }
434
490
 
@@ -3061,9 +3117,15 @@ var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
3061
3117
  // AWS ECS task role
3062
3118
  "168.63.129.16",
3063
3119
  // Azure WireServer
3064
- "fd00:ec2::254"
3120
+ "fd00:ec2::254",
3065
3121
  // AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
3122
+ // Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
3123
+ // classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
3124
+ // live credential endpoint. It has to be named here, above the range check,
3125
+ // and it is the reason relaxing cgnat is safe.
3126
+ "100.100.100.200"
3066
3127
  ]);
3128
+ var STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
3067
3129
  var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
3068
3130
  var v4Octets = (a) => {
3069
3131
  const p = a.split(".");
@@ -3086,7 +3148,7 @@ function classifySsrf(host) {
3086
3148
  if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
3087
3149
  const o = v4Octets(ip);
3088
3150
  if (o) {
3089
- if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", false);
3151
+ if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
3090
3152
  if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
3091
3153
  if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
3092
3154
  if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
@@ -3098,7 +3160,7 @@ function classifySsrf(host) {
3098
3160
  }
3099
3161
  const g = expandIpv6(ip);
3100
3162
  if (!g) return null;
3101
- if (g.every((x) => x === 0)) return hit("unspecified", false);
3163
+ if (g.every((x) => x === 0)) return hit("unspecified", true);
3102
3164
  if ((g[0] & 65472) === 65152) return hit("link-local", false);
3103
3165
  if ((g[0] & 65280) === 65280) return hit("multicast", false);
3104
3166
  if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
@@ -3111,7 +3173,7 @@ var TIER_REASON = {
3111
3173
  metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
3112
3174
  "link-local": "a link-local address",
3113
3175
  multicast: "a multicast address",
3114
- unspecified: "the unspecified address",
3176
+ unspecified: "the unspecified address, which reaches this host",
3115
3177
  cgnat: "a carrier-grade NAT address",
3116
3178
  private: "a loopback or private address"
3117
3179
  };
@@ -3122,7 +3184,7 @@ function ssrfFloor(tokens, opts = {}) {
3122
3184
  for (const { token, binary } of tokens) {
3123
3185
  const m = classifySsrf(token);
3124
3186
  if (!m) continue;
3125
- if (m.tier === "private" && !opts.ssrfStrict) continue;
3187
+ if (STRICT_TIERS.has(m.tier) && !opts.ssrfStrict) continue;
3126
3188
  if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
3127
3189
  return {
3128
3190
  ...m,
package/dist/index.mjs CHANGED
@@ -235,9 +235,9 @@ var SmartConditionSchema = z.object({
235
235
  "notMatchesGlob"
236
236
  ],
237
237
  {
238
- errorMap: () => ({
239
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
240
- })
238
+ // zod 4 replaced errorMap with `error`. The wording is kept verbatim:
239
+ // it is what a user sees when their config is rejected.
240
+ error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
241
241
  }
242
242
  ),
243
243
  value: z.string().optional(),
@@ -255,7 +255,7 @@ var SmartRuleSchema = z.object({
255
255
  conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
256
256
  conditionMode: z.enum(["all", "any"]).optional(),
257
257
  verdict: z.enum(["allow", "review", "block"], {
258
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
258
+ error: () => "verdict must be one of: allow, review, block"
259
259
  }),
260
260
  reason: z.string().optional(),
261
261
  description: z.string().optional(),
@@ -328,7 +328,7 @@ var ConfigFileSchema = z.object({
328
328
  sandboxPaths: z.array(z.string()).optional(),
329
329
  dangerousWords: z.array(noNewlines).optional(),
330
330
  ignoredTools: z.array(z.string()).optional(),
331
- toolInspection: z.record(z.string()).optional(),
331
+ toolInspection: z.record(z.string(), z.string()).optional(),
332
332
  smartRules: z.array(SmartRuleSchema).optional(),
333
333
  dlp: z.object({
334
334
  enabled: z.boolean().optional(),
@@ -373,32 +373,88 @@ var ConfigFileSchema = z.object({
373
373
  roots: z.array(z.string()).optional()
374
374
  }).optional()
375
375
  }).optional(),
376
- environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
377
- }).strict({ message: "Config contains unknown top-level keys" });
376
+ environments: z.record(z.string(), z.object({ requireApproval: z.boolean().optional() })).optional()
377
+ }).strict();
378
+ function formatIssues(issues) {
379
+ const lines = issues.map((issue) => {
380
+ const path15 = issue.path.length > 0 ? issue.path.map(String).join(".") : "root";
381
+ return ` \u2022 ${path15}: ${issue.message}`;
382
+ });
383
+ return `Invalid config:
384
+ ${lines.join("\n")}`;
385
+ }
386
+ function prunePaths(root, paths) {
387
+ let removed = false;
388
+ const ordered = [...paths].sort((x, y) => {
389
+ if (y.length !== x.length) return y.length - x.length;
390
+ const xi = x[x.length - 1];
391
+ const yi = y[y.length - 1];
392
+ return typeof xi === "number" && typeof yi === "number" ? yi - xi : 0;
393
+ });
394
+ for (const path15 of ordered) {
395
+ let cur = root;
396
+ for (const key of path15.slice(0, -1)) {
397
+ if (cur === null || typeof cur !== "object") {
398
+ cur = void 0;
399
+ break;
400
+ }
401
+ cur = cur[key];
402
+ }
403
+ if (cur === null || typeof cur !== "object") continue;
404
+ const last = path15[path15.length - 1];
405
+ if (Array.isArray(cur)) {
406
+ const i = Number(last);
407
+ if (Number.isInteger(i) && i >= 0 && i < cur.length) {
408
+ cur.splice(i, 1);
409
+ removed = true;
410
+ }
411
+ continue;
412
+ }
413
+ const obj = cur;
414
+ if (Object.prototype.hasOwnProperty.call(obj, String(last))) {
415
+ delete obj[String(last)];
416
+ removed = true;
417
+ }
418
+ }
419
+ return removed;
420
+ }
378
421
  function sanitizeConfig(raw) {
379
422
  const result = ConfigFileSchema.safeParse(raw);
380
423
  if (result.success) {
381
424
  return { sanitized: result.data, error: null };
382
425
  }
383
- const invalidTopLevelKeys = new Set(
384
- result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
385
- );
386
- const sanitized = {};
387
- if (typeof raw === "object" && raw !== null) {
388
- for (const [key, value] of Object.entries(raw)) {
389
- if (!invalidTopLevelKeys.has(key)) {
390
- sanitized[key] = value;
391
- }
426
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
427
+ return { sanitized: {}, error: formatIssues(result.error.issues) };
428
+ }
429
+ const working = structuredClone(raw);
430
+ for (let level = 0; level < 6; level++) {
431
+ for (let pass = 0; pass < 5; pass++) {
432
+ const attempt = ConfigFileSchema.safeParse(working);
433
+ if (attempt.success) break;
434
+ const paths = attempt.error.issues.flatMap((issue) => {
435
+ const at = issue.path;
436
+ if (issue.code === "unrecognized_keys") {
437
+ return issue.keys.map((k) => [...at, k]);
438
+ }
439
+ return [at.slice(0, -level || void 0)];
440
+ }).filter((path15) => path15.length > 0);
441
+ if (paths.length === 0 || !prunePaths(working, paths)) break;
392
442
  }
443
+ if (ConfigFileSchema.safeParse(working).success) break;
444
+ }
445
+ const after = ConfigFileSchema.safeParse(working);
446
+ const sanitized = {};
447
+ const invalidTopLevelKeys = after.success ? /* @__PURE__ */ new Set() : new Set(
448
+ after.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
449
+ );
450
+ for (const [key, value] of Object.entries(working)) {
451
+ if (!invalidTopLevelKeys.has(key)) sanitized[key] = value;
393
452
  }
394
- const lines = result.error.issues.map((issue) => {
395
- const path15 = issue.path.length > 0 ? issue.path.join(".") : "root";
396
- return ` \u2022 ${path15}: ${issue.message}`;
397
- });
398
453
  return {
399
454
  sanitized,
400
- error: `Invalid config:
401
- ${lines.join("\n")}`
455
+ // The message names what the USER should fix, so it is built from the
456
+ // original parse, not from whatever survived the prune.
457
+ error: formatIssues(result.error.issues)
402
458
  };
403
459
  }
404
460
 
@@ -3031,9 +3087,15 @@ var METADATA_ADDRESSES = /* @__PURE__ */ new Set([
3031
3087
  // AWS ECS task role
3032
3088
  "168.63.129.16",
3033
3089
  // Azure WireServer
3034
- "fd00:ec2::254"
3090
+ "fd00:ec2::254",
3035
3091
  // AWS IMDS over IPv6 (inside fc00::/7, which is NOT a tier)
3092
+ // Alibaba Cloud IMDS. It sits INSIDE 100.64/10, so before this line it was
3093
+ // classified as cgnat and therefore exemptable: one ssrfAllow entry opened a
3094
+ // live credential endpoint. It has to be named here, above the range check,
3095
+ // and it is the reason relaxing cgnat is safe.
3096
+ "100.100.100.200"
3036
3097
  ]);
3098
+ var STRICT_TIERS = /* @__PURE__ */ new Set(["private", "unspecified", "cgnat"]);
3037
3099
  var METADATA_HOSTNAMES = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.goog", "metadata"]);
3038
3100
  var v4Octets = (a) => {
3039
3101
  const p = a.split(".");
@@ -3056,7 +3118,7 @@ function classifySsrf(host) {
3056
3118
  if (METADATA_ADDRESSES.has(ip)) return hit("metadata", false);
3057
3119
  const o = v4Octets(ip);
3058
3120
  if (o) {
3059
- if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", false);
3121
+ if (o[0] === 0 && o[1] === 0 && o[2] === 0 && o[3] === 0) return hit("unspecified", true);
3060
3122
  if (o[0] === 169 && o[1] === 254) return hit("link-local", false);
3061
3123
  if (o[0] >= 224 && o[0] <= 239) return hit("multicast", false);
3062
3124
  if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return hit("cgnat", true);
@@ -3068,7 +3130,7 @@ function classifySsrf(host) {
3068
3130
  }
3069
3131
  const g = expandIpv6(ip);
3070
3132
  if (!g) return null;
3071
- if (g.every((x) => x === 0)) return hit("unspecified", false);
3133
+ if (g.every((x) => x === 0)) return hit("unspecified", true);
3072
3134
  if ((g[0] & 65472) === 65152) return hit("link-local", false);
3073
3135
  if ((g[0] & 65280) === 65280) return hit("multicast", false);
3074
3136
  if (g.slice(0, 7).every((x) => x === 0) && g[7] === 1) return hit("private", true);
@@ -3081,7 +3143,7 @@ var TIER_REASON = {
3081
3143
  metadata: "a cloud instance-metadata endpoint, the classic credential-theft target",
3082
3144
  "link-local": "a link-local address",
3083
3145
  multicast: "a multicast address",
3084
- unspecified: "the unspecified address",
3146
+ unspecified: "the unspecified address, which reaches this host",
3085
3147
  cgnat: "a carrier-grade NAT address",
3086
3148
  private: "a loopback or private address"
3087
3149
  };
@@ -3092,7 +3154,7 @@ function ssrfFloor(tokens, opts = {}) {
3092
3154
  for (const { token, binary } of tokens) {
3093
3155
  const m = classifySsrf(token);
3094
3156
  if (!m) continue;
3095
- if (m.tier === "private" && !opts.ssrfStrict) continue;
3157
+ if (STRICT_TIERS.has(m.tier) && !opts.ssrfStrict) continue;
3096
3158
  if (m.overridable && m.normalized && exempt.has(m.normalized)) continue;
3097
3159
  return {
3098
3160
  ...m,
package/dist/scan-ink.mjs CHANGED
@@ -61,9 +61,9 @@ var SmartConditionSchema = z.object({
61
61
  "notMatchesGlob"
62
62
  ],
63
63
  {
64
- errorMap: () => ({
65
- message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
66
- })
64
+ // zod 4 replaced errorMap with `error`. The wording is kept verbatim:
65
+ // it is what a user sees when their config is rejected.
66
+ error: () => "op must be one of: matches, notMatches, contains, notContains, exists, notExists, matchesGlob, notMatchesGlob"
67
67
  }
68
68
  ),
69
69
  value: z.string().optional(),
@@ -81,7 +81,7 @@ var SmartRuleSchema = z.object({
81
81
  conditions: z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
82
82
  conditionMode: z.enum(["all", "any"]).optional(),
83
83
  verdict: z.enum(["allow", "review", "block"], {
84
- errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
84
+ error: () => "verdict must be one of: allow, review, block"
85
85
  }),
86
86
  reason: z.string().optional(),
87
87
  description: z.string().optional(),
@@ -154,7 +154,7 @@ var ConfigFileSchema = z.object({
154
154
  sandboxPaths: z.array(z.string()).optional(),
155
155
  dangerousWords: z.array(noNewlines).optional(),
156
156
  ignoredTools: z.array(z.string()).optional(),
157
- toolInspection: z.record(z.string()).optional(),
157
+ toolInspection: z.record(z.string(), z.string()).optional(),
158
158
  smartRules: z.array(SmartRuleSchema).optional(),
159
159
  dlp: z.object({
160
160
  enabled: z.boolean().optional(),
@@ -199,8 +199,8 @@ var ConfigFileSchema = z.object({
199
199
  roots: z.array(z.string()).optional()
200
200
  }).optional()
201
201
  }).optional(),
202
- environments: z.record(z.object({ requireApproval: z.boolean().optional() })).optional()
203
- }).strict({ message: "Config contains unknown top-level keys" });
202
+ environments: z.record(z.string(), z.object({ requireApproval: z.boolean().optional() })).optional()
203
+ }).strict();
204
204
 
205
205
  // src/shields.ts
206
206
  import fs from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "2.9.1",
3
+ "version": "2.9.3",
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",
@@ -94,23 +94,23 @@
94
94
  "smol-toml": "^1.6.1",
95
95
  "string-width": "^4.2.3",
96
96
  "yaml": "^2.9.0",
97
- "zod": "^3.25.76"
97
+ "zod": "^4.5.4"
98
98
  },
99
99
  "bundleDependencies": [
100
100
  "mvdan-sh"
101
101
  ],
102
102
  "devDependencies": {
103
- "@anthropic-ai/sdk": "^0.78.0",
103
+ "@anthropic-ai/sdk": "^0.124.0",
104
104
  "@octokit/rest": "^22.0.1",
105
105
  "@semantic-release/commit-analyzer": "^13.0.1",
106
106
  "@semantic-release/git": "^10.0.1",
107
107
  "@semantic-release/github": "^12.0.6",
108
108
  "@semantic-release/npm": "^13.1.5",
109
109
  "@semantic-release/release-notes-generator": "^14.1.0",
110
- "@types/node": "^25.3.1",
110
+ "@types/node": "^26.4.1",
111
111
  "@types/picomatch": "^4.0.2",
112
112
  "@types/react": "^19.2.14",
113
- "@vitest/coverage-v8": "4.1.2",
113
+ "@vitest/coverage-v8": "^5.0.0",
114
114
  "cross-env": "^10.1.0",
115
115
  "ink-testing-library": "^4.0.0",
116
116
  "prettier": "^3.4.2",
@@ -119,7 +119,7 @@
119
119
  "tsx": "^4.21.0",
120
120
  "typescript": "^5.9.3",
121
121
  "typescript-eslint": "^8.20.0",
122
- "vitest": "4.1.2"
122
+ "vitest": "^5.0.0"
123
123
  },
124
124
  "overrides": {
125
125
  "undici": "^7.24.0",