Skip to content

feat: izba.yml manifest + diff/promote/export workflow#119

Merged
Lupus merged 35 commits into
mainfrom
worktree-izba-manifest-diff-promote
Jul 1, 2026
Merged

feat: izba.yml manifest + diff/promote/export workflow#119
Lupus merged 35 commits into
mainfrom
worktree-izba-manifest-diff-promote

Conversation

@Lupus

@Lupus Lupus commented Jun 28, 2026

Copy link
Copy Markdown
Owner

What

Adds a Kubernetes-style project manifest (izba.yml) plus an izba diff / izba promote / izba export reconciliation workflow, so a project's sandbox configuration (image/build, resources, volumes, ports, egress policy) can live in the repo and be version-controlled — while keeping the in-guest agent unable to silently change its own jail.

Design: docs/superpowers/specs/2026-06-28-izba-manifest-diff-promote-design.md · Plan: docs/superpowers/plans/2026-06-28-izba-manifest-diff-promote.md

Trust model (the spine)

izba.yml lives in the workspace, which is mounted into the guest at /workspace — so the in-guest agent can edit it. It is therefore an untrusted proposal. The managed truth (config.json + policy.yaml under ~/.local/share/izba/sandboxes/<name>/, host-only, outside the guest overlay) is authority. izba promote is the human-gated bridge:

  • izba diff records a host-only review token (manifest.review) over the exact manifest and referenced Dockerfile bytes.
  • izba promote requires that token to match: no token → fail (run diff first); stale token → fail (TOCTOU guard — manifest changed after review); --force overrides both, loudly.
  • manifest.review / manifest.base.yaml are host-only and never enter the overlay, so the guest cannot read or forge a reviewed state.

Surface

  • Schema (izba_core::manifest): apiVersion: izba.dev/v1alpha1 / kind: Sandbox / metadata / spec (image xor build recipe, resources, rootDisk, volumes, ports, egress), k8s quantity strings (4Gi), kind-extensible to a future Project. The egress block reuses the existing policy type verbatim.
  • izba diff — structural 3-way drift (base/repo/managed), per-field blast-radius class (live / restart / image), ⚠ weakens egress flag, records the review token.
  • izba promote — gated apply: live fields (policy/ports/volumes) apply immediately; cpus/mem/image are restart-class (--restart); image changes reset the rw scratch overlay (--reset-scratch, default yes) with a loud expert-only warning when kept; image changes require --restart; confinement posture is replayed from state.json on restart; live RPCs are skipped on a stopped sandbox.
  • izba export — write the managed truth back to izba.yml (declarative only — no secrets), for the app-edits-then-commit flow.
  • izba create / izba run honor an izba.yml when present (flags still win) and seed the manifest base.
  • Tauri app — backend manifest_diff / manifest_export commands + DiffView (drift badge / preview / save-to-repo).

No DAEMON_PROTO_VERSION bump — diff/promote/export are host-side over existing daemon RPCs + host-callable image resolution/build.

Deferred (follow-up)

  • In-app Promote button and create-from-manifest wizard + manifest frontend UI (the security-critical promote path ships fully in the CLI).
  • Persisting rootDisk size in SandboxConfig (diff currently ignores rw size, documented).
  • Build options beyond build-in-VM's current set (build-arg/secret/no-cache/target).

Testing

Built TDD, subagent-driven, with per-task spec+quality review and a final whole-branch review (all findings fixed, incl. duplicate-host egress-weakening detection and the non-restart image-promote scratch-reset safety gap). All six workspace gates + the App gate verified green locally:
cargo test --workspace, clippy --workspace -D warnings, fmt --check, izba-init musl build, x86_64-pc-windows-gnu check + clippy, and app (npm build + src-tauri fmt/clippy/test).

🤖 Generated with Claude Code

Greptile Summary

