@tormentalabs/claude-code-wire-compat 0.5.0 → 0.7.0-rc.1

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 (91) hide show
  1. package/CHANGELOG.md +226 -0
  2. package/README.md +19 -7
  3. package/dist/betas.d.ts +65 -7
  4. package/dist/betas.d.ts.map +1 -1
  5. package/dist/betas.js +143 -2
  6. package/dist/betas.js.map +1 -1
  7. package/dist/build-request.d.ts +12 -6
  8. package/dist/build-request.d.ts.map +1 -1
  9. package/dist/build-request.js +93 -50
  10. package/dist/build-request.js.map +1 -1
  11. package/dist/contracts.d.ts +7 -2
  12. package/dist/contracts.d.ts.map +1 -1
  13. package/dist/contracts.js.map +1 -1
  14. package/dist/fingerprint.d.ts.map +1 -1
  15. package/dist/fingerprint.js +5 -0
  16. package/dist/fingerprint.js.map +1 -1
  17. package/dist/headers.d.ts.map +1 -1
  18. package/dist/headers.js +5 -2
  19. package/dist/headers.js.map +1 -1
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/limits.d.ts +3 -0
  25. package/dist/limits.d.ts.map +1 -0
  26. package/dist/limits.js +4 -0
  27. package/dist/limits.js.map +1 -0
  28. package/dist/model-capabilities.d.ts +3 -3
  29. package/dist/model-capabilities.d.ts.map +1 -1
  30. package/dist/model-capabilities.js +55 -19
  31. package/dist/model-capabilities.js.map +1 -1
  32. package/dist/model-identity.d.ts +9 -1
  33. package/dist/model-identity.d.ts.map +1 -1
  34. package/dist/model-identity.js +19 -1
  35. package/dist/model-identity.js.map +1 -1
  36. package/dist/model-queries.d.ts +17 -5
  37. package/dist/model-queries.d.ts.map +1 -1
  38. package/dist/model-queries.js +33 -6
  39. package/dist/model-queries.js.map +1 -1
  40. package/dist/models.js +1 -1
  41. package/dist/models.js.map +1 -1
  42. package/dist/profiles/beta-registry-2.1.280.d.ts +211 -0
  43. package/dist/profiles/beta-registry-2.1.280.d.ts.map +1 -0
  44. package/dist/profiles/beta-registry-2.1.280.js +292 -0
  45. package/dist/profiles/beta-registry-2.1.280.js.map +1 -0
  46. package/dist/profiles/claude-code-2.1.280.d.ts +3 -0
  47. package/dist/profiles/claude-code-2.1.280.d.ts.map +1 -0
  48. package/dist/profiles/claude-code-2.1.280.js +359 -0
  49. package/dist/profiles/claude-code-2.1.280.js.map +1 -0
  50. package/dist/redaction.d.ts.map +1 -1
  51. package/dist/redaction.js +48 -2
  52. package/dist/redaction.js.map +1 -1
  53. package/dist/request-body.d.ts +4 -2
  54. package/dist/request-body.d.ts.map +1 -1
  55. package/dist/request-body.js +77 -48
  56. package/dist/request-body.js.map +1 -1
  57. package/dist/system-prompt.d.ts.map +1 -1
  58. package/dist/system-prompt.js +23 -22
  59. package/dist/system-prompt.js.map +1 -1
  60. package/dist/thinking.d.ts +53 -2
  61. package/dist/thinking.d.ts.map +1 -1
  62. package/dist/thinking.js +124 -26
  63. package/dist/thinking.js.map +1 -1
  64. package/dist/unicode.d.ts +41 -0
  65. package/dist/unicode.d.ts.map +1 -1
  66. package/dist/unicode.js +37 -0
  67. package/dist/unicode.js.map +1 -1
  68. package/dist/violation.d.ts +23 -0
  69. package/dist/violation.d.ts.map +1 -0
  70. package/dist/violation.js +210 -0
  71. package/dist/violation.js.map +1 -0
  72. package/package.json +6 -2
  73. package/src/betas.ts +208 -9
  74. package/src/build-request.ts +120 -56
  75. package/src/contracts.ts +7 -2
  76. package/src/fingerprint.ts +5 -0
  77. package/src/headers.ts +4 -2
  78. package/src/index.ts +2 -0
  79. package/src/limits.ts +4 -0
  80. package/src/model-capabilities.ts +55 -19
  81. package/src/model-identity.ts +14 -1
  82. package/src/model-queries.ts +38 -6
  83. package/src/models.ts +1 -1
  84. package/src/profiles/beta-registry-2.1.280.ts +309 -0
  85. package/src/profiles/claude-code-2.1.280.ts +364 -0
  86. package/src/redaction.ts +65 -2
  87. package/src/request-body.ts +107 -49
  88. package/src/system-prompt.ts +32 -24
  89. package/src/thinking.ts +148 -26
  90. package/src/unicode.ts +71 -0
  91. package/src/violation.ts +226 -0
