'box expose <box> <port>' — a deliberate, loopback-only door to a box's dev server #55

Closed
opened 2026-07-14 15:47:20 +00:00 by dan-claude-bot · 0 comments
dan-claude-bot commented 2026-07-14 15:47:20 +00:00 (Migrated from github.com)

The contract is no inbound path exists — and for the tool's own flagship workflow, that's one notch too absolute. You're coding in a box, the agent starts a dev server on :3000, and you cannot open it in your browser. Today the only route is box incus <b> -- config device add {} … proxy … hatch surgery, which (a) needs NAT-mode proxying on VMs plus a static lease, (b) collides with the ACL's default ingress drop — the very rule doing its job — and (c) leaves an unlabeled hole someone has to remember exists.

Proposal

box expose <box> <port> [<host-port>]    # forward 127.0.0.1:<host-port> → box:<port>
box expose <box> --list
box expose <box> --remove <port>

Opinionated where it matters:

  • Loopback only, always. The listen side is 127.0.0.1, never 0.0.0.0 — the network-facing contract (no inbound path) stays true; only the operator's own machine gets a door. No flag to widen it; someone who wants LAN exposure is leaving the tool's threat model and can use the hatch with its warning.
  • Explicit, per-port, reversible, visible. Each exposure is a named device; box info lists active exposures so a box with a hole in it says so.
  • A verb, not a mint flag. The need appears after the box exists ("the server is running, let me see it"), and it must be removable without re-minting.

Design points to settle live (not by reasoning — measure on a real box)

  1. Mechanism: Incus proxy device in NAT mode (VMs can't use the plain socket proxy). NAT mode requires the instance NIC to have a static ipv4.address — likely a device override pinning the current lease at expose time. Verify the DNAT'd traffic actually survives...
  2. ...the ACL ingress drop. security.acls.default.ingress.action=drop is the rule that makes A7 pass; whether host-originated DNAT traffic is caught by it, and whether the right carve-out is a per-box ACL rule scoped to the forwarded port or per-NIC config, is exactly the kind of question this repo answers with a probe, not a paragraph.
  3. The drill gains checks both ways: an exposed port answers on host loopback; a non-exposed box still drops host→box (A7 must keep passing with the feature merged); --remove closes the door and the probe confirms silence; and an exposure never makes the box reachable from anywhere but loopback.
  4. README's Isolation section amends honestly: no inbound path exists — unless you punch one with box expose, and that door only ever opens onto the host's own loopback.

Provenance: operator question after run 16 — "if you're coding in a box and want to see the local server, in the current design we're screwed." Correct diagnosis; this is the deliberate un-screwing.

🤖 Generated with Claude Code

The contract is *no inbound path exists* — and for the tool's own flagship workflow, that's one notch too absolute. You're coding in a box, the agent starts a dev server on :3000, and you cannot open it in your browser. Today the only route is `box incus <b> -- config device add {} … proxy …` hatch surgery, which (a) needs NAT-mode proxying on VMs plus a static lease, (b) collides with the ACL's default ingress drop — the very rule doing its job — and (c) leaves an unlabeled hole someone has to remember exists. ## Proposal ``` box expose <box> <port> [<host-port>] # forward 127.0.0.1:<host-port> → box:<port> box expose <box> --list box expose <box> --remove <port> ``` Opinionated where it matters: - **Loopback only, always.** The listen side is `127.0.0.1`, never `0.0.0.0` — the network-facing contract (*no inbound path*) stays true; only the operator's own machine gets a door. No flag to widen it; someone who wants LAN exposure is leaving the tool's threat model and can use the hatch with its warning. - **Explicit, per-port, reversible, visible.** Each exposure is a named device; `box info` lists active exposures so a box with a hole in it says so. - **A verb, not a mint flag.** The need appears after the box exists ("the server is running, let me see it"), and it must be removable without re-minting. ## Design points to settle live (not by reasoning — measure on a real box) 1. **Mechanism**: Incus `proxy` device in NAT mode (VMs can't use the plain socket proxy). NAT mode requires the instance NIC to have a static `ipv4.address` — likely a `device override` pinning the current lease at expose time. Verify the DNAT'd traffic actually survives... 2. **...the ACL ingress drop.** `security.acls.default.ingress.action=drop` is the rule that makes A7 pass; whether host-originated DNAT traffic is caught by it, and whether the right carve-out is a per-box ACL rule scoped to the forwarded port or per-NIC config, is exactly the kind of question this repo answers with a probe, not a paragraph. 3. **The drill gains checks both ways**: an exposed port answers on host loopback; a *non*-exposed box still drops host→box (A7 must keep passing with the feature merged); `--remove` closes the door and the probe confirms silence; and an exposure never makes the box reachable from anywhere but loopback. 4. README's Isolation section amends honestly: *no inbound path exists — unless you punch one with `box expose`, and that door only ever opens onto the host's own loopback.* Provenance: operator question after run 16 — "if you're coding in a box and want to see the local server, in the current design we're screwed." Correct diagnosis; this is the deliberate un-screwing. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/box#55
No description provided.