This PR introduces the izba.yml manifest system — a Kubernetes-style YAML config (apiVersion: izba.dev/v1alpha1, kind: Sandbox) enabling version-controlled sandbox configuration with a TOCTOU-safe diff/promote/export workflow.

  • Adds izba diff, izba promote, and izba export CLI subcommands backed by new manifest/ modules (schema, normalize, diff, ops, apply, store, quantity) in izba-core
  • Implements a SHA-256 review token written by diff and verified by promote to prevent TOCTOU races; path/name traversal guards (ensure_within, validate_name) protect agent-controlled fields
  • Extends SandboxConfig with build: Option<BuildSpec> and rw_size_gb: u64 (both serde(default)) for backward-compatible persistence, and makes reset_rw_scratch atomic via a tmp-file + fs::rename pattern

Confidence Score: 5/5

Safe to merge; no blocking issues found beyond minor P2 observations

All previously-raised issues are addressed. The three new observations are P2 quality/robustness notes (double manifest read, non-total sort order, noisy egress emit) with no correctness impact on the happy path. Security guards (ensure_within, validate_name, review token) are correctly implemented.

crates/izba-core/src/manifest/diff.rs and crates/izba-cli/src/commands/diff.rs for the double load_repo_manifest call

Important Files Changed

Filename Overview
crates/izba-core/src/manifest/ops.rs Core diff/export/apply helpers; ensure_within and validate_name guards correctly placed; managed_normalized rw_size_gb fallback chain is sound
crates/izba-core/src/manifest/apply.rs plan/write_managed/write_policy logic is correct; ImageSource::Build branch now persists cfg.build, eliminating false drift
crates/izba-core/src/manifest/diff.rs egress_weakens uses per-(host,port) BTreeMap correctly; classify 3-way logic is sound; minor TOCTOU window from double manifest read in diff command
crates/izba-core/src/manifest/normalize.rs from_manifest/from_managed correctly reconstruct Normalized; sort_canonical sort key for same-host multi-entry allow lists is not a stable total order; to_manifest always emits egress even when empty
crates/izba-core/src/manifest/store.rs review_token SHA-256 with 0x1f separator is correct; clear_review is idempotent; non-NotFound I/O errors propagated
crates/izba-core/src/manifest/quantity.rs parse_bytes/parse_mib/parse_gib accept only binary-SI suffixes; format(0) returns bare 0 but managed_normalized guards against it
crates/izba-core/src/manifest/schema.rs deny_unknown_fields on all structs; image XOR build validated in load_str; apiVersion/kind checked
crates/izba-cli/src/commands/diff.rs Writes review token after compute_diff; double load_repo_manifest call introduces minor TOCTOU window on name resolution
crates/izba-cli/src/commands/promote.rs gate() enforces review token; is_running guard prevents spurious Stop; build_opts_from uses ensure_within; recovery hint on Start failure is helpful
crates/izba-cli/src/commands/export.rs exists() guard before load_repo_manifest correctly propagates parse errors
crates/izba-cli/src/commands/mod.rs merge_manifest_into_opts uses load_manifest_yaml (not load_repo_manifest); sub-GiB volume MiB format fix is correct
crates/izba-core/src/sandbox.rs reset_rw_scratch atomic via tmp+rename with cleanup on error; create() initialises build:None and rw_size_gb
crates/izba-core/src/state.rs SandboxConfig gains build and rw_size_gb with serde(default) for backward compatibility

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Agent
    participant CLI as izba CLI
    participant Store
    participant Core as izba-core
    participant Daemon

    Agent->>CLI: izba diff [--name]
    CLI->>Core: load_repo_manifest(dir)
    Core-->>CLI: (manifest, name)
    CLI->>Core: compute_diff(paths, dir, name)
    Core->>Core: load_repo_manifest (2nd read)
    Core->>Core: managed_normalized
    Core->>Core: diff / classify
    Core-->>CLI: (DriftState, deltas, token)
    CLI->>Store: write_review(sandbox_dir, token)
    CLI-->>Agent: drift report

    Agent->>CLI: izba promote [--force]
    CLI->>Store: read_review(sandbox_dir)
    CLI->>Core: compute_diff (recompute token)
    Core-->>CLI: current_token
    CLI->>CLI: gate(stored, current, force)
    alt token matches
        CLI->>Core: plan(current, target)
        CLI->>Daemon: live RPCs (CPU/mem/ports/egress)
        CLI->>Core: write_managed
        CLI->>Core: write_policy
        CLI->>Daemon: ReloadPolicy
        opt image or restart required
            CLI->>Daemon: Stop
            CLI->>Core: reset_rw_scratch (atomic)
            CLI->>Daemon: Start
        end
        CLI->>Store: clear_review
    else token mismatch
        CLI-->>Agent: error: manifest changed since diff
    end

    Agent->>CLI: izba export [dir]
    CLI->>Core: export(paths, name, dir)
    Core->>Core: managed_normalized → to_manifest
    Core-->>CLI: Manifest YAML
    CLI-->>Agent: writes izba.yml
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Agent
    participant CLI as izba CLI
    participant Store
    participant Core as izba-core
    participant Daemon

    Agent->>CLI: izba diff [--name]
    CLI->>Core: load_repo_manifest(dir)
    Core-->>CLI: (manifest, name)
    CLI->>Core: compute_diff(paths, dir, name)
    Core->>Core: load_repo_manifest (2nd read)
    Core->>Core: managed_normalized
    Core->>Core: diff / classify
    Core-->>CLI: (DriftState, deltas, token)
    CLI->>Store: write_review(sandbox_dir, token)
    CLI-->>Agent: drift report

    Agent->>CLI: izba promote [--force]
    CLI->>Store: read_review(sandbox_dir)
    CLI->>Core: compute_diff (recompute token)
    Core-->>CLI: current_token
    CLI->>CLI: gate(stored, current, force)
    alt token matches
        CLI->>Core: plan(current, target)
        CLI->>Daemon: live RPCs (CPU/mem/ports/egress)
        CLI->>Core: write_managed
        CLI->>Core: write_policy
        CLI->>Daemon: ReloadPolicy
        opt image or restart required
            CLI->>Daemon: Stop
            CLI->>Core: reset_rw_scratch (atomic)
            CLI->>Daemon: Start
        end
        CLI->>Store: clear_review
    else token mismatch
        CLI-->>Agent: error: manifest changed since diff
    end

    Agent->>CLI: izba export [dir]
    CLI->>Core: export(paths, name, dir)
    Core->>Core: managed_normalized → to_manifest
    Core-->>CLI: Manifest YAML
    CLI-->>Agent: writes izba.yml
