Skip to content

Commit a4ce57c

Browse files
feat(runtime-v2): Wave 2 — Build/Container/Image entities, receipts, OpenAPI 3.1, schema contracts, drift verification
Entity additions: - Build: SQLite table, 6 CRUD methods, 5 events, 4 API endpoints, CLI subcommand - Container: SQLite table, CRUD, 7 events, 4 API endpoints - Image: SQLite table, CRUD, 2 API endpoints API improvements: - Idempotency keys wired to all mutating handlers (open_lease, create_execution, create_checkpoint, fork_checkpoint, create_container) - Receipt generation on 7 mutating operations with X-Receipt-Id header - Scoped event listing via ?scope= query parameter - Full OpenAPI 3.1 schema with 22+ endpoint definitions, 21 component schemas Control plane hardening: - control_metadata table with schema versioning - Startup drift verification (verify_startup_drift) with DriftDetected events - Phase 1 validation tests for health/allocator/reconcile round-trips Beads closed: vz-p02, vz-rxn, vz-h42, vz-85u, vz-v2n.1.4, vz-v2n.2.1, vz-v2n.2.2 659 tests passing (480 vz-stack + 13 vz-api + 166 vz-cli) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 043c667 commit a4ce57c

File tree

11 files changed

+5653
-1225
lines changed

11 files changed

+5653
-1225
lines changed

crates/vz-api/src/lib.rs

