PHAX: smolvm isolation spike - #60
Open
lbdremy wants to merge 7 commits into
Open
Conversation
Add the spikes/smolvm harness directory: a README stating the three unknowns (network/filesystem/agents-in-guest) and the execution-model caveat, a preflight script that verifies a smolvm install and reports backend/arch, and a findings template. No src/ changes; this is groundwork for the isolation probes. --- Run-Id: smolvm-isolation-spike-1782738661332 Short-Name: smolvm-isolation-spike Phase-Id: phase-01 Phase-Title: Spike scaffold and smolvm preflight Model: claude-sonnet-4-6 Effort: low Worktree: /Users/remyloubradou/.phax/worktrees/phax.smolvm-isolation-spike/phase-01 Session-Id: 0c33a1a2-8b3f-405d-aabd-e526821889c0 Gate-Log: /Users/remyloubradou/.phax/runs/phax.smolvm-isolation-spike/phase-01/checks-attempt-01.log
Add a harness that mounts one host dir as /workspace and asserts the rest of the host FS (HOME, repo root, /etc) is invisible to the guest, plus a read-only-mount check. Findings doc captures procedure; Results/Verdict are filled from a real run out-of-band per the spike's execution-model caveat. --- Run-Id: smolvm-isolation-spike-1782738661332 Short-Name: smolvm-isolation-spike Phase-Id: phase-02 Phase-Title: Filesystem isolation probe Model: claude-sonnet-4-6 Effort: medium Worktree: /Users/remyloubradou/.phax/worktrees/phax.smolvm-isolation-spike/phase-02 Session-Id: 2e26a154-2c61-42d1-93d2-e6b266001b6c Gate-Log: /Users/remyloubradou/.phax/runs/phax.smolvm-isolation-spike/phase-02/checks-attempt-01.log
Add the decisive network harness: a five-case matrix (allowed-by-name, denied-by-name, denied-by-hardcoded-IP, allowed-by-IP, ICMP) that determines whether smolvm's --allow-host is deny-by-default and a real egress boundary versus DNS-name filtering. Findings doc frames the two crux questions; Results/Verdict filled from a real run out-of-band. --- Run-Id: smolvm-isolation-spike-1782738661332 Short-Name: smolvm-isolation-spike Phase-Id: phase-03 Phase-Title: Network per-domain allowlist probe Model: claude-sonnet-4-6 Effort: medium Worktree: /Users/remyloubradou/.phax/worktrees/phax.smolvm-isolation-spike/phase-03 Session-Id: f5a8c7d9-6a2f-4972-a851-4489a77b16f2 Gate-Log: /Users/remyloubradou/.phax/runs/phax.smolvm-isolation-spike/phase-03/checks-attempt-01.log
Add a parameterized harness that runs Claude Code, Codex, and Mistral Vibe inside the libkrun Linux guest with env-injected creds and a per-provider network allowlist, confirming each edits the mounted worktree and observing denied-egress UX. Secrets are read from host env only. Per-provider findings table filled from a real run. --- Run-Id: smolvm-isolation-spike-1782738661332 Short-Name: smolvm-isolation-spike Phase-Id: phase-04 Phase-Title: Agents-in-guest execution probe Model: claude-sonnet-4-6 Effort: medium Worktree: /Users/remyloubradou/.phax/worktrees/phax.smolvm-isolation-spike/phase-04 Session-Id: 7803253c-40ba-480b-97f8-287f8a05a2f6 Gate-Log: /Users/remyloubradou/.phax/runs/phax.smolvm-isolation-spike/phase-04/checks-attempt-01.log
Add the synthesis doc: per-probe verdicts (filesystem, per-domain network, agents-in-guest), an explicit go/no-go on smolvm for phax's isolated mode driven by the hard-coded-IP egress result, and a non-implementing integration sketch (infra wrapper adapter, SecurityPolicy reuse, MCP domain declaration, capability-preamble prompt) plus residual risks and the recommended next step. --- Run-Id: smolvm-isolation-spike-1782738661332 Short-Name: smolvm-isolation-spike Phase-Id: phase-05 Phase-Title: Synthesis and go/no-go recommendation Model: claude-opus-4-8 Effort: high Worktree: /Users/remyloubradou/.phax/worktrees/phax.smolvm-isolation-spike/phase-05 Session-Id: 4410fda5-e889-40dd-b5d7-47eadcf5ffb4 Gate-Log: /Users/remyloubradou/.phax/runs/phax.smolvm-isolation-spike/phase-05/checks-attempt-01.log
Ran the filesystem and network probes on smolvm 1.3.2 (macOS/arm64) and filled in the findings docs and synthesis with actual results: - Probe 01 (filesystem): PASS — clean guest, :ro enforced at hypervisor layer. - Probe 02 (network): PASS — case 3 (raw IP to non-allowed host) is refused at L4, so --allow-host is a real IP-based egress boundary, not DNS filtering. GO. - Probe 03 (agents): BLOCKED — no API keys; recorded the infrastructure facts. Synthesis go/no-go is now a confirmed GO on the security question. Folds in two run findings: scripts don't run verbatim (pull/apk blocked by the allowlist), and a pre-baked image + --allow-host do not compose in smolvm 1.3.2 (new risk 8). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix the bugs that stopped the probe scripts from running as written; all four now run end-to-end on smolvm 1.3.2 (verified): - 00-preflight: printf header starting with "--" was parsed as an option; use '%s\n'. - 01-filesystem: omitted --net so the image pull failed. Bake a --from artifact once (cached) and boot every check from it — no network, no allowlist. Marker cleanup is now interrupt-safe via the trap. - 02-network: apk add curl is blocked by the allowlist (would falsely report every case blocked) and the image pull is itself subject to the allowlist. Use busybox wget with a block-vs-HTTP-error classifier, allowlist the Docker registry hosts so the pull succeeds, run the matrix in a single boot under smolvm --timeout. - 03-agents: replace the host `timeout` (absent on macOS, always false-positive) with smolvm --timeout; boot Step B offline from the --from artifact; allowlist package mirrors + registry for Step A install; make Vibe package overridable via VIBE_NPM_PKG (@mistralai/vibe 404s). Still requires API keys to produce a provider verdict. Findings docs and synthesis updated to note the fixes are now in the scripts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
PHAX Run Review Handoff
Generated by PHAX.
Run Review Handoff
Run summary
phax/smolvm-isolation-spike--phase-05Global File Reconciliation
Run: phax.smolvm-isolation-spike
Global unplanned changes
phax.json(touched in: phase-01)Global missing planned changes
None.
Global review attention points
phax.json(unplanned) — see phase-01/phase-handoff.md for detailsspikes/smolvm/README.md(extra-touch) — see phase-01/phase-handoff.md for detailsDeviations not explained in any handoff
None.
Plan compliance review
Compliance Review — smolvm isolation spike
Run-level verdict: conformant-with-deviations
All five phases delivered their planned artifacts with correct scope boundaries. The single deviation — an unplanned touch to
phax.jsonin phase-01 — is fully explained in that phase's handoff (oxfmt reformatting of the pre-existingagentCommandsarray; content identical) and carries no behavioral consequence.Per-phase findings
Phase phase-01 — Spike scaffold and smolvm preflight
Verdict: conformant-with-deviations
spikes/smolvm/README.md,spikes/smolvm/00-preflight.sh, andspikes/smolvm/findings/TEMPLATE.mdare all present with correct content. The preflight script usesset -eu, confirmssmolvmon PATH, prints version/arch, and exits non-zero if absent without booting a VM.phax.jsonappears in the file reconciliation as "unplanned". The handoff explains it: oxfmt reformatted the pre-existingagentCommandsarray from multi-line to single-line; content (including thesmolvmentry) is unchanged. The explanation is credible and the change is inconsequential.fastgate confirms repo health.spikes/.chore(spike): scaffold smolvm isolation spike and preflightmatches plan exactly.agentCommandsbefore this phase, and gate status.Phase phase-02 — Filesystem isolation probe
Verdict: conformant
spikes/smolvm/01-filesystem.shimplements five named checks (workspace round-trip, host HOME invisible, host repo root invisible, host /etc isolation, read-only mount rejection).spikes/smolvm/findings/01-filesystem.mdhas a fully populated## Procedurewith## Results/## Verdictintentionally left empty per the spike's execution-model caveat.spikes/.test(spike): add smolvm filesystem isolation probematches plan exactly.-v HOST:CONTAINER[:ro]), host paths probed, empty Results/Verdict explained, note that--netmust be explicit in phase-03.Phase phase-03 — Network per-domain allowlist probe
Verdict: conformant
spikes/smolvm/02-network.shimplements the five-case matrix (allowed-by-name, denied-by-name, denied-by-hardcoded-IP, allowed-by-IP, ICMP). Case 3 is labelled the decisive test. IP resolution usesnslookup/digwith a hard-coded fallback.spikes/smolvm/findings/02-network.mdframes both crux questions in## Verdict; Results/Verdict left empty.test(spike): add smolvm per-domain network allowlist probematches plan exactly.--allow-hostsyntax, all five cases documented, case 3 clearly identified as go/no-go signal, smolvm flags confirmed, note aboutapk add curlneeded in guest.Phase phase-04 — Agents-in-guest execution probe
Verdict: conformant
spikes/smolvm/03-agents.shis parameterized over three providers (claude,codex,vibe). Step A runs with--net --allow-host <provider-api-domain>and env-injected keys; Step B runs with zero egress to capture denied-egress UX via a 60-second timeout wrapper.spikes/smolvm/findings/03-agents.mdhas a per-provider results table; Results/Verdict left empty.SecurityPolicy, no hard-coded secrets.src/infra/.test(spike): add smolvm in-guest agent execution probematches plan exactly.vibe/@mistralai/vibecaveat), zero-egress design rationale for Step B, pre-baked image implication, no-secrets confirmation, empty results explained.Phase phase-05 — Synthesis and go/no-go recommendation
Verdict: conformant
docs/spikes/smolvm-isolation-findings.mdsynthesizes all three probes, gives an explicit provisional go/no-go pinned to probe 02 case 3 (hard-coded-IP egress), provides a non-implementing integration sketch (adapter location, SecurityPolicy reuse,secure/isolatedlayering), specifies the widenedmcp.allowshape withArray<{ name; domains }>and a "refuse to start" policy for undeclared MCP entries, defines the capability-preamble prompt template, lists seven residual risks, and specifies the recommended next step for both go and no-go outcomes. The provisional status is clearly stated at the top of the document.docs/spikes/smolvm-isolation-findings.mdcreated (planned).spikes/smolvm/README.mdupdated with a synthesis link (listed as "optional" in the plan, present in the reconciliation as "extra-touch" for phase-05 — this is expected and permitted). No unplanned files.docs(spike): synthesize smolvm isolation findings and go/no-gomatches plan exactly.Unplanned-change ledger
phax.jsonagentCommandsarray from multi-line to single-line; content unchangedUnmet-promise ledger
None. All planned artifacts are present. All
## Results/## Verdictsections are intentionally empty per the spike's execution-model caveat (real VM runs are out-of-band), which is stated in both the plan and the phase handoffs.Attention points
## Resultsand## Verdictsections filled in from an actual run.@mistralai/vibenpm package unverified — The phase-04 harness flags this with aNOTE:comment. Before running03-agents.sh, the correct npm package name for Mistral Vibe must be confirmed; an incorrect name will cause the install to fail and produce an inconclusive finding for that provider.apkandnpm installboth need internet, so anisolatedmode with zero egress cannot install the provider CLI at boot time. The synthesis captures this as residual risk 7 (image distribution). Any follow-up implementation plan must address this before shipping.phax.jsonformatter drift — The unplanned oxfmt reformat ofphax.jsonin phase-01 is benign but indicates the formatter is running on files outside the changed set. Worth noting for future spike phases that touch only shell/docs.Phase details
phase-01 — Spike scaffold and smolvm preflight
File reconciliation
PHAX File Reconciliation
Planned to create
Unplanned files edited
Summary: Deviations detected — see sections above.
Phase handoff
What was delivered
spikes/smolvm/README.md— spike overview: three unknowns (network/FS/agents-in-guest),isolated-mode context, execution-model caveat, and harness run order.spikes/smolvm/00-preflight.sh— POSIXsh,set -eu; confirmssmolvmis on PATH, prints version, host arch/OS, binary path; probessmolvm infothensmolvm configfor VMM backend; exits non-zero with a clear message if smolvm is absent.spikes/smolvm/findings/TEMPLATE.md— findings skeleton with sections: Environment, Procedure, Results (raw output, filled from real run), Verdict (PASS/FAIL/PARTIAL + one-line conclusion), Open questions.Key decisions and why
smolvm info/ try-smolvm config/ env-var fallback for backend detection because smolvm's CLI surface is not pinned; the script degrades gracefully rather than failing if neither subcommand exists.package.jsonor any phax gate — by plan design; they are human-run harnesses only.phax.jsonwas not intentionally edited by this phase; the change detected is oxfmt reformatting the pre-existingagentCommandsarray (added in the prior commit3537e7e) from multi-line to single-line. The array content — includingsmolvm— is unchanged.Exact locations (file paths and exported names)
spikes/smolvm/README.md— human-facing spike overview and run instructionsspikes/smolvm/00-preflight.sh— preflight script (shell only, no exported symbols)spikes/smolvm/findings/TEMPLATE.md— findings template to copy for each probeWhat the next phase needs to know
findings/TEMPLATE.mdas the base shape forfindings/01-filesystem.md; fill the Procedure section, leave Results/Verdict empty for the real run.smolvmis already present insecurity.agentCommandsinphax.json(added in commit3537e7e, pre-dating this phase); no furtherphax.jsonchange is needed.phax.jsondeviation: phax flagged it as an unplanned edit. The only change is oxfmt collapsing theagentCommandsarray to one line — content is identical, no behavioral change.ignoreBinarieshints (claude,gh,usage) are unrelated to this phase.phase-02 — Filesystem isolation probe
File reconciliation
PHAX File Reconciliation
Planned to create
Summary: No deviations from the planned file lists.
Phase handoff
What was delivered
spikes/smolvm/01-filesystem.sh— POSIXshharness (set -eu) with five named checks: workspace mount + write-back, host HOME invisible, host repo root invisible, host/etcnot leaking, read-only mount rejected. Prints PASS/FAIL per check and a final tally.spikes/smolvm/findings/01-filesystem.md— findings doc based on TEMPLATE.md;## Procedurefully populated for all five checks;## Resultsand## Verdictleft empty for the real run.Key decisions and why
alpine: minimal, multi-arch (works on Apple Silicon arm64), standard shell available, no heavy pull. Phase-03 and phase-04 can reuse the same image for their initial probing..smolvm-probe-$$) written before boot, then accessed at their exact host paths from inside the guest. More robust than checking directory existence — Alpine's/homeexists but is empty; macOS/Users/simply doesn't exist in the Linux rootfs.-v HOST:CONTAINER:rosuffix (confirmed viasmolvm machine run --help). The host side is also checked as secondary confirmation — a guest write-through would create the file on the host even if the guest shell masked the error.smolvm machine run(ephemeral) used throughout. No persistent machine state. Each check is an independent VM lifecycle to avoid state leakage between checks.Exact locations (file paths and exported names)
spikes/smolvm/01-filesystem.sh— shell harness (no exported symbols)spikes/smolvm/findings/01-filesystem.md— findings doc (procedure filled, results empty)What the next phase needs to know
--netexplicitly even for the "allowed domain" case.smolvm machine run --help):--net,--allow-host <HOSTNAME>,--allow-cidr <CIDR>.## Resultsand## Verdictinfindings/01-filesystem.mdare intentionally empty — filled from a real run out-of-band per the spike's execution-model caveat.src/,phax.json, orpackage.jsonin this phase.ignoreBinarieshints (claude,gh,usage) are unrelated to this phase.phase-03 — Network per-domain allowlist probe
File reconciliation
PHAX File Reconciliation
Planned to create
Summary: No deviations from the planned file lists.
Phase handoff
What was delivered
spikes/smolvm/02-network.sh— POSIXshharness (set -eu) with five named cases: (1) allowed domain reachable by name, (2) non-allowed domain blocked by name, (3) non-allowed host by hard-coded IP (the decisive test), (4) allowed host by raw IP (reveals enforcement mechanism), (5) ICMP probe for completeness. Blocked domain IP is resolved on the host before VM boot and injected via shell variable.spikes/smolvm/findings/02-network.md— findings doc based on TEMPLATE.md;## Procedurefully populated for all five cases including the two crux questions in## Verdict;## Resultsand## Verdictleft empty for the real run.Key decisions and why
example.com/httpbin.orgas probe domains.example.comis IANA-reserved and stable.httpbin.orgis a real host with a distinct IP, confirmed reachable on the public internet, making the raw-IP bypass test meaningful.nslookup+digfallback. If both fail it warns and uses a hard-coded fallback. This avoids a DNS dependency inside the guest (which may itself be filtered) for the decisive case 3.apk add curlinside each ephemeral VM. Alpine minimal image does not ship curl. Installing it per-case is noisy but guarantees a clean, hermetic probe. Phase-04 can reuse the same pattern.THE DECISIVE TEST. The plan names this the go/no-go signal. The label and explanatory comment appear in both the script and the findings## Procedure.--netmust be given explicitly even for the "allowed domain" case. All five cases include--net.Exact locations (file paths and exported names)
spikes/smolvm/02-network.sh— shell harness (no exported symbols)spikes/smolvm/findings/02-network.md— findings doc (procedure filled, results empty)What the next phase needs to know
## Resultsand## Verdictinfindings/02-network.mdare intentionally empty — filled from a real run out-of-band per the spike's execution-model caveat.--net,--allow-host <HOSTNAME>.--allow-cidr <CIDR>is also documented and included in the flag reference comment.alpinerequiresapk add curlbefore any HTTP probe. Phase-04 agent probes will need similar in-guest setup steps for non-interactive CLI invocations.src/,phax.json, orpackage.json. All gates pass (1980 tests, typecheck, lint, format, knip, audit:architecture). Three pre-existing knipignoreBinarieshints (claude,gh,usage) are unrelated to this phase.phase-04 — Agents-in-guest execution probe
File reconciliation
PHAX File Reconciliation
Planned to create
Summary: No deviations from the planned file lists.
Phase handoff
What was delivered
spikes/smolvm/03-agents.sh— POSIXshharness (set -eu) parameterized overthree providers (
claude,codex,vibe). Two steps per provider: STEP A bootswith
--net --allow-host <provider-api-domain>and an env-injected key, installsthe CLI via
npm, runs a trivial task (hello.txtin/workspace), and confirmswrite-back on the host; STEP B boots with zero egress (no
--net) and capturesdenied-egress UX via a 60-second
timeoutwrapper.spikes/smolvm/findings/03-agents.md— findings doc based on TEMPLATE.md;## Procedurefully populated with per-provider invocation table, installation method,and key-handling notes; per-provider results table ready for filling;
## Resultsand
## Verdictleft empty for the real run.Key decisions and why
claude(Claude Code),codex(Codex CLI),vibe(MistralVibe). Confirmed from
src/domain/routing/defaults.ts— do not usemistral-vibeor
mistralas the binary name for Mistral's provider.vibenpm package@mistralai/vibeis unverified: flagged with aNOTE:comment in the script. Must be confirmed before running; if wrong, install fails
and the findings table records it.
--net(not a wrong allowlist): zero egress is the cleanestsignal. Any connection attempt fails at the kernel level; no ambiguity about whether
the allowlist is partially active.
printf '<prompt>' | codex exec -C /workspace …. Thismatches how phax's own
spawnCodexfeeds the prompt (proc.stdin.write(prompt)).timeouton STEP B: guards against silent CLI retry loops. If thetimeout fires, the finding is "CLI hangs" — which is as important as a clean error.
Exact locations (file paths and exported names)
spikes/smolvm/03-agents.sh— shell harness (no exported symbols)spikes/smolvm/findings/03-agents.md— findings doc (procedure filled, results empty)What the next phase needs to know
## Resultsand## Verdictinfindings/03-agents.mdare intentionally empty —filled from a real run out-of-band per the spike's execution-model caveat.
the capability-preamble prompt design. Key question: does the CLI exit with a clear
error, hang until timeout, or enter a retry loop?
apk add nodejs npm+npm install -g.In STEP B (no
--net),apkitself needs internet → install fails before the CLIruns. This is a real finding: production
isolated-mode likely needs a pre-bakedguest image with the CLI already installed.
--net,--allow-host,-v HOST:CONTAINER[:ro],-e KEY=VALUE. The-eflag takesKEY=VALUEpairs.ignoreBinariesconfiguration hints (claude,gh,usage) pre-existin
knip.jsonand are unrelated to this phase. Knip still exits 0 (hints, not errors).src/,phax.json, orpackage.json. All gates pass (1980 tests,typecheck, lint, format, knip, architecture, build).
phase-05 — Synthesis and go/no-go recommendation
File reconciliation
PHAX File Reconciliation
Planned to create
Optional files touched
Summary: No deviations from the planned file lists.
Phase handoff
What was delivered
docs/spikes/smolvm-isolation-findings.md— single synthesis doc: per-probesummaries (filesystem / network / agents), a provisional go/no-go pinned to probe
02 case 3 (hard-coded-IP egress), a non-implementing integration sketch for a
future
isolatedSandboxadapter undersrc/infra/providers/, the proposedwidened
mcp.allowshape, the capability-preamble prompt motivated by probe 03'sdenied-egress UX, residual risks, and the recommended next step.
spikes/smolvm/README.md— link to the synthesis added with a provisional-statusnote (optional edit listed in the plan).
Key decisions and why
## Results/## Verdictis stillempty per the spike's execution-model caveat; the doc states this at the top and
in the conclusion. Treat go/no-go as unresolved until a real microVM run fills
those sections.
decides "real boundary" vs "DNS-name filtering". The synthesis binds the go/no-go
to that one fact so a follow-up plan cannot start from a misread.
SecurityMode, no newSecurityPolicyfield.isolatedalready exists insrc/domain/security/types.tsand
src/schemas/securityConfig.ts; the stub atsrc/cli/commands/run.ts:152-158is the entire swap point.
mcp.allowwidening is deliberately breaking. Per the project's "noback-compat shims in persisted schemas" rule, the proposed shape is
Array<{ name; domains }>, andisolatedmode refuses to start if any allowedMCP entry has no declared domains. Silent allow-everything would defeat the VM.
secureandisolatedare layered. Defence-in-depth —isolatedstillapplies the provider-native flags
secureuses, inside the VM.Exact locations (file paths and exported names)
docs/spikes/smolvm-isolation-findings.md— Markdown synthesis (no exports)spikes/smolvm/README.md— Markdown (no exports)src/domain/security/types.ts—SecurityModealready includes"isolated"src/schemas/securityConfig.ts:4—SecurityProfileSchemaaccepts itsrc/cli/commands/run.ts:152-158— currentisolated-rejection stubsrc/infra/providers/{claudeCode,codexCli,mistralVibe,dispatcher}.ts— spawnsurface a future
isolatedSandbox.tswould wrapspikes/smolvm/findings/{01-filesystem,02-network,03-agents}.md— source probesWhat the next phase needs to know
what comes after: if probe 02 case 3 blocks on the real run, write the
isolated-mode implementation plan (adapter,mcp.allowwidening,capability-preamble builder, pre-baked guest-image recipe, e2e test under
pnpm test:e2e:real); if it succeeds (IP literal escapes), stop and re-scope.provisional in the synthesis.
src/,phax.json, orpackage.json— strictly doc-only.