Loading

Reviews (20): Last reviewed commit: "ci(mutants): shard the linux incremental..." | Re-trigger Greptile

Context used:

  • Context used - CLAUDE.md (source)

Comment thread crates/izba-core/src/manifest/normalize.rs
Comment thread crates/izba-cli/src/commands/promote.rs
Comment thread crates/izba-cli/src/commands/promote.rs
Comment thread crates/izba-cli/src/commands/mod.rs
@Lupus

Lupus commented Jun 28, 2026

Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread crates/izba-cli/src/commands/mod.rs
@Lupus

Lupus commented Jun 28, 2026

Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread crates/izba-core/src/manifest/apply.rs
@Lupus

Lupus commented Jun 28, 2026

Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread crates/izba-cli/src/commands/promote.rs Outdated
Comment thread crates/izba-cli/src/commands/promote.rs
Comment thread crates/izba-core/src/manifest/ops.rs
@Lupus

Lupus commented Jun 28, 2026

Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread crates/izba-cli/src/commands/diff.rs
@Lupus

Lupus commented Jun 28, 2026

Copy link
Copy Markdown
Owner Author

@greptile review

Comment thread crates/izba-core/src/sandbox.rs
Comment thread crates/izba-cli/src/commands/export.rs Outdated
@Lupus Lupus force-pushed the worktree-izba-manifest-diff-promote branch from 5818d2b to 3ef8320 Compare June 28, 2026 14:34
Comment thread crates/izba-cli/src/commands/run.rs
Comment thread crates/izba-core/src/manifest/ops.rs Outdated
Comment thread crates/izba-cli/src/commands/export.rs
Lupus and others added 14 commits July 1, 2026 15:07
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s + 3-way state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
….yaml)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires izba_core::manifest into the CLI:
- commands/mod.rs: shared helpers load_repo_manifest, workspace_default_name,
  managed_normalized (reusable by promote/export tasks)