@@ -1,5 +1,7 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-or-later
2
2
 
3
+ import { MAX_INPUT_SIZE as MAX_SIZE } from "./limits.js";
4
+
3
5
  import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
4
6
  import { clampMaxTokens, resolveThinking } from "./thinking.js";
5
7
  import type { ThinkingDisplay, ThinkingRequest } from "./thinking.js";
@@ -29,11 +31,15 @@ import type {
29
31
  import { deriveCapabilities } from "./model-capabilities.js";
30
32
  import { stripModelMarkers } from "./model-identity.js";
31
33
  import { IDENTITY_TEXT } from "./system-prompt.js";
32
- import { classifySurrogateAt } from "./unicode.js";
34
+ import {
35
+ inspectText,
36
+ TEXT_POLICY_IDENTIFIER,
37
+ TEXT_POLICY_PROSE,
38
+ } from "./unicode.js";
39
+ import { violationDetails, type ViolationPathSegment } from "./violation.js";
33
40
 
34
41
  const MAX_DEPTH = 100;
35
42
  const MAX_ITEMS = 100_000;
36
- const MAX_SIZE = 1_000_000;
37
43
  const FORBIDDEN_KEYS = new Set(["__proto__", "prototype", "constructor"]);
38
44
  const MESSAGE_KEYS = new Set(["role", "content"]);
39
45
  const CACHE_CONTROL_KEYS = new Set(["type", "ttl"]);
@@ -357,13 +363,16 @@ type ModelResolution = Readonly<{
357
363
  contextManagement: boolean;
358
364
  temperature: boolean;
359
365
  rejectsDisabledThinking: boolean;
366
+ midConvToolChange: boolean;
367
+ perTurnEffort: boolean;
360
368
  }>;
361
369
  }>;
362
370
 
363
371
  function fail(
364
372
  code: ConstructorParameters<typeof ClaudeCodeWireError>[0],
373
+ safeDetails: Readonly<Record<string, string | number | boolean>> = {},
365
374
  ): never {
366
- throw new ClaudeCodeWireError(code);
375
+ throw new ClaudeCodeWireError(code, safeDetails);
367
376
  }
368
377
 
