Companion to DESIGN_IN_PROCESS_REQUESTER.md. Breaks the design into landable phases, each independently compileable and testable so nothing goes dark on main between merges.
| Phase | Status | Notes |
|---|---|---|
| 0 — baseline build + assumption check | ✅ done | Required two pre-Phase-1 fixes (see Phase 0 Resolution Log) |
| 1 — feature flag restructuring | ✅ done | direct-client is now the shared capability; role features in-process-responder / in-process-requester pick the FSM. Mutual-exclusion compile_error! in main.rs. |
| 2 — compileable requester stub | ✅ done | Forced creation of a second system_image package //target/ast1060-evb/mctp-requester so the standalone rust_app target actually compiles (standalone rust_app hits a pw_kernel/userspace platform-constraint error without a system image). |
| 3 — FSM happy path | ✅ done | Eight-state VCA FSM, one step per polling-loop iteration. |
| 4 — safety net + counters | ✅ done | AWAIT_STEP_BUDGET = 10_000, await_try! macro, counters req_send_ok / req_recv_ok / req_recv_pending. |
| 5 — Bazel target + system image | ✅ done (merged into Phase 2) | See Phase 2 note above. |
| 6 — two-board integration test | ⏸ blocked | No on-target hardware available in this environment. See IMPLEMENTATION_PLAN.md §A for measurements to collect when hardware is accessible. |
| 6a — QEMU integration test (virt-ast1060-evb) | ✅ done | QEMU patched with ASPEED I2C emulation; //target/virt-ast1060-evb/mctp-requester:mctp_requester_test added. No two-board setup needed. |
| 7 — docs + follow-up TODOs | ✅ done | INITIALIZATION.md updated with requester glossary + setup steps; follow-up design refs annotated at the FSM in main.rs. |
Build verification at end of Phase 7:
bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp:mctp # responder (hw) — clean bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp-requester:mctp_requester # requester (hw) — clean bazel build --config=k_virt_ast1060_evb //target/virt-ast1060-evb/mctp-requester:mctp_requester # requester (QEMU) — clean bazel test --config=k_virt_ast1060_evb //target/virt-ast1060-evb/mctp-requester:mctp_requester_test # requester QEMU test
Legend:
Role (requester vs. responder) is selected at build time, not at runtime. Same src/main.rs source, two binaries, chosen by which Bazel system-image target you build. Standalone rust_app targets do not build in isolation — pw_kernel/userspace requires a target_codegen → kernel_config chain that only exists inside a system_image.
Developer commands:
# Responder image (default, hardware) bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp:mctp # Requester image (hardware) bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp-requester:mctp_requester # Requester image (QEMU — requires patched QEMU with ASPEED I2C emulation) bazel build --config=k_virt_ast1060_evb //target/virt-ast1060-evb/mctp-requester:mctp_requester bazel test --config=k_virt_ast1060_evb //target/virt-ast1060-evb/mctp-requester:mctp_requester_test
Running any test or integration probe uses the same prefix (bazel test --config=k_ast1060_evb <target>).
Selection chain — each layer picks the next:
System image → rust_app label → Cargo features → #[cfg] in main.rs ──────────────────────────────────────────────── ───────────────────────────────────────────── ─────────────────────────────────────────────────────── ───────────────────────── //target/ast1060-evb/mctp:mctp → //services/mctp/server:mctp_server → ["i2c-polling", "direct-client", "in-process-responder"] → responder setup + step //target/ast1060-evb/mctp-requester:mctp_requester → //services/mctp/server:mctp_server_requester → ["i2c-polling", "direct-client", "in-process-requester"] → requester setup + FSM
Each rust_app has its own codegen_crate_name — app_mctp_server for the responder, app_mctp_server_requester for the requester — because two rust_app rules in the same Bazel package cannot share the codegen name. src/main.rs picks the right one via a cfg-gated use.
Guards:
compile_error! at the top of main.rs.i2c-polling → polling loop compiles with no SPDM in-process (still valid; drains I2C only).i2c-polling → the notification (WG + IRQ) loop compiles; role features are ignored because in-process-* code is also gated on i2c-polling.direct-client is listed explicitly in both crate_features sets; Bazel's rust_binary does not resolve Cargo-level feature implications (in-process-* = ["direct-client"]), so the transitive dep must be named directly.What flashes: each system image binds one of the two rust_app labels via its apps = [...] list, together with i2c_server. The two images are mutually exclusive — flash one or the other, not both.
Direct cargo build is not a supported path for the binary — the package sets autobins = false and main.rs is a Pigweed binary built only through Bazel. The Cargo features still matter because Bazel passes them through; they're just never consumed by a bare cargo invocation of main.rs.
Goal: confirm the starting source tree builds clean before any new code is written. Source-level only — no on-target execution is required or assumed at this stage.
Deliverables:
//target/ast1060-evb/mctp:mctp (responder system image) at current ocp-emea-demo-spdm-spdm-req-resp-anthony-i2c-polling HEAD.wait_for_messages(..., None) paired with the e.is_timeout() arm at src/main.rs:390 matches constraint §6.5.3).//target/ast1060-evb/spdm-req-resp-test:spdm_requester_app. The app is marked # DISABLED in the sibling system_image and fails platform analysis when built alone. The reference constants in spdm_requester.rs are consumed as source, not as a binary, so skipping this build does not block later phases.Files: none modified initially. See Phase 0 Resolution Log below.
Exit criteria: the responder system image builds clean; the §6.5 cross-check is recorded as “verified against HEAD” in the commit message of Phase 1.
Risks / rollback: none — read-only phase by default; any unblocker applied here gets its own commit (see log below).
Blocker discovered: at branch HEAD, commit 023e75e "Update mctp-lib (to hotfix branch)" changed mctp_lib::Router::inbound to return Result<Option<AppCookie>, MctpError>, but the services/mctp/server/src/server.rs:267-271 wrapper Server::inbound still declared Result<(), MctpError>. The error surfaces as E0308 and fails every Bazel target that depends on mctp_server_lib (responder system image, req-resp test image, etc.).
Verification that the break is isolated: a worktree at d4719a0 "Chasing down an MCTP packet issue." (the commit immediately preceding the mctp-lib hotfix) built //target/ast1060-evb/mctp:mctp clean. So the break is entirely within 023e75e.
Resolution — Fix A (discard the new cookie):
pub fn inbound(&mut self, pkt: &[u8]) -> Result<(), MctpError> { self.stack .inbound(pkt) .map(|_| ()) .map_err(mctp_error_to_server_error) }
Chosen over Fix B (propagate Option<AppCookie> to callers) because: no caller reads the return value today, the propagation case is cheap to add later if Shape-B from design §9.1 ever lands, and keeping the unblocker orthogonal to the requester work eases git blame. Landed as its own small commit before any Phase 1 work.
Second blocker (surfaced after Fix A unblocked rustc past the first error): the same mctp-lib hotfix changed MctpI2cReceiver::decode's return type from (&[u8], u8) to (&[u8], MctpI2cHeader) — a struct { dest: u8, source: u8, byte_count: usize } — but src/main.rs:350-361 still treated the second tuple element as a bare u8 source address. as u32 on a struct triggers E0605.
Resolution — Fix A′ (match the new type, minimum diff): rename the binding from src_addr to hdr and read the source address field explicitly: hdr.source as u32. Landed in the same “pre-Phase-1 unblocker” commit as Fix A.
Deferred to on-target bringup: measuring actual idle-poll rate, wait_for_messages latency, and worst-case fragment count is out of scope for Phase 0. AWAIT_STEP_BUDGET will ship with a generous compile-time default (§6.4) and be tuned when a runtime becomes available — see Appendix §A.
Goal: rename the responder role feature and add the requester role feature, without changing behavior. Pure refactor.
Deliverables:
services/mctp/server/Cargo.toml:in-process-responder = ["direct-client"].in-process-requester = ["direct-client"].default to ["i2c-polling", "in-process-responder"].direct-client as-is (library-exposed capability).services/mctp/server/src/main.rs:#[cfg(all(feature = "in-process-requester", feature = "in-process-responder"))] compile_error!("features `in-process-requester` and `in-process-responder` are mutually exclusive");
#[cfg(feature = "direct-client")] inside the polling loop and its setup block with #[cfg(feature = "in-process-responder")].mod direct_client; export and DirectMctpClient untouched — those belong to the direct-client feature, which still exists.services/mctp/server/BUILD.bazel:mctp_server target: replace crate_features = ["i2c-polling", "direct-client"] with ["i2c-polling", "in-process-responder"].rust_library mctp_server_lib with crate_features = ["direct-client"].Files: Cargo.toml, src/main.rs, BUILD.bazel.
Exit criteria:
bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp:mctp succeeds (the standalone rust_app target cannot be built in isolation because pw_kernel/userspace needs a system-image codegen context).compile_error!.Risks / rollback:
direct-client directly and silently lose the responder. Mitigation: grep the repo for direct-client in crate_features; the only consumer is services/mctp/server/BUILD.bazel.Goal: add the requester setup skeleton behind in-process-requester as a no-op Phase 2 step. Proves feature gating, imports, and the init sequence compile — no FSM logic yet.
Deliverables:
services/mctp/server/src/main.rs:const REMOTE_RESPONDER_EID: u8 = 42; (module-scope, not cfg-gated — a single #[allow(dead_code)] keeps it clean when the feature is off).#[cfg(all(feature = "i2c-polling", feature = "in-process-requester"))]: mock platform types, MctpSpdmTransport, SpdmContext, VCA generate_* functions, DemoPeerCertStore.mctp_loop() under the same cfg, add the full init sequence from §7 of the design doc:DirectMctpClient::new(&server)MctpSpdmTransport::new_requester(client, REMOTE_RESPONDER_EID)transport.init_sequence() with error log + returnCapabilityFlags / DeviceCapabilities matching spdm_requester.rsLocalDeviceAlgorithms (factor into a helper to avoid duplicating the responder's inline block)SpdmContext::new(..., Some(&mut peer_cert_store), ...)MessageBuf on stack bufferlet mut req_state = ReqState::SendVersion; — but Phase 2 step body is still just #[allow(unused_variables)] let _ = &mut req_state; with a log "requester FSM stub: state=..." every 0xfff loops.ReqState enum (full variant set — §6.1) even though only one variant is used, so Phase 3 just fills in match arms.Files: src/main.rs.
Exit criteria:
bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp-requester:mctp_requester succeeds. (Phase 2 verification required pulling Phase 5's system-image package forward — a standalone rust_app target is not buildable on its own in this tree; see mctp-requester/ for the system image created here.)Risks / rollback:
SpdmContext::new arg layout because transport and server share a lifetime. Mitigation: mirror the responder's exact let-binding order, which is already known to compile.DemoPeerCertStore import path differs from responder (responder uses None). Mitigation: path already resolved in spdm_requester.rs — copy.Goal: wire the six Send/Await states so a full VCA flow completes on the wire.
Deliverables:
msg_buf.reset()generate_<step>(...)ctx.requester_send_request(&mut msg_buf, REMOTE_RESPONDER_EID)Await* state.msg_buf.reset()ctx.requester_process_message(&mut msg_buf)Ok(_) → advance; Err(_) → stay (counted, not yet budgeted).ReqState::Done, log once: "SPDM VCA completed: version/caps/algs OK".Done and Failed states become no-ops in Phase 2 (Phase 1 keeps draining I2C unconditionally, so late inbound traffic is still handled).Files: src/main.rs.
Exit criteria:
spdm_responder app running on a second device (or the same device with the responder built separately) produces three successful request/response pairs.spdm_err / equivalent pending-counter stays bounded (not monotonically growing past expected poll-while-waiting count).Risks / rollback:
include_supported_algorithms = false / other capability fields diverge between the in-process requester and the responder's expectations. Mitigation: copy verbatim from spdm_requester.rs; already known to interoperate with spdm_responder.rs.Goal: bound failure latency and give on-target debugging the same counter set as the responder.
Deliverables:
AWAIT_STEP_BUDGET const + await_steps counter per design §6.4, reset on every state transition, transitions FSM to Failed on exhaustion with an error log naming the state that timed out.u32 wrapping, logged on first event and every 256th:req_send_ok, req_send_err, req_recv_ok, req_recv_pending, req_recv_err_terminal.Done transition log augmented with "completed in {N} steps, ~{idle_polls} idle polls".Files: src/main.rs.
Exit criteria:
Failed transition within the budget and emits the named-state error; device does not hang.Risks / rollback:
AWAIT_STEP_BUDGET = 10_000) and defer tightening until on-target measurements exist (see Appendix §A). Erring high costs only wall-clock on genuine failures; erring low wedges the happy path.Goal: make the requester role selectable as a first-class Bazel build without touching the responder's target.
Deliverables:
services/mctp/server/BUILD.bazel:rust_app( name = "mctp_server_requester", codegen_crate_name = "app_mctp_server", srcs = ["src/main.rs"], crate_features = ["i2c-polling", "in-process-requester"], edition = "2024", system_config = "//target/ast1060-evb/mctp:system_config", tags = ["kernel"], visibility = ["//visibility:public"], deps = [ # identical to mctp_server ":mctp_server_lib", "//services/i2c/api:i2c_api", "//services/i2c/client:i2c_client", "//services/mctp/api:mctp_api", "//services/mctp/transport-i2c:mctp_transport_i2c", "//services/spdm/transport-mctp:spdm_transport_mctp", "//target/ast1060-evb/spdm-req-resp-test:mock_platform", "@rust_crates//:mctp", "@rust_crates//:mctp-lib", "@pigweed//pw_kernel/syscall:syscall_user", "@pigweed//pw_kernel/userspace", "@pigweed//pw_log/rust:pw_log", "@pigweed//pw_status/rust:pw_status", "@oot_crates_no_std//:spdm-lib", ], )
system_requester) so both can be flashed side by side during bringup.Files actually landed:
services/mctp/server/BUILD.bazel — second rust_app(name = "mctp_server_requester", codegen_crate_name = "app_mctp_server_requester", ...) target.target/ast1060-evb/mctp-requester/BUILD.bazel — new sibling package with its own system_image, target_codegen, target_linker_script, rust_binary(name="target",...), and uart_boot_image.target/ast1060-evb/mctp-requester/system.json5 — copy of the mctp image's config with the MCTP app renamed to mctp_server_requester.target/ast1060-evb/mctp-requester/target.rs — copy of the kernel entry shim.Exit criteria:
bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp:mctp bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp-requester:mctp_requester
Risks / rollback:
codegen_crate_name collision when both rust_app targets live in the same Bazel package. Resolved: the two targets use distinct codegen crate names (app_mctp_server, app_mctp_server_requester) and src/main.rs selects between them with a cfg-gated use.rust_app standalone. Resolved: the rust_app target must be consumed by a system_image, never built directly.Goal: a repeatable test that exercises the in-process requester against the in-process responder on the bench.
Status: ⏸ blocked — no on-target hardware available. See §A for measurements to collect when hardware is accessible.
Deliverables:
mctp_server_requester system image, board B flashed with mctp_server (responder). Both use current OWN_EID / OWN_I2C_ADDR constants, with board A targeting board B at REMOTE_RESPONDER_EID = 42 — board B must be re-configured to EID 42 (its current OWN_EID = 0x08; this requires a new const RESPONDER_OWN_EID on the responder image, or swapping A↔B roles).README.md (new section) or a dedicated tests/ON_TARGET_README.md: flash steps, expected log lines, expected duration-to-Done.Files: docs only, plus any const bump in main.rs if we change OWN_EID on the responder side.
Exit criteria:
SPDM VCA completed line on the requester side and three matching processed-request lines on the responder side.Risks / rollback:
Goal: automated QEMU test that exercises the full I2C + MCTP + SPDM requester stack without physical hardware, using QEMU's ASPEED I2C device emulation.
Prerequisite: QEMU patched with ASPEED i2c device model (user confirmed April 2026).
Deliverables:
target/virt-ast1060-evb/mctp-requester/system.json5 — two-process (i2c_server + mctp_server_requester) virt image with ASPEED I2C MMIO mappings at the real hardware addresses (emulated by QEMU).target/virt-ast1060-evb/mctp-requester/target.rs — semihosting kernel shim; shutdown() calls exit(EXIT_SUCCESS/FAILURE) so QEMU terminates and the test harness can read the exit code.target/virt-ast1060-evb/mctp-requester/BUILD.bazel — system_image, system_image_test, target_codegen, target_linker_script, rust_binary(name="target") wired to //target/virt-ast1060-evb platform and linker template.services/mctp/server/BUILD.bazel — mctp_server_requester_virt rust_app, same feature set as mctp_server_requester but with system_config = "//target/virt-ast1060-evb/mctp-requester:system_config" so codegen (handle constants + linker script) is correct for QEMU.Files:
target/virt-ast1060-evb/mctp-requester/system.json5 (new)target/virt-ast1060-evb/mctp-requester/target.rs (new)target/virt-ast1060-evb/mctp-requester/BUILD.bazel (new)services/mctp/server/BUILD.bazel (add mctp_server_requester_virt)Build / test commands:
bazel build --config=k_virt_ast1060_evb //target/virt-ast1060-evb/mctp-requester:mctp_requester bazel test --config=k_virt_ast1060_evb //target/virt-ast1060-evb/mctp-requester:mctp_requester_test
Exit criteria:
bazel test passes: QEMU starts, the FSM reaches Done, semihosting exit(EXIT_SUCCESS) fires, QEMU terminates with code 0, Bazel marks the test green.//target/ast1060-evb/mctp:mctp) and hardware requester image (//target/ast1060-evb/mctp-requester:mctp_requester) still build clean.Key design notes:
mctp_server_requester_virt shares codegen_crate_name = "app_mctp_server_requester" with the hardware target — the handle layout is identical (I2C object is second in the process objects list in both system configs), so no source changes are needed.virt-ast1060-evb/spdm-req-resp-test; vector_table_size_bytes = 1280 (matches multi-process virt convention).0x7e7b0000 / 0x7e6e2000) are kept so the i2c_server binary can run unmodified against the QEMU device model.Risks / rollback:
target/virt-ast1060-evb/mctp-requester/ package and remove mctp_server_requester_virt from services BUILD.bazel; hardware targets are unaffected.Goal: close loose ends the design doc called out and make sure nothing is left under-documented.
Deliverables:
TransportError out of requester_process_message so TimedOut can be discriminated.DirectMctpClient variant (Shape B).main.rs module docs and INITIALIZATION.md to list the new requester row.Files: INITIALIZATION.md, src/main.rs (module-level doc comment only).
Exit criteria: follow-up issues filed; docs reviewed; no undocumented feature flag.
Risks / rollback: none — docs and issue filing.
Phase 0 ──► Phase 1 ──► Phase 2 ──► Phase 3 ──► Phase 4 ──► Phase 5 ──► Phase 6 ──► Phase 7
│ ▲
└────────────────────────────────────┘
(Phase 5 can start after Phase 2 in parallel
with Phase 3/4 if a separate hand is available)
Phases 1 and 2 are small (half-day each). Phase 3 is the bulk of the work — expect a full day including on-target debug. Phases 4–6 are each half-day. Phase 7 is a loose-ends sweep.
These values are not required to land Phases 0–5. Capture them once hardware execution is available (Phase 6 onward) and revisit §6.4 of the design doc if any value contradicts the back-of-envelope 10_000 default.
| Metric | Measured value | Source |
|---|---|---|
idle_polls increment rate (per wall-clock second) | deferred | responder log, counted over a fixed window |
wait_for_messages average return latency when idle | deferred | derived from idle_polls rate |
| Largest expected VCA message fragment count | deferred | receiver.decode SOM/EOM logs |
Observed peak await_steps on happy path | deferred | requester log at Done transition |
Tuned AWAIT_STEP_BUDGET (≥ ~3× observed peak) | deferred | computed |
Until these exist, the design ships with the default and relies on the Phase-4 state-timeout log line to surface a bad choice loudly rather than silently.