- commands/diff.rs: run() + pure render_deltas(); TDD (RED→GREEN on render tests)
- main.rs: Cmd::Diff { dir, name } + dispatch arm

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`izba diff` with no args defaults dir to ".", and Path::new(".").file_name()
is None, so workspace_default_name bailed with "has no basename". Canonicalize
the dir first so "." resolves to the current directory's basename.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add `izba promote` command: the review-gated apply of an izba.yml to the
managed sandbox truth. Pure `gate()` function (with 6 outcomes) guards the
path — NeverReviewed and Stale bail unless `--force`, Forced* variants warn.
Live effects (ReloadPolicy, PortPublish/Unpublish, VolumeAttach/Detach) apply
immediately; restart-class fields (cpus/mem/image) apply on `--restart` or
are printed as pending. On an image change with `--restart --reset-scratch`
(default), `reset_rw_scratch` in sandbox.rs recreates the rw overlay blank
at the same apparent size before the new Start, so the guest boots cleanly
on the new base. Advances `manifest.base.yaml` and clears `manifest.review`
on success.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
promote::run wrote config.json (via write_managed) BEFORE the live daemon
RPCs. A mid-apply RPC failure left config.json at the new state; on retry the
recomputed diff was empty so the live effects were skipped, yet base+review
still advanced — silently diverging live state from managed truth.

Reorder so the durable config.json commit lands AFTER all live effects succeed:
split apply::write_policy out of write_managed (policy.yaml lands before its
ReloadPolicy RPC, which reads it from disk; write_managed reuses it), run the
port/volume RPCs, THEN write_managed (config.json), then the Stop->Start
restart branch (reads config.json from disk), then write_base + clear_review
last. A failure now leaves config.json at the OLD state so a retry recomputes
the correct deltas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lupus and others added 21 commits July 1, 2026 15:07
… create

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… CLI+app reuse

Move load_repo_manifest, managed_normalized, compute_diff, export, and
manifest_with_header into izba_core::manifest::ops — a daemon-free module
reachable by both the CLI and the Tauri desktop app without pulling in
CLI-only crates. CLI commands become thin wrappers that handle name
resolution (which depends on name::sanitize) then delegate to ops.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add `DiffView`/`DeltaView` serializable view structs in `views.rs` that map
`izba_core::manifest::{DriftState, diff::FieldDelta}` to frontend-safe
strings (state as snake_case, class as "live"/"restart"/"image").

Add two pure-filesystem core functions in `commands.rs` that construct
`Paths::from_env_or_default(None)` directly (no DaemonApi) and delegate to
`izba_core::manifest::ops::{compute_diff, export}`. The diff review token is
discarded — the app diff is a read-only preview; promote is deferred.

Wire two `#[tauri::command]` async wrappers in `lib.rs` (`manifest_diff`,
`manifest_export`) via `spawn_blocking` and register them in
`generate_handler\![]`. No frontend/.tsx changes — UI is a follow-up task.

TDD: `diff_view_maps_state_and_deltas` test in `views.rs` was written first
(RED: E0433 × 5 on `DiffView`), then the structs were added (GREEN). All 43
app tests pass; fmt/clippy/npm-build green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a 'Project manifest (izba.yml)' section to README.md covering the
k8s-style schema example, the two-store trust model (untrusted proposal in
/workspace vs host-only managed truth), the diff→promote→export review loop,
field blast-radius classes (live/restart/image), the TOCTOU review gate,
--force/--restart/--reset-scratch semantics, and the ⚠ weakens-egress flag.
Also extends the Commands block with izba diff/promote/export signatures.

