feat(parser_http_server): HTTP+JSON pivot for TVC public ingress - #450
pepe-anchor wants to merge 30 commits into
Conversation
…446) ## Why Two host binaries have to serve the same Turnkey JSON envelope to the same client. - `parser_gateway`: REST in front, gRPC to `parser_grpc_server` behind. Non-TEE local dev and CI, so it emits a mock `bootProof`. - `parser_http_server` (#450): HTTP+JSON inside the enclave, calling `parser_app` in process, no gRPC hop. This is what gets deployed to the TVC, because Cloudflare in front of `app-<uuid>.turnkey.cloud` rejects gRPC with 403. It emits a real attested `bootProof`. Different transports, different trust levels, one wire contract. The Go [`visualsign-turnkeyclient`](https://github.com/anchorageoss/visualsign-turnkeyclient) and the wallet integrators behind it cannot tell the two apart, and must not be able to. Two features also land in that envelope during PRS-581: `bootProof` (#337) and `intermediateOutput` (#414). If the definition stays inline in `parser_gateway`, the pivot copies it, and every envelope change from here on gets made twice and kept in sync by hand. The unmerged x402 branch had already grown its own third copy. So: one home, two importers, and the `bootProof` difference becomes a parameter instead of a fork. Downstream, #450, #451 and #452 build on `parser_http_server`; #449 builds on `parser_gateway`. Both sides import the envelope from here, which is also what shrinks the PR #304 rebase from "reconcile two envelope definitions" to "add a module". ## What - `host_primitives::turnkey` becomes the single home for the Turnkey request/response envelope, as the union of both existing definitions. - `bootProof` is now an injected value rather than a hardcoded mock: `error_response(msg, boot_proof)`. `parser_gateway` keeps its stable local-dev mock through a local shim; the enclave pivot supplies a real attested one. - Construction moved with the types: `success_response` sits next to `error_response`, so `parse_handler` no longer assembles the success envelope field by field in the gateway. - `parser_gateway` imports the shared types and keeps the four `MOCK_BOOT_PROOF_*` constants and the tests that are about its own mock and top-level wire shape. Three tests that would have duplicated coverage now living in `host_primitives::turnkey` moved there instead of being maintained in both places. - `intermediate_output` gained `serde(default)` alongside its existing `skip_serializing_if`. Without it the omitted key serialized fine but failed to deserialize, so a response could be written and not read back. Nothing in tree deserializes a response today (both binaries only emit one), which is why no test caught it. The client-direction derives are there so the envelope is symmetric for the out-of-tree clients that do read it, and the new round-trip test is what keeps it that way. Happy to drop those derives and the `default` until something in tree needs them, if you'd rather not carry them. No wire change. The gateway's JSON output is byte-identical. ## Test evidence ``` cargo test -p host_primitives -p parser_gateway host_primitives: 5 passed parser_gateway: 11 passed cargo fmt --all -- --check: clean make -C src lint: clean ``` The gateway test count moves from 13 to 11 because three tests relocated to `host_primitives::turnkey`, where the types they cover now live: the six-key `bootProof` assertion (`boot_proof_wire_shape_is_exactly_six_camel_case_keys`), the empty `intermediateOutput` omission check, and the Solana chain-metadata discriminator test. `host_primitives` goes 4 to 5 with the new round-trip test. No coverage was dropped. The two regression tests that matter both still pass with unchanged constant values: `mock_boot_proof_matches_production_wire_shape`, `error_response_carries_mock_boot_proof`. The "no wire change" claim was checked rather than assumed: a throwaway test reconstructed the pre-refactor struct definitions verbatim from base and byte-compared `serde_json` output against the new `host_primitives::turnkey` types across success, error, signature-present, and intermediate-output-present cases. All identical. One trap worth recording: the x402 branch's `TurnkeyResponseWrapper` had no `rename_all = "camelCase"`, which the gateway's inline struct did have. Adding `boot_proof` without it would have serialized as `boot_proof` instead of `bootProof` and silently broken the wallet contract. The six-key wire-shape test catches it. ## Rollback Revert the commits. No deploy, no migration, no wire change, so a revert restores the previous state exactly. ## Linear PRS-581 Stack position: base of the PRS-581 stack. #337, #414. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
7fe0208 to
7bc3e65
Compare
There was a problem hiding this comment.
Pull request overview
Adds an in-process HTTP/JSON ingress for Turnkey TVC, including boot-proof extension points and deployment packaging.
Changes:
- Adds open v1/v2 parse routes and health endpoint.
- Adds static boot-proof generation and HTTP integration coverage.
- Extends container builds and stagex release metadata.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/parser/http-server/src/main.rs |
Implements HTTP ingress and parsing. |
src/parser/http-server/src/boot_proof.rs |
Adds boot-proof abstraction and manifest encoding. |
src/parser/http-server/Cargo.toml |
Configures server dependencies and features. |
src/parser/http-server/build.rs |
Injects build version metadata. |
src/integration/tests/http_server.rs |
Tests HTTP routes and response shape. |
src/integration/Cargo.toml |
Adds HTTP test dependency. |
src/Cargo.toml |
Registers the new workspace crate. |
src/Cargo.lock |
Locks new crate dependencies. |
Makefile |
Adds server image targets. |
images/parser_http_server/Containerfile |
Packages the HTTP pivot. |
images/parser_grpc_server/Containerfile |
Packages the gRPC server. |
.github/workflows/stagex.yml |
Builds images and publishes deployment notes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 3 comments.
Suppressed comments (1)
src/parser/http-server/src/main.rs:355
- This only exercises
parse_envelopedirectly, so it still passes if either route changes its extractor from rawBytestoJson<TurnkeyRequestWrapper>. The handler-level compile regression described in the resolved review thread is absent from the current diff; restore a test that callsparse_v1(State(state), Bytes::from_static(...))(and/or routes a raw body through the router) so the X-Stamp extension seam is actually pinned.
fn envelope_is_parsed_from_raw_bytes_not_reserialized() {
// A later PR verifies an X-Stamp signature over the exact request
// bytes. If a handler ever takes `Json<T>` and re-serializes, the
// bytes change (key order, whitespace, unicode escaping) and every
// stamp fails. Locking the seam here means that PR adds one call and
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/parser/http-server/src/main.rs:135
- Logging
{e}still records attacker-controlled values from the unauthenticated request: as noted above, a serde type mismatch can include the offending string verbatim, allowing each 400 to write nearly 64 KiB of request data (including transaction contents) into enclave logs. Log only bounded metadata such as the error location/category while keeping the client message generic.
eprintln!("invalid request body: {e}");
.github/workflows/stagex.yml:140
- Including
parser_gatewayhere generates the same paste-readytvc deploy createinstructions used for enclave pivots, even though the role note identifies it as host-side. Its image contains only the gateway, which defaultsGRPC_ADDRto localhost and reports/healthas unavailable without a separate gRPC backend (src/parser/gateway/src/main.rs:69-105,228-237), so following these generated instructions creates an unhealthy TVC deployment. Keep publishing its image, but restrict TVC deployment instructions to the actual pivot binaries (or give the gateway a separate non-TVC note).
if: matrix.target.name == 'parser_app' || matrix.target.name == 'parser_gateway' || matrix.target.name == 'parser_http_server'
prasanna-anchorage
left a comment
There was a problem hiding this comment.
Read the whole crate rather than the diff, and verified the claims that are load-bearing rather than taking the commit messages for them. The HTTP pivot itself is in good shape — this is careful work and the comments explain why in the places that matter. make lint and make test are both green locally on 73a93048 (exit 0, no failures; the http_server integration test runs and passes).
One thing I'd like resolved before approving, and it is not the pivot — it is the parser_grpc_server image that came along from the closed #447.
Verified
- The
vsockgate is actually wired. The Cargo.toml comment warns that without it the pivot reads the dev ephemeral path and crash-loops asHealthy / Desired Replicas: 0/N.images/parser_http_server/Containerfiledoes pass--features "vsock ${CHAIN_FEATURES}", with the rationale inline. This was my first check because it is the failure mode that is invisible until deploy. healthCheckType: TVC_HEALTH_CHECK_TYPE_HTTPis the right pairing for this binary, and correctly different fromparser_app's gRPC health check.- The byte-identical response claim holds.
TurnkeyPayload::intermediate_outputcarries#[serde(default, skip_serializing_if = "String::is_empty")], so base64-of-empty ("") really is omitted and existing consumers see an unchanged body. - The
block_in_placeprecedent is real —parser_app::service::Processor::processdoes exactly this around the sameparse()call (service.rs:34), so the pivot is consistent with the vsock path rather than inventing a policy. - The layering for the 413 rewrite is correct.
envelope_body_limit_rejectionis applied afterDefaultBodyLimit, so it is the outer layer and does see the limit's rejection on the way out. The reasoning for handling 413 by status but routing 404/405 throughfallback/method_not_allowed_fallbackinstead is right, and themod testscomment explaining thathandle_parselegitimately returns its own 404 is the kind of note that stops someone "simplifying" this later. - v2-being-open is the status quo, not new exposure. I started to flag registering an open
/v2, then foundparser_gatewayalready routes both v1 and v2 to the same openparse_handlerand says production Turnkey serves both from one backend. So this matches today's behaviour; withdrawn. - Integration coverage is genuinely good — health, v1, v2 parity, the
include_intermediate_outputopt-in on a Solana transfer (the path that actually emits a non-empty blob), 400, 404, 405, and 413, each asserting the six-keybootProofsurvives. Thetry_waitfail-fast with the "if built with--features vsock, rebuild without it" message will save someone an afternoon. aws_attestation_doc_b64: String::new()— empty, never faked, so a strict verifier rejects outright. Right call, ande3411cafcorrecting the release notes to stop claiming enforcement that does not exist is the right instinct; notes that overstate what is enforced really are worse than silence.
The one to resolve: the parser_grpc_server image cannot start
images/parser_grpc_server/Containerfile is new here, and the image it produces panics at startup:
parser_grpc_serverresolves its key fromstd::env::var("EPHEMERAL_FILE"), defaulting tointegration/fixtures/ephemeral.secret(main.rs:79-80).- The image rootfs contains only the binary —
mv ${RELEASE_DIR}/parser_grpc_server /rootfs/thenCOPY --from=build /rootfs/. .onto busybox. That fixture path does not exist in it. GrpcService::newdoes.expect("Failed to load ephemeral key"), so the process aborts rather than degrading.EPHEMERAL_FILEis set nowhere in the repo — no Containerfile, workflow, or doc mentions it.
So it builds green in CI (nothing runs it) and fails the moment anyone deploys it. Two things compound that: stagex.yml's TVC-deployment-details step is gated on parser_app || parser_gateway || parser_http_server, so this target is the only one in the matrix that publishes an image with no role note and no deployment guidance; and the default it falls back to is a committed test key (src/integration/fixtures/ephemeral.secret, 64 bytes, in git). Nothing here exposes it today because the file is absent from the image — but a default that points at a repo-committed signing key is a bad direction for an enclave binary to fail in, and it should probably be a required arg with no fallback rather than an env var with one.
Given #447 was closed unmerged, my suggestion is to either drop the parser_grpc_server target from this PR and let it land with its own deployment story, or add the role note plus make the key path required. Happy either way — I just do not think a release should carry an attested image that cannot boot.
Non-blocking
encode_borsh_b64swallows a borsh failure into""(unwrap_or_default()), which is indistinguishable from the "manifest unreadable in local dev" case that its siblingread_manifest_borsh_b64doeseprintln!about. Near-unreachable for these types (aVecwriter), so this is about the asymmetry rather than the risk — worth aneprintln!on the error arm so the two silent-empty paths are distinguishable in a log.- The integration test is one function with seven numbered steps, so a failure at step 2 hides 3-7. Understandable given how expensive server startup is, and it matches the other integration tests, so I would only split it if it starts flaking.
SignatureScheme::try_from(sig.scheme).unwrap_or(SignatureScheme::Unspecified)silently degrades an unknown scheme. Unreachable while the producer and consumer are the same binary, which they are — noting it only because it stops being true ifparse()ever moves back behind a wire boundary here.- All diagnostics go through
eprintln!. That is genuinely better than thelog::warn!situation I flagged on #453 (inert inparser_app/grpc-server, no subscriber installed), so no complaint — but it does mean this binary has no structured logging either, and the pivot is the one process an operator will most want fields from.
|
Fixed: dropped the parser_grpc_server release image entirely rather than making the key path required. It rode in from #447 (closed unmerged) and is orthogonal to this PR's HTTP pivot, so no reason to carry its deployment story here. Containerfile deleted, stagex matrix entry and root Makefile target removed. It can land later with a real required-key story instead of the test-key fallback. Also added the eprintln you flagged on encode_borsh_b64's error arm, so a borsh-encode failure no longer shares the silent-empty-string path with the "manifest file missing" case. ddf94f4. fmt/clippy/build/test all green locally. Addressed by Claude, code change applied. React 👎 if this doesn't land and I'll flag it for follow-up. |
|
Two more findings from the latest Copilot review (#450 (review)), listed under "Suppressed comments" rather than as inline threads (Copilot doesn't re-post comments on code unchanged since the previous pass, so there was nothing to reply to directly). Addressing both here:
Verified: Addressed by Claude, code changes applied to both findings. React 👎 if this doesn't land and I'll flag it for follow-up. |
There was a problem hiding this comment.
🟡 Changes recommended
Manifest loading is unbounded, and the promised parser_grpc_server Stagex artifact is absent.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
src/parser/http-server/src/boot_proof.rs:82
std::fs::readallocates for the entire manifest before validation, so a malformed or misprovisioned/qos.manifestcan cause unbounded startup memory use. Use the repository's bounded-reader pattern (for example,src/parser/cli-core/src/mapping_parser.rs:120-140) and reject files beyond a fixed limit before deserializing.
.github/workflows/stagex.yml:50- The PR description says this Stagex matrix builds both
parser_http_serverandparser_grpc_server, but this change adds only the HTTP target; there is also noout/parser_grpc_serverrule orimages/parser_grpc_server/Containerfilein the repository. Either add the promised gRPC image leg and its build files or update the PR scope so operators do not expect an artifact that this workflow never publishes.
- Files reviewed: 11/12 changed files
- Comments generated: 1
- Review effort level: Balanced
Generated by /finish P3 iteration 9. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prasanna's review on #454 flagged that this binary calls parse() with the old two-argument signature, and once #454's ParserConfig lands this fails to build. The trivial fix, defaulting to ParserConfig::accept_unsigned(), would be the wrong one: this is the binary Turnkey's Cloudflare-fronted TVC ingress actually talks to once it becomes the pivot, parser_app's gRPC turns into internal vsock IPC. Silently permissive here is worse than on parser_app, not better. Give it the same --accept-unsigned-abis / --accept-signatures-from-pubkey flags as parser_app, reusing ParserConfig::abi_trust_from_options rather than hand-rolling a second parser, and fail startup (exit 1) if neither or both are given. Threaded through AppState into the parse() call in handle_parse. The integration test that spawns the real binary now passes --accept-unsigned-abis; verified locally that the binary starts and serves requests with the flag and fails closed without it. This branch is rebased onto prs-556-parser-app (#454) so ParserConfig is available; it was previously based on main. Co-Authored-By: Claude <noreply@anthropic.com>
prs-556-parser-app folded PaymentPolicy into ParserConfig (ParserConfig::new now takes abi_trust + payment, and ParseRequest gained a payment_marker field). Wire parser_http_server through with PaymentPolicy::Disabled, since payment enforcement on this binary is still a later PR's scope: v1 and v2 share the same handle_parse path and config, so this is a no-op, not a behavior change. Co-Authored-By: Claude <noreply@anthropic.com>
c286788 to
7db0be2
Compare
Rebasing onto current main introduced a second major version of reqwest into the dependency graph, so cargo now needs the qualified "reqwest 0.12.28" form to disambiguate the integration crate's dependency, not the unqualified "reqwest" the lockfile carried in this PR before the rebase. `cargo build --locked` (used by every stagex Containerfile) rejects the stale entry outright, which is exactly what the "Continuous Integration" workflow's "Ensure working tree is clean" step flagged on this branch's post-rebase push. Co-Authored-By: Claude <noreply@anthropic.com>
A comment-sicko pass flagged roadmap phrasing ("a later PR", "PR 3
ships") and pure restatements of adjacent code (a doc comment
repeating the module doc, a helper doc repeating its signature, a
duplicate vsock explanation already in Cargo.toml). Trims those while
keeping every comment that documents a non-obvious invariant, past
bug, or security rationale (log-injection avoidance, TOCTOU bounds,
the 404/405 regression pin, private-key gitignore rationale, etc.).
Co-Authored-By: Claude <noreply@anthropic.com>
…ing bugs Post-review cleanup pass on the HTTP+JSON pivot before pushing: - Collapse the duplicated `StaticBootProof` constructor bodies and the duplicated manifest-borsh-encoding logic into shared helpers (`Self::new`, `encode_manifest_borsh_b64`). Also drops the unused `BootProofError::Nsm` variant reserved for a later PR. - Extract a `test_app_state()` helper in parser_http_server's tests to remove duplicated `AppState` construction, and move the regression-pin comment on `parse_v1_handler_extracts_raw_bytes_not_a_json_type` back above that test after the extraction displaced it. - Fix a real bug in the `http_server` integration test: swapping `RunningServer`'s manual `Drop` for `qos_test_primitives::PathWrapper` wrapped `work_dir` from the first line of `start()`, so any `.expect()`/`panic!()` before `RunningServer` finishes constructing (including the fail-fast panic this test added specifically for startup failures) ran the delete-on-drop cleanup during unwind and destroyed the working directory before it could be inspected. Keeps `work_dir` a plain `String` until construction succeeds, only wrapping it in `PathWrapper` at the end. - Correct that same fail-fast comment: it understated `wait_until_port_is_bound`'s real worst-case wall-clock ceiling by ~90x (its linear backoff sums to roughly two hours, not 90 seconds). No behavior change outside the `work_dir` cleanup-timing fix; verified with `cargo build --workspace`, `cargo test -p parser_http_server`, `cargo clippy --all-targets -D warnings`, and `cargo fmt --check`. Co-Authored-By: Claude <noreply@anthropic.com>
|
Addressed Copilot's review summary (review 5095495163):
Addressed by Claude. React 👎 on this comment if any of these don't land and I'll flag it for follow-up. |
Generated by /finish P3 iteration 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Deployment instructions omit mandatory startup arguments, and parse routes no longer enforce the existing JSON media-type contract.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
.github/workflows/stagex.yml:213
- The generated deployment instructions omit
pivotArgs, butparser_http_serverexits during startup unless exactly one ABI-trust option is present (main.rs:322-326). Following this release-note block therefore produces an unhealthy HTTP pivot. Add target-specific instructions forpivotArgsthat require the operator to choose either--accept-unsigned-abisor one or more--accept-signatures-from-pubkeyvalues.
echo 'tvc deploy init -o tvc-deploy.json'
echo '# Edit tvc-deploy.json to set:'
echo "# \"pivotContainerImageUrl\": \"${pinned_url}\""
echo "# \"pivotPath\": \"/${TARGET_NAME}\""
echo "# \"expectedPivotDigest\": \"${digest}\""
echo "# \"qosVersion\": \"${qos_version}\""
echo '# "appId": "<env-specific>"'
echo 'tvc deploy create tvc-deploy.json'
src/parser/http-server/src/main.rs:132
- The v2 route has the same media-type regression as v1: raw
Bytesaccepts JSON under arbitrary or missingContent-Type, unlike the existing gateway'sJson<TurnkeyRequestWrapper>contract. Apply the shared header validation here as well so both versions return an enveloped 415 without reserializing the signed bytes.
async fn parse_v2(
State(state): State<AppState>,
body: axum::body::Bytes,
) -> (StatusCode, Json<TurnkeyResponseWrapper>) {
tokio::task::block_in_place(|| handle_parse(&state, &body))
- Files reviewed: 12/13 changed files
- Comments generated: 1
- Review effort level: Balanced
|
Addressed Copilot's review summary (review 5134672719):
Addressed by Claude. React 👎 on this comment if any of these don't land and I'll flag it for follow-up. |
Generated by /finish P3 iteration 1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The generated parser_app deployment arguments omit required host options, and the startup test can stall CI for an extended period.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
src/integration/tests/http_server.rs:107
- The 100-iteration poll does not actually bound startup: after all polls miss, execution falls through to
wait_until_port_is_bound, whose increasing sleeps can total over two hours. A child that remains alive without binding will therefore stall CI until the job timeout instead of failing fast. Record whether this loop observed the listener and panic after its five-second budget when it did not.
- Files reviewed: 12/13 changed files
- Comments generated: 1
- Review effort level: Balanced
|
Addressed Copilot's review summary (review 5134732760):
Addressed by Claude. React 👎 on this comment if any of these don't land and I'll flag it for follow-up. |
Generated by /finish P3 iteration 2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Deployment guidance is incorrect for port overrides, and failed integration setup can leave private keys and child processes behind.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 12/13 changed files
- Comments generated: 3
- Review effort level: Balanced
Generated by /finish P3 iteration 3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The new public enclave ingress and deployment workflow are security-sensitive and require final human validation despite comprehensive coverage.
Review details
- Files reviewed: 12/13 changed files
- Comments generated: 0 new
- Review effort level: Balanced
|
Addressed Copilot's review summary (review 5134824013):
Addressed by Claude. React 👎 on this comment if any of these don't land and I'll flag it for follow-up. |
Why am I making this PR?
Turnkey's TVC public ingress is HTTP only: Cloudflare in front of
app-<uuid>.turnkey.cloudrejects gRPC with 403 (verified 2026-05-16). Moving the parse path onto the pivot needs a binary that speaks HTTP and JSON natively, instead of gRPC over vsock. This is the keystone of the PRS-581 stack; two PRs build directly on it (in-enclave X-Stamp auth, real NSM boot proof).What am I changing?
parser_http_serverbinary opens/health,/v1/parse,/v2/parse(v2 identical to v1 for now); no payment enforcement, no auth, no proto change.bootProof; the attestation doc stays empty until NSM support lands.parser_grpc_serverleg that can't boot.What is the Linear ticket?
PRS-581
What are the rollback steps?
This adds a new binary nothing calls yet and changes no existing behavior, so a revert is inert. Nothing is deployed by merging it.
Is this change backwards compatible?
Yes. Only a new binary and a new CI target are added; no proto, wire-format, or exported API changes to existing crates.
Does this require cross-team/service coordination?
No. Self-contained to this repo; the new binary is not wired into any deployment yet.
How do I know it works as designed? Which tests exercise this code?
CI, Sonar, and Copilot gates are green as of this rebase.
Rebase onto main after #446
#446 merged, so this branch is rebased onto current
mainand the merge commit is flattened away. Three commits now, no merge.The rebase itself was textually clean: this branch never edited
host_primitives/src/turnkey.rsorparser/gateway/src/main.rs, it only imports from them.What it did leave behind was a semantic mismatch. The pivot was written against
host_primitives::turnkeyas of the fork point, which haderror_responsebut not yetsuccess_response(that landed in87816dff, after the fork). So after the rebasehandle_parsewas still assembling the success wrapper field by field, which is exactly the second copy of the wire contract #446 existed to delete. Fixed in its own commit: the success path now goes throughsuccess_response, matching whatparser_gatewaydoes.No wire change.
success_responsefills the same three fields and setserror: None, so the emitted JSON is byte-identical, andbootProofstays camelCase with exactly its six camelCase keys. Three separate tests pin that and all still pass (host_primitives::turnkey::boot_proof_wire_shape_is_exactly_six_camel_case_keys,parser_http_server'sstatic_boot_proof_has_the_six_keys_and_a_real_ephemeral_pubkey, and thehttp_serverintegration test's key-set assertion on both the success and the 400 response).Review pass, four more commits
A pre-push review round on top of the rebase. Nothing here changes the wire
shape or the three seams the stacked PRs plug into.
Every response now really does carry
bootProof. The PR claimed that andwas wrong for three statuses. axum rejects an oversized body, an unmatched
route and a disallowed method before any handler runs, so 413, 404 and 405
went out as bare axum errors with no envelope. 413 is fixed with a middleware,
since
DefaultBodyLimitrejects while reading the body and no handler sees it.404 and 405 deliberately do not share that middleware:
handle_parselegitimately returns 404 itself for
Code::NotFoundwith the parser's realmessage, and a middleware keyed on status alone cannot tell that apart from an
unmatched route, so it would replace a genuine parse failure with "not found".
Those two go through
Router::fallbackandmethod_not_allowed_fallback,which axum invokes only when no handler ran. The integration test asserts the
envelope on all four error paths now.
The parse call no longer blocks the reactor.
parseis CPU bound, sorunning it on the async task pinned a Tokio worker per concurrent request and
would starve the health check Turnkey polls on a 1 to 2 vCPU replica.
block_in_placematches whatparser_app'sProcessoralready does aroundthis same call on the vsock path.
Error messages stopped reflecting request bodies.
serde_json'sDisplayfor a type mismatch embeds the offending value, so a malformed request echoed
up to the full body back to an unauthenticated caller. The client gets a fixed
message; the detail goes to stderr, along with new logging on three internal
error paths that previously failed silently.
The
TODO(#231)clippy exemptions are gone, not deferred: the ephemeralkey load propagates its error and SIGTERM registration falls back to ctrl-c,
so the crate-level
unwrap/expect/panicallows could be deleted outright.NEAR is compiled in. The default feature set said it mirrored
parser_app's and did not, so a NEAR request would have succeeded againstparser_appand failed against the pivot. That was a silent divergence, not adeliberate trim.
Two correctness fixes to the notes this branch generates. The release notes
told an operator that
/v2is TVC-enforced whenGATEWAY_SIGNING_PUBKEY_HEXis set and that
parser_gatewayperforms x402 verify and settle. Neither istrue yet: that variable is read nowhere in
src,/v2routes to the same openhandler as
/v1, and there is no payment code in the tree. Both notes nowdescribe the scaffolding as scaffolding.
Correction to
ac9e5a47's commit title. It says "serialize release-notewrites" and the mechanism does not serialize. The retry plus read-back is best
effort: it catches this leg's own write being clobbered before it reads back,
but a later leg writing from a stale read can still land after that read-back
succeeded and drop this leg's block. The in-file comment now says so plainly.
Blast radius is a cosmetic release body, not the build or deploy path, and a
manual re-run restores it. Left as best effort rather than adding If-Match or
per-target release assets, which is a real design change and out of scope here.
Not amending the commit message, since this branch has stacked children.
Also pins the borsh encoding of
qosManifestB64andqosManifestEnvelopeB64with a round-trip test. Nothing asserted it, so swapping to the JSON that
actually sits on disk would have broken Go-side verification with every test
still green.
Correction: dropped the
parser_grpc_serverrelease image. That matrixleg and
images/parser_grpc_server/Containerfilerode in from #447, which wasclosed unmerged. It can't boot: the binary resolves its ephemeral key from
EPHEMERAL_FILE, which nothing in the repo sets, so it falls back tointegration/fixtures/ephemeral.secret, a committed test key that doesn'texist in the image rootfs, and
GrpcService::newaborts on start. Nothing inCI runs the image, so it built green and would only have failed on first
deploy. It's orthogonal to this PR (the HTTP+JSON pivot), so it's dropped here
and lands separately once it has a real key-loading story. The crate, the
src/Makefiledev target, and the wallet-integration docs are untouched;only the attested release image goes away.
Stacked on #446 (merged). Supersedes the pivot portion of #304.
🤖 Generated with Claude Code