Lines changed: 2354 additions & 203 deletions
Large diffs are not rendered by default.
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
//! `vz build-mgmt` -- build entity lifecycle management commands.
2+
//!
3+
//! Provides `list`, `inspect`, and `cancel` subcommands backed by the
4+
//! `vz-stack` state store for build persistence.
5+
6+
#![allow(clippy::print_stdout)]
7+
8+
use std::path::PathBuf;
9+
10+
use anyhow::{Context, bail};
11+
use clap::{Args, Subcommand};
12+
use vz_runtime_contract::BuildState;
13+
use vz_stack::StateStore;
14+
15+
/// Manage asynchronous build operations.
16+
#[derive(Args, Debug)]
17+
pub struct BuildMgmtArgs {
18+
#[command(subcommand)]
19+
pub action: BuildMgmtCommand,
20+
}
21+
22+
#[derive(Subcommand, Debug)]
23+
pub enum BuildMgmtCommand {
24+
/// List all builds.
25+
List(BuildMgmtListArgs),
26+
27+
/// Show detailed build information.
28+
Inspect(BuildMgmtInspectArgs),
29+
30+
/// Cancel a running or queued build.
31+
Cancel(BuildMgmtCancelArgs),
32+
}
33+
34+
/// Arguments for `vz build-mgmt list`.
35+
#[derive(Args, Debug)]
36+
pub struct BuildMgmtListArgs {
37+
/// Path to the state database.
38+
#[arg(long, default_value = "stack-state.db")]
39+
state_db: PathBuf,
40+
41+
/// Filter by sandbox identifier.
42+
#[arg(long)]
43+
sandbox_id: Option<String>,
44+
45+
/// Output as JSON.
46+
#[arg(long)]
47+
json: bool,
48+
}
49+
50+
/// Arguments for `vz build-mgmt inspect`.
51+
#[derive(Args, Debug)]
52+
pub struct BuildMgmtInspectArgs {
53+
/// Build identifier.
54+
pub build_id: String,
55+
56+
/// Path to the state database.
57+
#[arg(long, default_value = "stack-state.db")]
58+
state_db: PathBuf,
59+
}
60+
61+
/// Arguments for `vz build-mgmt cancel`.
62+
#[derive(Args, Debug)]
63+
pub struct BuildMgmtCancelArgs {
64+
/// Build identifier.
65+
pub build_id: String,
66+
67+
/// Path to the state database.
68+
#[arg(long, default_value = "stack-state.db")]
69+
state_db: PathBuf,
70+
}
71+
72+
/// Run the build management subcommand.
73+
pub async fn run(args: BuildMgmtArgs) -> anyhow::Result<()> {
74+
match args.action {
75+
BuildMgmtCommand::List(list_args) => cmd_list(list_args),
76+
BuildMgmtCommand::Inspect(inspect_args) => cmd_inspect(inspect_args),
77+
BuildMgmtCommand::Cancel(cancel_args) => cmd_cancel(cancel_args),
78+
}
79+
}
80+
81+
fn cmd_list(args: BuildMgmtListArgs) -> anyhow::Result<()> {
82+
let store = StateStore::open(&args.state_db).context("failed to open state store")?;
83+
84+
let builds = if let Some(ref sandbox_id) = args.sandbox_id {
85+
store
86+
.list_builds_for_sandbox(sandbox_id)
87+
.context("failed to list builds for sandbox")?
88+
} else {
89+
store.list_builds().context("failed to list builds")?
90+
};
91+
92+
if args.json {
93+
let json = serde_json::to_string_pretty(&builds).context("failed to serialize builds")?;
94+
println!("{json}");
95+
return Ok(());
96+
}
97+
98+
if builds.is_empty() {
99+
println!("No builds found.");
100+
return Ok(());
101+
}
102+
103+
println!(
104+
"{:<40} {:<20} {:<12} {:<20} {:<12}",
105+
"BUILD ID", "SANDBOX ID", "STATE", "CONTEXT", "DIGEST"
106+
);
107+
for build in &builds {
108+
let state = serde_json::to_string(&build.state)
109+
.unwrap_or_default()
110+
.trim_matches('"')
111+
.to_string();
112+
let context_display = if build.build_spec.context.len() > 18 {
113+
format!("{}...", &build.build_spec.context[..15])
114+
} else {
115+
build.build_spec.context.clone()
116+
};
117+
let digest = build.result_digest.as_deref().unwrap_or("-");
118+
let digest_display = if digest.len() > 10 {
119+
format!("{}...", &digest[..7])
120+
} else {
121+
digest.to_string()
122+
};
123+
println!(
124+
"{:<40} {:<20} {:<12} {:<20} {:<12}",
125+
build.build_id, build.sandbox_id, state, context_display, digest_display
126+
);
127+
}
128+
129+
Ok(())
130+
}
131+
132+
fn cmd_inspect(args: BuildMgmtInspectArgs) -> anyhow::Result<()> {
133+
let store = StateStore::open(&args.state_db).context("failed to open state store")?;
134+
let build = store
135+
.load_build(&args.build_id)
136+
.context("failed to load build")?;
137+
138+
match build {
139+
Some(b) => {
140+
let json = serde_json::to_string_pretty(&b).context("failed to serialize build")?;
141+
println!("{json}");
142+
}
143+
None => bail!("build {} not found", args.build_id),
144+
}
145+
146+
Ok(())
147+
}
148+
149+
fn cmd_cancel(args: BuildMgmtCancelArgs) -> anyhow::Result<()> {
150+
let store = StateStore::open(&args.state_db).context("failed to open state store")?;
151+
let mut build = store
152+
.load_build(&args.build_id)
153+
.context("failed to load build")?
154+
.ok_or_else(|| anyhow::anyhow!("build {} not found", args.build_id))?;
155+
156+
if build.state.is_terminal() {
157+
println!("Build {} is already in terminal state.", args.build_id);
158+
return Ok(());
159+
}
160+
161+
let now = std::time::SystemTime::now()
162+
.duration_since(std::time::UNIX_EPOCH)
163+
.map(|d| d.as_secs())
164+
.unwrap_or(0);
165+
166+
build.ended_at = Some(now);
167+
168+
build
169+
.transition_to(BuildState::Canceled)
170+
.map_err(|e| anyhow::anyhow!("{e}"))?;
171+
172+
store.save_build(&build).context("failed to save build")?;
173+
174+
println!("Build {} canceled.", args.build_id);
175+
176+
Ok(())
177+
}

crates/vz-cli/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
#[cfg(target_os = "macos")]
44
pub mod build;
5+
pub mod build_mgmt;
56
#[cfg(target_os = "macos")]
67
pub mod cache;
78
pub mod checkpoint;

crates/vz-cli/src/commands/stack.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1858,7 +1858,20 @@ fn event_service_name(event: &StackEvent) -> Option<&str> {
18581858
| StackEvent::CheckpointReady { .. }
18591859
| StackEvent::CheckpointFailed { .. }
18601860
| StackEvent::CheckpointRestored { .. }
1861-
| StackEvent::CheckpointForked { .. } => None,
1861+
| StackEvent::CheckpointForked { .. }
1862+
| StackEvent::BuildQueued { .. }
1863+
| StackEvent::BuildRunning { .. }
1864+
| StackEvent::BuildSucceeded { .. }
1865+
| StackEvent::BuildFailed { .. }
1866+
| StackEvent::BuildCanceled { .. }
1867+
| StackEvent::ContainerCreated { .. }
1868+
| StackEvent::ContainerStarting { .. }
1869+
| StackEvent::ContainerRunning { .. }
1870+
| StackEvent::ContainerStopping { .. }
1871+
| StackEvent::ContainerExited { .. }
1872+
| StackEvent::ContainerFailed { .. }
1873+
| StackEvent::ContainerRemoved { .. }
1874+
| StackEvent::DriftDetected { .. } => None,
18621875
}
18631876
}
18641877