Add one load-bearing-contracts bullet to CLAUDE.md describing the trust
boundary: izba.yml is an untrusted proposal, managed truth is host-only
authority, promote is the human-gated bridge, manifest.review + manifest.base.yaml
are host-only, and no DAEMON_PROTO_VERSION bump is required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Re-key allow_index from host → (host, port), expanding each allow entry
into per-port rows with max-access for duplicate (host, port) pairs.
egress_weakens now correctly flags a verb widening on one port of a
duplicate-host entry even when another port for the same host tightens —
the previous host-keyed last-wins collapse missed this case entirely.

Tests added:
- duplicate_host_verb_widening_weakens_egress: read→read-write on one
  of two same-host entries must flag weakens_egress=true
- duplicate_host_pure_tightening_does_not_weaken: read-write→read on one
  entry must not flag weakens_egress

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ent on restart

Fix 2 (promote): bail early when plan.image_changed && !restart — writing
the new image_digest to config.json while the old rw scratch overlay
persists is a latent boot-failure; the caller must acknowledge the risk
by passing --restart (or --restart --reset-scratch=false for the overlay
preservation path).

Fix 3a (promote): read state.json BEFORE the Stop call in the restart
branch so the confinement mode (allow_unconfined) is captured before the
VMM teardown clears it. Defaults to false (confined, safe) when state.json
is absent or unreadable.

Fix 3b (promote): wrap the post-Stop Start call with a helpful retry
hint: "config already committed; run izba start <name> to retry".

Fix 4 (promote): print an egress-weakening WARNING to stderr before
applying any change (even under --force), by calling diff_normalized on
managed→repo and checking weakens_egress on each delta.

Fix 5 (promote): inspect sandbox liveness before live RPCs
(ReloadPolicy / PortPublish / PortUnpublish / VolumeAttach /
VolumeDetach); skip them with "sandbox not running — changes apply on
next start" when the status is "stopped". write_managed + write_base +
clear_review still run unconditionally.

Fix 6 (quantity): add parses_ti_round_trips unit test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`Normalized::from_managed` set `rw_size_gb: 0` because `SandboxConfig`
does not persist the scratch size after create. This caused `to_manifest()`
to emit `rootDisk: { size: "0" }`, which then fails to parse back through
`quantity::parse_gib` (no unit suffix). Re-read the real size from the
actual `rw.img` file length in `managed_normalized` so that `izba export`
always produces a valid, re-parseable manifest.

Test: `managed_normalized_recovers_rw_size_from_rw_img` seeds a 8 GiB
sparse rw.img, asserts `rw_size_gb == 8`, and verifies the exported YAML
round-trips through `Manifest::load_str` and `quantity::parse_gib`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…messages

Fix 1: `merge_manifest_into_opts` now mirrors the ports logic for volumes:
when the user passes no `--volume` flags, volumes declared in `izba.yml`
are serialised back into the `[NAME:]GUEST_PATH:SIZEg` flag strings the
parser expects and adopted into `opts.volumes`. Explicit flags always win.
Two tests cover the happy path (volumes adopted) and the guard (explicit
flag not overridden).