369
378
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -378,25 +387,21 @@ function inspectString(
378
387
  value: string,
379
388
  state: InspectionState,
380
389
  validateString?: (value: string) => void,
390
+ path: readonly ViolationPathSegment[] = [],
391
+ inKey = false,
381
392
  ): void {
382
393
  state.size += value.length;
383
394
  if (state.size > MAX_SIZE) fail("INPUT_TOO_LARGE");
384
395
  validateString?.(value);
385
396
 
386
- for (let index = 0; index < value.length; index += 1) {
387
- const unit = value.charCodeAt(index);
388
- if (
389
- unit <= 0x08 ||
390
- unit === 0x0b ||
391
- unit === 0x0c ||
392
- (unit >= 0x0e && unit <= 0x1f) ||
393
- unit === 0x7f
394
- ) {
395
- fail("INVALID_INPUT");
396
- }
397
- const classification = classifySurrogateAt(value, index);
398
- if (classification === "loneSurrogate") fail("INVALID_UNICODE");
399
- if (classification === "surrogatePair") index += 1;
397
+ // Body prose policy (P1.T1): only a lone surrogate can fail here now, so
398
+ // the historical INVALID_INPUT-for-controls branch is gone by construction.
399
+ const violation = inspectText(value, TEXT_POLICY_PROSE);
400
+ if (violation !== null) {
401
+ fail(
402
+ "INVALID_UNICODE",
403
+ violationDetails(violation, path, value.length, inKey),
404
+ );
400
405
  }
401
406
  }
402
407
 
@@ -405,11 +410,12 @@ function inspect(
405
410
  depth: number,
406
411
  state: InspectionState,
407
412
  validateString?: (value: string) => void,
413
+ path: ViolationPathSegment[] = [],
408
414
  ): void {
409
415
  if (depth > MAX_DEPTH) fail("INPUT_TOO_DEEP");
410
416
  if (value === null || typeof value === "boolean") return;
411
417
  if (typeof value === "string") {
412
- inspectString(value, state, validateString);
418
+ inspectString(value, state, validateString, path, false);
413
419
  return;
414
420
  }
415
421
  if (typeof value === "number") {
@@ -428,7 +434,9 @@ function inspect(
428
434
  if (state.size > MAX_SIZE) fail("INPUT_TOO_LARGE");
429
435
  for (let index = 0; index < value.length; index += 1) {
430
436
  if (!hasOwn(value, String(index))) fail("INVALID_INPUT");
431
- inspect(value[index], depth + 1, state, validateString);
437
+ path.push(index);
438
+ inspect(value[index], depth + 1, state, validateString, path);
439
+ path.pop();
432
440
  }
433
441
  } else {
434
442
  const prototype = Reflect.getPrototypeOf(value);
@@ -439,27 +447,37 @@ function inspect(
439
447
  if (typeof key !== "string" || FORBIDDEN_KEYS.has(key)) {
440
448
  fail("INVALID_INPUT");
441
449
  }
442
- inspectString(key, state, validateString);
450
+ // Object keys stay strings: digits inside user keys must not become
451
+ // readable numeric path segments (QA F1).
452
+ path.push(key);
453
+ inspectString(key, state, validateString, path, true);
443
454
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
444
455
  if (descriptor === undefined || !("value" in descriptor)) {
445
456
  fail("INVALID_INPUT");
446
457
  }
447
- inspect(descriptor.value, depth + 1, state, validateString);
458
+ inspect(descriptor.value, depth + 1, state, validateString, path);
459
+ path.pop();
448
460
  }
449
461
  }
450
462
  state.active.delete(value);
451
463
  }
452
464
 
465
+ /** Paths without a caller hint are relative to the inspected value's root. */
453
466
  export function inspectJsonInputs(
454
467
  values: readonly unknown[],
455
468
  validateString?: (value: string) => void,
469
+ paths?: readonly (readonly ViolationPathSegment[])[],
456
470
  ): void {
457
471
  const state: InspectionState = {
458
472
  active: new WeakSet(),
459
473
  items: 0,
460
474
  size: 0,
461
475
  };
462
- for (const value of values) inspect(value, 0, state, validateString);
476
+ for (let index = 0; index < values.length; index += 1) {
477
+ inspect(values[index], 0, state, validateString, [
478
+ ...(paths?.[index] ?? []),
479
+ ]);
480
+ }
463
481
  }
464
482
 
465
483
  function requireRecord(value: unknown): Record<string, unknown> {
@@ -481,6 +499,13 @@ function requireString(value: unknown): string {
481
499
  return value;
482
500
  }
483
501
 
502
+ function requireIdentifier(value: unknown): string {
503
+ const text = requireString(value);
504
+ if (inspectText(text, TEXT_POLICY_IDENTIFIER) !== null)
505
+ fail("INVALID_UNICODE");
506
+ return text;
507
+ }
508
+
484
509
  function requirePositiveInteger(value: unknown): number {
485
510
  if (!Number.isSafeInteger(value) || typeof value !== "number" || value <= 0) {
486
511
  fail("INVALID_INPUT");
@@ -827,7 +852,7 @@ function imageSource(value: unknown): ImageBlock["source"] {
827
852
  )
828
853
  fail("INVALID_INPUT");
829
854
  entries.push([key, item]);
830
- } else entries.push([key, requireString(item)]);
855
+ } else entries.push([key, requireIdentifier(item)]);
831
856
  }
832
857
  return Object.fromEntries(entries) as unknown as ImageBlock["source"];
833
858
  }
@@ -888,7 +913,11 @@ function documentSource(value: unknown): DocumentBlock["source"] {
888
913
  }),
