docs: correct the x25519 sibling record; add pin-drift check; fix test_x25519 skip-as-pass - #81
Conversation
…ns.py Three documentation-integrity defects in the x25519/nistcurves pin record, all verified mechanically, plus the tool that would have caught the last one. 1. CLAUDE.md claimed in four places that UCI is "the supported sibling-on path" for USE_X25519_SIBLING=1. Neither backend links. Re-measured at f0127a0 with fresh submodules: make USE_X25519_SIBLING=1 (ip65) X25519_RODATA overflows CRYPTO_OVERLAY by 2048 bytes LIB_NISTCURVES_P256_CODE overflows CRYPTO_RESIDENT by 103 bytes make BACKEND=uci USE_X25519_SIBLING=1 (UCI) <- previously undocumented LIB_NISTCURVES_P256_CODE overflows CRYPTO_HOT by 381 bytes make BACKEND=uci (control) -> links clean Root cause recorded with measured segment sizes: the sibling's CRYPTO_CODE claim is 4,207 B against the in-tree pair's 2,769 B. 2. CLAUDE.md cited `make -C libs/x25519 lib-x25519-scalarmult` as the integration wrapper's entry point. No such target exists upstream at any tag, and build_x25519.sh does not call `make -C` at all — it stages three sources, sed-rewrites .segment "CODE", and hand-emits the X25519_RODATA / X25519_BSS modules. Documented as it is, since that is what a version bump must be migrated through. 3. The nistcurves pin was recorded as v0.3.0 in the Crypto ABI section while the actual gitlink is v0.6.0 and the benchmark tables below it are v0.6.0 numbers — the file contradicted itself. Also records that the pinned x25519 v0.6.0 carries upstream c64-x25519 #64 (RFC 7748 decodeUCoordinate MSB desync, fixed in v0.7.0), and that the in-tree implementation is NOT affected because it still writes the mask back into x25_u. Verified: tools/test_x25519.py --slow 73/73 on the in-tree build, including RFC 7748 vector 2 whose u has bit 255 set. tools/check_upstream_pins.py reports pin-vs-upstream-tag drift for every submodule. Stdlib + git only, one `git ls-remote --tags` per submodule. It exists because `git submodule status` renders versions through `git describe` without --tags — annotated tags only — and c64-x25519 tagged v0.6.0 lightweight, so an exactly-on-v0.6.0 pin renders as "v0.5.0-5-g95fdd70" and reads like a stale pin. The script resolves refs/tags/<n>^{} when present and the bare ref otherwise, and reads the gitlink from `git ls-tree` so it works on an uninitialised submodule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JC-000
commented
Aug 13, 2026
Spot-checked by the supervising session — including a correction to my own earlier claim. The pin "discrepancy" was mine, not the docs'. I opened this lane telling the team that CLAUDE.md's
The bigger finding reproduces exactly. with On not bumping: correct call, and the reasoning holds up. v0.8.0's 9224→8383 being a cold-section move rather than a real shrink, and ip65's 2,048 B The upstream correctness bug in the pinned version (c64-x25519#64, wrong results when peer u has bit 255 set, fixed in v0.7.0) is the most consequential item for planning: it makes the pending
|
…roup tools/test_x25519.py reported "RESULTS: 71/71 passed, 0/71 failed" while skipping both RFC 7748 scalarmult vectors. Those two are the only end-to-end x25519_scalarmult coverage in the file — everything else in the 71 is field arithmetic — so the default invocation certified a clean pass without ever multiplying a scalar. Skipped groups were never added to test_groups, so they left the denominator entirely and the counters could not express the gap. Same skip-as-pass shape as audit finding F3 (credit: Lane C confirmed the mechanism independently). The gate's justification was a "~100 min each" comment. Measured under VICE warp on the in-tree ip65 build: full suite with both vectors 37.9 s full suite without them 4.8 s ~16.5 s per vector — the comment was off by a factor of ~360, and the gate was buying 33 seconds while hiding the only test that matters. So both halves: - The vectors now run BY DEFAULT. `--fast` skips them; `--slow` is kept as an accepted no-op so existing invocations don't break. - Any skipped group is recorded and named in the verdict: RESULTS: 71/71 passed, 0/71 failed -- 2 group(s) SKIPPED: \ x25519 RFC 7748 vector 1, x25519 RFC 7748 vector 2 WARNING: end-to-end x25519_scalarmult coverage did NOT run; \ this run does not certify X25519. An unqualified clean pass is no longer printable over a group that did not run. Exit code is unchanged (0 iff nothing failed): unlike F3's missing-label case, `--fast` is an explicit operator choice, so it is not an error — but it can no longer be a silent one. run_tests now returns (passed, failed, skipped_groups). tools/run_all_tests.py is the only in-tree importer; it unpacks the third value and raises if it is ever non-empty, so a future gate cannot quietly drop coverage from the aggregate verdict. That run gains the vectors too: x25519 72/72 -> 73/73, aggregate 255/255, +34.8 s for the suite. Also corrected in passing: the vectors were labelled "RFC 7748 Section 6.1" but are the §5.2 scalarmult vectors (§6.1 is the Alice/Bob DH pair), and U_2 ends 0x93 — bit 255 set — which makes vector 2 the decodeUCoordinate MSB regression test that catches upstream c64-x25519 #64, the bug live in our pinned libs/x25519 v0.6.0. Noted at the vectors so nobody drops it as redundant. Verified: default -> 73/73, both vectors PASS, 38.2 s --fast -> 71/71 + both groups named + warning --slow -> 73/73 (back-compat no-op) run_all_tests.py --skip-slow -> x25519 73/73, TOTAL 255/255 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JC-000
commented
Aug 13, 2026
Second commit verified by the supervising session. Vectors run by default now — confirmed here: The near-miss is worth recording as much as the fix. Changing Your new finding reproduces exactly, and it is the same defect one level up:
On the exit-code divergence from #83: I agree with your call, keep it. A missing label is a broken environment and must be an error; The vector-labelling correction is the sharpest detail in this PR. Confirmed independently: RFC 7748's second scalarmult vector has |
…cond door) `tools/run_all_tests.py --skip-slow` omitted "x509" from the suite list by never adding it, so the aggregate printed a TOTAL and exited 0 with no trace that the whole X.509/ECDSA suite had not run. That is F3's shape one level up: the skipped assertions left the denominator instead of being accounted for, and the dropped suite is precisely the one F3 is about. The numbers make the failure mode concrete. A full aggregate run reports TOTAL: 264/264 with `x509 11/11`; `--skip-slow` reported TOTAL: 253/253. Same "everything passed" shape, 11 assertions lighter, and nothing in the output says which 11 or why. Skipped suites are now recorded as (name, reason) and surfaced three ways: named on the TOTAL line, given their own SKIP row in the per-suite table, and followed by an explicit warning that the aggregate does not certify them. Exit code stays 0. An operator passing --skip-slow made a deliberate choice, unlike F3's missing label, which is an unrequested environmental failure. The rule the two cases share is the one that matters: an involuntary skip is a failure, an explicit skip is allowed but must never be silent. Same convention Lane G adopted for test_x25519.py's --fast gate in PR #81. Acceptance (`--skip-slow --workers 4`, real aggregate runs): before TOTAL: 253/253 passed, 0 failed (no x509 row, no mention anywhere in the output) EXIT_CODE=0 after TOTAL: 253/253 passed, 0 failed -- 1 suite(s) SKIPPED: x509 ... SKIP x509 --- did not run: --skip-slow (X.509 DER parsing + ECDSA P-256 verify) WARNING: the suite(s) above did not run. This aggregate result does not certify them, and their assertions are absent from the TOTAL. EXIT_CODE=0 Reported by Lane G while reviewing the F3 fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-application of this branch's own finding. The script's docstring said a full run is "a couple of seconds" — true, but I had never measured it, which is the exact shape of the `~100 min each` comment in test_x25519.py that this branch just deleted: a cost assertion sitting in prose where no run can contradict it, quietly setting policy. Measured instead: 2.1 s wall-clock, 0.15 s CPU, 3 submodules, warm DNS. Network-bound, scales with submodule count rather than repo size. Also verified rather than assumed, by stubbing subprocess.run: exactly 3 `git ls-remote --tags` and 3 `git ls-tree HEAD` for 3 submodules, so the "one per submodule" claim is accurate. Imports are stdlib-only (__future__, argparse, json, os, re, subprocess, sys). All three claims held; none needed correcting. The change is that they are now specific, dated, and paired with the command that falsifies them — a vague-but-true cost note is one refactor away from a false one. Docstring only; no behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolves the two CLAUDE.md conflicts by taking the better side of each rather than picking a branch: - Known-issues x25519 block: took the merged-stack version. It is a strict superset and this branch's side still asserted "UCI remains the supported sibling-on path", which #81 disproved by measurement (UCI overflows CRYPTO_HOT by 381 B; nothing links the sibling today). It also still cited a `make -C libs/x25519 lib-x25519-scalarmult` target that exists at no tag. - ECDSA wall-clock note: took this branch's version, which adds the "(the pin is v0.6.0 today)" clarification correcting the file's own v0.3.0-vs-v0.6.0 self-contradiction. The c64-nist-curves#54 CLOSED correction that lived in the dropped hunk survives via this branch's two non-conflicting hunks; verified both remaining mentions state CLOSED and no "optional headroom" phrasing is left. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… regression Archaeology from the deferred-followups lane, re-verified here before landing. #102's 'the sibling links' evidence was honest on its own branch: at 76d876c the nistcurves pin was still v0.6.0, and the two bumps were parallel branches neither of which could see the other. One clause tightened rather than pasted. The contributed wording said v0.6.0 'does not export reu_mul_tables_init at all'; it does, at src/main.s:254. What is true — and is the actual mechanism — is that main.o is excluded from every lib-* archive target, and c64-https links archives only. Upstream #81 moved the provider into reu_mul_init.s so it would ship to consumers, which is what put it in our link at v0.9.1. A claim a reader can disprove with one grep is worse than no claim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lane G — X25519 sibling alignment + upstream monitoring. Docs, one new tool, and one
test-coverage fix; no submodule bump, no cfg change — not a byte of any PRG moves.
That last clause is a claim, so here it is as a check rather than an assurance —
clean build at the base and at this branch's tip:
Byte-identical. (Lane C's
fix/audit-f3-f7-ecdsa-coverageindependently produces thesame PRG in a separate worktree, and the underlying
ip65-build/ip65-c64.bin—6,951 B, sha256
cf1a5ff7809af4e4655e385b378b936054f41046ff2b7604828af3240c2d90dd—now has agreeing from-source builds in three worktrees plus a copy built 2026-05-06.
See #83 and #86.)
make clean && makeis load-bearing in that reproduction, not decoration. This isa property of the source tree, not of whatever is sitting in
build/.BACKENDisnot a
-Dflag — it selects an include path (-I src/net/$(BACKEND)), andsrc/tls13.s:30does.include "net_tuning.inc". So a plainmakeafter aBACKEND=ucibuild findstls13.oup to date by timestamp, never recompiles it, andlinks an ip65 PRG carrying UCI's post-ServerHello drain budget (1x16 instead of
8x250) — issue #73's regression reintroduced by build order rather than by any code
change, in the backend where that budget is load-bearing. Reproduced here
deliberately after Lane E hit it by accident:
Same size, no warning, and it would pass any size check. Lane E hit a second
outcome of the same trap on GNU Make 3.81 (what macOS ships): if the recompiles land
inside the same 1-second mtime granule as the previous link, make skips the link
entirely, exits 0, and leaves the other backend's PRG at the output path — wrong
backend, wrong size. So size catches that one, hash catches both, exit code catches
neither.
Do not cite
.ohashes as evidence — ca65 objects are not reproducible. Ioriginally quoted
tls13.ohashes here as a proof chain and have removed them. Sixconsecutive clean builds of the same source in the same worktree gave six different
tls13.ohashes. The cause is a build timestamp in the object header: a ca65variable-length integer (7-bit groups, LSB first, continuation bit in bit 7) starting
at offset 100, which decodes to exactly
date +%sof the build — confirmed twicewith
delta = 0 s, most recentlyvarlen@100 = 1786623518against a captureddate +%sof1786623518.Two corrections worth recording, since both readings that preceded this one were
wrong in ways that looked right:
(
src/tls13.s,src/constants.inc,src/net/ip65/net_tuning.inc) and there arezero absolute paths, so two worktrees would otherwise agree. The objects are not
deterministic in place.
make clean && makemovestwo bytes here, other rebuilds up to five. That is the variable-length encoding: how
many bytes change depends on how many 7-bit groups roll over between builds. Reading
the first byte as "the low byte of the timestamp" holds only when bit 7 of the value
is set (~50% of builds).
build stamp: a plain
LE32at offset 107 is the source file's mtime(
LE32@107 = 1786619514 = stat -f %m src/tls13.s, constant across builds — verified,not inferred). It equals the build time only if you
touchthe source first, whichis exactly how it can look like a build stamp under an incremental-build test. The
build stamp is the varlen at 100; the mtime at 107 cannot explain non-determinism,
since it does not change unless the source does.
ld65does not propagate that field, which is exactly why the PRG hash above istrustworthy: it held at
db31111031e2…across every one of those builds. Thatasymmetry is a checkable property of the toolchain, not a convention — and the whole
neutrality matrix rests on it.
Every hash and test result in this PR comes from a
make cleanbuild; the onemeasurement that could conceivably have been affected — the
od65segment sizes below— is provably immune, since
fe25519.s/x25519.sinclude onlyconstants.inc, whichneither backend directory shadows. Documented by #86 at the top of the Variables block;
CLAUDE.md previously carried the rule only in Packaging and only about flags.
Three commits:
fb8ba56(docs +check_upstream_pins.py),0080274(thetest_x25519.pycoverage fix, added after Lane C confirmed the finding and declinedthe handoff — see the last section), and
54498cc(docstring only, below).54498cc— turning the finding on my own file.check_upstream_pins.py'sdocstring claimed a full run is "a couple of seconds". True, but unmeasured — the
same shape as the
~100 min eachcomment this branch deletes: a cost assertionsitting in prose where no run can contradict it. Measured: 2.1 s wall-clock,
0.15 s CPU (3 submodules, warm DNS), and verified by stubbing
subprocess.runthat it really is one
git ls-remote --tags+ onegit ls-treeper submodule.All three claims held; none needed correcting. What changed is that they are now
specific, dated, and paired with the command that falsifies them.
Base
docs/benchmark-refresh@f0127a0. All measurements 2026-08-13, freshsubmodule clones, cc65 from homebrew, VICE via
default_vice_config()(-reu).Preamble — the pin discrepancy that started this, and its resolution
The lane was opened because
git submodule statusreportslibs/x25519asv0.5.0-5-g95fdd70while CLAUDE.md says the pin isv0.6.0.CLAUDE.md was right. The pinned gitlink is
95fdd705b0f7d780cada3dee08158084d327c3f9, identical onorigin/docs/benchmark-refresh, localdocs/benchmark-refresh, andorigin/master, and it is exactly thev0.6.0tag commit:v0.5.0/v0.7.0/v0.8.0are annotated;v0.6.0is lightweight.git describewithout
--tagswalks past it, andgit submodule statusrenders through exactlythat call. A tooling artefact, not a stale clone and not a doc bug — but one that
will recur, so it is now written down and the new script is immune to it.
Defect 1 —
USE_X25519_SIBLING=1links on neither backendCLAUDE.md said in four places that ip65 is broken and "UCI remains the supported
sibling-on path". UCI is broken too, and that failure was nowhere on record.
A/B, verbatim:
The ip65 pair (2048 / 103) reproduces CLAUDE.md's 2026-07-29 measurement exactly —
good sign for the rest of that record. The UCI 381 B is new.
Cause, measured (
od65 --dump-segsize) rather than asserted:fe25519_raw.o+x25519_raw.o+x25519_init_raw.ofe25519.o+x25519.odata_x25519_rodata_raw.odata_x25519_bss_raw.o+1,438 B of CRYPTO_CODE into a
CRYPTO_HOTwith ~1,057 B of slack. ip65 additionallycannot fit 2,304 B of
X25519_RODATAinto its 4,212 BCRYPTO_OVERLAYbesideTLS_CODE + CRYPTO_AUX_CODE.
Blast radius: no shipped or buildable artifact contains the x25519 sibling, so
anything downstream of "the sibling is in use" is moot today. That is reassuring for
Defect 2.
Defect 2 — the pinned v0.6.0 carries an upstream correctness bug (recorded, not fixed)
c64-x25519 #64, fixed in v0.7.0:
x25519_scalarmultreturns deterministicallywrong results for a peer u-coordinate with bit 255 set, across v0.4.0–v0.6.0
— our pin. v0.4.0 stopped writing the RFC 7748
decodeUCoordinatemask back intox25_ubut left the ladder'sz_3 = x_1 * (DA-CB)^2site reading the unmaskedbuffer;
2^255 == 19 (mod p), so x1 and x3 disagree by 19.Verified in source at both tags, not taken from release notes:
The in-tree implementation is not affected.
src/crypto/x25519.s:116still doessta x25_u+31after theand #$7f, so its x_1 read at :441 sees the masked value —c64-https never took v0.4.0's no-mutation change, so it never acquired the bug.
A/B, in-tree positive control:
U_2is...c715a493— last byte0x93, bit 255 set: precisely the vector thatcaught #64 upstream. So our own suite would catch the sibling bug the moment the
sibling links. Coverage exists; the build does not.
In TLS terms the peer u is the server's ServerHello key share; conforming servers
send canonical keys (u < p), so this is interop/conformance, not secret leakage. But
it is a hard blocker on the flag-flip decision CLAUDE.md records as pending: do not
default the sibling on at v0.6.0.
Defect 3 — a cited build target that does not exist
CLAUDE.md pointed at
make -C libs/x25519 lib-x25519-scalarmult.No such target at any tag. And
build_x25519.shdoes not callmake -Cat all — itstages three sources,
sed-rewrites.segment "CODE"toCRYPTO_CODE, andhand-emits the
X25519_RODATA/X25519_BSSmodules. Documented as it actually is,because that is what a version bump has to be migrated through.
(
docs/library-ingestion-architecture.md:294proposes such a target in the futuretense — the likely origin of the error — and was left alone.)
Defect 4 — the file contradicted itself about the nistcurves pin
Crypto ABI section said
libs/nistcurves@v0.3.0; the gitlink is00d2626ce…== thev0.6.0tag, and every benchmark table below it is v0.6.0 data. Same shape as auditfinding F8. Corrected, and the stale v0.3.0 wall-clock paragraph relabelled as the
historical REU baseline it is. Found by the new script, not by reading.
New tool —
tools/check_upstream_pins.pyStdlib +
gitonly. Nogh, no tokens, no HTTP libraries. Onegit ls-remote --tagsper submodule; a full run is a couple of seconds.Also
--jsonand--ref. Two load-bearing design points:refs/tags/<n>^{}when the peeled ref exists, falls back to the bare ref, so thevalue is always a commit SHA. This is exactly the trap that produced the preamble's
discrepancy: the tool that raised the false alarm would not have.
git ls-tree <ref> -- <path>, not the working copy — correct fora submodule that was never
--init'd (this worktree started that way) and immuneto a dirty checkout.
--strictexits 1 on drift, so it drops into cron/CI unchanged. Suggested cadenceweekly; needs network, no credentials.
Also recorded in CLAUDE.md (no code change)
CODE/DATA->LIB_X25519_CODE/LIB_X25519_DATAand addsLIB_X25519_INIT_CODE(which must be the last file-emitting segment before anybss-type segment); our
sedthen matches nothing silently. v0.7.0's docs: confirm U64E comb 48 MHz point at n=6 — 18.39 s median #64 fixadds an
x25_x1buffer our hand-emitted BSS stub does not export.LIB_X25519_RESIDENT_BYTES9224 -> 8383 is not a shrink — 826 B of itmoved to reclaimable
COLD_BYTES(8383 + 826 == v0.7.0's 9209; net ~15 B), and ourwrapper stages only three of the sibling's sources anyway, so upstream manifest
deltas do not subtract from the overflows above. Easy to read the wrong way.
X25519_ONCHIP_MULshipsLIB_X25519_REU_BANKS_USED = 0with thewhole §8.2 export surface gated out (
src/reu_config.s:199), includingreu_fetch_mul_row— which is the stated reason for theUSE_NISTCURVES_ONCHIP/USE_X25519_SIBLINGmutual exclusion atMakefile:90.Recorded as a lead; whether the combination links is unverified.
Not fixed here, reported instead
be met: ip65's 2,048 B
X25519_RODATAoverflow is ours, not upstream's — thosetables are hand-emitted by
build_x25519.sh— so no submodule move touches it.onchip_x25519_reu_dependentis stale and should be retired: PR fix(crypto): REU-less X25519 under USE_NISTCURVES_ONCHIP — fe_mul rows via og_common #69(in this baseline) gates the
reu_fetch_mul_rowimport out entirely underUSE_NISTCURVES_ONCHIP(src/crypto/fe25519.s:68-78), importingog_common/og_src_ldinstead, so the REU-DMA path is unlinkable rather than merely unused —and PR feat(e2e): hardware-free REU-less HTTPS e2e in VICE + fix(tls): post-ServerHello drain #71's REU-less ip65+onchip VICE e2e exercises a full handshake with no REU.
The shipped "no REU required" claim is true at HEAD.
tools/run_all_tests.py --skip-slowdrops the whole x509 suite — a widerinstance of the same class, untouched here. Flagged for Lane C.
Commit 2 (
0080274) —tools/test_x25519.pywas F3-shapedAdded after Lane C independently confirmed the mechanism and asked me to keep it here
rather than split one file across two PRs. Lane C's reading of the mechanism:
the skip is announced once inside
run_tests, the skipped groups were never added totest_groups, and the summary reads only the counters — so skipped assertions leavethe denominator instead of counting against it.
Before:
The two RFC 7748 vectors are the only end-to-end
x25519_scalarmultcoverage in thefile — everything else in that 71 is field arithmetic. The default invocation
certified a clean pass without ever multiplying a scalar.
The
~100 min eachcomment was the whole justification for the gate, and it iswrong. Measured under VICE warp on the in-tree ip65 build:
~16.5 s per vector — off by a factor of ~360. The gate was buying 33 seconds while
hiding the only test that matters. So both of Lane C's halves apply:
--fastskips them;--slowis kept as anaccepted no-op so existing invocations don't break.
longer printable over a group that did not run.
A/B, verbatim:
Exit code deliberately unchanged (0 iff nothing failed). Unlike F3's missing-label
case,
--fastis an explicit operator choice, so it is not an error — but it can nolonger be a silent one. Lane C and I converged on one rule covering
test_x509.py,test_x25519.pyandrun_all_tests.py, with the exit-code difference falling out ofit rather than reading as two lanes disagreeing:
(Wording is Lane C's, from #83.) A missing label is an unrequested environmental
failure and goes red;
--fast/--skip-sloware operator choices and stay green,but neither may print an unqualified clean pass.
Merge with #83 — confirmed by running it, not by reading line numbers. Both PRs
touch
tools/run_all_tests.pyin different hunks (I changed the x25519 import branch~line 90; #83 changed the suite-list construction and the summary block). Trial merge
of
fix/audit-f3-f7-ecdsa-coverageinto this branch:Automatic merge went well, andthe merged tree behaves correctly in both directions —
Note for whoever lands second: #83's illustrative totals (
264/264full,253/253under--skip-slow) are correct on its own branch but become 266/266 and255/255 once this PR's +2 x25519 assertions land. The numbers drift, the behaviour
does not.
Blast radius, found and handled:
run_testsnow returns(passed, failed, skipped_groups).tools/run_all_tests.py:90is the only in-treeimporter and unpacked a 2-tuple — it would have broken. It now unpacks the third value
and raises if it is ever non-empty, so a future gate cannot quietly drop coverage
from the aggregate verdict. That run gains the vectors too:
Also corrected at the vectors: they were labelled "RFC 7748 Section 6.1" but are the
§5.2 scalarmult vectors (§6.1 is the Alice/Bob DH pair). And
U_2ends0x93—bit 255 set — which makes vector 2 the
decodeUCoordinateMSB regression test thatcatches upstream c64-x25519 #64, the bug live in our pinned v0.6.0. Noted inline so
nobody retires it as redundant: per Defect 2 above, it is the regression gate for
exactly the bump this audit is about to authorise.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com