@tormentalabs/claude-code-wire-compat 0.6.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.
- package/CHANGELOG.md +59 -0
- package/dist/build-request.d.ts.map +1 -1
- package/dist/build-request.js +49 -35
- package/dist/build-request.js.map +1 -1
- package/dist/limits.d.ts +3 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +4 -0
- package/dist/limits.js.map +1 -0
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +45 -2
- package/dist/redaction.js.map +1 -1
- package/dist/request-body.d.ts +3 -1
- package/dist/request-body.d.ts.map +1 -1
- package/dist/request-body.js +61 -43
- package/dist/request-body.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +23 -22
- package/dist/system-prompt.js.map +1 -1
- package/dist/unicode.d.ts +41 -0
- package/dist/unicode.d.ts.map +1 -1
- package/dist/unicode.js +37 -0
- package/dist/unicode.js.map +1 -1
- package/dist/violation.d.ts +23 -0
- package/dist/violation.d.ts.map +1 -0
- package/dist/violation.js +210 -0
- package/dist/violation.js.map +1 -0
- package/package.json +1 -1
- package/src/build-request.ts +66 -35
- package/src/limits.ts +4 -0
- package/src/redaction.ts +62 -2
- package/src/request-body.ts +82 -46
- package/src/system-prompt.ts +32 -24
- package/src/unicode.ts +71 -0
- package/src/violation.ts +226 -0
package/src/redaction.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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
|
ClaudeCodeCapabilityDecisions,
|
|
5
7
|
ClaudeCodeModelFamily,
|
|
@@ -10,6 +12,11 @@ import type {
|
|
|
10
12
|
} from "./contracts.js";
|
|
11
13
|
import { ClaudeCodeWireError } from "./contracts.js";
|
|
12
14
|
import { classifySurrogateAt } from "./unicode.js";
|
|
15
|
+
import {
|
|
16
|
+
isValidViolationCodeUnit,
|
|
17
|
+
isValidViolationPath,
|
|
18
|
+
isValidViolationReason,
|
|
19
|
+
} from "./violation.js";
|
|
13
20
|
|
|
14
21
|
/** Excludes values used only while validating and assembling the request. */
|
|
15
22
|
export type NormalizedRequestInput = Omit<
|
|
@@ -63,7 +70,6 @@ export interface BuildRedactedEvidenceInput {
|
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
const MAX_INPUT_DEPTH = 100;
|
|
66
|
-
const MAX_INPUT_SIZE = 1_000_000;
|
|
67
73
|
const ENDPOINT = "https://api.anthropic.com/v1/messages?beta=true";
|
|
68
74
|
/**
|
|
69
75
|
* The profile ids evidence may claim, spelled out here for the same reason
|
|
@@ -395,7 +401,7 @@ export function toSafeErrorDetails(
|
|
|
395
401
|
"maximumDepth",
|
|
396
402
|
"maximumSize",
|
|
397
403
|
] as const;
|
|
398
|
-
const booleanKeys = ["hasSystem", "hasTools"] as const;
|
|
404
|
+
const booleanKeys = ["hasSystem", "hasTools", "violationInKey"] as const;
|
|
399
405
|
|
|
400
406
|
for (const key of numericKeys) {
|
|
401
407
|
const descriptor = Object.getOwnPropertyDescriptor(safeDetails, key);
|
|
@@ -415,6 +421,60 @@ export function toSafeErrorDetails(
|
|
|
415
421
|
: undefined;
|
|
416
422
|
if (typeof detail === "boolean") details[key] = detail;
|
|
417
423
|
}
|
|
424
|
+
|
|
425
|
+
// Violation diagnostics (P1.T1, D4): strings pass only through closed-set
|
|
426
|
+
// or pattern validators, and numbers only through integer/range guards, so
|
|
427
|
+
// no caller text, key name or excerpt can ride these fields out of an error.
|
|
428
|
+
const stringValidators: Readonly<Record<string, (value: string) => boolean>> =
|
|
429
|
+
{
|
|
430
|
+
violationReason: isValidViolationReason,
|
|
431
|
+
violationPath: isValidViolationPath,
|
|
432
|
+
};
|
|
433
|
+
for (const [key, validator] of Object.entries(stringValidators)) {
|
|
434
|
+
const descriptor = Object.getOwnPropertyDescriptor(safeDetails, key);
|
|
435
|
+
const detail: unknown =
|
|
436
|
+
descriptor !== undefined && "value" in descriptor
|
|
437
|
+
? descriptor.value
|
|
438
|
+
: undefined;
|
|
439
|
+
if (typeof detail === "string" && validator(detail)) {
|
|
440
|
+
details[key] = detail;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const integerKeys = ["violationOffset", "violationTextLength"] as const;
|
|
444
|
+
for (const key of integerKeys) {
|
|
445
|
+
const descriptor = Object.getOwnPropertyDescriptor(safeDetails, key);
|
|
446
|
+
const detail: unknown =
|
|
447
|
+
descriptor !== undefined && "value" in descriptor
|
|
448
|
+
? descriptor.value
|
|
449
|
+
: undefined;
|
|
450
|
+
if (
|
|
451
|
+
typeof detail === "number" &&
|
|
452
|
+
Number.isSafeInteger(detail) &&
|
|
453
|
+
!Object.is(detail, -0) &&
|
|
454
|
+
detail >= 0 &&
|
|
455
|
+
detail <= MAX_INPUT_SIZE
|
|
456
|
+
) {
|
|
457
|
+
details[key] = detail;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
{
|
|
461
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
462
|
+
safeDetails,
|
|
463
|
+
"violationCodeUnit",
|
|
464
|
+
);
|
|
465
|
+
const detail: unknown =
|
|
466
|
+
descriptor !== undefined && "value" in descriptor
|
|
467
|
+
? descriptor.value
|
|
468
|
+
: undefined;
|
|
469
|
+
if (
|
|
470
|
+
typeof detail === "number" &&
|
|
471
|
+
isValidViolationCodeUnit(detail) &&
|
|
472
|
+
((details["violationReason"] === "lone-surrogate" && detail >= 0xd800) ||
|
|
473
|
+
(details["violationReason"] === "control-char" && detail <= 0x9f))
|
|
474
|
+
) {
|
|
475
|
+
details["violationCodeUnit"] = detail;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
418
478
|
return Object.freeze(details);
|
|
419
479
|
}
|
|
420
480
|
|
package/src/request-body.ts
CHANGED
|
@@ -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 {
|
|
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"]);
|
|
@@ -364,8 +370,9 @@ type ModelResolution = Readonly<{
|
|
|
364
370
|
|
|
365
371
|
function fail(
|
|
366
372
|
code: ConstructorParameters<typeof ClaudeCodeWireError>[0],
|
|
373
|
+
safeDetails: Readonly<Record<string, string | number | boolean>> = {},
|
|
367
374
|
): never {
|
|
368
|
-
throw new ClaudeCodeWireError(code);
|
|
375
|
+
throw new ClaudeCodeWireError(code, safeDetails);
|
|
369
376
|
}
|
|
370
377
|
|
|
371
378
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -380,25 +387,21 @@ function inspectString(
|
|
|
380
387
|
value: string,
|
|
381
388
|
state: InspectionState,
|
|
382
389
|
validateString?: (value: string) => void,
|
|
390
|
+
path: readonly ViolationPathSegment[] = [],
|
|
391
|
+
inKey = false,
|
|
383
392
|
): void {
|
|
384
393
|
state.size += value.length;
|
|
385
394
|
if (state.size > MAX_SIZE) fail("INPUT_TOO_LARGE");
|
|
386
395
|
validateString?.(value);
|
|
387
396
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
(
|
|
395
|
-
|
|
396
|
-
) {
|
|
397
|
-
fail("INVALID_INPUT");
|
|
398
|
-
}
|
|
399
|
-
const classification = classifySurrogateAt(value, index);
|
|
400
|
-
if (classification === "loneSurrogate") fail("INVALID_UNICODE");
|
|
401
|
-
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
|
+
);
|
|
402
405
|
}
|
|
403
406
|
}
|
|
404
407
|
|
|
@@ -407,11 +410,12 @@ function inspect(
|
|
|
407
410
|
depth: number,
|
|
408
411
|
state: InspectionState,
|
|
409
412
|
validateString?: (value: string) => void,
|
|
413
|
+
path: ViolationPathSegment[] = [],
|
|
410
414
|
): void {
|
|
411
415
|
if (depth > MAX_DEPTH) fail("INPUT_TOO_DEEP");
|
|
412
416
|
if (value === null || typeof value === "boolean") return;
|
|
413
417
|
if (typeof value === "string") {
|
|
414
|
-
inspectString(value, state, validateString);
|
|
418
|
+
inspectString(value, state, validateString, path, false);
|
|
415
419
|
return;
|
|
416
420
|
}
|
|
417
421
|
if (typeof value === "number") {
|
|
@@ -430,7 +434,9 @@ function inspect(
|
|
|
430
434
|
if (state.size > MAX_SIZE) fail("INPUT_TOO_LARGE");
|
|
431
435
|
for (let index = 0; index < value.length; index += 1) {
|
|
432
436
|
if (!hasOwn(value, String(index))) fail("INVALID_INPUT");
|
|
433
|
-
|
|
437
|
+
path.push(index);
|
|
438
|
+
inspect(value[index], depth + 1, state, validateString, path);
|
|
439
|
+
path.pop();
|
|
434
440
|
}
|
|
435
441
|
} else {
|
|
436
442
|
const prototype = Reflect.getPrototypeOf(value);
|
|
@@ -441,27 +447,37 @@ function inspect(
|
|
|
441
447
|
if (typeof key !== "string" || FORBIDDEN_KEYS.has(key)) {
|
|
442
448
|
fail("INVALID_INPUT");
|
|
443
449
|
}
|
|
444
|
-
|
|
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);
|
|
445
454
|
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
446
455
|
if (descriptor === undefined || !("value" in descriptor)) {
|
|
447
456
|
fail("INVALID_INPUT");
|
|
448
457
|
}
|
|
449
|
-
inspect(descriptor.value, depth + 1, state, validateString);
|
|
458
|
+
inspect(descriptor.value, depth + 1, state, validateString, path);
|
|
459
|
+
path.pop();
|
|
450
460
|
}
|
|
451
461
|
}
|
|
452
462
|
state.active.delete(value);
|
|
453
463
|
}
|
|
454
464
|
|
|
465
|
+
/** Paths without a caller hint are relative to the inspected value's root. */
|
|
455
466
|
export function inspectJsonInputs(
|
|
456
467
|
values: readonly unknown[],
|
|
457
468
|
validateString?: (value: string) => void,
|
|
469
|
+
paths?: readonly (readonly ViolationPathSegment[])[],
|
|
458
470
|
): void {
|
|
459
471
|
const state: InspectionState = {
|
|
460
472
|
active: new WeakSet(),
|
|
461
473
|
items: 0,
|
|
462
474
|
size: 0,
|
|
463
475
|
};
|
|
464
|
-
for (
|
|
476
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
477
|
+
inspect(values[index], 0, state, validateString, [
|
|
478
|
+
...(paths?.[index] ?? []),
|
|
479
|
+
]);
|
|
480
|
+
}
|
|
465
481
|
}
|
|
466
482
|
|
|
467
483
|
function requireRecord(value: unknown): Record<string, unknown> {
|
|
@@ -483,6 +499,13 @@ function requireString(value: unknown): string {
|
|
|
483
499
|
return value;
|
|
484
500
|
}
|
|
485
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
|
+
|
|
486
509
|
function requirePositiveInteger(value: unknown): number {
|
|
487
510
|
if (!Number.isSafeInteger(value) || typeof value !== "number" || value <= 0) {
|
|
488
511
|
fail("INVALID_INPUT");
|
|
@@ -829,7 +852,7 @@ function imageSource(value: unknown): ImageBlock["source"] {
|
|
|
829
852
|
)
|
|
830
853
|
fail("INVALID_INPUT");
|
|
831
854
|
entries.push([key, item]);
|
|
832
|
-
} else entries.push([key,
|
|
855
|
+
} else entries.push([key, requireIdentifier(item)]);
|
|
833
856
|
}
|
|
834
857
|
return Object.fromEntries(entries) as unknown as ImageBlock["source"];
|
|
835
858
|
}
|
|
@@ -890,7 +913,11 @@ function documentSource(value: unknown): DocumentBlock["source"] {
|
|
|
890
913
|
}),
|
|
891
914
|
]);
|
|
892
915
|
}
|
|
893
|
-
} else
|
|
916
|
+
} else
|
|
917
|
+
entries.push([
|
|
918
|
+
key,
|
|
919
|
+
type === "text" ? requireString(item) : requireIdentifier(item),
|
|
920
|
+
]);
|
|
894
921
|
}
|
|
895
922
|
return Object.fromEntries(entries) as unknown as DocumentBlock["source"];
|
|
896
923
|
}
|
|
@@ -925,7 +952,11 @@ function thinkingBlock(
|
|
|
925
952
|
if (key === "type") entries.push([key, "thinking"]);
|
|
926
953
|
else if (key === "cache_control")
|
|
927
954
|
entries.push([key, nullable(item, cacheControl)]);
|
|
928
|
-
else
|
|
955
|
+
else
|
|
956
|
+
entries.push([
|
|
957
|
+
key,
|
|
958
|
+
key === "signature" ? requireIdentifier(item) : requireString(item),
|
|
959
|
+
]);
|
|
929
960
|
}
|
|
930
961
|
return Object.fromEntries(entries) as unknown as ThinkingBlock;
|
|
931
962
|
}
|
|
@@ -950,7 +981,7 @@ function redactedThinkingBlock(
|
|
|
950
981
|
if (key === "type") entries.push([key, "redacted_thinking"]);
|
|
951
982
|
else if (key === "cache_control")
|
|
952
983
|
entries.push([key, nullable(item, cacheControl)]);
|
|
953
|
-
else entries.push([key,
|
|
984
|
+
else entries.push([key, requireIdentifier(item)]);
|
|
954
985
|
}
|
|
955
986
|
return Object.fromEntries(entries) as unknown as RedactedThinkingBlock;
|
|
956
987
|
}
|
|
@@ -966,8 +997,8 @@ function searchResultBlock(value: unknown): SearchResultBlock {
|
|
|
966
997
|
if (key === "content") {
|
|
967
998
|
if (!Array.isArray(item)) fail("INVALID_INPUT");
|
|
968
999
|
entries.push([key, item.map((block) => textBlock(block))]);
|
|
969
|
-
} else if (key === "source"
|
|
970
|
-
|
|
1000
|
+
} else if (key === "source") entries.push([key, requireIdentifier(item)]);
|
|
1001
|
+
else if (key === "title") entries.push([key, requireString(item)]);
|
|
971
1002
|
else if (key === "type") entries.push([key, "search_result"]);
|
|
972
1003
|
else if (key === "cache_control")
|
|
973
1004
|
entries.push([key, nullable(item, cacheControl)]);
|
|
@@ -984,7 +1015,7 @@ function toolReferenceBlock(value: unknown): ToolReferenceBlock {
|
|
|
984
1015
|
const entries: [string, unknown][] = [];
|
|
985
1016
|
for (const key of Object.keys(record)) {
|
|
986
1017
|
const item = record[key];
|
|
987
|
-
if (key === "tool_name") entries.push([key,
|
|
1018
|
+
if (key === "tool_name") entries.push([key, requireIdentifier(item)]);
|
|
988
1019
|
else if (key === "type") entries.push([key, "tool_reference"]);
|
|
989
1020
|
else entries.push([key, nullable(item, cacheControl)]);
|
|
990
1021
|
}
|
|
@@ -1000,7 +1031,7 @@ function toolUseBlock(value: unknown): ToolUseBlock {
|
|
|
1000
1031
|
for (const key of Object.keys(record)) {
|
|
1001
1032
|
const item = record[key];
|
|
1002
1033
|
if (key === "id" || key === "name")
|
|
1003
|
-
entries.push([key,
|
|
1034
|
+
entries.push([key, requireIdentifier(item)]);
|
|
1004
1035
|
else if (key === "input") entries.push([key, validatedJson(item)]);
|
|
1005
1036
|
else if (key === "type") entries.push([key, "tool_use"]);
|
|
1006
1037
|
else if (key === "cache_control")
|
|
@@ -1025,7 +1056,7 @@ function toolCaller(value: unknown): ToolUseBlock["caller"] {
|
|
|
1025
1056
|
return Object.fromEntries(
|
|
1026
1057
|
Object.keys(record).map((key) => [
|
|
1027
1058
|
key,
|
|
1028
|
-
key === "type" ? type :
|
|
1059
|
+
key === "type" ? type : requireIdentifier(record[key]),
|
|
1029
1060
|
]),
|
|
1030
1061
|
) as unknown as NonNullable<ToolUseBlock["caller"]>;
|
|
1031
1062
|
}
|
|
@@ -1048,7 +1079,7 @@ function toolResultBlock(value: unknown): ToolResultBlock {
|
|
|
1048
1079
|
const entries: [string, unknown][] = [];
|
|
1049
1080
|
for (const key of Object.keys(record)) {
|
|
1050
1081
|
const item = record[key];
|
|
1051
|
-
if (key === "tool_use_id") entries.push([key,
|
|
1082
|
+
if (key === "tool_use_id") entries.push([key, requireIdentifier(item)]);
|
|
1052
1083
|
else if (key === "type") entries.push([key, "tool_result"]);
|
|
1053
1084
|
else if (key === "cache_control")
|
|
1054
1085
|
entries.push([key, nullable(item, cacheControl)]);
|
|
@@ -1327,8 +1358,8 @@ function customToolDefinition(record: Record<string, unknown>): ToolDefinition {
|
|
|
1327
1358
|
for (const key of Object.keys(record)) {
|
|
1328
1359
|
const item = record[key];
|
|
1329
1360
|
if (key === "input_schema") entries.push([key, toolInputSchema(item)]);
|
|
1330
|
-
else if (key === "name"
|
|
1331
|
-
|
|
1361
|
+
else if (key === "name") entries.push([key, requireIdentifier(item)]);
|
|
1362
|
+
else if (key === "description") entries.push([key, requireString(item)]);
|
|
1332
1363
|
else if (key === "allowed_callers")
|
|
1333
1364
|
entries.push([key, allowedCallers(item)]);
|
|
1334
1365
|
else if (key === "cache_control")
|
|
@@ -1357,7 +1388,7 @@ function builtInToolDefinition(
|
|
|
1357
1388
|
if (item !== spec.name) fail("INVALID_INPUT");
|
|
1358
1389
|
entries.push([key, item]);
|
|
1359
1390
|
} else if (key === "mcp_server_name" || key === "model")
|
|
1360
|
-
entries.push([key,
|
|
1391
|
+
entries.push([key, requireIdentifier(item)]);
|
|
1361
1392
|
else if (key === "allowed_callers")
|
|
1362
1393
|
entries.push([key, allowedCallers(item)]);
|
|
1363
1394
|
else if (key === "cache_control" || key === "caching")
|
|
@@ -1401,7 +1432,7 @@ function tools(value: unknown): readonly ToolDefinition[] {
|
|
|
1401
1432
|
? builtInToolDefinition(record)
|
|
1402
1433
|
: customToolDefinition(record);
|
|
1403
1434
|
if (hasOwn(record, "name")) {
|
|
1404
|
-
const name =
|
|
1435
|
+
const name = requireIdentifier(record["name"]);
|
|
1405
1436
|
if (names.has(name)) fail("INVALID_INPUT");
|
|
1406
1437
|
names.add(name);
|
|
1407
1438
|
}
|
|
@@ -1462,8 +1493,8 @@ function modelResolution(
|
|
|
1462
1493
|
fail("INVALID_INPUT");
|
|
1463
1494
|
}
|
|
1464
1495
|
return {
|
|
1465
|
-
id:
|
|
1466
|
-
wireId:
|
|
1496
|
+
id: requireIdentifier(record["id"]),
|
|
1497
|
+
wireId: requireIdentifier(record["wireId"]),
|
|
1467
1498
|
capabilities: {
|
|
1468
1499
|
thinking: capabilityBoolean(capabilities.thinking, derived.thinking),
|
|
1469
1500
|
adaptiveThinking: capabilityBoolean(
|
|
@@ -1647,7 +1678,8 @@ function toolChoice(value: unknown): Readonly<Record<string, unknown>> {
|
|
|
1647
1678
|
const entries: [string, unknown][] = [];
|
|
1648
1679
|
for (const key of Object.keys(record)) {
|
|
1649
1680
|
if (key === "type") entries.push([key, type]);
|
|
1650
|
-
else if (key === "name")
|
|
1681
|
+
else if (key === "name")
|
|
1682
|
+
entries.push([key, requireIdentifier(record[key])]);
|
|
1651
1683
|
else entries.push([key, requireBoolean(record[key])]);
|
|
1652
1684
|
}
|
|
1653
1685
|
return Object.fromEntries(entries);
|
|
@@ -1747,13 +1779,17 @@ export function buildCanonicalBody(
|
|
|
1747
1779
|
profile?: ClaudeCodeProtocolProfile,
|
|
1748
1780
|
thinkingDisplayOverride?: "updates",
|
|
1749
1781
|
): Readonly<Record<string, unknown>> {
|
|
1750
|
-
inspectJsonInputs(
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1782
|
+
inspectJsonInputs(
|
|
1783
|
+
[
|
|
1784
|
+
rawInput,
|
|
1785
|
+
rawResolvedModel,
|
|
1786
|
+
rawSystemBlocks,
|
|
1787
|
+
rawMetadata,
|
|
1788
|
+
...(profile === undefined ? [] : [profile]),
|
|
1789
|
+
],
|
|
1790
|
+
undefined,
|
|
1791
|
+
[[], ["model"], ["system"], ["metadata"], ["profileOverride"]],
|
|
1792
|
+
);
|
|
1757
1793
|
|
|
1758
1794
|
const input = requireRecord(rawInput);
|
|
1759
1795
|
assertExactKeys(input, INPUT_KEY_SET);
|
package/src/system-prompt.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
}
|
package/src/unicode.ts
CHANGED
|
@@ -22,3 +22,74 @@ export function classifySurrogateAt(
|
|
|
22
22
|
if (unit >= 0xdc00 && unit <= 0xdfff) return "loneSurrogate";
|
|
23
23
|
return "notSurrogate";
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
export type TextViolationReason = "control-char" | "lone-surrogate";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One character-level policy violation found while screening a string.
|
|
30
|
+
*
|
|
31
|
+
* `offset` is the UTF-16 code-unit index of the offending unit and `codeUnit`
|
|
32
|
+
* is the unit itself. Both are safe diagnostics: offsets are numbers and the
|
|
33
|
+
* only code units ever reported are control characters or surrogate halves,
|
|
34
|
+
* never prose content.
|
|
35
|
+
*/
|
|
36
|
+
export interface TextViolation {
|
|
37
|
+
readonly reason: TextViolationReason;
|
|
38
|
+
readonly offset: number;
|
|
39
|
+
readonly codeUnit: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Character-level acceptance policy for one string lane.
|
|
44
|
+
*
|
|
45
|
+
* `rejectControls` rejects every C0 control except TAB (0x09), LF (0x0A) and
|
|
46
|
+
* CR (0x0D), plus DEL (0x7F). Lone surrogates are always rejected, rather than
|
|
47
|
+
* depending on whether a caller serializes or directly encodes the string.
|
|
48
|
+
*/
|
|
49
|
+
export interface TextPolicy {
|
|
50
|
+
readonly rejectControls: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Body prose: every well-formed UTF-16 string is accepted. */
|
|
54
|
+
export const TEXT_POLICY_PROSE: TextPolicy = Object.freeze({
|
|
55
|
+
rejectControls: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Identifier graph screening: C0 except TAB/LF/CR, plus DEL, plus lone
|
|
60
|
+
* surrogates. This is exactly the set the graph inspectors rejected before the
|
|
61
|
+
* shared validator existed.
|
|
62
|
+
*/
|
|
63
|
+
export const TEXT_POLICY_IDENTIFIER: TextPolicy = Object.freeze({
|
|
64
|
+
rejectControls: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Screens one string under `policy`, returning the first violation or null.
|
|
69
|
+
*
|
|
70
|
+
* The walk order matches the historical per-module inspectors: at each index
|
|
71
|
+
* the control check runs before the surrogate check, and a well-formed pair
|
|
72
|
+
* consumes two code units. `classifySurrogateAt` is the single surrogate
|
|
73
|
+
* authority; this function never re-implements it.
|
|
74
|
+
*/
|
|
75
|
+
export function inspectText(
|
|
76
|
+
value: string,
|
|
77
|
+
policy: TextPolicy,
|
|
78
|
+
): TextViolation | null {
|
|
79
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
80
|
+
const unit = value.charCodeAt(index);
|
|
81
|
+
if (
|
|
82
|
+
policy.rejectControls &&
|
|
83
|
+
((unit <= 0x1f && unit !== 0x09 && unit !== 0x0a && unit !== 0x0d) ||
|
|
84
|
+
unit === 0x7f)
|
|
85
|
+
) {
|
|
86
|
+
return { reason: "control-char", offset: index, codeUnit: unit };
|
|
87
|
+
}
|
|
88
|
+
const classification = classifySurrogateAt(value, index);
|
|
89
|
+
if (classification === "loneSurrogate") {
|
|
90
|
+
return { reason: "lone-surrogate", offset: index, codeUnit: unit };
|
|
91
|
+
}
|
|
92
|
+
if (classification === "surrogatePair") index += 1;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|