Repository navigation
T55 — Expose a port from inside the box, end to end, on the native lane - #1974
norrietaylor merged 10 commits into
Conversation
The runtime-publish proofs were gated on MINVMD_GVPROXY_BIN alone, so a native minimald lane — the one the gvproxy provisioning of #1963 was built for — skipped them. Accept E2E_NATIVE_SWITCH beside it in both gates, the way the own-IP proofs do, and take the box's published address from the daemon's own records (the zone answerer's answer, the expose record) rather than the 100.64/16 constants a VM switch guarantees: the proofs' native arms already read nothing else, so the gate was the only native-lane blocker. VM lanes keep MINVMD_GVPROXY_BIN and are unchanged.
Add the expose_from_inside_box case: `min net expose 3000` sent from inside the box by its own helper, every stance end to end. On a native lane the allow stance publishes at the address the daemon's expose record names, the reply and the record agree, and the host probe meets the contract the tree pins for a runtime-only port — bound and listed as (pending; not yet reachable), the relay gate not admitting it, the listener standing inside the box so the refusal cannot be blamed on a dead port (the reach itself is T97's, #1897). An ask stance routes its dialog to a human attached through a real pty — the request driven from an exec outside the attach, so the dialog on the attached terminal is NET-045's routing — and publishes on their Allow, records nothing on their deny, and fails closed with its own typed error when nobody is attached; a deny box is refused with the stance's own words. Every decision carries its line in the daemon's audit log, asserted and printed beside the leg that produced it. A VM lane asserts the create's typed refusal of allow and ask (T82) and leaves the positive legs to T97. The pty driver gains E2E_PTY_ASK: the ask's dialog renders on the attached terminal exactly as the session-exit prompt does, so it too can only be answered through a real pty; the answer's keystrokes are the daemon's own test driver's (Down then Enter for Allow, Enter for the highlighted Deny).
|
Important Review skippedBot user detected. To trigger a single review, invoke the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
The expose_from_inside_box case read the daemon log for NET-040's attach-time record, but that record is written only for the mappings a create declared — and every box in the case declares none — so on a host where the case runs for real (a CI runner with /dev/net/tun) the polled record never existed and the case failed at its own read; on a host without the device it skipped at the tun gate and passed vacuously. Read the publish's own one-info-line record instead (the session actor's answer_expose line, NET-044's observability) and pin the daemon's filter for the module that writes it, minimald::session, rather than gvproxy_network's. The answered-no negative check grepped the same absent record, so it could never fire; it now asserts the one thing that matters — no published record for the box the human denied.
The record's local field is the whole address the forward bound, host and port, and the host probe built its URL by appending the port to it — 127.0.64.21:3000:3000, a URL curl refuses to parse, which would have made the pending-contract probe vacuous: it can only fail, and a gate that wrongly admitted the port would have passed it all the same. Probe the recorded address directly.
…ort-from-inside-the-box-end-to-end-on
|
The runtime will not revise this pull request again on its own: round 2 would exceed the cap of 1 (the last trigger was a failed check suite). A person decides now. Remove |
|
Round 1 of 1 on a failed check suite, pushed as
Verify Outside the task's
Converted to a draft: something above needs a person before review. On the native lane, Notes from the agent: Revise round for the failed checks ci-linux-native-success / native-daemon-e2e. Root cause of the CI failure: the expose_from_inside_box case read the daemon log for NET-040's attach-time record ('exposed ingress port on the host loopback'), but that record is written only for the port mappings a create DECLARED (finish_own_ip_attach over ingress.port_mappings) — every box in the case declares none, and the runtime publish never writes that phrase — so on a host where the case runs for real (the CI runner carries /dev/net/tun, as its green own-IP proofs prove) the 2.5s poll always expired and the case failed at its own record read; on a dev host without the device it skipped at the tap gate and passed vacuously. Compounding: the case's RUST_LOG pin carried minimald::net::gvproxy_network=info, the wrong module, so even the correct record would not have reached the file log. Fixed by reading the publish's own record instead (answer_expose's 'dynamic ingress expose' line from minimald::session, NET-044's one-info-line-per-request observability): per-field compact-JSON greps (message value, name, port with the delimiter that ends the JSON number, outcome published), a minimald::session=info pin, the answered-no negative check rewritten to assert no published record for the box the human denied (it grepped the never-written phrase and so could never fire), and the host probe using the record's full local address as its URL (a follow-up bug in my own first fix: the probe appended :port to an address that already carried it). Verifi…[truncated] |
The native lane's staged gvproxy reaches a daemon only through mnl's call-local MINIMAL_BIN, so only a daemon mnl autospawns carries it. The expose cases inherited the daemon proof_hostnames_recover pinned with a bare `minimald run --detach`, which fell back to the system gvproxy path and failed every own-IP box at spawn. Stop the inherited daemon before the first own-IP box in the min net expose and listen-published cases, and force the stop the expose-from-inside case already makes, so mnl autospawns one that carries the switch, as proof_own_ip's first call does. A gvproxy spawn failure at the exec gate now fails naming the switch, never skips as a host that cannot run a sandbox.
|
The runtime will not revise this pull request again on its own: round 2 would exceed the cap of 1 (the last trigger was a failed check suite). A person decides now. Remove |
The listener watcher commits to the box's publications, never to the runtime-ingress table min session policy lists, so the allow-stance listing check could never pass. NET-016 asks for reach; the peer and host legs prove it. The deny stance still asserts nothing is listed.
|
The runtime will not revise this pull request again on its own: round 2 would exceed the cap of 1 (the last trigger was a failed check suite). A person decides now. Remove |
Until the watcher's next poll withdraws the publication, it accepts the peer's connect and resets it against the closed listener (curl exit 56). The close leg broke on that first reset; it now polls on until the refusal (7) stands, and still fails if the reset outlasts the bound.
|
/review |
|
Approved: no findings at Nothing to do. Clobber: None. |
An in-range listen under allow is a dynamic ingress request, so NET-044 owes its row in min session policy and NET-046 its decisions.log line; the watcher writes neither yet. Both checks stay, as KNOWN GAP lines citing gominimal/inbox#912 and counted in the lane summary, and turn into hard assertions when it lands. The close leg logs its time to refusal.
|
Follow-up: gominimal/inbox#912. In-range listen publications under |
|
The runtime will not revise this pull request again on its own: round 2 would exceed the cap of 1 (the last trigger was the runtime's review findings). A person decides now. Remove |
|
/review |
|
/review |
There was a problem hiding this comment.
Reviewed against the T55 plan block, the native split. ARCH: PASS, with conditions (a), (b) and (d) met and the macOS e2e green. All lanes green at d161f44. minimal-review approved with no findings. The listen listing and audit gaps are carried as KNOWN GAP checks citing gominimal/inbox#912.
T55 — Expose a port from inside the box, end to end, on the native lane
slice: S13
kind: integration
covers: none
size: M
est: 90 code, 220 tests
executor: agent
difficulty: needs care
model: strongest
diagnostics: the bundle from the run carries the audit log tail and the daemon log
observability: the case prints each expose request, the decision and the audit record it produced
layers:
- e2e:
- scripts/session-e2e.sh: on the native lane, where the switch is provisioned as E2E_NATIVE_SWITCH_DIR (#1963), un-gates min_net_expose_publishes_lists_and_refuses and listen_published_port_reaches_peer_and_host by reading the box's address from the daemon's expose record and the native daemon's own switch /24 instead of the VM switch's 100.64/16 constants; VM lanes keep MINVMD_GVPROXY_BIN and are unchanged
- scripts/session-e2e.sh: adds the case expose_from_inside_box on the native lane:
min net expose 3000run inside an allow box publishes and the host reaches it at the recorded address; inside an ask box, answered yes through a real PTY attach (scripts/e2e-attach-pty.py) it publishes and answered no it records nothing; an ask with nobody attached is refused with its typed error; a deny box is refused; every one of those decisions has its line in the daemon's audit/decisions.log. On a VM lane the case asserts T82's typed create refusal of allow and ask and leaves the positive legs to T97test: ./scripts/session-e2e.sh expose_from_inside_box
test: just e2e
produces: none
consumes: none
proof: On the native lane,
min net expose 3000inside a box publishes it under an allow decision, prompts the attached human on ask and applies the answer, refuses on deny or when nobody is attached, and every decision is in 8000 the local audit log; the existing expose and listen-publish proofs run on the native lane instead of skipping.depends on:
- blocked by #1625
- blocked by #1666
- blocked by #1669
- waits on #1963 (native lane provisions gvproxy, gominimal/inbox#887)
Proof
On the native lane, the new
expose_from_inside_boxsession-e2e case drivesmin net expose 3000from inside the box through every stance: the allow stance publishes, with the reply, the daemon's expose record and the audit line agreeing on the recorded address; an ask stance routes its dialog to a human attached through a real pty (the request driven from an exec outside the attach, so the dialog on the attached terminal is the routing), publishing on their Allow and recording nothing on their deny; an ask with nobody attached and a deny box are refused with their own typed errors; and every decision is asserted and printed from the daemon's audit/decisions.log. The allow leg's host probe asserts the tree's pinned pending contract (bound at the recorded address, live row reading '(pending; not yet reachable)', nothing of the box delivered) — the reach itself is T97's, per the code's own gate. The two existing expose and listen-publish proofs now run on the native lane instead of skipping (verified: their gates pass E2E_NATIVE_SWITCH here and they proceed to the /dev/net/tun check instead of the old no-switch skip).Notes from the agent: Layer-2's host-reach clause is asserted as the tree's pinned contract, not as a reach. The layer text says the allow publish is one 'the host reaches it at the recorded address'; on this tree a runtime-only publish is bound on the host but the box's relay gate refuses connections to it: publish_exposed_port (crates/minimald/src/session.rs) reserves, binds and records without admitting at the gate, whose TCP set is the declared ports plus the listen watcher's publishes (crates/minimald/src/net/switch.rs admits_tcp; only the watcher calls admit_published, and listeners.rs settles a port the expose already published without admitting it); docs/reference/cli-min.md and write_live_ingress pin '(pending; not yet reachable)'; the mnx proof 'asserts only the publish line itself' and never dials its responder from outside; ses…[truncated]
Tests
./scripts/session-e2e.sh expose_from_inside_boxjust e2eVerify
min task run agent-verifyexited 101, so this PR is a draft.Out of scope
These files are outside the task's
layers:, so this PR is a draft:scripts/e2e-attach-pty.pyCloses #1671