@tormentalabs/claude-code-wire-compat 0.1.0-rc.16 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +132 -1
- package/README.md +53 -1
- package/dist/build-request.d.ts.map +1 -1
- package/dist/build-request.js +164 -26
- package/dist/build-request.js.map +1 -1
- package/dist/contracts.d.ts +103 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/redaction.d.ts +11 -0
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +11 -0
- package/dist/redaction.js.map +1 -1
- package/dist/request-body.d.ts.map +1 -1
- package/dist/request-body.js +74 -19
- package/dist/request-body.js.map +1 -1
- package/dist/system-prompt.d.ts +12 -5
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +29 -13
- package/dist/system-prompt.js.map +1 -1
- package/package.json +2 -1
- package/src/anti-verbosity.ts +219 -0
- package/src/beta-registry.ts +140 -0
- package/src/betas.ts +219 -0
- package/src/build-request.ts +1655 -0
- package/src/contracts.ts +1233 -0
- package/src/count-tokens.ts +84 -0
- package/src/fingerprint.ts +85 -0
- package/src/headers.ts +442 -0
- package/src/index.ts +62 -0
- package/src/metadata.ts +331 -0
- package/src/model-capabilities.ts +295 -0
- package/src/model-identity.ts +45 -0
- package/src/models.ts +46 -0
- package/src/profiles/claude-code-2.1.195.ts +154 -0
- package/src/redaction.ts +521 -0
- package/src/request-body.ts +1924 -0
- package/src/sha256.ts +114 -0
- package/src/system-prompt.ts +222 -0
- package/src/thinking.ts +266 -0
- package/src/unicode.ts +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,138 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [0.1.0
|
|
5
|
+
## [0.1.0] - 2026-07-28
|
|
6
|
+
|
|
7
|
+
First stable release.
|
|
8
|
+
|
|
9
|
+
There is NO runtime code change relative to `0.1.0-rc.17`:
|
|
10
|
+
`git diff --stat 5c6881b d7e2901 -- src package.json` is empty, so `dist/` is
|
|
11
|
+
byte-identical to the `0.1.0-rc.17` build. Everything this release contains is
|
|
12
|
+
described in the release candidate entries below; they are not repeated here.
|
|
13
|
+
The packed content differs from `0.1.0-rc.17` by `README.md` and by the added
|
|
14
|
+
TypeScript sources described below — the executed code is unchanged, which
|
|
15
|
+
`npm run test:pack` confirms by an unchanged consumer digest.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **The tarball now ships the TypeScript sources.** `src` is added to the
|
|
20
|
+
`files` allowlist. The build emits 40 `.js.map` and `.d.ts.map` files that
|
|
21
|
+
reference `../src/*.ts` and carry no `sourcesContent`, so every source map in
|
|
22
|
+
the published package resolved to nothing in a consumer's debugger. Shipping
|
|
23
|
+
the sources is also the coherent option for a GPL-3.0-or-later package whose
|
|
24
|
+
`NOTICE` already offers corresponding source: the offer and the tarball now
|
|
25
|
+
agree. Only `.ts` files are packed under `src/`, pinned exactly by
|
|
26
|
+
`test/pack/pack-policy.test.ts`; the allowlist itself is pinned by
|
|
27
|
+
`test/governance/release-policy.test.ts` and
|
|
28
|
+
`test/governance/package-policy.test.ts`.
|
|
29
|
+
- The ported protocol-documentation corpus and `docs/ATTRIBUTION.md` are now in
|
|
30
|
+
the repository. Neither is packed into the tarball.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- `README.md` no longer describes the package as a bootstrap release candidate
|
|
35
|
+
whose request-building behavior arrives later. That statement was false — the
|
|
36
|
+
package builds and parses requests — and it was the text rendered on the
|
|
37
|
+
npmjs.com package page.
|
|
38
|
+
|
|
39
|
+
## [0.1.0-rc.17] - 2026-07-28
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **Root `suppressIdentityBlock` (seam S8).** The canonical identity block was
|
|
44
|
+
composed unconditionally, so a consumer switch whose meaning is "send neither
|
|
45
|
+
canonical block" — `token_economy.lean_system_non_main` in the `opencode`
|
|
46
|
+
plugin, which removed billing AND identity before it migrated to this package
|
|
47
|
+
— was a silent no-op. `suppressIdentityBlock: true` omits the identity block
|
|
48
|
+
entirely, which with `suppressBillingBlock` makes four canonical prefixes
|
|
49
|
+
legitimate: `[billing, identity]`, `[identity]`, `[billing]` and `[]`. Only a
|
|
50
|
+
boolean is accepted; anything else is `INVALID_INPUT`.
|
|
51
|
+
`evidence.identityBlockSuppressed` is emitted only when the block was actually
|
|
52
|
+
removed, mirroring `billingBlockSuppressed` exactly, so evidence stays
|
|
53
|
+
byte-identical for every request that ignores the seam. Recorded in
|
|
54
|
+
`docs/source-trace.md` as governance ledger L16.
|
|
55
|
+
|
|
56
|
+
This is a DIFFERENT field from the L10 `cacheControl.suppressIdentityBlock`,
|
|
57
|
+
which keeps the block and drops only its `cache_control` marker. The name
|
|
58
|
+
collision is deliberate — symmetry with `suppressBillingBlock` at the root was
|
|
59
|
+
judged worth more than a novel name — and the JSDoc of each field states what
|
|
60
|
+
it does and names the other by its full path.
|
|
61
|
+
|
|
62
|
+
The caller-block drop stays UNCONDITIONAL: a caller block byte-equal to the
|
|
63
|
+
identity text is removed even when the canonical one was suppressed, matching
|
|
64
|
+
the genuine client and keeping the parser's absence check below sound.
|
|
65
|
+
|
|
66
|
+
- **`preserveThinkingBlockCacheControl` (seam S9).** `thinking` and
|
|
67
|
+
`redacted_thinking` blocks were pinned to a strict allowlist — `signature`,
|
|
68
|
+
`thinking`, `type` and `data`, `type` — so a request carrying `cache_control`
|
|
69
|
+
on a reasoning block was rejected outright with `INVALID_INPUT`. The consumer
|
|
70
|
+
had no legal way out, and this is a PRODUCTION failure rather than a test
|
|
71
|
+
artefact: the Anthropic API answers a mutated reasoning block with
|
|
72
|
+
|
|
73
|
+
> `400 ... thinking or redacted_thinking blocks in the latest assistant`
|
|
74
|
+
> `message cannot be modified. These blocks must remain as they were in the`
|
|
75
|
+
> `original response.`
|
|
76
|
+
|
|
77
|
+
so `delete block.cache_control` before the call is itself the modification
|
|
78
|
+
that triggers the 400. `preserveThinkingBlockCacheControl: true` accepts the
|
|
79
|
+
key and copies it to the body VERBATIM — caller key order intact, no TTL
|
|
80
|
+
applied, no breakpoint placed. Only a boolean is accepted; anything else is
|
|
81
|
+
`INVALID_INPUT`.
|
|
82
|
+
|
|
83
|
+
The allowlist grows by `cache_control` and by NOTHING else: an unknown key on
|
|
84
|
+
a reasoning block is still `INVALID_INPUT` with the seam active. The value
|
|
85
|
+
passes the same `cache_control` validator every other block uses —
|
|
86
|
+
`{ type: "ephemeral" }` with an optional `ttl` — so a malformed marker still
|
|
87
|
+
fails closed; the `scope` key that `text` blocks tolerate for legacy reasons
|
|
88
|
+
is deliberately NOT accepted, the API never returning it on a reasoning block.
|
|
89
|
+
The marker takes no part in this package's cache-control machinery:
|
|
90
|
+
`applySystemCacheControl` is untouched and `applyMessageCacheControl` already
|
|
91
|
+
exempts reasoning blocks from both the strip and the breakpoint pass.
|
|
92
|
+
|
|
93
|
+
`evidence.thinkingBlockCacheControlPreserved` is emitted only when the seam
|
|
94
|
+
was active AND at least one emitted block actually carried a marker — opting
|
|
95
|
+
in without using it records nothing — mirroring `billingBlockSuppressed`
|
|
96
|
+
exactly, so evidence stays byte-identical for every request that ignores the
|
|
97
|
+
seam. `parseBuiltClaudeCodeRequest` CONFIRMS that claim against the body
|
|
98
|
+
before it checks byte length or digest, so a forgery that is byte-length
|
|
99
|
+
preserving and evidence-self-consistent is refused by the structural check
|
|
100
|
+
rather than incidentally by arithmetic. Recorded in `docs/source-trace.md` as
|
|
101
|
+
governance ledger L17.
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
- **`parseBuiltClaudeCodeRequest` no longer INFERS the canonical prefix from the
|
|
106
|
+
identity block's position; it READS the length from evidence and VERIFIES it
|
|
107
|
+
structurally.** With two independent seams an empty prefix is
|
|
108
|
+
indistinguishable from a caller-only array, so position inference is no longer
|
|
109
|
+
decidable: the rc.16 discriminator would have hit its unconditional failure
|
|
110
|
+
path on every request built with both seams active. The parser now takes
|
|
111
|
+
`evidence.billingBlockSuppressed` and `evidence.identityBlockSuppressed` as
|
|
112
|
+
the claimed prefix length and confirms every block that claim implies —
|
|
113
|
+
billing by its fixed `x-anthropic-billing-header: cc_version=` head (the tail
|
|
114
|
+
is per-request), identity by the byte-exact identity text. This is strictly
|
|
115
|
+
stronger than what it replaces, which never inspected the billing slot at all:
|
|
116
|
+
an envelope built with `suppressBillingBlock` whose evidence hid that fact was
|
|
117
|
+
previously accepted.
|
|
118
|
+
|
|
119
|
+
Verification is **asymmetric, deliberately**. A claim that identity was
|
|
120
|
+
suppressed is refuted by finding the identity text ANYWHERE in the array,
|
|
121
|
+
which is sound because `buildCanonicalSystem` drops caller blocks equal to it
|
|
122
|
+
unconditionally and merges runs with `\n`, so the text cannot legitimately
|
|
123
|
+
survive. There is no mirror check for billing: a caller block may legitimately
|
|
124
|
+
begin with the billing header text, so its presence proves nothing.
|
|
125
|
+
|
|
126
|
+
The match is on TEXT, never on `cache_control` — the L10 seam can legitimately
|
|
127
|
+
emit the identity block with no marker — and the assertion remains an
|
|
128
|
+
EQUALITY.
|
|
129
|
+
|
|
130
|
+
- **`emittedSystemBlockCount` is computed from both seams instead of a
|
|
131
|
+
constant.** `CANONICAL_SYSTEM_BLOCKS` and `CANONICAL_SYSTEM_BLOCKS_WITHOUT_BILLING`
|
|
132
|
+
were removed: a constant cannot express the empty prefix the two seams produce
|
|
133
|
+
together. The build path now subtracts one slot per canonical block that
|
|
134
|
+
actually survived.
|
|
135
|
+
|
|
136
|
+
## [0.1.0-rc.16] - 2026-07-28
|
|
6
137
|
|
|
7
138
|
### Added
|
|
8
139
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Claude Wire Compat
|
|
|
4
4
|
|
|
5
5
|
An ESM-only, runtime-neutral TypeScript package for constructing a pinned Claude Code Messages wire contract.
|
|
6
6
|
|
|
7
|
-
> **Status:**
|
|
7
|
+
> **Status:** stable. `buildClaudeCodeRequest` and `parseBuiltClaudeCodeRequest` build and read back the pinned wire contract. The package performs no I/O: credentials, transport, persistence, refresh coordination, and retries remain the consumer's responsibility.
|
|
8
8
|
|
|
9
9
|
## Package
|
|
10
10
|
|
|
@@ -18,6 +18,58 @@ The package targets Node.js 20 or newer and is designed to remain portable to Bu
|
|
|
18
18
|
|
|
19
19
|
The only accepted `profile` value is the exported `CLAUDE_CODE_2_1_195_PROFILE` singleton. Any other object, even a structurally identical clone, is rejected with `ClaudeCodeWireError` code `INVALID_INPUT`. This deliberate fail-closed behaviour prevents callers from substituting an unpinned protocol profile.
|
|
20
20
|
|
|
21
|
+
## Protocol documentation
|
|
22
|
+
|
|
23
|
+
The wire contract this package pins was reverse engineered before it was
|
|
24
|
+
implemented. That research is preserved under [`docs/protocol/`](./docs/protocol)
|
|
25
|
+
so a future maintainer can re-derive the contract when Claude Code ships a new
|
|
26
|
+
version. It is ported verbatim from
|
|
27
|
+
[opencode-anthropic-fix](https://github.com/marco-jardim/opencode-anthropic-fix)
|
|
28
|
+
at commit `466d500` under GPL-3.0-or-later; every file carries a provenance
|
|
29
|
+
header and is listed in [docs/ATTRIBUTION.md](./docs/ATTRIBUTION.md).
|
|
30
|
+
|
|
31
|
+
**These documents describe the upstream plugin and Claude Code itself, not this
|
|
32
|
+
package's API.** The normative mapping from those findings to this package's
|
|
33
|
+
contract is [docs/source-trace.md](./docs/source-trace.md).
|
|
34
|
+
|
|
35
|
+
Protocol knowledge corpus:
|
|
36
|
+
|
|
37
|
+
- [Reverse engineering](./docs/protocol/reverse-engineering.md) — the full
|
|
38
|
+
authentication and API reverse-engineering record.
|
|
39
|
+
- [HTTP headers and system prompt](./docs/protocol/http-headers-and-system-prompt.md)
|
|
40
|
+
— header composition and system-prompt mimicry.
|
|
41
|
+
- [Fingerprint extraction](./docs/protocol/fingerprint-extraction.md) — how the
|
|
42
|
+
client fingerprint and metadata are derived.
|
|
43
|
+
- [Message flow](./docs/protocol/message-flow.md) — end-to-end request and
|
|
44
|
+
response flow.
|
|
45
|
+
- [Tool use examples](./docs/protocol/tool-use-examples.md) — tool_use and
|
|
46
|
+
tool_result pairing in practice.
|
|
47
|
+
- [Code comparison reference](./docs/protocol/code-comparison-reference.md) —
|
|
48
|
+
side-by-side comparison against the genuine client.
|
|
49
|
+
- [Divergence analysis](./docs/protocol/divergence-analysis.md) — every observed
|
|
50
|
+
divergence from the genuine client.
|
|
51
|
+
- [Divergence executive summary](./docs/protocol/divergence-executive-summary.md)
|
|
52
|
+
— the condensed version of that analysis.
|
|
53
|
+
- [Quick reference](./docs/protocol/quick-reference.md) — condensed lookup of
|
|
54
|
+
headers, betas, and switches.
|
|
55
|
+
- [System-prompt search results](./docs/protocol/system-prompt-search-results.md)
|
|
56
|
+
— where each system-prompt fragment was found in the analysed build.
|
|
57
|
+
- [Cache transparency](./docs/protocol/cache-transparency.md) — anti-verbosity
|
|
58
|
+
and prompt-cache observability.
|
|
59
|
+
- [Beta decision table](./docs/protocol/beta-decision-table.md) — which beta
|
|
60
|
+
identifiers are sent under which conditions.
|
|
61
|
+
|
|
62
|
+
Per-version wire analyses — [why they exist and when a new one is
|
|
63
|
+
required](./docs/protocol/versions/README.md):
|
|
64
|
+
|
|
65
|
+
- [Claude Code 2.1.119](./docs/protocol/versions/claude-code-2.1.119-analysis.md)
|
|
66
|
+
- [Claude Code 2.1.133](./docs/protocol/versions/claude-code-2.1.133-analysis.md)
|
|
67
|
+
- [Claude Code 2.1.143](./docs/protocol/versions/claude-code-2.1.143-analysis.md)
|
|
68
|
+
- [Claude Code 2.1.150](./docs/protocol/versions/claude-code-2.1.150-analysis.md)
|
|
69
|
+
- [Claude Code 2.1.159](./docs/protocol/versions/claude-code-2.1.159-analysis.md)
|
|
70
|
+
- [Claude Code 2.1.195](./docs/protocol/versions/claude-code-2.1.195-analysis.md)
|
|
71
|
+
— the release this package's profile pins.
|
|
72
|
+
|
|
21
73
|
## Development
|
|
22
74
|
|
|
23
75
|
```sh
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-request.d.ts","sourceRoot":"","sources":["../src/build-request.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,iCAAiC,EACjC,sBAAsB,EAMtB,yBAAyB,EACzB,0BAA0B,EAC1B,sBAAsB,EAGvB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"build-request.d.ts","sourceRoot":"","sources":["../src/build-request.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,iCAAiC,EACjC,sBAAsB,EAMtB,yBAAyB,EACzB,0BAA0B,EAC1B,sBAAsB,EAGvB,MAAM,gBAAgB,CAAC;AAwvCxB,2DAA2D;AAC3D,wBAAsB,iCAAiC,CACrD,KAAK,EAAE,0BAA0B,EACjC,OAAO,GAAE,yBAAuD,GAC/D,OAAO,CAAC,iCAAiC,CAAC,CAiF5C;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,sBAAsB,EAC7B,OAAO,GAAE,yBAAuD,GAC/D,OAAO,CAAC,sBAAsB,CAAC,CA8JjC;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,OAAO,EACd,OAAO,GAAE,yBAAuD,GAC/D,sBAAsB,CA+FxB"}
|
package/dist/build-request.js
CHANGED
|
@@ -54,6 +54,8 @@ const INPUT_KEYS = new Set([
|
|
|
54
54
|
"additionalBetas",
|
|
55
55
|
"suppressBetas",
|
|
56
56
|
"suppressBillingBlock",
|
|
57
|
+
"suppressIdentityBlock",
|
|
58
|
+
"preserveThinkingBlockCacheControl",
|
|
57
59
|
"betaOverrides",
|
|
58
60
|
"metadataOverrides",
|
|
59
61
|
"extraHeaders",
|
|
@@ -81,10 +83,16 @@ const COUNT_TOKENS_INPUT_KEYS = new Set([
|
|
|
81
83
|
"extraHeaders",
|
|
82
84
|
]);
|
|
83
85
|
const BUILT_KEYS = new Set(["url", "method", "headers", "body", "evidence"]);
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
/**
|
|
87
|
+
* The self-describing prefix of the canonical billing block's text.
|
|
88
|
+
*
|
|
89
|
+
* `buildBillingBlock` emits
|
|
90
|
+
* `x-anthropic-billing-header: cc_version=<version>.<fingerprint>; ...`, whose
|
|
91
|
+
* tail varies per request, so only this fixed head can anchor a structural
|
|
92
|
+
* check. It is what lets the parser CONFIRM that a request which claims to
|
|
93
|
+
* carry the billing block actually carries it.
|
|
94
|
+
*/
|
|
95
|
+
const BILLING_BLOCK_TEXT_PREFIX = "x-anthropic-billing-header: cc_version=";
|
|
88
96
|
const EVIDENCE_KEYS = new Set([
|
|
89
97
|
"profileId",
|
|
90
98
|
"url",
|
|
@@ -100,6 +108,8 @@ const EVIDENCE_KEYS = new Set([
|
|
|
100
108
|
"droppedExtraHeaderNames",
|
|
101
109
|
"suppressedBetaNames",
|
|
102
110
|
"billingBlockSuppressed",
|
|
111
|
+
"identityBlockSuppressed",
|
|
112
|
+
"thinkingBlockCacheControlPreserved",
|
|
103
113
|
]);
|
|
104
114
|
const CAPABILITY_KEYS = [
|
|
105
115
|
"thinking",
|
|
@@ -511,6 +521,26 @@ function validateExtraHeaderPolicy(value) {
|
|
|
511
521
|
function validateSuppressBillingBlock(value) {
|
|
512
522
|
return parseBoolean(value);
|
|
513
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* Validates the package-extension identity-block suppression flag.
|
|
526
|
+
*
|
|
527
|
+
* Same contract as `validateSuppressBillingBlock`: only a boolean states a
|
|
528
|
+
* decision, because coercing a truthy string would silently drop a canonical
|
|
529
|
+
* block the genuine client always sends.
|
|
530
|
+
*/
|
|
531
|
+
function validateSuppressIdentityBlock(value) {
|
|
532
|
+
return parseBoolean(value);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Validates the package-extension thinking-block `cache_control` seam flag.
|
|
536
|
+
*
|
|
537
|
+
* Same contract as the suppression flags: only a boolean states a decision,
|
|
538
|
+
* because coercing a truthy string would silently widen the thinking-block
|
|
539
|
+
* allowlist this package pins against the genuine client.
|
|
540
|
+
*/
|
|
541
|
+
function validatePreserveThinkingBlockCacheControl(value) {
|
|
542
|
+
return parseBoolean(value);
|
|
543
|
+
}
|
|
514
544
|
function createEffectiveProfile(pinnedProfile, override) {
|
|
515
545
|
if (override === undefined)
|
|
516
546
|
return pinnedProfile;
|
|
@@ -573,6 +603,12 @@ function validateInput(input) {
|
|
|
573
603
|
const suppressBillingBlock = Object.hasOwn(input, "suppressBillingBlock")
|
|
574
604
|
? validateSuppressBillingBlock(ownValue(input, "suppressBillingBlock"))
|
|
575
605
|
: false;
|
|
606
|
+
const suppressIdentityBlock = Object.hasOwn(input, "suppressIdentityBlock")
|
|
607
|
+
? validateSuppressIdentityBlock(ownValue(input, "suppressIdentityBlock"))
|
|
608
|
+
: false;
|
|
609
|
+
const preserveThinkingBlockCacheControl = Object.hasOwn(input, "preserveThinkingBlockCacheControl")
|
|
610
|
+
? validatePreserveThinkingBlockCacheControl(ownValue(input, "preserveThinkingBlockCacheControl"))
|
|
611
|
+
: false;
|
|
576
612
|
return {
|
|
577
613
|
source: input,
|
|
578
614
|
clientRequestId,
|
|
@@ -581,6 +617,8 @@ function validateInput(input) {
|
|
|
581
617
|
betaOverrides,
|
|
582
618
|
extraHeaderPolicy,
|
|
583
619
|
suppressBillingBlock,
|
|
620
|
+
suppressIdentityBlock,
|
|
621
|
+
preserveThinkingBlockCacheControl,
|
|
584
622
|
};
|
|
585
623
|
}
|
|
586
624
|
function validateCountTokensInput(input) {
|
|
@@ -818,33 +856,102 @@ function parseEvidence(value) {
|
|
|
818
856
|
billingBlockSuppressed: parseBoolean(ownValue(value, "billingBlockSuppressed")),
|
|
819
857
|
}
|
|
820
858
|
: {}),
|
|
859
|
+
...(Object.hasOwn(value, "identityBlockSuppressed")
|
|
860
|
+
? {
|
|
861
|
+
identityBlockSuppressed: parseBoolean(ownValue(value, "identityBlockSuppressed")),
|
|
862
|
+
}
|
|
863
|
+
: {}),
|
|
864
|
+
// Rehydrated here because `assertExactKeys` above already accepts the key:
|
|
865
|
+
// omitting this branch would silently DROP it, and the round-trip equality
|
|
866
|
+
// every seam test asserts would fail on an envelope that is entirely legal.
|
|
867
|
+
...(Object.hasOwn(value, "thinkingBlockCacheControlPreserved")
|
|
868
|
+
? {
|
|
869
|
+
thinkingBlockCacheControlPreserved: parseBoolean(ownValue(value, "thinkingBlockCacheControlPreserved")),
|
|
870
|
+
}
|
|
871
|
+
: {}),
|
|
821
872
|
};
|
|
822
873
|
}
|
|
874
|
+
/** Reads an own property of a value that is not asserted to be a record. */
|
|
875
|
+
function ownProperty(value, key) {
|
|
876
|
+
return isRecord(value) && Object.hasOwn(value, key)
|
|
877
|
+
? ownValue(value, key)
|
|
878
|
+
: undefined;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Reports whether the emitted `messages` carry a reasoning block that actually
|
|
882
|
+
* kept a `cache_control` key.
|
|
883
|
+
*
|
|
884
|
+
* This is what makes `evidence.thinkingBlockCacheControlPreserved` a record of
|
|
885
|
+
* what the seam DID rather than of what it was allowed to do, and it is the
|
|
886
|
+
* check that refutes an envelope claiming the seam over a body carrying no such
|
|
887
|
+
* block. Presence of the KEY is the test, not truthiness: `cache_control: null`
|
|
888
|
+
* is a preserved marker too, exactly as it is on every other block type.
|
|
889
|
+
*/
|
|
890
|
+
function hasThinkingBlockCacheControl(messages) {
|
|
891
|
+
if (!Array.isArray(messages))
|
|
892
|
+
return false;
|
|
893
|
+
return messages.some((message) => {
|
|
894
|
+
const content = ownProperty(message, "content");
|
|
895
|
+
return (Array.isArray(content) &&
|
|
896
|
+
content.some((block) => {
|
|
897
|
+
const type = ownProperty(block, "type");
|
|
898
|
+
return ((type === "thinking" || type === "redacted_thinking") &&
|
|
899
|
+
ownProperty(block, "cache_control") !== undefined);
|
|
900
|
+
}));
|
|
901
|
+
});
|
|
902
|
+
}
|
|
823
903
|
/** Reads a system block's `text` without asserting the block's shape. */
|
|
824
904
|
function systemBlockText(block) {
|
|
825
|
-
return
|
|
826
|
-
|
|
827
|
-
|
|
905
|
+
return ownProperty(block, "text");
|
|
906
|
+
}
|
|
907
|
+
/** Recognises the canonical billing block by its fixed, self-describing head. */
|
|
908
|
+
function isBillingBlockText(text) {
|
|
909
|
+
return typeof text === "string" && text.startsWith(BILLING_BLOCK_TEXT_PREFIX);
|
|
828
910
|
}
|
|
829
911
|
/**
|
|
830
|
-
*
|
|
912
|
+
* VERIFIES how many canonical blocks the emitted `system` array carries.
|
|
913
|
+
*
|
|
914
|
+
* The root seams `suppressBillingBlock` and `suppressIdentityBlock` make four
|
|
915
|
+
* prefixes legitimate — `[billing, identity]`, `[identity]`, `[billing]` and
|
|
916
|
+
* `[]` — so no probe over the array alone can tell them apart: an empty prefix
|
|
917
|
+
* is indistinguishable from a caller-only array. The prefix length is therefore
|
|
918
|
+
* READ from the evidence flags, which the builder emits only when suppression
|
|
919
|
+
* actually removed a block.
|
|
831
920
|
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
921
|
+
* Evidence is not trusted blindly. Each block the flags claim is present is
|
|
922
|
+
* confirmed in place: billing by its `x-anthropic-billing-header: cc_version=`
|
|
923
|
+
* head (its tail is per-request), identity by the byte-exact `IDENTITY_TEXT`;
|
|
924
|
+
* a claim that identity was suppressed is confirmed by that text being absent
|
|
925
|
+
* from the whole array. This is strictly stronger than the position probe it
|
|
926
|
+
* replaces, which never checked the billing slot at all. Never `cache_control`:
|
|
927
|
+
* the
|
|
928
|
+
* `cacheControl.suppressIdentityBlock` seam can emit the identity block with no
|
|
929
|
+
* marker, so a marker probe would misread a legitimate request.
|
|
839
930
|
*/
|
|
840
|
-
function canonicalSystemPrefixLength(system) {
|
|
841
|
-
|
|
842
|
-
|
|
931
|
+
function canonicalSystemPrefixLength(system, billingSuppressed, identitySuppressed) {
|
|
932
|
+
let index = 0;
|
|
933
|
+
if (!billingSuppressed) {
|
|
934
|
+
if (!isBillingBlockText(systemBlockText(system[index])))
|
|
935
|
+
fail();
|
|
936
|
+
index += 1;
|
|
843
937
|
}
|
|
844
|
-
if (
|
|
845
|
-
|
|
938
|
+
if (identitySuppressed) {
|
|
939
|
+
// The identity text cannot appear ANYWHERE in a body built with the seam
|
|
940
|
+
// active: `buildCanonicalSystem` drops a caller block equal to it
|
|
941
|
+
// unconditionally, and merging joins with `\n`, so no merged run can equal
|
|
942
|
+
// it either. Absence is therefore checkable, which is what refutes a claim
|
|
943
|
+
// of suppression made over a body that still carries the block.
|
|
944
|
+
//
|
|
945
|
+
// No mirror check exists for billing: a caller block may legitimately begin
|
|
946
|
+
// with the billing header text, so its presence proves nothing.
|
|
947
|
+
if (system.some((block) => systemBlockText(block) === IDENTITY_TEXT)) {
|
|
948
|
+
fail();
|
|
949
|
+
}
|
|
950
|
+
return index;
|
|
846
951
|
}
|
|
847
|
-
|
|
952
|
+
if (systemBlockText(system[index]) !== IDENTITY_TEXT)
|
|
953
|
+
fail();
|
|
954
|
+
return index + 1;
|
|
848
955
|
}
|
|
849
956
|
function parseBody(value) {
|
|
850
957
|
let parsed;
|
|
@@ -916,6 +1023,12 @@ function evidenceRequest(input, callerModel) {
|
|
|
916
1023
|
request.capabilities = input.capabilities;
|
|
917
1024
|
if (input.betaOverrides !== undefined)
|
|
918
1025
|
request.betaOverrides = input.betaOverrides;
|
|
1026
|
+
// The body builder owns the thinking-block allowlist, so the seam flag has to
|
|
1027
|
+
// reach it. It is forwarded only when the caller stated it, keeping the
|
|
1028
|
+
// normalized request shape identical for every request that ignores the seam.
|
|
1029
|
+
if (input.preserveThinkingBlockCacheControl !== undefined)
|
|
1030
|
+
request.preserveThinkingBlockCacheControl =
|
|
1031
|
+
input.preserveThinkingBlockCacheControl;
|
|
919
1032
|
if (input.thinking !== undefined)
|
|
920
1033
|
request.thinking = input.thinking;
|
|
921
1034
|
if (input.effort !== undefined)
|
|
@@ -1036,7 +1149,7 @@ export async function buildClaudeCodeRequest(input, profile = CLAUDE_CODE_2_1_19
|
|
|
1036
1149
|
});
|
|
1037
1150
|
const billing = await createBillingBlock(fingerprintText(validated.source), effectiveProfile.cliVersion, validated.crypto);
|
|
1038
1151
|
const metadata = buildCorrelatedMetadata(identity, validated.source.metadata, validated.source.metadataOverrides);
|
|
1039
|
-
const system = buildCanonicalSystem(validated.source.system, billing, identity, validated.suppressBillingBlock);
|
|
1152
|
+
const system = buildCanonicalSystem(validated.source.system, billing, identity, validated.suppressBillingBlock, validated.suppressIdentityBlock);
|
|
1040
1153
|
const canonicalBody = buildCanonicalBody(evidenceRequest(validated.source, validated.source.model), effectiveModel, system, metadata, effectiveProfile);
|
|
1041
1154
|
const composedBetas = composeBetasWithAudit({
|
|
1042
1155
|
rawModel: validated.source.model,
|
|
@@ -1089,10 +1202,12 @@ export async function buildClaudeCodeRequest(input, profile = CLAUDE_CODE_2_1_19
|
|
|
1089
1202
|
// The canonical system merges adjacent caller blocks and drops any
|
|
1090
1203
|
// block equal to the identity text, so only the emitted count keeps
|
|
1091
1204
|
// `systemBlockCount === body.system.length - <canonical>` true.
|
|
1205
|
+
// Symmetric arithmetic over the two suppression seams: each canonical
|
|
1206
|
+
// block that survived costs one slot. A single constant cannot express
|
|
1207
|
+
// the empty prefix both seams together produce.
|
|
1092
1208
|
emittedSystemBlockCount: system.length -
|
|
1093
|
-
(validated.suppressBillingBlock
|
|
1094
|
-
|
|
1095
|
-
: CANONICAL_SYSTEM_BLOCKS),
|
|
1209
|
+
(validated.suppressBillingBlock ? 0 : 1) -
|
|
1210
|
+
(validated.suppressIdentityBlock ? 0 : 1),
|
|
1096
1211
|
// Emitted only for the opted-in policy, so evidence for every other
|
|
1097
1212
|
// request keeps the shape it had before the seam existed.
|
|
1098
1213
|
...(validated.extraHeaderPolicy === "dropConflicting"
|
|
@@ -1109,6 +1224,20 @@ export async function buildClaudeCodeRequest(input, profile = CLAUDE_CODE_2_1_19
|
|
|
1109
1224
|
...(validated.suppressBillingBlock
|
|
1110
1225
|
? { billingBlockSuppressed: true }
|
|
1111
1226
|
: {}),
|
|
1227
|
+
// Emitted only when the identity block was actually removed, on the
|
|
1228
|
+
// same terms as `billingBlockSuppressed`: the parser reads both flags
|
|
1229
|
+
// to know the length of the canonical prefix it must verify.
|
|
1230
|
+
...(validated.suppressIdentityBlock
|
|
1231
|
+
? { identityBlockSuppressed: true }
|
|
1232
|
+
: {}),
|
|
1233
|
+
// Emitted only when the seam was active AND a reasoning block actually
|
|
1234
|
+
// carried the marker. Opting in without using it records nothing, so
|
|
1235
|
+
// the audit states what happened on the wire rather than what the
|
|
1236
|
+
// caller was permitted to do.
|
|
1237
|
+
...(validated.preserveThinkingBlockCacheControl &&
|
|
1238
|
+
hasThinkingBlockCacheControl(canonicalBody["messages"])
|
|
1239
|
+
? { thinkingBlockCacheControlPreserved: true }
|
|
1240
|
+
: {}),
|
|
1112
1241
|
}, validated.crypto);
|
|
1113
1242
|
return deepFreeze({
|
|
1114
1243
|
url: effectiveProfile.endpoint,
|
|
@@ -1148,6 +1277,15 @@ export function parseBuiltClaudeCodeRequest(value, profile = CLAUDE_CODE_2_1_195
|
|
|
1148
1277
|
const parsedBody = parseBody(body);
|
|
1149
1278
|
const headers = parseHeaders(ownValue(value, "headers"));
|
|
1150
1279
|
const evidence = parseEvidence(ownValue(value, "evidence"));
|
|
1280
|
+
// Reading evidence is not trusting evidence. A claim that the seam
|
|
1281
|
+
// preserved a marker is confirmed against the body, and it is confirmed
|
|
1282
|
+
// HERE — before the byte-length and digest checks — so that a forgery which
|
|
1283
|
+
// is byte-length preserving and evidence-self-consistent is refused by the
|
|
1284
|
+
// structural check rather than incidentally by arithmetic.
|
|
1285
|
+
if (evidence.thinkingBlockCacheControlPreserved === true &&
|
|
1286
|
+
!hasThinkingBlockCacheControl(parsedBody["messages"])) {
|
|
1287
|
+
fail();
|
|
1288
|
+
}
|
|
1151
1289
|
const sessionId = headerValue(headers, "x-claude-code-session-id");
|
|
1152
1290
|
const additionalHeaders = splitDynamicAndExtraHeaders(headers);
|
|
1153
1291
|
const expectedHeaders = buildOrderedHeaders({
|
|
@@ -1186,7 +1324,7 @@ export function parseBuiltClaudeCodeRequest(value, profile = CLAUDE_CODE_2_1_195
|
|
|
1186
1324
|
evidence.systemBlockCount !==
|
|
1187
1325
|
(Array.isArray(parsedBody["system"])
|
|
1188
1326
|
? parsedBody["system"].length -
|
|
1189
|
-
canonicalSystemPrefixLength(parsedBody["system"])
|
|
1327
|
+
canonicalSystemPrefixLength(parsedBody["system"], evidence.billingBlockSuppressed === true, evidence.identityBlockSuppressed === true)
|
|
1190
1328
|
: -1)) {
|
|
1191
1329
|
fail();
|
|
1192
1330
|
}
|