PRD: smolmachines sidecar VM #334
Reference in New Issue
Block a user
Delete Branch "smolmachines-sidecar-vm"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #332.
PRD:
1a2d46ee56/docs/prds/prd-new-smolmachines-sidecar-vm.mdSummary
Runs the smolmachines sidecar bundle as a sidecar smolVM instead of a Docker container.
Adds a stdlib host TCP forwarder that binds only the per-bottle address and forwards to raw smolVM-published loopback ports, so the agent receives only per-bottle egress/git/supervise URLs and never the raw sidecar VM ports.
Updates smolmachines egress reload to signal the sidecar VM and keeps sidecar VMs out of active-agent enumeration.
Merge rule(s)
Run smolmachines integration coverage on a host with smolVM/KVM or Hypervisor.framework before merge, including alias reachability, host-localhost denial, other-bottle alias denial, internal sidecar port denial, and teardown cleanup.
Implementation is blocked on sidecar VM port publishing semantics. Issue #332 requires publishing agent-facing sidecar services only on the per-bottle address, but current smolVM docs/source shape indicate
-p/--portacceptsHOST:GUESTas two port numbers, not an address-scoped bind such as127.0.0.16:<random>:9099. Because TSI is IP-only, publishing on generic localhost would not preserve the security property that the agent can reach only its own sidecar surface. The draft PRD records the required decision: add/confirm address-scoped smolVM publishing, add a host-side per-address forwarding layer, or choose an equivalent fail-closed networking design before implementation proceeds.@didericis-codex paste the result of our discussion RE a python stdlib TCP forwarder that lives on the host. Also factor in any security concerns/risk of some kind of package designed to escape the forwarder. Am assuming that's easy to mitigate given the simplicity of the forwarder/not really an issue.
@didericis Summary from the discussion: the practical fallback is a host-local TCP forwarder owned by bot-bottle. smolVM publishes each sidecar guest port on a raw host loopback port, then bot-bottle exposes only a per-bottle alias listener to the agent:
The agent env is stamped from the alias listener ports, never from the raw smolVM ports. Example:
Recommended implementation: a small Python stdlib helper process, probably one process per bottle managing all active forwards. It can use
socketplusselectorsor a small thread-per-direction pipe. I would keep the public interface narrow:ForwardSpec(label, listen_host, listen_port, target_host, target_port)plus start/stop/readiness. That leaves room to swap insocator a tiny compiled helper later if measurements show Python is a problem. My expectation is Python is fine here because traffic is already dominated by mitmproxy/DLP, git hooks/gitleaks, smolVM networking, and provider latency. The risk to manage is correctness/lifecycle, not throughput.Security notes:
0.0.0.0,::) and reject127.0.0.1for agent-facing listeners. It should only bind the allocated per-bottle address.ForwardSpec./32, not127.0.0.1/32.Tests I would require with this design:
127.0.0.1;Net: a stdlib forwarder is acceptable if we treat it as a dumb, fixed-route TCP pipe with strict bind validation and no client-controlled target selection.
@didericis-codex Summary looks good, implement it.
Acknowledged. I’ll implement the stdlib host TCP forwarder path with strict per-bottle bind validation, switch the smolmachines sidecar launch to the sidecar VM + forwarder lifecycle, and keep the PRD updated with that design.
Implemented in
1a2d46e:bot-bottle-sidecars-<slug>smolVM from the packed sidecar image;-p HOST:GUESTports are wrapped by a stdlib TCP forwarder bound only to the per-bottle address;smolvm machine exec;Validation run locally:
python3 -m unittest discover tests/unitpassed.pyrightwas not available in this bottle (python3 -m pyrightandpyrightmissing).Remaining merge gate: run the smolmachines integration tests on a host with real smolVM support to validate alias reachability, host-localhost denial, other-bottle alias denial, internal sidecar port denial, and teardown cleanup.
@@ -328,26 +342,33 @@ def _init_vm(plan: SmolmachinesBottlePlan) -> None:def _proxy_host(plan: SmolmachinesBottlePlan, loopback_ip: str) -> str:This is probably irrelevant now/can get rid of it, right?
@didericis-codex am not seeing where we prevent collisions if we launch two sidecars, each of which (I think) would bind the same port to localhost. We need to account for that, correct?
Am also wondering whether or not we should change the architecture here and have the orchestrator process also be responsible for doing port forwarding for everything. Then we'd just have one persistent host process, and teardown/spin up would be simplified. Downside is less robustness, but I think it'd be robust enough.
@didericis Correct concern, but the current implementation does not bind the same host port for each sidecar.
start_bundle_vm()callsallocate_raw_host_ports(spec.ports_to_publish), which asks the kernel for a free host loopback port withbind(("127.0.0.1", 0))for each guest port, then passes those random host ports to smolVM as(host_port, guest_port)pairs. So two sidecars both exposing guest9099should become something like127.0.0.1:55001 -> guest:9099and127.0.0.1:55017 -> guest:9099, then each gets wrapped by its own per-bottle alias forwarder port.There is still a normal reserve-then-bind race because the socket used to discover the free port is closed before smolVM binds it. If another process grabs the port in that gap, smolVM launch should fail closed rather than silently cross-wire. That is probably acceptable for v1, but we can make it more robust with a retry loop around sidecar VM creation if we see real collisions.
On the architecture question: I agree a single persistent orchestrator-owned forwarder is attractive long-term. It would centralize listener ownership, avoid one helper process per bottle, make collision/retry bookkeeping cleaner, and give us one place to reconcile desired forwards against running bottles. I would keep that as a follow-up rather than change this PR’s shape. This PR has a narrower failure domain: each bottle owns its own helper, teardown is just an
ExitStackcallback, and a wedged forwarder only affects that bottle. Moving forwarding into the orchestrator creates a shared availability dependency and needs a small control-plane protocol plus reconciliation logic, which is doable but materially broadens this PR.My recommendation: keep per-bottle helpers here, with the current random raw host-port allocation and fail-closed behavior. Add retry-on-raw-port-collision if we want to harden the launch race now. Treat orchestrator-managed forwarding as the next architecture step once this sidecar-VM path is proven.
@didericis re: #issuecomment-3049, yes, that helper is redundant now. I removed
_proxy_host, useloopback_ipdirectly as the per-bottle forwarder/TSI address, and updated the stale doc comment/test in820bb40.Validation:
python3 -m unittest tests.unit.test_smolmachines_provision tests.unit.test_smolmachines_sidecar_bundle tests.unit.test_smolmachines_port_forwardpassed.820bb40a5ftobfd3e659e8@@ -110,1 +184,4 @@warn(f"smolvm machine delete {name} failed: {exc}")def create_bundle_network(network_name: str, subnet: str, gateway: str) -> None:This is made irrelevant, right?
fe82dbc7b8to2b53c36608Pull request closed