@@ -2523,6 +2536,53 @@ fn format_event_summary(event: &StackEvent) -> String {
25232536
new_checkpoint_id,
25242537
..
25252538
} => format!("checkpoint forked: {parent_checkpoint_id} -> {new_checkpoint_id}"),
2539+
StackEvent::BuildQueued {
2540+
sandbox_id,
2541+
build_id,
2542+
} => format!("build queued: {build_id} for {sandbox_id}"),
2543+
StackEvent::BuildRunning { build_id } => {
2544+
format!("build running: {build_id}")
2545+
}
2546+
StackEvent::BuildSucceeded {
2547+
build_id,
2548+
result_digest,
2549+
} => format!("build succeeded: {build_id} ({result_digest})"),
2550+
StackEvent::BuildFailed { build_id, error } => {
2551+
format!("build failed: {build_id}: {error}")
2552+
}
2553+
StackEvent::BuildCanceled { build_id } => {
2554+
format!("build canceled: {build_id}")
2555+
}
2556+
StackEvent::ContainerCreated {
2557+
container_id,
2558+
sandbox_id,
2559+
} => format!("container created: {container_id} in {sandbox_id}"),
2560+
StackEvent::ContainerStarting { container_id } => {
2561+
format!("container starting: {container_id}")
2562+
}
2563+
StackEvent::ContainerRunning { container_id } => {
2564+
format!("container running: {container_id}")
2565+
}
2566+
StackEvent::ContainerStopping { container_id } => {
2567+
format!("container stopping: {container_id}")
2568+
}
2569+
StackEvent::ContainerExited {
2570+
container_id,
2571+
exit_code,
2572+
} => format!("container exited: {container_id} (code {exit_code})"),
2573+
StackEvent::ContainerFailed {
2574+
container_id,
2575+
error,
2576+
} => format!("container failed: {container_id}: {error}"),
2577+
StackEvent::ContainerRemoved { container_id } => {
2578+
format!("container removed: {container_id}")
2579+
}
2580+
StackEvent::DriftDetected {
2581+
category,
2582+
description,
2583+
severity,
2584+
..
2585+
} => format!("drift [{severity}] {category}: {description}"),
25262586
}
25272587
}
25282588

crates/vz-cli/src/commands/stack_output.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,12 @@ impl StackOutput {
299299
lease_id
300300
));
301301
}
302-
StackEvent::ExecutionQueued { .. }
302+
StackEvent::BuildQueued { .. }
303+
| StackEvent::BuildRunning { .. }
304+
| StackEvent::BuildSucceeded { .. }
305+
| StackEvent::BuildFailed { .. }
306+
| StackEvent::BuildCanceled { .. }
307+
| StackEvent::ExecutionQueued { .. }
303308
| StackEvent::ExecutionRunning { .. }
304309
| StackEvent::ExecutionExited { .. }
305310
| StackEvent::ExecutionFailed { .. }
@@ -308,7 +313,15 @@ impl StackOutput {
308313
| StackEvent::CheckpointReady { .. }
309314
| StackEvent::CheckpointFailed { .. }
310315
| StackEvent::CheckpointRestored { .. }
311-
| StackEvent::CheckpointForked { .. } => {}
316+
| StackEvent::CheckpointForked { .. }
317+
| StackEvent::ContainerCreated { .. }
318+
| StackEvent::ContainerStarting { .. }
319+
| StackEvent::ContainerRunning { .. }
320+
| StackEvent::ContainerStopping { .. }
321+
| StackEvent::ContainerExited { .. }
322+
| StackEvent::ContainerFailed { .. }
323+
| StackEvent::ContainerRemoved { .. }
324+
| StackEvent::DriftDetected { .. } => {}
312325
}
313326
self.update_header();
314327
}