889
914
  ]);
890
915
  }
891
- } else entries.push([key, requireString(item)]);
916
+ } else
917
+ entries.push([
918
+ key,
919
+ type === "text" ? requireString(item) : requireIdentifier(item),
920
+ ]);
892
921
  }
893
922
  return Object.fromEntries(entries) as unknown as DocumentBlock["source"];
894
923
  }
@@ -923,7 +952,11 @@ function thinkingBlock(
923
952
  if (key === "type") entries.push([key, "thinking"]);
924
953
  else if (key === "cache_control")
925
954
  entries.push([key, nullable(item, cacheControl)]);
926
- else entries.push([key, requireString(item)]);
955
+ else
956
+ entries.push([
957
+ key,
958
+ key === "signature" ? requireIdentifier(item) : requireString(item),
959
+ ]);
927
960
  }
928
961
  return Object.fromEntries(entries) as unknown as ThinkingBlock;
929
962
  }
@@ -948,7 +981,7 @@ function redactedThinkingBlock(
948
981
  if (key === "type") entries.push([key, "redacted_thinking"]);
949
982
  else if (key === "cache_control")
950
983
  entries.push([key, nullable(item, cacheControl)]);
951
- else entries.push([key, requireString(item)]);
984
+ else entries.push([key, requireIdentifier(item)]);
952
985
  }
953
986
  return Object.fromEntries(entries) as unknown as RedactedThinkingBlock;
954
987
  }
@@ -964,8 +997,8 @@ function searchResultBlock(value: unknown): SearchResultBlock {
964
997
  if (key === "content") {
965
998
  if (!Array.isArray(item)) fail("INVALID_INPUT");
966
999
  entries.push([key, item.map((block) => textBlock(block))]);
967
- } else if (key === "source" || key === "title")
968
- entries.push([key, requireString(item)]);
1000
+ } else if (key === "source") entries.push([key, requireIdentifier(item)]);
1001
+ else if (key === "title") entries.push([key, requireString(item)]);
969
1002
  else if (key === "type") entries.push([key, "search_result"]);
970
1003
  else if (key === "cache_control")
971
1004
  entries.push([key, nullable(item, cacheControl)]);
