Skip to content

Commit e5fce00

Browse files
feat(parser_http_server): CLI args for TVC pivot deploys
`tvc deploy create` has no env-var injection mechanism; pivot config must arrive via `pivotArgs`. Convert parser_http_server's startup from env-vars to clap-derive args: --port (env fallback HTTP_PORT) --gateway-signing-pubkey-hex (env fallback GATEWAY_SIGNING_PUBKEY_HEX) The ephemeral key path is no longer configurable — `EphemeralKeyHandle` reads `qos_core::EPHEMERAL_KEY_FILE` directly, which already resolves to `/qos.ephemeral.key` in TVC builds and `./local-enclave/...` in dev via qos_core's cfg. No caller of parser_http_server overrides this path today (compose targets parser_grpc_server, not http-server). Drop `PaymentPolicy::from_env()` for `PaymentPolicy::from_hex(&str)` when the arg is supplied, `Disabled` otherwise — identical behavior to the previous env-only path, with no env coupling left in main. Fix x402-devnet-playbook Part 2 which documented an `envVars` block in the TVC deploy config that doesn't exist; show `pivotArgs` instead. Also gitignore deploy-*.json so locally-generated TVC deploy configs don't get staged accidentally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bb4bf19 commit e5fce00

5 files changed

Lines changed: 57 additions & 30 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ docs/superpowers/
44
.surfpool/
55
node_modules/
66
*.log
7+
deploy-*.json

