Skip to content

Add ts cli#799

Open
ChristianPavilonis wants to merge 12 commits into
mainfrom
feature/ts-cli-base
Open

Add ts cli#799
ChristianPavilonis wants to merge 12 commits into
mainfrom
feature/ts-cli-base

Conversation

@ChristianPavilonis

Copy link
Copy Markdown
Collaborator

Summary

  • Adds the host-target ts operator CLI crate and binary.
  • Wires EdgeZero-backed lifecycle delegation and Trusted Server config init/validate/push flows.
  • Moves runtime settings loading to flattened app-config entries and updates docs/CI for the host-target CLI.

Changes

File Change
crates/trusted-server-cli/ Adds the base CLI crate, clap args, dispatch, EdgeZero delegate, config commands, and tests.
crates/trusted-server-core/src/config_payload.rs Adds canonical flattened config payload generation and hash metadata.
crates/trusted-server-core/src/settings_data.rs Loads settings from the platform config store instead of embedded build output.
crates/trusted-server-adapter-fastly/src/app.rs, src/main.rs Reads settings through the Fastly config-store adapter.
.github/workflows/*, .cargo/config.toml Adds host-target CLI check/test coverage and local aliases.
edgezero.toml, trusted-server.example.toml, .gitignore Adds EdgeZero manifest/template config and ignores operator-owned config.
docs/guide/cli.md, docs/guide/getting-started.md, README.md Documents base CLI install/config workflow.

Closes

No issue provided; this PR is split out from the combined feature/ts-cli-next branch.

Test plan

  • cargo test --workspace
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check
  • JS tests: cd crates/js/lib && npx vitest run
  • JS format: cd crates/js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: cargo test --package trusted-server-cli --target aarch64-apple-darwin — 12 passed

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • Uses tracing/log macros (not println!)
  • New code has tests
  • No secrets or credentials committed

Comment thread crates/trusted-server-core/src/settings.rs Fixed
Replace the custom trusted-server CLI lifecycle and config payload plumbing with a thin EdgeZero delegation layer using typed config push/validate flows.

Add TrustedServerAppConfig wrapper in core with deploy-time validation and move blob reconstruction into runtime helpers.

Drop flattened config entry publishing and route app-config through EdgeZero blob envelope handling while keeping edgezero flag reads in trusted_server_config.

Update CLI and architecture docs for the new model and adjust fastly adapter store selection.
@ChristianPavilonis ChristianPavilonis changed the title Add EdgeZero-backed ts CLI Add ts cli Jun 23, 2026

@prk-Jr prk-Jr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Introduces the host-target ts operator CLI and, more significantly, re-architects runtime settings loading: config moves from a build-time-embedded TOML (include_bytes!(target/trusted-server-out.toml), baked by build.rs) to a runtime read of an EdgeZero BlobEnvelope from a platform config store, with per-chunk + envelope SHA-256 integrity verification and a Fastly chunk-pointer reassembly path. build.rs becomes a no-op, the config crate drops to a dev-dependency, and EdgeZero git deps are pinned to a fixed rev.

Clean, well-tested work — strong unit coverage (round-trip, tamper-rejection, chunk reassembly, placeholder rejection) and good error messages. No blocking issues; all findings below are non-blocking.

3 of the inline comments carry a one-click GitHub suggestion (verified in a scratch worktree: cargo fmt --check, cargo clippy -p trusted-server-core --all-targets --all-features -D warnings, and cargo test -p trusted-server-core — 1426 passed). The canary comment is prose because it has no single clean fix.

Non-blocking

♻️ refactor

  • request_body_bytes is an incomplete abstraction (proxy) — see inline at crates/trusted-server-core/src/proxy.rs:44
  • request_body_bytes is an incomplete abstraction (request signing) — see inline at crates/trusted-server-core/src/request_signing/endpoints.rs:27

🤔 thinking

  • Unbounded pre-allocation from operator-controlled envelope_len — see inline at crates/trusted-server-core/src/settings_data.rs:113
  • EdgeZero entry-point canary downgraded to a no-op — see inline at crates/trusted-server-integration-tests/tests/common/ec.rs:283

Cross-cutting / body-level findings

  • 🤔 Startup INSECURE: proxy.certificate_check is disabled warning dropped — the old load_settings emitted a startup log::warn! when proxy.certificate_check was off; the new blob path (settings_from_config_blob / get_settings_from_config_store) does not. A per-backend warning still exists at request time (adapter-fastly/src/backend.rs:219), so coverage isn't zero, but the loud startup signal is gone. Cheap to re-add in get_settings_from_config_store.
  • 📝 #[serde(deny_unknown_fields)] added broadly (settings.rs, consent_config.rs, auction_config_types.rs) — good hardening (catches typo'd keys; the from_toml test flipped from "extra fields ignored" to "rejected"), but it's a behavior change: any deployed config carrying extra/forward-compat keys now fails to load. Validated at ts config push time, which is the right place — flagging the migration implication for existing operator configs.
  • 🌱 Minor: adapter-fastly/src/main.rs opens the same TRUSTED_SERVER_CONFIG_STORE twice per request (open_trusted_server_config_store for the flag + edgezero_config_store_handle for dispatch) — harmless, mildly wasteful. Separately, .github/workflows/test.yml's "Verify Fastly WASM release build" step still sets TRUSTED_SERVER__PUBLISHER__ORIGIN_URL, now vestigial since build.rs no longer bakes env-derived config.

👍 praise

  • EdgeZero git deps pinned from branch = "main" to a fixed rev — reproducible builds.
  • Integrity model in resolve_fastly_chunk_pointer + settings_from_config_blob: per-chunk len+sha, envelope len+sha, and BlobEnvelope::verify() — solid defense-in-depth, with tampered_blob_hash_is_rejected and strings_that_look_like_json_scalars_round_trip_as_strings guarding the TOML→JSON→TOML secret round-trip.
  • build.rs reduced from 72 lines of #[path]-included module hacks to a 3-line no-op.

CI Status — all 14 checks PASS

  • cargo test: PASS (required)
  • cargo fmt: PASS (required)
  • format-typescript: PASS (required)
  • format-docs: PASS (required)
  • Analyze (rust): PASS
  • Analyze (javascript-typescript): PASS
  • Analyze (actions): PASS
  • CodeQL: PASS
  • vitest: PASS
  • integration tests: PASS
  • integration tests (EdgeZero entry point): PASS
  • browser integration tests: PASS
  • prepare integration artifacts: PASS

Comment on lines +44 to 49
fn request_body_bytes(
body: EdgeBody,
_endpoint: &str,
) -> Result<bytes::Bytes, Report<TrustedServerError>> {
Ok(body.into_bytes().unwrap_or_default())
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ refactorrequest_body_bytes takes an _endpoint: &str it never uses and returns a Result it never fails — it just maps a streaming body to empty bytes via unwrap_or_default() (the pinned edgezero Body::into_bytes() returns None for streams). The signature implies the intent — matching the new auction_rejects_streaming_body_instead_of_treating_as_empty test — was to reject streaming bodies. Make it actually reject, naming the endpoint:

Suggested change
fn request_body_bytes(
body: EdgeBody,
_endpoint: &str,
) -> Result<bytes::Bytes, Report<TrustedServerError>> {
Ok(body.into_bytes().unwrap_or_default())
}
fn request_body_bytes(
body: EdgeBody,
endpoint: &str,
) -> Result<bytes::Bytes, Report<TrustedServerError>> {
if body.is_stream() {
return Err(Report::new(TrustedServerError::BadRequest {
message: format!("{endpoint} request body must be buffered, not streamed"),
}));
}
Ok(body.into_bytes().unwrap_or_default())
}

(Scratch-verified: cargo fmt --check, cargo clippy -p trusted-server-core --all-targets --all-features -D warnings, cargo test -p trusted-server-core — 1426 passed, 0 failed.)

Comment on lines +27 to +32
fn request_body_bytes(
body: EdgeBody,
_endpoint: &str,
) -> Result<bytes::Bytes, Report<TrustedServerError>> {
Ok(body.into_bytes().unwrap_or_default())
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ refactor — Same incomplete abstraction as in proxy.rs: _endpoint is unused and the Result never fails, so a streaming body becomes empty bytes. This matters more here — handle_rotate_key treats an empty body as a valid no-kid rotation, so a streamed rotate-key body would be silently accepted rather than rejected. Make the helper actually reject streaming bodies:

Suggested change
fn request_body_bytes(
body: EdgeBody,
_endpoint: &str,
) -> Result<bytes::Bytes, Report<TrustedServerError>> {
Ok(body.into_bytes().unwrap_or_default())
}
fn request_body_bytes(
body: EdgeBody,
endpoint: &str,
) -> Result<bytes::Bytes, Report<TrustedServerError>> {
if body.is_stream() {
return Err(Report::new(TrustedServerError::BadRequest {
message: format!("{endpoint} request body must be buffered, not streamed"),
}));
}
Ok(body.into_bytes().unwrap_or_default())
}

(Scratch-verified: cargo fmt --check, cargo clippy -p trusted-server-core --all-targets --all-features -D warnings, cargo test -p trusted-server-core — 1426 passed, 0 failed.)

.change_context(TrustedServerError::Configuration {
message: "Failed to validate configuration".to_string(),
})?;
let mut envelope_json = String::with_capacity(pointer.envelope_len);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinkingenvelope_len comes straight from the operator-written config-store pointer and is used to pre-size the buffer before any chunk is read or validated. A corrupt/huge value triggers a large allocation before the post-concat length check can reject it. Operator-controlled surface, so low severity — but the capacity hint buys nothing here; push_str grows as needed and the existing length/sha checks still validate the result:

Suggested change
let mut envelope_json = String::with_capacity(pointer.envelope_len);
let mut envelope_json = String::new();

(Scratch-verified: cargo fmt --check, cargo clippy -p trusted-server-core --all-targets --all-features -D warnings, cargo test -p trusted-server-core — 1426 passed, 0 failed.)

/// Keep the request as a non-fatal diagnostic so the EdgeZero CI job still runs
/// the EC lifecycle scenarios instead of failing on a routing canary that is not
/// stable across runtime versions.
pub fn assert_edgezero_entry_point(base_url: &str) -> TestResult<()> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinking / 📌 out of scope — This canary was downgraded from a hard 405 assertion to a non-fatal log::warn! + Ok(()). Its original purpose was to prove the integration-tests-edgezero CI job actually exercises edgezero_main rather than silently falling back to legacy_main (the EC lifecycle scenarios pass on both paths). As a warning it can no longer fail CI, so a fixture/config-store regression that drops the EdgeZero job onto the legacy path would green both jobs — which is the exact failure this guard existed to catch, and directly relevant given this PR rewires runtime config loading.

The flaky-405 concern is legitimate, but consider restoring a deterministic EdgeZero-only signal (e.g. a header or route that only edgezero_main emits) rather than removing the guard, or track it as a follow-up issue. Was the downgrade intentional, or just to get CI green?

Apply manually — no single-line fix; needs a stable EdgeZero-only assertion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants