8000 feat(minimald): record VM refusals on the host and admit RPC only from the host (T107) by minimal-foundry[bot] · Pull Request #2721 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

feat(minimald): record VM refusals on the host and admit RPC only from the host (T107) - #2721

Merged
norrietaylor merged 13 commits into
mainfrom
agent/2694-keep-the-vm-s-security-records-on-the-host-and-a
Oct 28, 2026
Merged

norrietaylor merged 13 commits into
mainfrom
agent/2694-keep-the-vm-s-security-records-on-the-host-and-a

Conversation

@minimal-foundry
@minimal-foundry minimal-foundry Bot commented Oct 28, 2026 •
Copy link
Copy Markdown
Contributor

T107 — Keep the VM's security records on the host and admit RPC only from the host

slice: S16
kind: task
covers: NET-139, NET-046, NET-157
size: L
est: 340 code, 280 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: the diagnostics bundle's VM host daemon audit log carries each expose decision and each refused own-address egress the guest reported, and the in-VM daemon log carries the audited line for each refused vsock peer
observability: the VM host daemon appends one audit line per reported expose decision and one rate-limited line per refused own-address egress, naming the box, the destination and the rule; the in-VM daemon logs one rate-limited audited line per refused vsock peer, naming its CID
layers:
- rpc:
- crates/minimald/src/server.rs: the vsock listener admits a peer only at CID 2, deciding at accept before any request byte is read, and refuses every other CID with one rate-limited audited line naming it
- crates/minimald/src/server/tests.rs: the refusal test for a peer at another CID
test: cargo nextest run -p minimald vsock_rpc_refuses_non_host_cid
- guest-reports:
- crates/minimald-rpc/src/lib.rs: the guest door gains a report for an expose decision and one for a refused egress
- crates/minimald/src/net/listeners.rs: the reporter sends both new reports over the guest door
- crates/minimald/src/audit.rs: each recorded decision is also reported to the host, and the local record still lands when the host is unreachable
- crates/minimald/src/net/switch.rs: a dropped own-address egress frame is reported to the host under the same rate limit as its drop line
- crates/minimald/src/box_runtime/tests.rs: the audit test asserts the host report beside the local record
test: cargo nextest run -p minimald expose_unenrolled_decision_audited
- host-log:
- crates/minvmd/src/control.rs: the guest door appends each reported expose decision and each refused egress to the host-side audit log, one record per refused flow and never per frame, rate-capped per box with a line counting the records suppressed, so a hostile box cannot fill the host's disk
test: cargo nextest run -p minvmd egress_refusal_logged_on_host
test: cargo nextest run -p minvmd expose_decision_recorded_on_host
produces: none
consumes: none
proof: After a box's disallowed connection and a denied min net expose, both are recorded in the audit log on the host, outside the VM.

Proof

On a VM-backed host, a box's refused egress flow and a denied min net expose each produce one record in the VM host daemon's audit log (audit/box-admissions.log beside its sockets, outside the VM), with event egress_refused or expose_decision and the box named from the host's own row. The in-VM daemon's vsock RPC listener now drops any peer that is not the host (CID 2) at accept, and writes one rate-limited audited line that names the CID.

Notes from the agent: Outside the layers, two files: crates/minimald/src/rpc.rs and crates/minimal-client/src/box_registration.rs. Each has an exhaustive match on BoxControlRequest or BoxControlReply. The new verbs (ReportExposeDecision, ReportRefusedEgress) and the new reply (Audited) do not compile without one arm in each. The excerpt in spec.md has no text for NET-157. I read it from the task's own lines on own-address egress. Design: the in-VM daemon finds the host door through a route table in listeners.rs. The table is keyed by the box's switch address and holds an entry only while that box's listen watcher runs on a vsock (VM) control channel. A box with no watcher, or on a native host, reports nothing to the host. Its local record is not changed. Reports go out in the background, best-effort, and at most 64 at a time. The expose report carries only closed enums and the port, never the refusal's reason text, and the host names the box from its row. A refused-egress report goes only when the relay's rate-limited drop line fires. The host clamps its proto and rule text to 64 printable ASCII bytes. Host cap: 30 records per box per 60 s window. A repeat of the same flow inside a window writes nothing. Over the cap, records are counted, and an audit_suppressed line with the count is written before the first record of the next window. If a box goes quiet after a flood, that count line does not appear until the box's next report. The harness runs boxes on a native-shaped switch, so expose_unenrolled_decision_audited…[truncated]