docs/x402-devnet-playbook.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -714,21 +714,24 @@ Steps:
714714
# Prints GATEWAY_SIGNING_PUBKEY_HEX=<260-char-hex>
715715
```
716716

717-
2. **Deploy `parser_http_server` to TVC** with the public half pinned in
718-
its env. Add to the TVC deploy config's env block:
717+
2. **Deploy `parser_http_server` to TVC** with the public half pinned at
718+
pivot launch. `tvc deploy create` has no env-injection mechanism, so
719+
the pubkey is passed via `pivotArgs` (clap on the binary side):
719720

720721
```json
721722
{
722-
"envVars": [
723-
{ "name": "GATEWAY_SIGNING_PUBKEY_HEX",
724-
"value": "<260-char-hex from step 1>" }
723+
"pivotArgs": [
724+
"--gateway-signing-pubkey-hex",
725+
"<260-char-hex from step 1>"
725726
]
726727
}
727728
```
728729

729730
After re-deploy, `parser_app` (linked into `parser_http_server`)
730731
rejects every parse request whose `payment_marker` doesn't carry a
731-
VPM signed by exactly this key.
732+
VPM signed by exactly this key. The HTTP listener defaults to port
733+
3000; the ephemeral key is read from `qos_core::EPHEMERAL_KEY_FILE`
734+
(provisioned by QOS) with no override.
732735

733736
3. **Deploy `parser_gateway` outside TVC** with `HTTP_BACKEND_URL`
734737
pointing at the TVC app URL:

src/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/parser/http-server/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ serde = { workspace = true }
2121
serde_json = { workspace = true }
2222
base64 = "0.22"
2323

24+
# CLI args. TVC pivots receive config via `pivotArgs`, not env vars; clap's
25+
# `env = "..."` attribute keeps compose + integration tests working unchanged.
26+
clap = { version = "4.0", features = ["derive", "env"] }
27+
2428
[lints]
2529
workspace = true
2630

src/parser/http-server/src/main.rs

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@
2121
//! visualsign-turnkey-client (and any HTTP-only client) keeps working
2222
//! byte-for-byte.
2323
//! - `POST /visualsign/api/v2/parse` — same payload; additionally
24-
//! enforces `GATEWAY_SIGNING_PUBKEY_HEX` when set (TVC-enforced mode
25-
//! from plan v3). When unset, behaves the same as v1.
24+
//! enforces a pinned gateway pubkey when supplied (TVC-enforced mode
25+
//! from plan v3). When omitted, behaves the same as v1.
2626
//!
27-
//! Env vars:
28-
//! - `HTTP_PORT` (default: 3000) — Turnkey TVC public ingress default.
29-
//! - `EPHEMERAL_FILE` (required) — path to the parser_app ephemeral key.
30-
//! - `GATEWAY_SIGNING_PUBKEY_HEX` (optional) — pinned gateway P256 sign
31-
//! pubkey for VPM verification on v2.
27+
//! Configuration (CLI args; env vars listed are clap fallbacks):
28+
//! - `--port <u16>` / `HTTP_PORT` (default 3000) — Turnkey TVC public ingress.
29+
//! - `--gateway-signing-pubkey-hex <hex>` / `GATEWAY_SIGNING_PUBKEY_HEX`
30+
//! (optional) — pinned gateway P256 sign pubkey for VPM verification on v2.
31+
//!
32+
//! The ephemeral key is read from `qos_core::EPHEMERAL_KEY_FILE` (provisioned
33+
//! by QOS inside the enclave). No override flag — if a deployment ever needs
34+
//! a non-canonical path, bind-mount it instead.
3235
3336
use axum::{
3437
Json, Router,
@@ -37,6 +40,7 @@ use axum::{
3740
routing::{get, post},
3841
};
3942
use base64::Engine;
43+
use clap::Parser;
4044
use generated::parser::{Chain, ChainMetadata, SignatureScheme};
4145
use host_primitives::turnkey::{
4246
TurnkeyParsedTransaction, TurnkeyPayload, TurnkeyRequestWrapper, TurnkeyResponse,
@@ -49,11 +53,26 @@ use qos_p256::P256Pair;
4953
use std::net::SocketAddr;
5054
use std::sync::Arc;
5155

56+
#[derive(Parser, Debug)]
57+
#[command(version = env!("VERSION"))]
58+
struct Args {
59+
/// HTTP port to listen on.
60+
#[arg(long, env = "HTTP_PORT", default_value_t = 3000)]
61+
port: u16,
62+
63+
/// Hex-encoded P256 SEC1 uncompressed sign pubkey of `parser_gateway`.
64+
/// When supplied, `/visualsign/api/v2/parse` is TVC-enforced: it
65+
/// requires a valid VerifiedPaymentMarker signed by this key.
66+
/// When omitted, v2 behaves the same as v1.
67+
#[arg(long, env = "GATEWAY_SIGNING_PUBKEY_HEX")]
68+
gateway_signing_pubkey_hex: Option<String>,
69+
}
70+
5271
#[derive(Clone)]
5372
struct AppState {
5473
ephemeral_key: Arc<P256Pair>,
55-
/// Disabled when GATEWAY_SIGNING_PUBKEY_HEX is unset. The v1 route
56-
/// always passes `Disabled`; the v2 route uses this policy.
74+
/// Disabled when `--gateway-signing-pubkey-hex` is unset. The v1
75+
/// route always passes `Disabled`; the v2 route uses this policy.
5776
policy: Arc<PaymentPolicy>,
5877
}
5978

@@ -180,28 +199,27 @@ fn handle_parse(
180199

181200
#[tokio::main]
182201
async fn main() -> Result<(), Box<dyn std::error::Error>> {
183-
let port: u16 = std::env::var("HTTP_PORT")
184-
.ok()
185-
.and_then(|s| s.parse().ok())
186-
.unwrap_or(3000);
187-
188-
let ephemeral_file = std::env::var("EPHEMERAL_FILE")
189-
.unwrap_or_else(|_| "integration/fixtures/ephemeral.secret".to_string());
190-
let handle = EphemeralKeyHandle::new(ephemeral_file.clone());
202+
let args = Args::parse();
203+
204+
let handle = EphemeralKeyHandle::new(qos_core::EPHEMERAL_KEY_FILE.to_string());
191205
let ephemeral_key = handle
192206
.get_ephemeral_key()
193207
.expect("failed to load ephemeral key");
194208
eprintln!(
195-
"parser_http_server {} loaded ephemeral key from {ephemeral_file}",
196-
env!("VERSION")
209+
"parser_http_server {} loaded ephemeral key from {}",
210+
env!("VERSION"),
211+
qos_core::EPHEMERAL_KEY_FILE,
197212
);
198213

199-
let policy =
200-
PaymentPolicy::from_env().expect("invalid GATEWAY_SIGNING_PUBKEY_HEX configuration");
214+
let policy = match args.gateway_signing_pubkey_hex.as_deref() {
215+
Some(hex) => PaymentPolicy::from_hex(hex)
216+
.expect("invalid --gateway-signing-pubkey-hex configuration"),
217+
None => PaymentPolicy::Disabled,
218+
};
201219
if matches!(policy, PaymentPolicy::Required { .. }) {
202-
eprintln!("v2 route is TVC-enforced (GATEWAY_SIGNING_PUBKEY_HEX is set)");
220+
eprintln!("v2 route is TVC-enforced (gateway signing pubkey supplied)");
203221
} else {
204-
eprintln!("v2 route is open (GATEWAY_SIGNING_PUBKEY_HEX unset)");
222+
eprintln!("v2 route is open (no gateway signing pubkey)");
205223
}
206224

207225
let state = AppState {
@@ -215,7 +233,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
215233
.route("/visualsign/api/v2/parse", post(parse_v2))
216234
.with_state(state);
217235

218-
let addr = SocketAddr::from(([0, 0, 0, 0], port));
236+
let addr = SocketAddr::from(([0, 0, 0, 0], args.port));
219237
eprintln!("parser_http_server listening on {addr}");
220238
let listener = tokio::net::TcpListener::bind(addr).await?;
221239
axum::serve(listener, app)

0 commit comments

Comments
 (0)