Fix 2/3: Two `bail!` strings in `promote.rs` had literal runs of spaces
from multi-line string indentation (`"...reset              on the new..."`).
Replaced with `\` line-continuation so words join with a single space.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ncation

A manifest volume with size < 1 GiB (e.g. 512Mi) was formatted as
`{n}g` with n = size_bytes >> 30 = 0, producing an invalid `0g` flag
that parse_size rejected. Now chooses GiB when the size is an exact
GiB multiple, MiB otherwise — so 512Mi → `512m` and 8Gi → `8g`.

Adds a TDD test covering both cases via parse_volume_flag round-trip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rough promote/diff/export

Add `build: Option<BuildSpec>` (serde default=None) to SandboxConfig so
write_managed records build provenance for Build-spec sandboxes. from_managed
now reconstructs ImageSource::Build from cfg.build instead of always emitting
ImageSource::Ref, eliminating the perpetual image-delta drift and the silent
clobbering of build: blocks during izba export.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix A: Stop is now only sent when the sandbox is_running; --restart on a
stopped sandbox simply resets scratch (if needed) and Starts without an
unconditional Stop that would error on a non-running daemon.  The
"changes apply on next start" warning is suppressed when --restart will
Start the sandbox.

Fix B: build_opts_from now returns anyhow::Result and propagates a
parse_mib failure on build.resources.memory with context instead of
silently falling back to 4096.  Three unit tests added: 4Gi→4096,
4GB→Err with context message, None→4096 default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…space (path traversal)

Two Greptile-flagged path-traversal vulnerabilities fixed:

1. Sandbox name from manifest metadata was never validated before being
   joined onto the sandboxes dir.  A hostile izba.yml with
   `metadata.name: "../../tmp/evil"` escaped ~/.local/share/izba/sandboxes/.
   Fix: call `crate::sandbox::validate_name(name)?` at the top of
   `managed_normalized`, `compute_diff`, and `export` in ops.rs.

2. Agent-controlled `context`/`dockerfile` fields in the build: spec were
   joined onto the workspace dir and read without any containment check.
   A manifest with `context: "../../../../"` + `dockerfile: "etc/shadow"`
   caused `izba diff` / `izba promote` to read arbitrary host files.
   Fix: add `pub fn ensure_within(base, candidate) -> Result<PathBuf>` that
   canonicalizes existing paths and rejects any `..` component for non-existent
   ones; apply it in `load_repo_manifest` (ops.rs) and `build_opts_from`
   (promote.rs).  Error messages deliberately omit the escaped absolute path.

Tests: 7 new tests (3 name-validation + 4 ensure_within/load_repo_manifest).
All 6 CI gates pass: core 678/cli 119/clippy clean/fmt clean/win-cross/app.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…p) for Windows CI

Replace PathBuf::from("/tmp") with tempfile::tempdir() + a real Dockerfile
so ensure_within's canonicalize() succeeds on Windows (where /tmp doesn't
exist), allowing each test to reach its intended assertion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dows)

The non-existent-path branch built a lexical PathBuf from candidate.components()
and compared it against canon_base (from base.canonicalize()). On Windows,
canonicalize() returns a verbatim extended path (\\?\C:\...) while the plain
candidate is C:\..., so starts_with always returned false and any non-existent
child (e.g. tmp/subdir/Dockerfile) was wrongly rejected as "escapes the workspace".

Fix: walk up to the longest existing ancestor, canonicalize that (giving a verbatim
path on Windows), verify it starts_with(canon_base), then re-append the non-existent
tail. Both sides of the comparison are now canonical so the check is consistent
across platforms. The ..  rejection is preserved as-is (dotdot test unchanged).

New test: ensure_within_accepts_nonexistent_nested_child (a/b/c.txt where a does
not exist) validates the ancestor-walk path.

Windows host-path audit: all other /data /cache /workspace assertions in manifest
and command tests are logical guest paths or flag-string values — no host-path
portability issues found.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…le image-pending note

Hoist validate_name before the first paths.sandbox_dir(&name) call so a
traversal name (e.g. "../../etc") is rejected before any path construction.
Add sentinel test validate_name_rejects_traversal.

Remove the dead image-change pending note in the \!restart branch (lines
251-257): the earlier `if p.image_changed && \!restart { bail\! }` guard
ensures image_changed is always false when reaching that branch, making
the println unreachable. cpus/mem pending-restart messaging is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add KVM-gated integration test `manifest_diff_promote_live_path` that
exercises the full manifest live-apply loop against a running sandbox:
boot with an initial enforcing policy, write izba.yml adding a second
egress host + a port relay, run `izba diff` (assert both deltas appear),
`izba promote` (no --restart), then assert the live policy reloaded and
the port relay is reachable via http_get.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e errors in export

reset_rw_scratch now writes to rw.img.tmp in the same directory, runs
set_len/mark_sparse/best_effort_mkfs on the tmp file, then atomically
renames it over rw.img. If any step before the rename fails the original
rw.img is left untouched and the temp file is cleaned up. Extended the
existing test to also assert no .tmp debris after a successful reset.

export.rs name-resolution now only falls back to the directory-basename
default when izba.yml does NOT exist; when the file exists but is malformed
the parse error is propagated (previously Err(_) swallowed both cases,
silently exporting the wrong sandbox on a broken manifest). Added two unit
tests: malformed-exists → Err, missing → no error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ix(cli): skip Dockerfile read during name resolution

Fix 1 (CLI): merge_manifest_into_opts now reads only the YAML without calling
load_repo_manifest, which previously read the Dockerfile for build: specs. This
prevents "reading Dockerfile: No such file" errors on healthy existing sandboxes
whose Dockerfile was deleted/cleaned — izba run . on a build-spec sandbox now
correctly reaches reconcile_existing without erroring on missing Dockerfile bytes.

Fix 2 (core): add rw_size_gb: u64 (#[serde(default)]) to SandboxConfig, set it
from opts.rw_size_gb at create time (sandbox.rs), and use it in managed_normalized
(ops.rs) as the primary source with rw.img file-length as a back-compat fallback
for pre-existing sandboxes. Sub-GiB fallback rounds up to 1 GiB so to_manifest()
never emits the un-parseable bare "0" in rootDisk.size. write_managed preserves
the existing rw_size_gb since it is immutable post-create.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The incremental mutation gate failed two ways on the manifest PR:

1. Real caught-nowhere survivors on the new code:
   - core (host-testable) gaps now covered by killing tests:
     - normalize::from_manifest build-spec arm (+ both-set guard)
     - diff::image_str ref/build rendering; egress_weakens git-rule path
       (new rule, read->read-write widening, tightening/identity negatives)
     - apply::plan removed-volume filter + image-as-restart-field
     - merge_manifest_into_opts mem / rw_size_gb adoption guards
   - CLI orchestration entry points (live daemon+VM, e2e-only via daemon_e2e)
     get #[mutants::skip] with a justification, matching the established
     run.rs/build.rs convention. The pure logic they compose stays under test.

2. The windows gate timed out: this PR's large changed-line set is ~196
   mutants, and each izba-core rebuild is ~4x slower on windows, pushing a
   single job past the 60-min cap. Shard the windows incremental gate 4 ways
   (mirrors the scheduled full run): cargo-mutants assigns mutant i to shard
   i%4 after the stable --no-shuffle order, spreading the slow core rebuilds
   round-robin. The aggregator already merges N dirs with the caught-nowhere
   rule, so the partition is invisible to the verdict.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first round skip/test pass missed survivors in files the timed-out
windows gate never reached; the now-sharded gate ran to completion and
the aggregator surfaced 7 caught-nowhere mutants. Add precise killing
tests:

- diff::egress_weakens git guard `&&` (95:43): an UNCHANGED Read git rule
  paired with an unrelated tightening (removed allow host) — isolates
  `from==Read && to==ReadWrite` (true && false) from a `||` that would
  wrongly fire.
- ops::managed_normalized rw.img guard `> 0` (139:41): a sub-GiB rw.img
  (len>>30 == 0) must round UP to 1 GiB, not pass 0 through.
- store::{read_base,read_review,clear_review} NotFound match guards
  (44/57/65): a directory at the path forces a non-NotFound I/O error
  that must propagate (not be swallowed as Ok(None)/Ok(())); plus
  clear_review idempotency on an absent token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The unsharded Linux gate hit its 45-min cap on this PR's changed-line set
(run 28436162656: "The operation was canceled" at exactly 45m), which made
the aggregator fail fast on linux=cancelled before ever reconciling — so the
mutation gate could never go green regardless of coverage.

Shard gate-linux 2x the same way gate-windows is sharded (i%n round-robin
after the stable --no-shuffle order); the aggregator already merges any number
of shard dirs per platform via the caught-nowhere rule, so the partition stays
invisible to the verdict. Run the exclude-pin guard only on shard 0. Aggregator
now downloads gate-linux-* by glob like gate-windows-*.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Lupus Lupus force-pushed the worktree-izba-manifest-diff-promote branch from 81e326e to 1375117 Compare July 1, 2026 11:07
@sonarqubecloud

sonarqubecloud Bot commented Jul 1, 2026

Copy link
Copy Markdown

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.

1 participant