Tests

line exit tail
cargo nextest run -p minimald vsock_rpc_refuses_non_host_cid 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-55bb... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald expose_unenrolled_decision_audited 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-f637... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minvmd egress_refusal_logged_on_host 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-9370... Compiling ot v0.0.1 (/workbench/crates/ot) Compiling args v0.0.1 (/workbench/crates/args) Compiling version v0.8.0 (/workbench/crates/version) Compiling minvmd v0.8.0 (/workb
cargo nextest run -p minvmd expose_decision_recorded_on_host 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-f649... Compiling version v0.8.0 (/workbench/crates/version) Compiling mlog v0.0.1 (/workbench/crates/mlog) Compiling minvmd v0.8.0 (/workbench/crates/minvmd) Finished 'test' profil

Verify

min task run agent-verify passed ([tasks.verify]).

Base

The base branch moved while the coder worked and was merged in before the tests ran. These files changed on BOTH sides since the branch point — read them first, because a rewrite from a stale reading can undo a merge without a conflict:

  • crates/minimald/src/server.rs
  • crates/minimald/src/server/tests.rs
  • crates/minvmd/src/control.rs

Out of scope

These files are outside the task's layers:, so this PR is a draft:

  • crates/minimal-client/src/box_registration.rs
  • crates/minimald/src/rpc.rs

Layer exception

crates/minimald/src/rpc.rs and crates/minimal-client/src/box_registration.rs are outside T107's layers:. Each holds an exhaustive match on BoxControlRequest or BoxControlReply, so the new verbs ReportExposeDecision and ReportRefusedEgress and the new reply Audited need one arm in each to compile. No behaviour in either file changes beyond those arms.

scripts/session-e2e.sh is also outside the layers. This PR adds a host audit record (minvmd::control, "recorded the box's refused egress in the host audit log") for a refusal the box's relay makes. Two e2e groups ("a box without a credentialed lane cannot reach the proxy's address" and "the Box Egress Proxy sees each box by its own address") checked that no line naming egress-uncredentialed-proxy-destination reached the host, so the new record tripped them. Rounds 3 and 5 narrowed both checks to the host gate's own drop line and assert the new audit record is present. The /revise notes on this PR asked for that change.

Closes #2694


Generated by Claude Code

The in-VM daemon's vsock listener now drops a peer at any CID but 2 at
accept, before reading a byte, and writes one rate-limited audited line
naming the CID (NET-139).
The in-VM daemon reports each expose decision and each refused
own-address egress flow over the guest door. The VM host daemon appends
them to its audit log, one record per flow, capped per box with a line
that counts the suppressed records (NET-046).
@coderabbitai
coderabbitai Bot commented Oct 28, 2026 •
Copy link
Copy Markdown
Contributor

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ec0fbe86-fd14-4212-a12b-cbec22673f6b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@minimal-foundry
minimal-foundry Bot commented Oct 28, 2026 •
Copy link
Copy Markdown
Contributor Author

Approved: no findings at 47b8fa1e0cea.

Nothing to do.

Verdict APPROVE, posted as a comment: the App cannot review its own pull request; the check and the label carry it.

Clobber: none seen in the changed hunks.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — advisory review

Verdict COMMENT — No findings.

Panel: 2 of 3 personas ran (security, supply-chain); did not complete: correctness.

@minimal-foundry minimal-foundry Bot added the review:approved minimal-review approved the current head label Oct 28, 2026
Copy link
Copy Markdown
Member

/revise Fix the test-kvm failure: the session e2e still asserts the old T84 rule, which this PR's NET-046 change replaces.

  • Failing line (test-kvm on 855704e5): ##[error]the VM host daemon's host audit carries a line for e2e-inside-deny — a deny decided in the box never crosses the report door, so the host must record nothing (T84). The host audit holds {"event":"expose_decision","box":"e2e-inside-deny",...,"decision":"deny","decided_by":"box-policy","outcome":"refused"}.
  • That line is what this PR is for: every expose decision now reaches the host through report_to_host (crates/minimald/src/audit.rs lines 163 and 203).
  • In scripts/session-e2e.sh (the T84 check, near line 22358), stop asserting that the host audit has no line for $eib_deny_name. Assert that it has exactly one expose_decision line for that box with "decision":"deny", "decided_by":"box-policy" and "outcome":"refused". Update the "the deny recorded nothing" pass line to say what is now checked. Keep the T94 runtime [] check.
  • If the spec text near NET-046 quotes the old rule, change it to match.
  • Change nothing else. The macOS lane failed only because await-kvm reported the KVM lane red.

Generated by Claude Code

The VM lane asserted that a deny decided in the box left no host audit
line (T84). NET-046 now reports every expose decision to the host, so
assert exactly one expose_decision line with decision deny, decided_by
box-policy and outcome refused. The T94 empty runtime check stays.
@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — approved

Verdict APPROVE — No findings.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 1 of 10 on a /revise note, pushed as 7c121bbcb7b4.

line exit tail
cargo nextest run -p minimald vsock_rpc_refuses_non_host_cid 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-25a2... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald expose_unenrolled_decision_audited 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-592c... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minvmd egress_refusal_logged_on_host 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-e33c... Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling version v0.8.0 (/workbench/crates/version) Compiling minvmd v0.8.0 (/workb
cargo nextest run -p minvmd expose_decision_recorded_on_host 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-d4a1... Compiling version v0.8.0 (/workbench/crates/version) Compiling mlog v0.0.1 (/workbench/crates/mlog) Compiling minvmd v0.8.0 (/workbench/crates/minvmd) Finished 'test' profil

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimal-client/src/box_registration.rs
  • crates/minimald/src/rpc.rs
  • scripts/session-e2e.sh

Converted to a draft: something above needs a person before review.

After a box's disallowed connection and a denied min net expose, both are recorded in the audit log on the host, outside the VM. On the VM lane the session e2e now checks this for the deny box: the host audit holds exactly one expose_decision line with decision deny, decided_by box-policy and outcome refused.

Notes from the agent: Revise round 1. Only scripts/session-e2e.sh changed. The VM-lane T84 check that wanted no host audit line for e2e-inside-deny is replaced. The new check polls up to 5 s, then wants exactly one expose_decision line for that box with "decision":"deny", "decided_by":"box-policy" and "outcome":"refused". The T94 runtime [] check is kept. I updated the pass line, the VM-lane summary line, and two comments that stated the old rule. The native-lane line near 21907 is a different leg, so I did not change it. The spec excerpt near NET-046 does not quote the old rule. Per my instructions I did not open the full spec document, so a person can grep it for 'never crosses' if they want to be sure. I could not run the session e2e here because it needs a KVM host. The test-kvm lane owes that evidence.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft October 28, 2026 10:15
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 3 of 10 on a failed check suite: the coder changed nothing. Revise round 3. I made no change because none of the failed checks points to a defect in this branch. (1) cargo-deny: the advisories check fails on a RUSTSEC advisory in a dependency, and on a toolchain that the container does not have. This branch does not change Cargo.lock, deny.toml or rust-toolchain.toml, which match origin/main. So the failure comes from main and is outside the task's layers. ci-success fails only because of this job. (2) test-kvm: the excerpt shows only the echo lines of the workflow script and the post-job cleanup. It does not show the step or the assertion that failed. This box has no /dev/kvm, so I cannot run just test-vm or just e2e. I read the paths that the excerpts can implicate. The vsock refusal admits CID 2, and libkrun sends host-to-guest vsock traffic from CID 2. No guest-side code connects to the RPC listener. The deny box in the e2e uses --network own_ip, so its launch gets a listen plan and registers the host audit route. The host w…[truncated]

Copy link
Copy Markdown
Member

/revise Fix the e2e failure on test-kvm at 7c121bb (job 122750210460). Change nothing else.

  • Failing check: group "a box without a credentialed lane cannot reach the proxy's address (NET-134)" in scripts/session-e2e.sh (line ~16882): ::error::the minvmd gate logged a proxy-lane drop for the no-lane box.
  • The line it matched is the record this PR adds, not a gate drop: {"fields":{"box":"e2e-cred-nolane","destination":"100.64.255.252:8118","message":"recorded the box's refused egress in the host audit log","proto":"tcp","rule":"egress-uncredentialed-proxy-destination"},"level":"INFO","target":"minvmd::control"}. The relay refused the frame in the guest and reported it, as this PR intends.
  • Fix: narrow the cred_case_log | grep absence check to the host gate's own drop line, so the host audit record that minvmd::control writes for a relay refusal does not match. Then assert that record is present for e2e-cred-nolane with the rule egress-uncredentialed-proxy-destination, because it proves the relay refused and reported the frame. Update the comment above the check to say the host now sees the relay's audit record and never the gate's drop line.
  • Run bash -n scripts/session-e2e.sh and shellcheck on it. The "hostname proxy port 7654 held by pid … (python3.12)" line in the diagnostics is the passing "two named VMs" fixture; ignore it.
  • cargo-deny fails on main (RUSTSEC); leave it.

Generated by Claude Code

The no-lane check matched the host audit record that minvmd::control writes
for a relay refusal. Match only the gate's rule_matched field, and assert the
relay's refused-egress record for e2e-cred-nolane is on the host.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Revise round 3 ran against ci-success, cargo-deny and left it red; a coder round cannot clear it, a person should look. No round was started for the failed suite on 1b8dba923ac7.

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 3 of 10 on a /revise note, pushed as 1b8dba923ac7.

line exit tail
cargo nextest run -p minimald vsock_rpc_refuses_non_host_cid 0 ling poly1305 v0.9.1 Compiling opentelemetry-appender-tracing v0.33.1 Compiling blowfish v0.10.0 Compiling blake2 v0.11.0-rc.6 Compiling getrandom v0.2.17 Compiling tagptr v0.2.0 Compiling compression-core v0.4.33 Compiling moka v0.12.16 Compiling compression-codecs v0.4.45
cargo nextest run -p minimald expose_unenrolled_decision_audited 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-988f... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minvmd egress_refusal_logged_on_host 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-fbb7... Compiling smallvec v1.16.3 Compiling rustix v1.1.4 Compiling crossbeam-epoch v0.9.20 Compiling unicode-xid v0.2.6 Compiling parking_lot_core v0.9.12 Compiling icu_norma
cargo nextest run -p minvmd expose_decision_recorded_on_host 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-4fff... Compiling version v0.8.0 (/workbench/crates/version) Compiling mlog v0.0.1 (/workbench/crates/mlog) Compiling minvmd v0.8.0 (/workbench/crates/minvmd) Finished 'test' profil

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimal-client/src/box_registration.rs
  • crates/minimald/src/rpc.rs
  • scripts/session-e2e.sh

Converted to a draft: something above needs a person before review.

After a box's disallowed connection and a denied min net expose, both are recorded in the audit log on the host, outside the VM. The e2e no-lane group now asserts the host record for e2e-cred-nolane under the rule egress-uncredentialed-proxy-destination, and still asserts that the host gate logged no drop line.

Notes from the agent: Revise round 3 changed only scripts/session-e2e.sh, as the note asked. The absence check now matches only the gate's own field, '"rule_matched":"egress-uncredentialed-proxy-destination"'. The minvmd::control audit record uses a 'rule' field, so it no longer matches. A new check waits up to 10 s for the line 'recorded the box's refused egress in the host audit log' with '"box":"e2e-cred-nolane"' and that rule. The comment above the check now says the host sees the relay's audit record and never the gate's drop line. I did not run the e2e on a KVM host, so the test-kvm lane still owes that evidence. Risk: the laneless-box check in the PSBA group (near line 17304) uses the same broad grep. If that box's relay also reports its refusal, that check will fail in the same way. I left it unchanged because the note said to change nothing else. The cargo-deny failure on main is unchanged, as the note asked. No cargo test lines ran this round because no Rust code changed.

Copy link
Copy Markdown
Member

/revise Fix the e2e failure on test-kvm at 1b8dba9 (job 122791473579). The NET-134 group you fixed now passes; the same bug sits in a second group. Change nothing else.

  • Failing group: "the Box Egress Proxy sees each box by its own address, end to end (NET-132, NET-133, NET-134)" in scripts/session-e2e.sh (line ~17325): ::error::the minvmd gate logged a proxy-lane drop for the laneless box.
  • The line it matched is again the host audit record this PR adds, not a gate drop: {"fields":{"box":"e2e-psba-nolane","destination":"100.64.255.252:8118","message":"recorded the box's refused egress in the host audit log","proto":"tcp","rule":"egress-uncredentialed-proxy-destination"},"target":"minvmd::control"}.
  • Fix: narrow that group's absence grep the same way you narrowed the no-lane group's (line ~16886), so the minvmd::control audit record does not match, and assert the record is present for e2e-psba-nolane. Then check every other egress-uncredentialed-proxy-destination grep in scripts/session-e2e.sh (there are 8 mentions) and fix any other absence check that the audit record would trip, so no third group fails on the next run.
  • Run bash -n scripts/session-e2e.sh and shellcheck on it. cargo-deny fails on main; leave it.

Generated by Claude Code

Copy link
Copy Markdown
Member

/revise (Re-post: round 4 failed inside the runtime on a git fetch reset, so nothing changed.) Fix the e2e failure on test-kvm at 1b8dba9 (job 122791473579). The NET-134 group you fixed now passes; the same bug sits in a second group. Change nothing else.

  • Failing group: "the Box Egress Proxy sees each box by its own address, end to end (NET-132, NET-133, NET-134)" in scripts/session-e2e.sh (line ~17325): ::error::the minvmd gate logged a proxy-lane drop for the laneless box.
  • The line it matched is again the host audit record this PR adds, not a gate drop: {"fields":{"box":"e2e-psba-nolane","destination":"100.64.255.252:8118","message":"recorded the box's refused egress in the host audit log","proto":"tcp","rule":"egress-uncredentialed-proxy-destination"},"target":"minvmd::control"}.
  • Fix: narrow that group's absence grep the same way you narrowed the no-lane group's (line ~16886), so the minvmd::control audit record does not match, and assert the record is present for e2e-psba-nolane. Then check every other egress-uncredentialed-proxy-destination grep in scripts/session-e2e.sh (there are 8 mentions) and fix any other absence check that the audit record would trip, so no third group fails on the next run.
  • Run bash -n scripts/session-e2e.sh and shellcheck on it. cargo-deny fails on main; leave it.

Generated by Claude Code

Copy link
Copy Markdown
Member

The /revise above (comment 6469148922) got no reaction from the runtime, so no round started for it. The fix it asks for is still open: the absence grep in the "Box Egress Proxy sees each box by its own address" group of scripts/session-e2e.sh (around line 17325) matches the minvmd::control host audit record (job 122791473579). It is queued for the owner.


Generated by Claude Code

Copy link
Copy Markdown
Member

/revise (Re-post: rounds for the two earlier posts of this note did not start.) Fix the e2e failure on test-kvm at 1b8dba9 (job 122791473579). The NET-134 group you fixed now passes; the same bug sits in a second group. Change nothing else.

  • Failing group: "the Box Egress Proxy sees each box by its own address, end to end (NET-132, NET-133, NET-134)" in scripts/session-e2e.sh (line ~17325): ::error::the minvmd gate logged a proxy-lane drop for the laneless box.
  • The line it matched is again the host audit record this PR adds, not a gate drop: {"fields":{"box":"e2e-psba-nolane","destination":"100.64.255.252:8118","message":"recorded the box's refused egress in the host audit log","proto":"tcp","rule":"egress-uncredentialed-proxy-destination"},"target":"minvmd::control"}.
  • Fix: narrow that group's absence grep the same way you narrowed the no-lane group's (line ~16886), so the minvmd::control audit record does not match, and assert the record is present for e2e-psba-nolane. Then check every other egress-uncredentialed-proxy-destination grep in scripts/session-e2e.sh (there are 8 mentions) and fix any other absence check that the audit record would trip, so no third group fails on the next run.
  • Run bash -n scripts/session-e2e.sh and shellcheck on it. cargo-deny fails on main; leave it.

Generated by Claude Code

The laneless box check in the proxy-views group matched the host audit
record that minvmd::control writes for a relay refusal. Match only the
gate's rule_matched field, and assert the relay's refused-egress record
for e2e-psba-nolane is on the host.
@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 5 of 10 on a /revise note, pushed as 57ff12ed9d2d.

line exit tail
cargo nextest run -p minimald vsock_rpc_refuses_non_host_cid 0 ling poly1305 v0.9.1 Compiling blowfish v0.10.0 Compiling opentelemetry-appender-tracing v0.33.1 Compiling blake2 v0.11.0-rc.6 Compiling getrandom v0.2.17 Compiling compression-core v0.4.33 Compiling tagptr v0.2.0 Compiling compression-codecs v0.4.45 Compiling moka v0.12.16
cargo nextest run -p minimald expose_unenrolled_decision_audited 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-3f7a... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minvmd egress_refusal_logged_on_host 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-3689... Compiling smallvec v1.16.3 Compiling rustix v1.1.4 Compiling unicode-xid v0.2.6 Compiling crossbeam-epoch v0.9.20 Compiling parking_lot_core v0.9.12 Compiling icu_norma
cargo nextest run -p minvmd expose_decision_recorded_on_host 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-2d3c... Compiling version v0.8.0 (/workbench/crates/version) Compiling mlog v0.0.1 (/workbench/crates/mlog) Compiling minvmd v0.8.0 (/workbench/crates/minvmd) Finished 'test' profil

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimal-client/src/box_registration.rs
  • crates/minimald/src/rpc.rs
  • scripts/session-e2e.sh

Converted to a draft: something above needs a person before review.

After a box's disallowed connection and a denied min net expose, both are recorded in the audit log on the host, outside the VM. The e2e proxy-views group now checks for the relay's refused-egress record for e2e-psba-nolane in the host minvmd log. It no longer reads that record as a gate drop.

Notes from the agent: Revise round 5 changes only scripts/session-e2e.sh, as the note asks. The proxy-views group's absence grep (near line 17328) now matches only '"rule_matched":"egress-uncredentialed-proxy-destination"'. A new poll asserts the minvmd::control audit record for e2e-psba-nolane with '"rule":"egress-uncredentialed-proxy-destination"'. I checked every other mention of the rule. The hook_log_has check near line 16866 is a presence check, and it reads only minimald.log.* files, so the minvmd audit record cannot trip it. The no-lane group's checks (lines 16885-16902) were fixed in the earlier round. No other absence check matches the rule. The cargo test: lines were not re-run this round, because no Rust file changed. The KVM e2e lane cannot run on this box, so the fix is unverified until test-kvm runs again. cargo-deny fails on main, and I left it as the note asks.

@norrietaylor
norrietaylor marked this pull request as ready for review October 28, 2026 15:56
@norrietaylor
norrietaylor marked this pull request as ready for review October 28, 2026 17:21
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS · the full ruling

This push adds reported_by: "guest" to the two host audit records that the in-VM daemon reports. Section 7.6 line 296 says guest output is untrusted, so the label records the true source of each record. The design gives no field names for these records, and the e2e patterns match the serde field order. All other parts of the change keep their PASS from b057dbfd.

Ruled at 6509c40 against gominimal/arch@109541a48c31 by claude-opus-5-5.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — approval withheld

Verdict COMMENT — APPROVE withheld: 4 unresolved threads — minimal-foundry[bot] crates/minvmd/src/control.rs:2143, minimal-foundry[bot] crates/minvmd/src/control.rs, minimal-arch[bot] crates/minvmd/src/control.rs:2155, minimal-arch[bot] crates/minvmd/src/control.rs:2181. No findings.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

@norrietaylor norrietaylor left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Babysitter review at 6509c40b. I read the RPC, guest-report and host-log changes against T107's plan block. The in-VM daemon's vsock listener now refuses every peer but the host (CID 2) at accept, before any request byte is read, with one rate-limited refusal line per CID (NET-139). Each recorded expose decision and each dropped own-address egress frame is also reported to the VM host over the guest door, in the background, and the local record still lands when the host is unreachable. minvmd's guest door appends them to the host audit log, capped at 30 records per box per 60 s with a suppressed count, and every such record carries reported_by: "guest" (arch C1). The e2e groups now check the host gate's own drop line and assert the new audit record. The out-of-layer files are recorded as a layer exception. No findings.


Generated by Claude Code

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — approved

Verdict APPROVE — every review thread is resolved; @norrietaylor closed the last one. The review of 6509c40b0e3f withheld this approval on the threads open then. Posted as a comment: the App cannot approve its own pull request; the check and the label carry it.

@minimal-foundry minimal-foundry Bot added the review:approved minimal-review approved the current head label Oct 28, 2026
@norrietaylor norrietaylor changed the title T107 — Keep the VM's security records on the host and admit RPC only from the host feat(minimald): record VM refusals on the host and admit RPC only from the host (T107) Oct 28, 2026
@norrietaylor norrietaylor left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Babysitter approval at 6509c40: every merge gate passes at this head (arch PASS at 6509c40, zero open threads, every lane aggregator green). Squash title feat(minimald): record VM refusals on the host and admit RPC only from the host (T107) and the squashed message pass commitlint. Queued for squash merge.


Generated by Claude Code

Copy link
Copy Markdown
Member

/revise Fix the unbounded host audit log that arch noted at 57ff12e (arch_rulings 524, "Notes, not conditions"). The owner chose an in-PR fix over a residual issue. Change nothing else.

  • Gap: audit/box-admissions.log on the VM host has no size bound or rotation (append_audit_line in crates/minvmd/src/control.rs). The per-box cap limits the rate (30 per kind per 60 s), not the total, so a hostile guest can grow the file without end, and the task's claim "cannot fill the host's disk" is stronger than the code.
  • Fix: bound the file. Rotate box-admissions.log at a fixed size (keep a small fixed number of rotated files, oldest dropped), or cap its total size and count what was dropped in one line. Use the same approach crates/minimald/src/audit.rs uses for its own log (try_append_capped, MAX_LOG_BYTES) if it fits. The host's own admission records must still land after rotation.
  • Tests: a unit test in control.rs that writes past the bound and asserts the file stays within it and the newest records are kept.
  • Keep every commit message line at or under 100 characters.

Generated by Claude Code

Copy link
Copy Markdown
Member

/revise (Re-post: the note above got no reaction.) Fix the unbounded host audit log that arch noted at 57ff12e (arch_rulings 524, "Notes, not conditions"). The owner chose an in-PR fix over a residual issue. Change nothing else.

  • Gap: audit/box-admissions.log on the VM host has no size bound or rotation (append_audit_line in crates/minvmd/src/control.rs). The per-box cap limits the rate (30 per kind per 60 s), not the total, so a hostile guest can grow the file without end, and the task's claim "cannot fill the host's disk" is stronger than the code.
  • Fix: bound the file. Rotate box-admissions.log at a fixed size (keep a small fixed number of rotated files, oldest dropped), or cap its total size and count what was dropped in one line. Use the same approach crates/minimald/src/audit.rs uses for its own log (try_append_capped, MAX_LOG_BYTES) if it fits. The host's own admission records must still land after rotation.
  • Tests: a unit test in control.rs that writes past the bound and asserts the file stays within it and the newest records are kept.
  • Keep every commit message line at or under 100 characters.

Generated by Claude Code

box-admissions.log rotates to one kept generation (.1) at 1 MiB, the
same bound the in-VM daemon's audit log uses, so a guest cannot grow
it without end. The per-box caps bound only the rate.
@minimal-arch minimal-arch Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arch-review: PASS · the full ruling

The change conforms to the design. The new commit rotates the host audit log at 1 MiB and keeps one generation. That bounds the disk a guest can fill. The design gives no size or retention rule for this log. The rotation stays on the host and adds no message from the guest. The main merge is clean, and every other part keeps the PASS from the earlier head.

Ruled at 47b8fa1 against gominimal/arch@109541a48c31 by claude-opus-5-5.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — approved

Verdict APPROVE — No findings.

Panel: 3 of 3 personas ran (correctness, security, supply-chain).

@minimal-foundry
Copy link
Copy Markdown
Contributor Author

Round 8 of 10 on a /revise note, pushed as 47b8fa1e0cea.

line exit tail
cargo nextest run -p minimald vsock_rpc_refuses_non_host_cid 0 Applying loadouts: default Running task runtime-implement-test-0 in session task-runtime-implement-test-0-7fa8... Compiling paths v0.0.1 (/workbench/crates/paths) Compiling args v0.0.1 (/workbench/crates/args) Compiling ot v0.0.1 (/workbench/crates/ot) Compiling client-config v0.0.1 (/wo
cargo nextest run -p minimald expose_unenrolled_decision_audited 0 Applying loadouts: default Running task runtime-implement-test-1 in session task-runtime-implement-test-1-0f59... Compiling stdlib v0.0.22 (/workbench/crates/stdlib) Compiling version v0.8.0 (/workbench/crates/version) Compiling decode v0.0.1 (/workbench/crates/decode) Compiling mlog v0.
cargo nextest run -p minvmd egress_refusal_logged_on_host 0 Applying loadouts: default Running task runtime-implement-test-2 in session task-runtime-implement-test-2-2d1d... Compiling ot v0.0.1 (/workbench/crates/ot) Compiling args v0.0.1 (/workbench/crates/args) Compiling version v0.8.0 (/workbench/crates/version) Compiling minvmd v0.8.0 (/workb
cargo nextest run -p minvmd expose_decision_recorded_on_host 0 Applying loadouts: default Running task runtime-implement-test-3 in session task-runtime-implement-test-3-bc53... Compiling version v0.8.0 (/workbench/crates/version) Compiling mlog v0.0.1 (/workbench/crates/mlog) Compiling minvmd v0.8.0 (/workbench/crates/minvmd) Finished 'test' profil

Verify min task run agent-verify passed.

Outside the task's layers::

  • crates/minimal-client/src/box_registration.rs
  • crates/minimald/src/rpc.rs
  • scripts/session-e2e.sh

Converted to a draft: something above needs a person before review.

After a box's disallowed connection and a denied min net expose, both are recorded in audit/box-admissions.log on the host, outside the VM. That log now rotates to one kept generation (box-admissions.log.1) at 1 MiB, so a guest cannot grow it without end.

Notes from the agent: Revise round 8: only the unbounded host audit log was addressed. append_audit_line in crates/minvmd/src/control.rs now rotates box-admissions.log to box-admissions.log.1 when the next line would carry it past MAX_AUDIT_LOG_BYTES (1 MiB). The previous .1 is dropped. A process-wide mutex serialises appends so that two writers cannot both rotate. This is the same rule as rotates() in crates/minimald/src/audit.rs. The test host_audit_log_rotates_within_its_bound_and_keeps_the_newest_records writes past a 512-byte cap. It asserts that both files stay within the cap, that only one generation is kept, and that the kept records are the newest, contiguous and in order, with a new record still landing after rotation. Not changed: the diagnostics bundle and scripts/session-e2e.sh read only the live box-admissions.log, not the .1 generation. The note said to change nothing else. cargo clippy -p minvmd --all-targets is clean. just clippy-strict was not run.

@minimal-foundry
minimal-foundry Bot marked this pull request as draft October 28, 2026 19:30
auto-merge was automatically disabled October 28, 2026 19:30

Pull request was converted to draft

@norrietaylor
norrietaylor marked this pull request as ready for review October 28, 2026 20:27
@norrietaylor norrietaylor left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Babysitter review at 47b8fa1e. Since my review at 6509c40b this head bounds the host audit log, as the owner ruled: minvmd's append_audit_line rotates box-admissions.log at 1 MiB (MAX_AUDIT_LOG_BYTES, the same size as minimald's own audit log), keeping one rotated generation, so a hostile guest can no longer grow the host file without end. Rotation is serialised so two doors cannot both rotate, and the host's own admission records still land after it. The out-of-layer files are unchanged and stay recorded as a layer exception. No findings.


Generated by Claude Code

@norrietaylor norrietaylor left a comment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Babysitter approval at 47b8fa1: every merge gate passes at this head (arch PASS at 47b8fa1, zero open threads, every lane aggregator green). Squash title feat(minimald): record VM refusals on the host and admit RPC only from the host (T107) and the squashed message pass commitlint. Queued for squash merge.


Generated by Claude Code

@norrietaylor
norrietaylor added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit abe99f7 Oct 28, 2026
37 checks passed
@norrietaylor
norrietaylor deleted the agent/2694-keep-the-vm-s-security-records-on-the-host-and-a branch October 28, 2026 20:56
norrietaylor pushed a commit that referenced this pull request Nov 5, 2026
… the host (#2721)

T107. The in-VM daemon's vsock listener drops a peer at any CID but 2 at accept, before
reading a byte, and writes one rate-limited audited line naming the CID (NET-139). The
in-VM daemon reports each expose decision and each refused own-address egress flow over
the guest door, and the VM host daemon appends them to its audit log under per-box caps,
with a line counting suppressed records (NET-046). Expose decisions keep their own cap, a
withdrawn box flushes its window, guest-reported records carry reported_by=guest, and
box-admissions.log rotates to one kept generation at 1 MiB. The session e2e script expects
the new host audit records.

Backport of #2721, folded from #2735.

Adapted to the line:
- boxes::DynamicIngress becomes sessions::DynamicIngress.
- The line has no ProxyListening verb or reply, so ReportExposeDecision,
  ReportRefusedEgress and Audited stand without it.
- The listen watcher keeps tokio::spawn, since crate::traced is absent. The audit route
  is registered before the spawn and removed after the watcher's withdraw_all.
- minvmd has no serde_json here, so its host audit tests parse with serde_json_lenient.
- The vsock peer-CID tests join the inline tests in server.rs.

Dropped: ask_audit_line_spells_box_id_as_32_hex, which reached the cherry-pick as conflict
context from main's proxy work and is not part of #2721.

(cherry picked from commit abe99f7)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review:approved minimal-review approved the current head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

T107 — Keep the VM's security records on the host and admit RPC only from the host

1 participant

0