From 1ef928b1cbfd525f8deb47344fa339fdb69b997c Mon Sep 17 00:00:00 2001 From: Cristian Spinetta Date: Sat, 18 Apr 2026 15:16:08 +0100 Subject: [PATCH] dev: fix macOS quick-start and runtime template ergonomics - docs: add macOS quick-start, drop Linux-only paths, drop the fake non-serde test tier, stop pointing the dev UI at void-box cross-origin. - AGENTS.md: consolidate environment variables (adds VOID_CONTROL_LLM_PROVIDER and the VITE_* / VOID_BOX_BASE_URL docs); CLAUDE.md slimmed to a routing file matching void-box's pattern. - bridge.rs: create_dir_all spec_dir + execution_dir at startup. - runtime/mod.rs: VOID_CONTROL_LLM_PROVIDER env patches llm.provider on every candidate at launch; per-candidate overrides still win. - contract tests: rename step `produce` -> `main` in the StructuredOutput* fixtures to fix stage-name drift. - RunsList.tsx: dedup items by kind:id to stop duplicate React keys. - .gitignore: ignore *.local.md, examples/**/*.local.yaml, and **/*.tsbuildinfo; untrack the stray tsbuildinfo copy. - .github: add PR template (repo had none). --- .github/PULL_REQUEST_TEMPLATE.md | 64 ++++++++++ .gitignore | 14 +-- AGENTS.md | 24 +++- CLAUDE.md | 111 ++++-------------- README.md | 37 +++++- examples/README.md | 61 ++++++++-- src/bridge.rs | 10 ++ src/runtime/mod.rs | 21 +++- tests/void_box_contract.rs | 16 +-- .../src/components/RunsList.tsx | 9 +- web/void-control-ux/tsconfig.app.tsbuildinfo | 1 - 11 files changed, 242 insertions(+), 126 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 web/void-control-ux/tsconfig.app.tsbuildinfo diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..f4a67e7 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,64 @@ +## Summary + + + +## Type of Change + + + +- [ ] Bug fix +- [ ] New feature +- [ ] Breaking change (contract or API) +- [ ] Documentation update +- [ ] Code refactoring +- [ ] Test addition or update +- [ ] Dev UX / tooling + +## Related Issues + + + +Fixes # +Related to # + +## Changes + + + +- +- +- + +## Test Plan + + + +- [ ] `cargo test --features serde` +- [ ] `cargo clippy --features serde --all-targets -- -D warnings` +- [ ] `cargo fmt --all -- --check` +- [ ] Web UI typecheck (`cd web/void-control-ux && npx tsc -b --noEmit`) +- [ ] Web UI build (`cd web/void-control-ux && npm run build`) — if UI changed +- [ ] Live contract gate against a running void-box daemon — if runtime client changed +- [ ] Manual execution run (swarm / supervision) — if orchestration behavior changed + +### Commands run + +```bash +# Paste the commands and their summarized output. +``` + +## Documentation + +- [ ] Updated README.md / CLAUDE.md if user-facing behavior changed +- [ ] Updated examples/ if orchestration or runtime template behavior changed +- [ ] Updated spec/ if the contract changed +- [ ] Updated skills/void-control/SKILL.md if the operator workflow changed + +## Compatibility + + + +## Notes for Reviewers + + diff --git a/.gitignore b/.gitignore index 5b7d5f2..9aabaec 100644 --- a/.gitignore +++ b/.gitignore @@ -4,15 +4,12 @@ package-lock.json package.json node_modules -.claude -!.claude/ -!.claude/skills/ -!.claude/skills/void-control/ -!.claude/skills/void-control/SKILL.md +# Claude Code local scheduler state (per-machine) +.claude/scheduled_tasks.lock .idea .local-utils -CLAUDE.local.md -.claude/*.local.* +# Local-only scratch files (*.local.md, *.local.yaml, etc.) +*.local.* .worktrees # Frontend build output and local env files @@ -22,6 +19,9 @@ web/void-control-ux/.env.local web/void-control-ux/.env.*.local web/void-control-ux/tmp_*.mjs +# TypeScript incremental build cache (per-machine, regenerated by tsc -b) +**/*.tsbuildinfo + .github-release-notes.md # Keep the UI lockfile tracked for CI/npm ci diff --git a/AGENTS.md b/AGENTS.md index 7af95f7..92ed2fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -190,7 +190,29 @@ VOID_BOX_BASE_URL=http://127.0.0.1:43100 \ cargo test --features serde --test void_box_contract -- --ignored --nocapture ``` -Optional policy fixture overrides: +## Environment variables + +Control-plane / bridge: + +- `VOID_BOX_BASE_URL` — void-box daemon endpoint (default: + `http://127.0.0.1:43100`). +- `VOID_CONTROL_LLM_PROVIDER` — optional global override that patches + `llm.provider` on every runtime template at launch. Set to + `claude-personal` to use OAuth from the macOS Keychain or + `~/.claude/.credentials.json` without editing tracked templates. + Per-candidate `variation.explicit[].overrides` still win. + +Web UI: + +- `VITE_VOID_BOX_BASE_URL` — daemon URL for the operator dashboard. + Leave unset during local dev so the Vite `/api` proxy is used; + void-box serves no CORS headers, so setting this sends the browser + straight into the CORS pit. +- `VITE_VOID_CONTROL_BASE_URL` — bridge URL for the operator dashboard + (e.g., `http://127.0.0.1:43210`). The bridge sets CORS, so this can + point at a direct origin. + +Optional policy fixture overrides (used by contract tests): - `VOID_BOX_TIMEOUT_SPEC_FILE` - `VOID_BOX_PARALLEL_SPEC_FILE` diff --git a/CLAUDE.md b/CLAUDE.md index 7e941ed..28b76e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,92 +2,25 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## Project Overview - -void-control is the control-plane orchestration layer for the `void-box` runtime. It launches/manages runs, tracks run/stage/event lifecycle, and enforces runtime contract compatibility. The project has two main parts: a Rust library/CLI and a React operator dashboard. - -## Build & Test Commands - -```bash -# Core unit tests (no serde feature) -cargo test - -# JSON compatibility + fixture-based tests -cargo test --features serde - -# Mocked transport contract tests for void-box client -cargo test --features serde runtime::void_box:: - -# Live daemon contract tests (requires running void-box on port 43100) -VOID_BOX_BASE_URL=http://127.0.0.1:43100 \ - cargo test --features serde --test void_box_contract -- --ignored --nocapture - -# Run a single test -cargo test --features serde test_name_here - -# Terminal console -cargo run --features serde --bin voidctl - -# Bridge server (port 43210) -cargo run --features serde --bin voidctl -- serve -``` - -**Always validate both paths before PRs:** `cargo test` AND `cargo test --features serde`. - -### Web UI (web/void-control-ux/) - -```bash -cd web/void-control-ux -npm install -VITE_VOID_BOX_BASE_URL=http://127.0.0.1:43100 npm run dev # dev server on port 5174 -npm run build # production build (tsc -b && vite build) -``` - -## Architecture - -### Rust Crate (src/) - -Single crate with two main modules, feature-gated with `serde`: - -- **`contract/`** — Control-plane type definitions (no runtime dependencies) - - `api.rs` — Request/response types: `StartRequest`, `StopRequest`, `RuntimeInspection`, `ConvertedRunView` - - `state.rs` — `RunState` enum with strict lifecycle transitions: Pending→Starting→Running→{Succeeded|Failed|Canceled} - - `event.rs` — `EventEnvelope`, `EventType` enum, `EventSequenceTracker` (enforces monotonic seq ordering) - - `policy.rs` — `ExecutionPolicy` (max_parallel_microvms, stage_timeout, retry config) - - `error.rs` — `ContractError` with code, message, retryable flag - - `compat.rs` / `compat_json.rs` — Normalization from void-box raw format to canonical types - -- **`runtime/`** — Client implementations (behind `serde` feature) - - `void_box.rs` — `VoidBoxRuntimeClient` (HTTP client to void-box daemon) - - `mock.rs` — `MockRuntime` for testing - -- **`bin/`** — `voidctl` (console + bridge server), `normalize_fixture` - -### Web UI (web/void-control-ux/src/) - -React 18 + TypeScript dashboard: - -- **State:** Zustand (`store/ui.ts`) for selection state; TanStack Query for server state with tiered polling (active runs 2.5s, terminal 5s, events 1.2s) -- **Key components:** `RunsList`, `RunGraph` (Sigma/Graphology DAG), `EventTimeline`, `NodeInspector`, `LaunchRunModal` -- **API layer:** `lib/api.ts` wraps daemon endpoints (`/v1/runs/*`) and bridge endpoint (`/v1/launch`) -- **Types:** `lib/types.ts` mirrors the Rust contract types - -### API Surface - -- Daemon (void-box): `/v1/runs`, `/v1/runs/{id}`, `/v1/runs/{id}/events`, `/v1/runs/{id}/stages`, `/v1/runs/{id}/telemetry`, `/v1/runs/{id}/cancel` -- Bridge (voidctl serve): `POST /v1/launch` - -## Coding Conventions - -- **Naming:** Use boundary-focused names from the spec: `Run`, `Stage`, `Attempt`, `Runtime`, `Controller` -- **Testing:** Keep contract tests in `#[cfg(test)]` blocks near conversion/runtime logic. Fixture-based tests require `--features serde` -- **Feature gating:** JSON serialization, HTTP client, and server code live behind the `serde` feature flag -- **Commits:** Imperative style, format `area: concise action` (e.g., `spec: clarify cancellation semantics`) -- **Specs:** Add new specs to `spec/` with version in filename (e.g., `*-v0.2.md`) - -## Environment Variables - -- `VOID_BOX_BASE_URL` — void-box daemon endpoint (default: `http://127.0.0.1:43100`) -- `VITE_VOID_BOX_BASE_URL` — daemon URL for web UI -- `VITE_VOID_CONTROL_BASE_URL` — bridge URL for web UI (e.g., `http://127.0.0.1:43210`) -- `VOID_BOX_TIMEOUT_SPEC_FILE`, `VOID_BOX_PARALLEL_SPEC_FILE`, `VOID_BOX_RETRY_SPEC_FILE`, `VOID_BOX_NO_POLICY_SPEC_FILE` — Optional spec file overrides for policy behavior tests +For general development guidelines — architecture, module map, local +commands, testing, environment variables, and commit/PR conventions — +see @AGENTS.md. + +## Claude-specific guidance + +- Before implementing non-trivial changes, propose a plan and explain + the tradeoffs. Wait for alignment before editing. +- Preserve the control-plane / runtime boundary described in @AGENTS.md. + Runtime-transport and VM-isolation concerns belong to `void-box`, not + here. When in doubt, prefer a normalization layer in `src/contract/` + over leaking runtime details into orchestration. +- Prefer LSP operations (`goToDefinition`, `findReferences`, `hover`) + over Grep/Glob for Rust code navigation. Fall back to Grep/Glob only + for comments, config files, and non-Rust code. +- For UI work in `web/void-control-ux`, use browser automation (MCP or + Playwright) for DOM, layout, resize, console, and network validation. + Screenshots are fallback only. +- Every test target is currently gated on `--features serde`, so + `cargo test --features serde` is the one validation path — plain + `cargo test` runs zero tests. +- Be terse: skip end-of-turn summaries. diff --git a/README.md b/README.md index 1666df7..206d9f5 100644 --- a/README.md +++ b/README.md @@ -118,17 +118,38 @@ What to look for: ### 1) Start `void-box` daemon +Linux: + ```bash cargo run --bin voidbox -- serve --listen 127.0.0.1:43100 ``` +macOS (Apple Silicon, Virtualization.framework): + +```bash +# after building the guest kernel + rootfs per void-box's macOS guide +VOID_BOX_KERNEL=target/vmlinuz \ +VOID_BOX_INITRAMFS=target/void-box-claude.cpio.gz \ + cargo run --bin voidbox -- serve --listen 127.0.0.1:43100 +``` + +macOS requires `VOID_BOX_KERNEL` and `VOID_BOX_INITRAMFS` pointing at the +pre-built guest artifacts. The initramfs filename on macOS is +`target/void-box-claude.cpio.gz` (not the Linux `void-box-rootfs.cpio.gz`). +Running through `cargo run` also applies `codesign` automatically — direct +binary invocation needs manual codesigning. See the `void-box` macOS guide +for full details. + ### 2) Run `void-control` tests ```bash -cargo test cargo test --features serde ``` +All test targets are currently gated behind the `serde` feature, so +`cargo test` without it runs zero tests. Use `--features serde` as the +one validation path. + ### 3) Run live daemon contract gate ```bash @@ -141,9 +162,15 @@ cargo test --features serde --test void_box_contract -- --ignored --nocapture ```bash cd web/void-control-ux npm install -VITE_VOID_BOX_BASE_URL=http://127.0.0.1:43100 npm run dev +npm run dev ``` +The dev server proxies `/api` to the daemon at `http://127.0.0.1:43100` (see +`vite.config.ts`), so the browser stays same-origin. `void-box` does not set +CORS headers, so do **not** set `VITE_VOID_BOX_BASE_URL` during local dev — +leave it unset to use the proxy. Only override it when the daemon is reachable +from a host that returns CORS (e.g., a reverse proxy in front of void-box). + ### 5) Launch from YAML editor/upload (bridge) Run bridge mode in another terminal: @@ -156,11 +183,14 @@ Then start UI with bridge URL: ```bash cd web/void-control-ux -VITE_VOID_BOX_BASE_URL=http://127.0.0.1:43100 \ VITE_VOID_CONTROL_BASE_URL=http://127.0.0.1:43210 \ npm run dev ``` +The bridge serves CORS headers, so `VITE_VOID_CONTROL_BASE_URL` can point +directly at it. Continue to leave `VITE_VOID_BOX_BASE_URL` unset so the +Vite `/api` proxy is used for daemon calls. + ### 6) Run the canonical live swarm test Use the three-candidate swarm as the default validation path: @@ -207,7 +237,6 @@ Rust validation: ```bash cargo fmt --all -- --check cargo clippy --all-targets --all-features -- -D warnings -cargo test cargo test --features serde RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features ``` diff --git a/examples/README.md b/examples/README.md index 265106d..4ac8090 100644 --- a/examples/README.md +++ b/examples/README.md @@ -83,43 +83,78 @@ Metric source of truth: ## Prerequisites +These examples assume a sibling checkout layout: + +```text +/void-box # the runtime repo +/void-control # this repo +``` + +Substitute `` for your local path (e.g. `~/dev/repos`, +`~/github`, etc.). The runtime template mount path +`../../void-control/examples/runtime-assets` resolves to +`/void-control/examples/runtime-assets` when the daemon is +started from `/void-box`. + Build the production `void-box` rootfs in the sibling repo: ```bash -cd /home/diego/github/agent-infra/void-box +cd /void-box TMPDIR=$PWD/target/tmp scripts/build_claude_rootfs.sh ``` -Start the `void-box` daemon with a real Anthropic key: +Start the `void-box` daemon (Linux): ```bash -cd /home/diego/github/agent-infra/void-box -export ANTHROPIC_API_KEY=sk-ant-... +cd /void-box +export ANTHROPIC_API_KEY=sk-ant-... # or use provider: claude-personal, see below export VOID_BOX_KERNEL=/boot/vmlinuz-$(uname -r) export VOID_BOX_INITRAMFS=$PWD/target/void-box-rootfs.cpio.gz cargo run --bin voidbox -- serve --listen 127.0.0.1:43100 ``` -The transform example also assumes the daemon is started from the sibling -`void-box` repo root so this runtime template mount resolves correctly: +Start the `void-box` daemon (macOS, Apple Silicon): -```yaml -../../void-control/examples/runtime-assets -> /workspace/runtime-assets +```bash +cd /void-box +export VOID_BOX_KERNEL=$PWD/target/vmlinuz +export VOID_BOX_INITRAMFS=$PWD/target/void-box-claude.cpio.gz +# ANTHROPIC_API_KEY is not needed if the runtime template sets +# `llm.provider: claude-personal` and you have a Claude subscription +# (credentials come from the macOS Keychain or ~/.claude/.credentials.json). +cargo run --bin voidbox -- serve --listen 127.0.0.1:43100 ``` Start the `void-control` bridge: ```bash -cd /home/diego/github/void-control +cd /void-control cargo run --features serde --bin voidctl -- serve ``` +### Using a Claude personal plan instead of an API key + +The checked-in runtime templates hardcode `llm.provider: claude` so CI keeps +using an API key. To opt into `claude-personal` without editing tracked +templates, set `VOID_CONTROL_LLM_PROVIDER` when starting the bridge (or any +other process that launches runs through `void-control`): + +```bash +cd /void-control +VOID_CONTROL_LLM_PROVIDER=claude-personal \ + cargo run --features serde --bin voidctl -- serve +``` + +This patches `llm.provider` on every candidate's runtime template at launch +time. Per-candidate `variation.explicit[].overrides` still win, so you can +keep a mixed setup if needed. + ## Launch From CLI Swarm: ```bash -cd /home/diego/github/void-control +cd /void-control curl -sS -X POST http://127.0.0.1:43210/v1/executions \ -H 'Content-Type: text/yaml' \ --data-binary @examples/swarm-transform-optimization-3way.yaml @@ -128,7 +163,7 @@ curl -sS -X POST http://127.0.0.1:43210/v1/executions \ Supervision: ```bash -cd /home/diego/github/void-control +cd /void-control curl -sS -X POST http://127.0.0.1:43210/v1/executions \ -H 'Content-Type: text/yaml' \ --data-binary @examples/supervision-transform-review.yaml @@ -137,7 +172,7 @@ curl -sS -X POST http://127.0.0.1:43210/v1/executions \ Stress swarm: ```bash -cd /home/diego/github/void-control +cd /void-control curl -sS -X POST http://127.0.0.1:43210/v1/executions \ -H 'Content-Type: text/yaml' \ --data-binary @examples/swarm-transform-optimization.yaml @@ -148,7 +183,7 @@ curl -sS -X POST http://127.0.0.1:43210/v1/executions \ 1. Start the UI: ```bash -cd /home/diego/github/void-control/web/void-control-ux +cd /void-control/web/void-control-ux npm run dev -- --host 127.0.0.1 --port 3000 ``` diff --git a/src/bridge.rs b/src/bridge.rs index 4fddc96..ceae1da 100644 --- a/src/bridge.rs +++ b/src/bridge.rs @@ -270,6 +270,16 @@ pub fn run_bridge() -> Result<(), String> { use tiny_http::{Method, Response, Server, StatusCode}; let config = BridgeConfig::from_env(); + + for dir in [&config.spec_dir, &config.execution_dir] { + if let Err(err) = std::fs::create_dir_all(dir) { + return Err(format!( + "failed to create bridge storage dir {}: {err}", + dir.display() + )); + } + } + let worker_config = config.clone(); thread::spawn(move || loop { let runtime = VoidBoxRuntimeClient::new(worker_config.base_url.clone(), 250); diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index d6dd534..3bda2e5 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -54,10 +54,15 @@ impl ProviderLaunchAdapter for LaunchInjectionAdapter { ) -> Result { debug_assert_eq!(candidate.candidate_id, inbox.candidate_id); let launch_context = serde_json::to_string(inbox).expect("serialize inbox snapshot"); - let workflow_spec = if candidate.overrides.is_empty() { + let env_overrides = env_provider_overrides(); + let workflow_spec = if candidate.overrides.is_empty() && env_overrides.is_empty() { request.workflow_spec.clone() } else { - write_patched_workflow_spec(&request.workflow_spec, &candidate.overrides)? + let mut merged = env_overrides; + for (key, value) in &candidate.overrides { + merged.insert(key.clone(), value.clone()); + } + write_patched_workflow_spec(&request.workflow_spec, &merged)? }; Ok(StartRequest { workflow_spec, @@ -67,6 +72,18 @@ impl ProviderLaunchAdapter for LaunchInjectionAdapter { } } +#[cfg(feature = "serde")] +fn env_provider_overrides() -> BTreeMap { + let mut overrides = BTreeMap::new(); + if let Ok(provider) = std::env::var("VOID_CONTROL_LLM_PROVIDER") { + let trimmed = provider.trim(); + if !trimmed.is_empty() { + overrides.insert("llm.provider".to_string(), trimmed.to_string()); + } + } + overrides +} + #[cfg(feature = "serde")] fn write_patched_workflow_spec( original_path: &str, diff --git a/tests/void_box_contract.rs b/tests/void_box_contract.rs index 95d1b7c..59a5fb1 100644 --- a/tests/void_box_contract.rs +++ b/tests/void_box_contract.rs @@ -154,7 +154,7 @@ sandbox: workflow: steps: - - name: produce + - name: main run: program: sh args: @@ -163,7 +163,7 @@ workflow: cat > result.json <<'JSON' {"status":"success","summary":"ok","metrics":{"latency_p99_ms":87,"cost_usd":0.018},"artifacts":[]} JSON - output_step: produce + output_step: main "# } DefaultSpecKind::StructuredOutputWithArtifact => { @@ -177,7 +177,7 @@ sandbox: workflow: steps: - - name: produce + - name: main run: program: sh args: @@ -190,7 +190,7 @@ workflow: # report artifact content MD - output_step: produce + output_step: main "# } DefaultSpecKind::MissingStructuredOutput => { @@ -204,14 +204,14 @@ sandbox: workflow: steps: - - name: produce + - name: main run: program: sh args: - -lc - | echo "completed without result.json" - output_step: produce + output_step: main "# } DefaultSpecKind::MalformedStructuredOutput => { @@ -225,7 +225,7 @@ sandbox: workflow: steps: - - name: produce + - name: main run: program: sh args: @@ -234,7 +234,7 @@ workflow: cat > result.json <<'JSON' {"status":"success","summary":"ok","metrics":not-json,"artifacts":[]} JSON - output_step: produce + output_step: main "# } }; diff --git a/web/void-control-ux/src/components/RunsList.tsx b/web/void-control-ux/src/components/RunsList.tsx index 85b8c42..fbec8a9 100644 --- a/web/void-control-ux/src/components/RunsList.tsx +++ b/web/void-control-ux/src/components/RunsList.tsx @@ -37,7 +37,7 @@ export function RunsList({ onStateFilterChange }: RunsListProps) { const all = [...activeRuns, ...terminalRuns]; - const items: Array< + const rawItems: Array< | { kind: 'execution'; id: string; execution: ExecutionInspection } | { kind: 'run'; id: string; run: RunInspection } > = [ @@ -52,6 +52,13 @@ export function RunsList({ run })) ]; + const seen = new Set(); + const items = rawItems.filter((item) => { + const key = `${item.kind}:${item.id}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); const statePills: Array<{ value: 'all' | 'running' | 'failed' | 'succeeded' | 'cancelled'; label: string }> = [ { value: 'all', label: 'All' }, { value: 'running', label: 'Running' }, diff --git a/web/void-control-ux/tsconfig.app.tsbuildinfo b/web/void-control-ux/tsconfig.app.tsbuildinfo deleted file mode 100644 index 0712f5a..0000000 --- a/web/void-control-ux/tsconfig.app.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/EventTimeline.tsx","./src/components/LaunchRunModal.tsx","./src/components/NodeInspector.tsx","./src/components/RunGraph.tsx","./src/components/RunLogs.tsx","./src/components/RunsList.tsx","./src/lib/api.ts","./src/lib/selectors.ts","./src/lib/types.ts","./src/store/ui.ts"],"version":"5.9.3"} \ No newline at end of file