@@ -982,7 +1015,7 @@ function toolReferenceBlock(value: unknown): ToolReferenceBlock {
982
1015
  const entries: [string, unknown][] = [];
983
1016
  for (const key of Object.keys(record)) {
984
1017
  const item = record[key];
985
- if (key === "tool_name") entries.push([key, requireString(item)]);
1018
+ if (key === "tool_name") entries.push([key, requireIdentifier(item)]);
986
1019
  else if (key === "type") entries.push([key, "tool_reference"]);
987
1020
  else entries.push([key, nullable(item, cacheControl)]);
988
1021
  }
@@ -998,7 +1031,7 @@ function toolUseBlock(value: unknown): ToolUseBlock {
998
1031
  for (const key of Object.keys(record)) {
999
1032
  const item = record[key];
1000
1033
  if (key === "id" || key === "name")
1001
- entries.push([key, requireString(item)]);
1034
+ entries.push([key, requireIdentifier(item)]);
1002
1035
  else if (key === "input") entries.push([key, validatedJson(item)]);
1003
1036
  else if (key === "type") entries.push([key, "tool_use"]);
1004
1037
  else if (key === "cache_control")
@@ -1023,7 +1056,7 @@ function toolCaller(value: unknown): ToolUseBlock["caller"] {
1023
1056
  return Object.fromEntries(
1024
1057
  Object.keys(record).map((key) => [
1025
1058
  key,
1026
- key === "type" ? type : requireString(record[key]),
1059
+ key === "type" ? type : requireIdentifier(record[key]),
1027
1060
  ]),
1028
1061
  ) as unknown as NonNullable<ToolUseBlock["caller"]>;
1029
1062
  }
@@ -1046,7 +1079,7 @@ function toolResultBlock(value: unknown): ToolResultBlock {
1046
1079
  const entries: [string, unknown][] = [];
1047
1080
  for (const key of Object.keys(record)) {
1048
1081
  const item = record[key];
1049
- if (key === "tool_use_id") entries.push([key, requireString(item)]);
1082
+ if (key === "tool_use_id") entries.push([key, requireIdentifier(item)]);
1050
1083
  else if (key === "type") entries.push([key, "tool_result"]);
1051
1084
  else if (key === "cache_control")
1052
1085
  entries.push([key, nullable(item, cacheControl)]);
@@ -1325,8 +1358,8 @@ function customToolDefinition(record: Record<string, unknown>): ToolDefinition {
1325
1358
  for (const key of Object.keys(record)) {
1326
1359
  const item = record[key];
1327
1360
  if (key === "input_schema") entries.push([key, toolInputSchema(item)]);
1328
- else if (key === "name" || key === "description")
1329
- entries.push([key, requireString(item)]);
1361
+ else if (key === "name") entries.push([key, requireIdentifier(item)]);
1362
+ else if (key === "description") entries.push([key, requireString(item)]);
1330
1363
  else if (key === "allowed_callers")
1331
1364
  entries.push([key, allowedCallers(item)]);
1332
1365
  else if (key === "cache_control")
@@ -1355,7 +1388,7 @@ function builtInToolDefinition(
1355
1388
  if (item !== spec.name) fail("INVALID_INPUT");
1356
1389
  entries.push([key, item]);
1357
1390
  } else if (key === "mcp_server_name" || key === "model")
1358
- entries.push([key, requireString(item)]);
1391
+ entries.push([key, requireIdentifier(item)]);
1359
1392
  else if (key === "allowed_callers")
1360
1393
  entries.push([key, allowedCallers(item)]);
1361
1394
  else if (key === "cache_control" || key === "caching")
@@ -1399,7 +1432,7 @@ function tools(value: unknown): readonly ToolDefinition[] {
1399
1432
  ? builtInToolDefinition(record)
1400
1433
  : customToolDefinition(record);
1401
1434
  if (hasOwn(record, "name")) {
1402
- const name = requireString(record["name"]);
1435
+ const name = requireIdentifier(record["name"]);
1403
1436
  if (names.has(name)) fail("INVALID_INPUT");
1404
1437
  names.add(name);
1405
1438
  }
@@ -1451,13 +1484,17 @@ function modelResolution(
1451
1484
  (capabilities.temperature !== undefined &&
1452
1485
  typeof capabilities.temperature !== "boolean") ||
1453
1486
  (capabilities.rejectsDisabledThinking !== undefined &&
1454
- typeof capabilities.rejectsDisabledThinking !== "boolean")
1487
+ typeof capabilities.rejectsDisabledThinking !== "boolean") ||
1488
+ (capabilities.midConvToolChange !== undefined &&
1489
+ typeof capabilities.midConvToolChange !== "boolean") ||
1490
+ (capabilities.perTurnEffort !== undefined &&
1491
+ typeof capabilities.perTurnEffort !== "boolean")
1455
1492
  ) {
1456
1493
  fail("INVALID_INPUT");
1457
1494
  }
1458
1495
  return {
1459
- id: requireString(record["id"]),
1460
- wireId: requireString(record["wireId"]),
1496
+ id: requireIdentifier(record["id"]),
1497
+ wireId: requireIdentifier(record["wireId"]),
1461
1498
  capabilities: {
1462
1499
  thinking: capabilityBoolean(capabilities.thinking, derived.thinking),
1463
1500
  adaptiveThinking: capabilityBoolean(
@@ -1486,6 +1523,14 @@ function modelResolution(
1486
1523
  capabilities.rejectsDisabledThinking,
1487
1524
  derived.rejectsDisabledThinking,
1488
1525
  ),
1526
+ midConvToolChange: capabilityBoolean(
1527
+ capabilities.midConvToolChange,
1528
+ derived.midConvToolChange,
1529
+ ),
1530
+ perTurnEffort: capabilityBoolean(
1531
+ capabilities.perTurnEffort,
1532
+ derived.perTurnEffort,
1533
+ ),
1489
1534
  },
1490
1535
  };
1491
1536
  }
@@ -1633,7 +1678,8 @@ function toolChoice(value: unknown): Readonly<Record<string, unknown>> {
1633
1678
  const entries: [string, unknown][] = [];
1634
1679
  for (const key of Object.keys(record)) {
1635
1680
  if (key === "type") entries.push([key, type]);
1636
- else if (key === "name") entries.push([key, requireString(record[key])]);
1681
+ else if (key === "name")
1682
+ entries.push([key, requireIdentifier(record[key])]);
1637
1683
  else entries.push([key, requireBoolean(record[key])]);
1638
1684
  }
1639
1685
  return Object.fromEntries(entries);
@@ -1731,14 +1777,19 @@ export function buildCanonicalBody(
1731
1777
  rawSystemBlocks: unknown,
1732
1778
  rawMetadata: unknown,
1733
1779
  profile?: ClaudeCodeProtocolProfile,
1780
+ thinkingDisplayOverride?: "updates",
1734
1781
  ): Readonly<Record<string, unknown>> {
1735
- inspectJsonInputs([
1736
- rawInput,
1737
- rawResolvedModel,
1738
- rawSystemBlocks,
1739
- rawMetadata,
1740
- ...(profile === undefined ? [] : [profile]),
1741
- ]);
1782
+ inspectJsonInputs(
1783
+ [
1784
+ rawInput,
1785
+ rawResolvedModel,
1786
+ rawSystemBlocks,
1787
+ rawMetadata,
1788
+ ...(profile === undefined ? [] : [profile]),
1789
+ ],
1790
+ undefined,
1791
+ [[], ["model"], ["system"], ["metadata"], ["profileOverride"]],
1792
+ );
1742
1793
 
1743
1794
  const input = requireRecord(rawInput);
1744
1795
  assertExactKeys(input, INPUT_KEY_SET);
@@ -1837,6 +1888,7 @@ export function buildCanonicalBody(
1837
1888
  effectiveProfile.betaPolicy,
1838
1889
  maxTokens,
1839
1890
  effectiveProfile,
1891
+ thinkingDisplayOverride,
1840
1892
  );
1841
1893
  if (resolved.emitted !== undefined) result["thinking"] = resolved.emitted;
1842
1894
 
@@ -1902,9 +1954,15 @@ export function buildCanonicalBody(
1902
1954
  result["output_format"] = nullable(item, outputFormat);
1903
1955
  else if (key === "toolChoice") {
1904
1956
  const validatedToolChoice = toolChoice(item);
1957
+ // Upstream demotes only `tool` here. This package deliberately also
1958
+ // demotes `any`: the Messages API rejects every forced tool choice while
1959
+ // extended thinking is on, so passing `any` through could only produce
1960
+ // an HTTP 400. Shared by every profile; see `MEMORY.md`, 2026-09-24.
1961
+ const forcedToolChoice =
1962
+ validatedToolChoice["type"] === "tool" ||
1963
+ validatedToolChoice["type"] === "any";
1905
1964
  result["tool_choice"] =
1906
- validatedToolChoice["type"] === "tool" &&
1907
- resolved.extendedThinkingActive
1965
+ forcedToolChoice && resolved.extendedThinkingActive
1908
1966
  ? { type: "auto" }
1909
1967
  : validatedToolChoice;
1910
1968
  } else if (key === "topP") result["top_p"] = requireNumber(item);
@@ -1,12 +1,15 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-or-later
2
2
 
3
+ import { MAX_INPUT_SIZE } from "./limits.js";
4
+
3
5
  import type {
4
6
  ClaudeCodeRuntimeIdentity,
5
7
  SystemInput,
6
8
  TextBlock,
7
9
  } from "./contracts.js";
8
10
  import { ClaudeCodeWireError } from "./contracts.js";
9
- import { classifySurrogateAt } from "./unicode.js";
11
+ import { inspectText, TEXT_POLICY_PROSE } from "./unicode.js";
12
+ import { violationDetails, type ViolationPathSegment } from "./violation.js";
10
13
 
11
14
  /**
12
15
  * The pinned identity text, byte-exact.
@@ -26,7 +29,6 @@ import { classifySurrogateAt } from "./unicode.js";
26
29
  export const IDENTITY_TEXT =
27
30
  "You are Claude Code, Anthropic's official CLI for Claude.";
28
31
  const MAX_INPUT_DEPTH = 64;
29
- const MAX_INPUT_SIZE = 1_000_000;
30
32
  type UnknownRecord = Readonly<Record<PropertyKey, unknown>>;
31
33
 
32
34
  function fail(
@@ -36,38 +38,34 @@ function fail(
36
38
  | "INPUT_TOO_DEEP"
37
39
  | "INPUT_TOO_LARGE"
38
40
  | "CYCLIC_INPUT",
41
+ safeDetails: Readonly<Record<string, string | number | boolean>> = {},
39
42
  ): never {
40
- throw new ClaudeCodeWireError(code);
43
+ throw new ClaudeCodeWireError(code, safeDetails);
41
44
  }
42
45
 
43
- function validateText(text: string): void {
44
- for (let index = 0; index < text.length; index += 1) {
45
- const codeUnit = text.charCodeAt(index);
46
-
47
- if (
48
- codeUnit === 0 ||
49
- (codeUnit < 0x20 &&
50
- codeUnit !== 0x09 &&
51
- codeUnit !== 0x0a &&
52
- codeUnit !== 0x0d) ||
53
- (codeUnit >= 0x7f && codeUnit <= 0x9f)
54
- ) {
55
- fail("INVALID_UNICODE");
56
- }
57
-
58
- const classification = classifySurrogateAt(text, index);
59
- if (classification === "loneSurrogate") fail("INVALID_UNICODE");
60
- if (classification === "surrogatePair") index += 1;
46
+ function validateText(
47
+ text: string,
48
+ path: readonly ViolationPathSegment[],
49
+ inKey: boolean,
50
+ ): void {
51
+ // Body prose policy (P1.T1): the system field follows the same rule as
52
+ // every other body lane — only lone surrogates are rejected.
53
+ const violation = inspectText(text, TEXT_POLICY_PROSE);
54
+ if (violation !== null) {
55
+ fail(
56
+ "INVALID_UNICODE",
57
+ violationDetails(violation, path, text.length, inKey),
58
+ );
61
59
  }
62
60
  }
63
61
 
64
62
  function isUnknownRecord(value: unknown): value is UnknownRecord {
65
63
  return value !== null && typeof value === "object";
66
64
  }
67
-
68
65
  function validateStructure(value: unknown): void {
69
66
  const ancestors = new WeakSet();
70
67
  let size = 0;
68
+ const path: ViolationPathSegment[] = ["system"];
71
69
 
72
70
  function visit(current: unknown, depth: number): void {
73
71
  if (depth > MAX_INPUT_DEPTH) fail("INPUT_TOO_DEEP");
@@ -75,7 +73,7 @@ function validateStructure(value: unknown): void {
75
73
  if (typeof current === "string") {
76
74
  size += current.length;
77
75
  if (size > MAX_INPUT_SIZE) fail("INPUT_TOO_LARGE");
78
- validateText(current);
76
+ validateText(current, path, false);
79
77
  return;
80
78
  }
81
79
 
@@ -84,12 +82,22 @@ function validateStructure(value: unknown): void {
84
82
 
85
83
  ancestors.add(current);
86
84
  for (const key of Reflect.ownKeys(current)) {
85
+ // Only ARRAY indices become numeric segments (QA F1/F4); object keys
86
+ // stay strings and digits inside them render as `*`.
87
+ const segment: ViolationPathSegment =
88
+ typeof key !== "string"
89
+ ? "*"
90
+ : Array.isArray(current) && /^(?:0|[1-9]\d{0,5})$/u.test(key)
91
+ ? Number(key)
92
+ : key;
93
+ path.push(segment);
87
94
  if (typeof key === "string") {
88
95
  size += key.length;
89
96
  if (size > MAX_INPUT_SIZE) fail("INPUT_TOO_LARGE");
90
- validateText(key);
97
+ validateText(key, path, true);
91
98
  }
92
99
  visit(current[key], depth + 1);
100
+ path.pop();
93
101
  }
94
102
  ancestors.delete(current);
95
103
  }