crates/vz-cli/src/main.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ enum Commands {
101101
/// Manage checkpoint fingerprints and lineage.
102102
Checkpoint(commands::checkpoint::CheckpointArgs),
103103

104+
/// Manage asynchronous build operations.
105+
BuildMgmt(commands::build_mgmt::BuildMgmtArgs),
106+
104107
// ── VM management (macOS only) ──
105108
/// Manage virtual machines.
106109
#[cfg(target_os = "macos")]
@@ -191,6 +194,9 @@ fn main() -> anyhow::Result<()> {
191194
// Checkpoint management
192195
Commands::Checkpoint(args) => commands::checkpoint::run(args).await,
193196

197+
// Build management
198+
Commands::BuildMgmt(args) => commands::build_mgmt::run(args).await,
199+
194200
// VM management (macOS only)
195201
#[cfg(target_os = "macos")]
196202
Commands::Vm(args) => commands::vm::run(args).await,

crates/vz-cli/src/tui.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,19 @@ fn event_color(event: &StackEvent) -> Color {
379379
StackEvent::CheckpointFailed { .. } => Color::Red,
380380
StackEvent::CheckpointRestored { .. } => Color::Blue,
381381
StackEvent::CheckpointForked { .. } => Color::Blue,
382+
StackEvent::BuildQueued { .. } => Color::Cyan,
383+
StackEvent::BuildRunning { .. } => Color::Blue,
384+
StackEvent::BuildSucceeded { .. } => Color::Green,
385+
StackEvent::BuildFailed { .. } => Color::Red,
386+
StackEvent::BuildCanceled { .. } => Color::Yellow,
387+
StackEvent::ContainerCreated { .. } => Color::Cyan,
388+
StackEvent::ContainerStarting { .. } => Color::Cyan,
389+
StackEvent::ContainerRunning { .. } => Color::Green,
390+
StackEvent::ContainerStopping { .. } => Color::Yellow,
391+
StackEvent::ContainerExited { .. } => Color::DarkGray,
392+
StackEvent::ContainerFailed { .. } => Color::Red,
393+
StackEvent::ContainerRemoved { .. } => Color::DarkGray,
394+
StackEvent::DriftDetected { .. } => Color::Yellow,
382395
}
383396
}
384397

@@ -517,6 +530,53 @@ fn format_event_summary(event: &StackEvent) -> String {
517530
new_checkpoint_id,
518531
..
519532
} => format!("CkptForked {parent_checkpoint_id} -> {new_checkpoint_id}"),
533+
StackEvent::BuildQueued {
534+
sandbox_id,
535+
build_id,
536+
} => format!("BuildQueued {build_id} -> {sandbox_id}"),
537+
StackEvent::BuildRunning { build_id } => {
538+
format!("BuildRunning {build_id}")
539+
}
540+
StackEvent::BuildSucceeded {
541+
build_id,
542+
result_digest,
543+
} => format!("BuildSucceeded {build_id} ({result_digest})"),
544+
StackEvent::BuildFailed { build_id, error } => {
545+
format!("BuildFailed {build_id}: {error}")
546+
}
547+
StackEvent::BuildCanceled { build_id } => {
548+
format!("BuildCanceled {build_id}")
549+
}
550+
StackEvent::ContainerCreated {
551+
container_id,
552+
sandbox_id,
553+
} => format!("CtrCreated {container_id} in {sandbox_id}"),
554+
StackEvent::ContainerStarting { container_id } => {
555+
format!("CtrStarting {container_id}")
556+
}
557+
StackEvent::ContainerRunning { container_id } => {
558+
format!("CtrRunning {container_id}")
559+
}
560+
StackEvent::ContainerStopping { container_id } => {
561+
format!("CtrStopping {container_id}")
562+
}
563+
StackEvent::ContainerExited {
564+
container_id,
565+
exit_code,
566+
} => format!("CtrExited {container_id} (code {exit_code})"),
567+
StackEvent::ContainerFailed {
568+
container_id,
569+
error,
570+
} => format!("CtrFailed {container_id}: {error}"),
571+
StackEvent::ContainerRemoved { container_id } => {
572+
format!("CtrRemoved {container_id}")
573+
}
574+
StackEvent::DriftDetected {
575+
category,
576+
description,
577+
severity,
578+
..
579+
} => format!("Drift [{severity}] {category}: {description}"),
520580
}
521581
}
522582

crates/vz-runtime-contract/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ pub const PRIMITIVE_CONFORMANCE_MATRIX: &[PrimitiveConformanceEntry] = &[
379379
PrimitiveConformanceEntry {
380380
operation: RuntimeOperation::GetReceipt,
381381
openapi: Some(OpenApiPrimitiveSurface {
382-
path: "/v1/receipts",
382+
path: "/v1/receipts/{receipt_id}",
383383
surface: "receipts",
384384
}),
385385
manager: false,

0 commit comments

Comments
 (0)