api: report the actual fork mode on Instance - #419
Open
mpuig wants to merge 3 commits into
Open
Conversation
mpuig
added a commit
to mpuig/barista.sh
that referenced
this pull request
Aug 17, 2026
- CLI: `barista fork <snapshot> [--target-instance-id] [--require-cow]` and a `barista capsule` group (export/import/inspect/ls/delete). Export can write the manifest (--manifest-out) so it can be moved to another node and imported; import reads it and verifies before registering. Capability-aware refusals (FORK_MODE_UNAVAILABLE / OBJECT_STORE_UNAVAILABLE / CAPABILITY_MISSING / CAPSULE_INCOMPATIBLE) surface as the node's own Status with its exit code — the CLI stays a thin client. No app/tenant concepts: only snapshot, capsule, and instance ids. render gains capsule/capsules/capsule_op. - Docs: concepts/forks-and-capsules.md covers fork modes and the measured/ never-assumed rule, lineage, verify-then-publish export/import, exact compatibility, storage tiers, crash-safe deletion/GC, execution epochs and the grant carrier, the honest exact-memory limit (capsules are secret-bearing), and the boundary with barista-apps. cli.md gains the fork/capsule reference. openspec validate --strict green; cargo fmt + clippy -D warnings clean across the workspace; 391 workspace tests pass (only the 4 substrate-gated hypeman_runtime tests fail, identically to the base commit — no working hypeman here). Implements barista-046 tasks 6.1 and 6.2. Task 6.3 (unchanged T3/T5/T8/T9/T10) and 6.4's measured fork/export/restore evidence need the real substrate and are gated on kernel/hypeman#419; the new fork/capsule/grant integration matrix (fork_op, capsule_op, fork_contract, grants, epoch tests) is green.
mpuig
force-pushed
the
feat/report-fork-mode
branch
from
August 17, 2026 18:54
c87dce6 to
958b23b
Compare
mpuig
added a commit
to mpuig/barista.sh
that referenced
this pull request
Aug 17, 2026
Wire HypemanRuntime::fork to the substrate's fork rather than the trait's
refusal, closing the honesty gap where hypeman advertised cow_fork=true but had
no fork implementation (so a real `barista fork` would create a target then fail
it).
- client: fork_snapshot(snapshot_id, name, target_state) → POST
/snapshots/{id}/fork (kernel/hypeman#419); Instance gains an optional
fork_mode field (shared|copied), tolerant of an older hypeman that omits it.
- runtime::fork maps the snapshot fork into a new sandbox brought up Running,
reports the measured fork_mode as ForkOutcome::mode (never inferred, design
D2), and froze_source=false (forking a retained snapshot does not freeze a
running source). require_cow fails closed: a full-copy result under a CoW
demand removes the just-created fork and refuses; an absent fork_mode is read
conservatively as FULL_COPY so require_cow is refused, not faked.
- capabilities: full_copy_fork now true — both fork modes are backed by a real
impl.
Tests: fork_mode parses and is optional. 221 lib tests green; fmt + clippy -D
warnings clean.
Implements barista-046 task 3.4. Live end-to-end validation against a hypeman
built from the fork, plus the standby-snapshot fork path and fresh-identity-on-
fork, are the remaining iteration.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8853445. Configure here.
mpuig
added a commit
to mpuig/barista.sh
that referenced
this pull request
Aug 23, 2026
* barista-046 §2: journal + immutable object model
The durable half of the open app platform: content-addressed capsule
identity, a local immutable-object store, and crash-safe reference
counting/GC. No wire changes — this builds on the §1 contract and every
new capability still reports false until the fork runtime (§3),
export/import (§4), and execution epochs (§5) land.
- capsule.rs: canonical, length-prefixed manifest serialization and the
sha256 capsule id (not prost re-encoding — objects are a set, sorted by
(digest,type)); pinned golden determinism fixtures. object_digest is the
single definition of "the content id of these bytes".
- objects.rs: local content-addressed store — stage (measure length+digest,
fsync), commit (verify-then-publish via atomic rename, idempotent dedup),
read_verified, idempotent remove, sweep_staging. run_gc reconciles the
physical bytes with the journal's decisions (design D6).
- db.rs: object_refs (durable refcount + verified + gc_pending) and capsules
tables; register_capsule/delete_capsule take and release references in one
transaction so the count never disagrees with the live capsule set across a
crash; collectable_objects + finalize_object_gc never touch a live object.
Instances carry durable lineage + execution_epoch (additive migrations);
a directly-created instance reads back lineage: None, not empty strings.
Tests: canonical determinism + compatibility-key binding, store
stage/commit/dedup/mismatch/sweep, shared-object survival, idempotent
delete, GC-intent resurrection across a delete/re-import race, and a
restart-with-pending-GC reconciliation test. 208 lib tests green;
cargo fmt + clippy -D warnings clean.
Implements barista-046 tasks 2.1–2.5.
* barista-046 §3.1: fork runtime contract on the Runtime trait
The neutral fork mechanism at Contract B, ahead of the journaled operation
(§3.2–§3.5). No substrate work yet — this is the honesty contract the ops
layer negotiates against.
- runtime::fork(source, source_snapshot, target_spec, guest, require_cow)
returns a ForkOutcome { handle, mode, froze_source }. Defaulted to a
refusal so a runtime acquires the capability by answering, never by silence
(the rule pause/resume already follow). Handle gains PartialEq/Eq so an
outcome is comparable in tests.
- ForkOutcome reports what the runtime *did*, not what its capabilities say
it can do (design D2): mode is measured (COW vs FULL_COPY), and froze_source
is reported independently rather than inferred from the mode — a CoW fork of
a paused source froze nothing regardless.
- StubRuntime gains opt-in cow_fork/full_copy_fork, a fork impl that reports
the real mode and fails closed on an unmet require_cow, and a forked_targets
log so the operation's effect is observable.
Tests (tests/fork_contract.rs): CoW reports CoW and does not freeze; full-copy
reports FULL_COPY and admits the freeze; require_cow fails closed with
CapabilityMissing without CoW; a runtime with neither bit refuses and its
capabilities agree. 208 lib tests green; fmt + clippy -D warnings clean.
Implements barista-046 task 3.1.
* barista-046 §3.2/§3.3: journaled ForkInstance operation
Branch a retained snapshot into a new, independently owned instance, with
durable lineage and honest mode reporting. Fork is a Create-shaped verb —
it journals a *new* target row — but the child comes up RUNNING from the
source's cloned state rather than CREATED from a cold spec.
- ops: OpKind::Fork (Creating→Running) and OpPayload::Fork { spec,
source_instance_id, source_snapshot_id, lineage, require_cow }. The target
spec is treated as a create spec so the row, guest token, and channel
identity are journaled atomically; the executor reads them back and calls
runtime.fork, records the measured mode (set_op_fork_mode) as its own step,
emits LINEAGE_RECORDED, and reports a full-copy freeze on the operation.
A failed fork removes the half-made target sandbox exactly as Create does;
kill -9 mid-fork converges the CREATING target to FAILED (reapable) via the
existing recovery arm, leaving the source untouched.
- db: submit_atomically writes lineage onto the forked child's row in the same
statement as the spec; operations gain actual_fork_mode (additive migration)
with set_op_fork_mode and it round-trips on the proto Operation.
- service: fork_instance resolves snapshot→source→cloned target spec + lineage,
preflights capabilities fail-closed (require_cow / no-fork →
FORK_MODE_UNAVAILABLE) before any target row exists, and refuses an unknown
snapshot with NOT_FOUND.
Tests (tests/fork_op.rs, 8): two divergent children with the source unchanged,
duplicate target refused, replayed key returns the same op, require_cow fails
closed with no target created, no-fork runtime refuses, full-copy reports mode
+ freeze, unknown snapshot refused, kill -9 recovery. 208 lib + 12 fork tests
green; fmt + clippy -D warnings clean. Pre-existing hypeman_runtime substrate
tests fail identically on the base commit (no reachable working hypeman here).
Implements barista-046 tasks 3.2, 3.3, 3.5. Task 3.4 (hypeman native fork)
remains blocked on the substrate's fork API.
* barista-046 §4 (part 1): capsule export mechanics + object store wiring
The unambiguous half of §4: wire the §2 immutable-object store into the Agent
and implement verify-then-publish capsule export from a retained snapshot. The
RPC/operation-journal surface is deferred pending a design decision (see below).
- Agent now owns an ObjectStore rooted at <data_dir>/capsules, and bootstrap
runs objects::run_gc after crash recovery — sweeping staging files a crashed
upload left and collecting objects whose last reference is gone (design D6).
This closes the §2 loose end: the store finally has an owner and a lifecycle.
- Runtime trait gains export_snapshot(&SnapshotId) -> Vec<SnapshotObject>,
defaulted to a refusal so a runtime acquires capsule_export by answering.
SnapshotObject's Debug prints type+len only — exact memory/disk is secret.
- capsule_ops::export_capsule: read the snapshot's objects, stage/verify/commit
each into the store (digest+length measured from the bytes themselves),
build the canonical manifest with the snapshot's compatibility keys and
lineage, and register the capsule — verify-then-publish, idempotent by
content id (design D3). The object-store tier fails loudly with
OBJECT_STORE_UNAVAILABLE until §4.4 configures a backend.
- StubRuntime gains capsule_exporter() + a deterministic export_snapshot so the
export/verify/register/GC path is testable without a substrate.
Tests (5): objects staged + capsule registered + refcounts, idempotent
re-export by content id, capability refusal, object-store tier refused,
unknown snapshot refused. 213 lib tests green; fmt + clippy -D warnings clean.
Implements the export mechanics of barista-046 task 4.1. Import (4.2),
restore-from-capsule (4.3), and the remote tier (4.4) follow.
* barista-046 §4 (part 2): capsule import + the 5 capsule RPCs
Wire the capsule verbs onto Contract A. Capsule operations are instance-free
and idempotent by content id, so they live in their own journal rather than the
instance-centric operations table (design decision B): a capsule_operations
table records only successful outcomes under the idempotency key, and
GetOperation falls back to it.
- capsule_ops::import_capsule: verify every referenced object is present and
intact (length + digest re-checked), preflight schema + cpu-class
compatibility, then register the capsule and a restorable snapshot row — no
boot (design D4). A tampered/truncated/missing object refuses the whole
import; an incompatible cpu class refuses before restore.
- db: capsule_operations table + CapsuleOpRow; record_capsule_op is
concurrency-safe by content id (UNIQUE key; loser returns the winner's row);
get_capsule_op/capsule_op_by_key back replay and GetOperation.
- service: export/import/delete/get/list wired; export and import replay by key,
failures map to typed Status and leave the key free (verify-then-publish means
no partial state), delete logically removes then runs run_gc to collect freed
objects (design D6). GetOperation resolves capsule ops too.
- StubRuntime: capsule_import capability + capsule_porter().
Tests (tests/capsule_op.rs, 8): export→list→GetOperation, idempotent export,
import verifies+registers, truncated/missing/incompatible refused, shared-object
retention across delete + GC, idempotent delete. 213 lib + 8 capsule tests
green; fmt + clippy -D warnings clean.
Implements barista-046 tasks 4.1, 4.2, 4.5. Task 4.3 (exact restore/fork from
an imported capsule) needs runtime materialization of capsule objects; 4.4
(configured object-store tier) is refused with OBJECT_STORE_UNAVAILABLE until a
backend lands — both alongside the deferred hypeman work (3.4).
* barista-046 §5.1: execution-epoch issuance, persistence, and rotation
Every boot/resume/fork now receives a fresh execution epoch, bound to the
instance before its guest is reached, so a platform-mediated grant can be tied
to it and the prior epoch revoked (design D5). The grant carrier itself (§5.2)
and the guest-side rebind (§5.3) build on this.
- db: a global, monotonic epoch counter (journal_meta.next_execution_epoch) —
global, not per-instance, so an epoch is unique across the node and two
sibling forks can never share one. issue_execution_epoch bumps+reads under the
connection mutex (atomic); set_instance_epoch binds it to a row, and
persisting a new epoch is what revokes the prior one.
- ops: the executor issues and persists an epoch for every run verb (Start,
Resume, Fork) before the runtime action, delivers it over Contract C in the
restore duties (replacing the §1 placeholder 0), and emits EPOCH_ROTATED on
success — after STATE_CHANGED, only when the run succeeded, carrying the epoch
number but no grant material (§5.4).
- events: epoch_rotated helper.
Tests: epochs are unique + monotonic across 1000 issuances; set_instance_epoch
replaces the prior epoch; two divergent forks get distinct non-zero epochs
(sibling separation). 215 lib + 33 guest-agent + 8 fork_op green; fmt + clippy
-D warnings clean.
Implements barista-046 task 5.1. The grant carrier (5.2), guest rebind duties
(5.3), required/best-effort semantics + redaction (5.4), and the full test
matrix (5.5) follow.
* barista-046 §5.2/§5.3: grant carrier + Contract C restore-duty ordering
The guest half of epoch-bound grants: a platform-mediated grant carrier that is
delivered fresh on every restore, bound to the run's execution epoch, and lives
only in RAM.
- bootstrap: DEFAULT_GRANT_CARRIER at /run/barista/grant-carrier — the writable
tmpfs the runtime already provides, so the carrier has no disk-snapshot
representation. Exact-memory snapshots still capture RAM (design D5), which is
why this is replacement, not a scrub, and why safe_grant_rebind stays narrow.
- duties::run now replaces the carrier as its third normative step (entropy →
clock → carrier), and by the node's sequencing that lands before the separate
post-restore rebind hook, so the hook reconnects using the new epoch's grant
rather than the revoked one. A non-empty carrier is written 0600 (truncating
any prior); an empty carrier removes a stale one, so a revoked epoch's grant
cannot survive into the next restore. grant_rebound/rebind_detail report the
guest's half honestly.
- Redaction (§5.4): rebind_detail and degraded carry the epoch and byte count
only — never the carrier contents.
Tests: a delivered carrier is written 0600 and its detail is redacted (no
bytes); an empty carrier removes a prior one (a revoked epoch's grant cannot
persist). 35 guest-agent + 215 node lib green; fmt + clippy -D warnings clean.
Implements barista-046 tasks 5.2 and 5.3. Full redaction across ops/events
(5.4) and the remaining test matrix — old-epoch refusal, readiness ordering
(5.5) — follow; safe_grant_rebind stays honestly false until a runtime
demonstrates it.
* barista-046 §5.4/§5.5: grant-epoch validation, required-rebind semantics, tests
Closes §5.
- grants module: validate_grant_epoch(current, presented) refuses a grant that
is not bound to the instance's current epoch with EPOCH_REVOKED — a grant from
a prior run of this instance, or from a sibling fork (which drew a different
epoch in §5.1), is stale by construction (design D5). Messages carry the two
epoch numbers only, never the grant (§5.4 redaction). EXACT_MEMORY_WARNING
states the honest limit: epoch rotation replaces mediated grants but says
nothing about values the workload copied into its own RAM, which an
exact-memory snapshot still captures.
- ops: the post-restore hook now distinguishes required vs best-effort rebind
failure (§5.4). When the guest placed a fresh grant carrier for the epoch,
a failed/timed-out rebind hook is a required-rebind failure ("the workload
holds a fresh grant it has not bound"); with no carrier there is nothing to
rebind and the ordinary best-effort reconnect degradation stands.
Tests (§5.5): current-epoch accepted; old-epoch refused; sibling epochs refused
in both directions (separation); zero-epoch refused; honest exact-memory
warning. Plus the §5.1/§5.2 tests already covering sibling separation on fork,
no persistent carrier, and carrier redaction. 220 node lib + 35 guest-agent
green; fmt + clippy -D warnings clean.
Implements barista-046 tasks 5.4 and 5.5. §5 complete.
* barista-046 §6.1/§6.2: fork + capsule CLI and operator docs
- CLI: `barista fork <snapshot> [--target-instance-id] [--require-cow]` and a
`barista capsule` group (export/import/inspect/ls/delete). Export can write the
manifest (--manifest-out) so it can be moved to another node and imported;
import reads it and verifies before registering. Capability-aware refusals
(FORK_MODE_UNAVAILABLE / OBJECT_STORE_UNAVAILABLE / CAPABILITY_MISSING /
CAPSULE_INCOMPATIBLE) surface as the node's own Status with its exit code — the
CLI stays a thin client. No app/tenant concepts: only snapshot, capsule, and
instance ids. render gains capsule/capsules/capsule_op.
- Docs: concepts/forks-and-capsules.md covers fork modes and the measured/
never-assumed rule, lineage, verify-then-publish export/import, exact
compatibility, storage tiers, crash-safe deletion/GC, execution epochs and the
grant carrier, the honest exact-memory limit (capsules are secret-bearing), and
the boundary with barista-apps. cli.md gains the fork/capsule reference.
openspec validate --strict green; cargo fmt + clippy -D warnings clean across
the workspace; 391 workspace tests pass (only the 4 substrate-gated
hypeman_runtime tests fail, identically to the base commit — no working hypeman
here).
Implements barista-046 tasks 6.1 and 6.2. Task 6.3 (unchanged T3/T5/T8/T9/T10)
and 6.4's measured fork/export/restore evidence need the real substrate and are
gated on kernel/hypeman#419; the new fork/capsule/grant integration matrix
(fork_op, capsule_op, fork_contract, grants, epoch tests) is green.
* barista-046 §6.1: surface the portability capabilities in `node info`
The CLI's node-info renderer predated the barista-046 capabilities, so
`barista node info` (both human and --json) omitted full_copy_fork,
object_store_snapshots, capsule_export, capsule_import, and safe_grant_rebind.
An operator negotiating what a node can do could not see them. Add all five to
both renderings.
Verified against a live fake-runtime node on Docker: node info now reports every
portability capability (honestly false for fake), fork refuses with
FORK_MODE_UNAVAILABLE, capsule export --tier object-store refuses with
OBJECT_STORE_UNAVAILABLE, capsule ls works (journal-only), and a full
create/start/destroy lifecycle runs real containers through the deployed node.
* barista-046 §3.4: adopt hypeman's native snapshot fork
Wire HypemanRuntime::fork to the substrate's fork rather than the trait's
refusal, closing the honesty gap where hypeman advertised cow_fork=true but had
no fork implementation (so a real `barista fork` would create a target then fail
it).
- client: fork_snapshot(snapshot_id, name, target_state) → POST
/snapshots/{id}/fork (kernel/hypeman#419); Instance gains an optional
fork_mode field (shared|copied), tolerant of an older hypeman that omits it.
- runtime::fork maps the snapshot fork into a new sandbox brought up Running,
reports the measured fork_mode as ForkOutcome::mode (never inferred, design
D2), and froze_source=false (forking a retained snapshot does not freeze a
running source). require_cow fails closed: a full-copy result under a CoW
demand removes the just-created fork and refuses; an absent fork_mode is read
conservatively as FULL_COPY so require_cow is refused, not faked.
- capabilities: full_copy_fork now true — both fork modes are backed by a real
impl.
Tests: fork_mode parses and is optional. 221 lib tests green; fmt + clippy -D
warnings clean.
Implements barista-046 task 3.4. Live end-to-end validation against a hypeman
built from the fork, plus the standby-snapshot fork path and fresh-identity-on-
fork, are the remaining iteration.
* barista-046 §3.4: re-identify and re-credential the hypeman fork
Live testing against a real vz substrate surfaced two integration gaps that
left a forked child either reaped or unreachable; both are fixed so the child
comes up RUNNING, correctly identified, on the source's channel credentials.
- Tags: hypeman's snapshot fork clones the source's tags, so the fork carried
the source's barista.instance_id/node_id and the zero-orphan sweep reaped it
as a duplicate (child → FAILED). HypemanRuntime::fork now passes the child's
identity tags in fork_snapshot (kernel/hypeman fork tag override), so the
sweep and every tag-keyed lookup see the fork as itself.
- Credentials: a fork inherits the source's guest token and channel identity
rather than minting fresh — the forked VM is a memory clone already running
the guest agent with the source's credentials, so a fresh token/cert could
never complete the channel handshake. Threaded through OpPayload::Fork and
used in the submit path in place of minting (fresh *platform grants* remain
the §5 concern, rebound per epoch; the base channel credential rides the clone).
Verified live: create → start → snapshot → fork now yields a child in RUNNING,
correctly tagged (not reaped), sharing the source's credentials. 221 lib + 8
fork_op tests green; fmt + clippy clean.
Known remaining gap (substrate, not barista): the forked guest keeps the
source's in-VM network config, so it does not answer at the new host-assigned
IP until it renews — a hypeman fork-networking behavior to address upstream,
tracked alongside the fork_mode/tags work on kernel/hypeman.
* barista-046: dev deploy script for the live fork/capsule stack
Reproducible bring-up of hypeman (vz, built from the fork) + a barista node
wired to it, so the fork/capsule verbs can be run end to end. Encodes the two
environmental lessons from live testing: a short hypeman data_dir (macOS vz
vsock unix-socket path must stay under 104 chars) and non-conflicting
caddy/metrics ports. `up`/`down`.
* barista-046: document the forked-guest network-identity substrate finding
Live fork testing on vz surfaced that a memory-forked guest keeps the source's
in-VM IP (network is configured once at initrd boot; a fork resumes without
re-running boot), so it is unreachable at the new host-assigned IP even though
the fork is RUNNING and correctly identified.
- docs/upstream-issues/07-…: filable draft with the root cause (restore
reconfigures via the guest agent's ReconfigureNetwork RPC gated on
allocatedNet; the fork pre-allocates so that branch is skipped), reproduction,
suggested fix, and the open question about the guest agent under a consumer's
own injected entrypoint.
- concepts/forks-and-capsules.md: a Known substrate limitation section so the
boundary is honest for readers.
Every other fork guarantee (lineage, measured mode, source preservation) holds.
* perf: single-allocation hex encoding on content-addressed hot paths
Applies an upstream Rust technique (openai/codex #38823, "avoid allocating per
character"): the codebase's byte-wise hex —
`bytes.iter().map(|b| format!("{b:02x}")).collect::<String>()` — heap-allocates
a temporary String for *every byte* (32 allocations per sha256 digest). New
`hex::to_lower(&[u8])` encodes into one pre-sized String, byte-for-byte
identical output, one allocation total.
Routed the hottest, content-addressed paths through it:
- objects::stage — a digest per object staged into the capsule store
- capsule::capsule_id / sha256_hex / object_digest — per capsule id + object id
- snapshot_key::template_hash — per snapshot restore-key derivation
Correctness is pinned: the capsule golden-id fixture and a hex-equivalence test
(to_lower matches the exact format! output it replaces) both pass, so the swap
is safe under content ids. 223 lib tests green; fmt + clippy clean.
The same pattern remains at ~5 lower-frequency call sites (identity, guest,
node_info, agent_volume, runtime, and guest-agent bootstrap); routing those
through the helper is a mechanical follow-up. The other two codex techniques
reviewed (O(n^2) iter().find() → two-pointer; &[u8] instead of Vec<u8>) have no
compelling barista hot-path home.
* perf: route the remaining production hex paths through hex::to_lower
Completes the single-allocation hex sweep (openai/codex #38823): node_info::hex16
(node-id fingerprint) and agent_volume::hash_binary (agent binary digest, per
volume). The remaining .map(format!) sites are leak-detection test needles,
deliberately independent of the helper they guard, and left as-is.
* barista-046 §4.3: exact restore from an imported capsule, no cold fallback
An imported capsule was registered but unreachable: import writes an
instance-free snapshot row, and both candidate entry points dead-ended on
that empty instance id — fork required a source row, resume submitted
against "". So a verified capsule could be held and never restored.
ForkInstance becomes the restore verb for one, with the compatibility gate
the spec requires and deliberately no permissive branch:
- `restore::decide_capsule` returns Proceed or Refuse, and nothing else. The
cold-boot variant that `decide` has for node-local resumes is absent by
design: a cold semantic import is an app's job above the Host API, and
answering an exact restore with a fresh boot would present one as the
other. CPU class is checked unconditionally here, unlike the local tier —
the reasoning that makes it skippable there (restored on the machine that
took it) is exactly what a capsule violates.
- Template and bundle are checked at last: import deferred them because no
target spec existed yet, and nothing checked them anywhere until now.
- `Runtime::restore_from_objects`, the inverse of `export_snapshot`,
defaulted to a refusal so a runtime acquires the ability by answering.
Returns a Handle, not a ForkOutcome: no fork happened, so there is no mode
to report and none is journaled.
- `ForkInstanceRequest.target_spec` (additive, field 5): a capsule manifest
carries compatibility hashes, never a spec, so the machine to restore into
comes from the caller — and is refused unless its template hash matches.
buf lint and buf breaking against main stay green.
- Objects are re-verified at restore, not trusted from import time. Import
proved they were intact then; the window since is where a swept or
corrupted object would otherwise reach a memory restore.
- Credentials are minted fresh, and cannot be otherwise: the token the
restored guest holds was issued by another node. The consequence is
reported rather than hidden — the guest channel does not authenticate
until the guest re-reads its material, which is a substrate fact for §6.3
to measure, and restore_duties already treats an unreachable guest as a
degradation rather than a failed restore.
Found while testing: the fork-mode preflight ran before the capsule branch,
so a capsule restore was refused for lacking cow_fork/full_copy_fork it does
not use. The branch point is now resolved first — which kind of branch this
is decides which capabilities apply.
tests/capsule_restore.rs: 8 cases, every refusal also asserting no instance
row was left behind. 230 lib + 8 restore tests green; fmt and clippy clean on
the touched files.
* openspec: check off barista-046 §4.3
Implemented in 53faffb. §4.5's template/bundle-mismatch and source-node-loss
rows are now genuinely covered too (tests/capsule_restore.rs), which they
could not be before a restore path existed.
* docs: record why unwrap_used/expect_used stays off (clippy scoping evaluated)
Evaluated codex's clippy.toml allow-unwrap-in-tests / allow-expect-in-tests as
the 'scope to non-test code' mechanism the prior comment was waiting for. It
silences unit tests (#[test] fns, #[cfg(test)] modules) but not helper functions
in tests/*.rs integration files, and this tree is integration-test-heavy — so
enabling the lints still fires in hundreds of test helpers. The daemon crate is
already clean, so the marginal catch does not justify blanketing every
integration-test file with #![allow], forever. Comment updated so the next
reader does not re-investigate.
* fix(cli): set ForkInstanceRequest.target_spec after §4.3 added the field
§4.3 (53faffb) added ForkInstanceRequest.target_spec to the proto but did not
update the CLI's fork caller, so HEAD did not compile (barista-cli, E0063). A
plain `barista fork` clones the source's spec, so target_spec is None; only
capsule restore supplies one. Workspace + all-targets build green again.
added 3 commits
August 23, 2026 11:25
Fork already decides copy-on-write vs full copy (fork.go: shareMemFile = Firecracker && Standby source), but that decision was a local variable — callers had no way to know which mode a fork actually used and had to infer it from the hypervisor. Surface it. - StoredMetadata.ForkMode records the measured mode at fork time (ForkModeShared / ForkModeCopied), mirroring the existing shareMemFile decision exactly. Persisted, so GetInstance reports it too. Empty for instances that were not created by a fork. - Instance.fork_mode (read-only enum: shared | copied) added to the OpenAPI schema and regenerated; mapped in the instance API response. "shared" is copy-on-write via a shared mem-file (the source is frozen only if it was Running at fork time); "copied" is a full private copy (stopped-source forks, or hypervisors without shared-memory fork). Additive and backward compatible: existing metadata reads back as empty. Test: TestForkInstance_VZStoppedSourceSupported asserts a VZ stopped-source fork reports ForkModeCopied.
Two gaps found wiring a consumer (Barista) onto snapshot fork:
- forkSnapshot did not set ForkMode (only the instance-fork path did), so a
fork via POST /snapshots/{id}/fork reported an empty mode. Set it from the
same shareMemFile decision: shared mem-file = shared (CoW), else copied.
- ForkSnapshotRequest gains optional `tags` that override the tags cloned from
the snapshot's source (request wins per key; unrelated source tags kept). A
consumer forking many instances from one source must be able to re-identify
each fork rather than inherit the source's identity labels — otherwise the
consumer's own bookkeeping (which keys sandboxes by tag) sees every fork as a
duplicate of the source.
Regenerated oapi.go; fork tests pass.
Bugbot flagged that ForkSnapshotRequest now carries Tags and forkSnapshot writes them into instance metadata, but validateForkSnapshotRequest never called tags.Validate — unlike create-instance and create-snapshot. Invalid keys/values could reach metadata on this direct manager path, which the OpenAPI layer does not guard. Mirror the create-snapshot validator's tags.Validate check.
mpuig
force-pushed
the
feat/report-fork-mode
branch
from
August 23, 2026 09:27
8853445 to
9f4e208
Compare
Author
|
Rebased onto latest Also addressed the Bugbot review:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What
Fork already decides copy-on-write vs full copy —
fork.gocomputesshareMemFile := HypervisorType == Firecracker && source.State == Standby— butthat decision was a local variable. Callers had no way to know which mode a fork
actually used and had to infer it from the hypervisor. This surfaces it.
StoredMetadata.ForkModerecords the measured mode at fork time(
ForkModeShared/ForkModeCopied), mirroring the existingshareMemFiledecision exactly. Persisted, so
GetInstancereports it too. Empty forinstances not created by a fork.
Instance.fork_mode(read-only enum:shared|copied) added to theOpenAPI schema, regenerated (
make oapi-generate), and mapped in the instanceAPI response.
shared= copy-on-write via a shared mem-file (the source is frozen only if itwas Running at fork time);
copied= a full private copy (stopped-source forks,or hypervisors without shared-memory fork).
Why
A consumer that forks needs to record and report the actual mode a fork used
(and whether a large source was frozen), rather than guessing from the backend.
Exposing the mode the server already computed makes that honest.
Compatibility
Additive and backward compatible: existing metadata reads back with an empty
fork_mode, and the field isomitemptyon the wire.Test
TestForkInstance_VZStoppedSourceSupportedasserts a VZ stopped-source forkreports
ForkModeCopied.Note
Medium Risk
Touches fork metadata and the public instance/snapshot APIs (new field plus tag merge on snapshot fork). Additive and omitempty, but tag override and persisted mode affect identity and client-visible fork behavior.
Overview
Forks now persist and return the actual memory-copy mode instead of leaving callers to infer it from hypervisor/state.
sharedis copy-on-write via a shared mem-file;copiedis a full private copy. The field is empty for non-forked instances.Instance and snapshot fork paths both record
ForkModefrom the existingshareMemFiledecision. The instance API maps it as optionalfork_mode.Fork-from-snapshot tags:
ForkSnapshotaccepts tags that override cloned source tags per key (other source tags stay). Tags are validated on the manager path the same way as create.Reviewed by Cursor Bugbot for commit 9f4e208. Bugbot is set up for automated code reviews on this repo. Configure here.