Skip to content

Commit 336be46

Browse files
aviadshiberaviadsTaboolaclaude
authored
feat(preflight): detect and surface Podman machine hypervisor provider (#438)
* feat(preflight): detect and surface Podman machine hypervisor provider Adds get_podman_machine_provider() (compat.sh) to detect whether a macOS Podman machine is running on "applehv" (vfkit/AVF) or "libkrun" (krunkit), following up on PR #433's process-matching fix. Informational only, no behavioral branch: logged at launch and preflight, and recorded as a new status.json field (machine_provider) per the Dashboard Sync Rule. Adds docs/KRUNKIT-PROVIDER.md as the opt-in adoption guide referenced from Issue #409's research comment (install, migration cost, mitigation status under libkrun). Leaves #409 open — benchmarking and sleep/wake burn-in still need a real libkrun machine, which wasn't available for this change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(dashboard): add machine_provider to typed AgentStatus test fixtures CI's dashboard-build "Typecheck server" step (bunx tsc --noEmit) failed on the 3 test files that build a typed AgentStatus fixture via a Partial<AgentStatus>-override helper (gist-history, health-rules, health): without a base value, the merged type inferred machine_provider as `string | null | undefined`, and `undefined` isn't assignable to the interface's `string | null`. Other status.test.ts-style fixtures using untyped JSON.stringify({...}) were unaffected — grepped for AgentStatus usage across dashboard/{server,ui} tests to confirm these were the only 3. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: address review finding: resolve KAPSIS_PODMAN_MACHINE in preflight provider report Ensemble review (noncritical): the preflight provider line passed the literal "podman-machine-default" as an explicit argument, overriding get_podman_machine_provider's own KAPSIS_PODMAN_MACHINE fallback chain — inconsistent with the launch-agent.sh call site and with how podman-health.sh / kapsis-cleanup.sh / vfkit-watchdog.sh resolve the machine name. Drop the argument so a custom KAPSIS_PODMAN_MACHINE gets the right provider line. (check_podman's pre-existing inspect/state checks also hardcode the literal — out of scope for this PR.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: address noncritical review findings on provider detection tests - test-compat.sh: cover the no-timeout-cmd fallback branch of get_podman_machine_provider (_KAPSIS_TIMEOUT_CMD empty) - test-compat.sh: assert KAPSIS_PODMAN_MACHINE is resolved as the default machine name via an argv-capturing fake podman - test-status-reporting.sh: validate full status-file JSON with python3 inside both machine_provider tests - test-preflight-check.sh: assert provider line absent on Linux, and add macOS test that check_podman passes without a provider line when the VMType query fails Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Aviad Shiber <aviad.s@taboola.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 1ede392 commit 336be46

16 files changed

Lines changed: 531 additions & 1 deletion

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ kapsis/
156156
| Audit system | `docs/AUDIT-SYSTEM.md` |
157157
| Network isolation | `docs/NETWORK-ISOLATION.md` |
158158
| K8s backend | `docs/K8S-BACKEND.md` |
159+
| Podman `libkrun`/`krunkit` provider (opt-in, macOS) | `docs/KRUNKIT-PROVIDER.md` |
159160
| Installation | `docs/INSTALL.md` |
160161
| Initial setup | `docs/SETUP.md` |
161162
| Logging & testing | `CONTRIBUTING.md` |

dashboard/server/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,8 @@
2626
// Issue #430 sync: AgentStatus gained transcript_content_missing (defect 2
2727
// instrumentation) and the new ArtifactEntry type (defect 3, side-channel
2828
// artifact listing) — both defined in dashboard/shared/src/index.ts.
29+
//
30+
// Sync added AgentStatus.machine_provider for Issue #409 (Podman machine
31+
// hypervisor backend — "applehv"/"libkrun" — detected at launch by
32+
// scripts/lib/compat.sh::get_podman_machine_provider, informational only).
2933
export * from "@kapsis/dashboard-shared";

dashboard/server/tests/gist-history.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ function statusFixture(overrides: Partial<AgentStatus> = {}): AgentStatus {
3737
uncommitted_files: 0,
3838
heartbeat_at: null,
3939
error_type: null,
40+
machine_provider: null,
4041
...overrides,
4142
};
4243
}

dashboard/server/tests/health-rules.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const baseStatus = (over: Partial<AgentStatus> = {}): AgentStatus => ({
1212
push_status: null, local_commit: null, remote_commit: null,
1313
push_fallback_command: null, commit_status: null, commit_sha: null,
1414
uncommitted_files: 0, heartbeat_at: null, error_type: null,
15+
machine_provider: null,
1516
...over,
1617
});
1718

dashboard/server/tests/health.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const base = (overrides: Partial<AgentStatus> = {}): AgentStatus => ({
3232
uncommitted_files: 0,
3333
heartbeat_at: null,
3434
error_type: null,
35+
machine_provider: null,
3536
...overrides,
3637
});
3738

dashboard/shared/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ export interface AgentStatus {
5959
// scripts/lib/transcript.sh::_transcript_is_boilerplate_only. Optional/
6060
// absent on status files written before this field existed.
6161
transcript_content_missing?: boolean;
62+
//
63+
// Podman machine hypervisor backend detected at launch (Issue #409):
64+
// "applehv" (Apple Virtualization.framework, vfkit) or "libkrun"
65+
// (Hypervisor.framework, krunkit). Informational only — null on Linux
66+
// or when detection failed/was skipped.
67+
machine_provider: "applehv" | "libkrun" | string | null;
6268
}
6369

6470
export type AgentKey = { project: string; agentId: string };

dashboard/ui/src/views/AgentDetail.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,9 @@ function OverviewTab({ status, health }: { status: AgentStatus; health: AgentHea
182182
<div className="card">
183183
<h3>Worktree</h3>
184184
<div className="sub" style={{ fontFamily: "var(--mono)", wordBreak: "break-all" }}>{status.worktree_path ?? "—"}</div>
185+
{status.machine_provider && (
186+
<div className="sub" style={{ marginTop: 4 }}>Podman provider: <code>{status.machine_provider}</code></div>
187+
)}
185188
</div>
186189
{status.push_fallback_command && (
187190
<div className="card" style={{ gridColumn: "1 / -1" }}>

docs/KRUNKIT-PROVIDER.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Podman `libkrun`/`krunkit` Machine Provider (Opt-In)
2+
3+
Tracks [Issue #409](https://github.com/aviadshiber/kapsis/issues/409). This document is the
4+
opt-in adoption guide referenced from `docs/STATUS-TRACKING.md`.
5+
6+
## Background
7+
8+
Kapsis's macOS mitigations for virtio-fs mount drops (sleep-prevention `caffeinate`, the
9+
vfkit watchdog, status-volume mirroring — see `CLAUDE.md`'s Mount Failure Detection section)
10+
exist because of a bug in Apple's Virtualization.framework (AVF) virtio-fs implementation,
11+
tracked upstream as Apple Feedback FB16008360. Podman's `libkrun` machine provider (hypervisor
12+
process `krunkit`) bypasses AVF entirely — it talks to `Hypervisor.framework` directly and
13+
implements its own virtio-fs server — so it structurally cannot hit that bug class. Podman
14+
made `libkrun` the **default** macOS provider in v6.0.0 (2026-06-24).
15+
16+
**Kapsis does not default to `libkrun`.** As of this writing the switch is recent, there's an
17+
open filesystem-performance regression report against it
18+
([podman#29087](https://github.com/containers/podman/issues/29087)), and sleep/wake behavior
19+
under `libkrun` is publicly unvalidated. Kapsis's existing mitigation stack keeps working
20+
under either provider (see the table below) — nothing is removed for `libkrun` adopters.
21+
22+
## Requirements
23+
24+
- macOS 14+, **Apple Silicon only** (Intel Macs cannot use `libkrun`)
25+
- `krunkit` >= 1.3.1 (earlier versions had virtio-fs permission-semantics bugs, fixed through
26+
June 2026 in [libkrun#759](https://github.com/libkrun/libkrun/pull/759),
27+
[libkrun#734](https://github.com/libkrun/libkrun/pull/734))
28+
- Podman >= 6.0.0
29+
30+
## Installing krunkit
31+
32+
`brew install podman` does **not** bundle `krunkit`. Install it separately:
33+
34+
```bash
35+
brew tap libkrun/krun
36+
brew install krunkit
37+
```
38+
39+
(The Podman GitHub `.pkg` installer bundles `krunkit`, so this step is only needed for
40+
Homebrew-installed Podman.)
41+
42+
## Trying it
43+
44+
There is no in-place conversion — a new machine is required, and Podman machines from
45+
different providers coexist (only one runs at a time):
46+
47+
```bash
48+
podman machine stop # stop the current machine
49+
podman machine init --provider libkrun kapsis-libkrun
50+
podman machine start kapsis-libkrun
51+
```
52+
53+
**Cost of switching:** images and named volumes (including Kapsis's per-agent Maven/Gradle
54+
caches and `kapsis-*-status` volumes) live inside the machine's disk, not shared across
55+
machines. Expect to rebuild the Kapsis base image (`./scripts/build-image.sh`) and warm caches
56+
again on a new `libkrun` machine.
57+
58+
**No Rosetta under `libkrun`** (Rosetta requires AVF). Kapsis builds arch-native arm64 images,
59+
so this has low impact — it would only matter if you deliberately run amd64-only images.
60+
61+
## Verifying detection
62+
63+
Kapsis detects the active provider at launch (macOS + Podman backend only) and records it in
64+
`status.json` as `machine_provider` (see `docs/STATUS-TRACKING.md`):
65+
66+
```bash
67+
./scripts/launch-agent.sh ~/project --agent claude --task "..." &
68+
./scripts/kapsis-status.sh --json | grep machine_provider
69+
```
70+
71+
You can also query it directly:
72+
73+
```bash
74+
podman machine inspect kapsis-libkrun --format '{{.VMType}}'
75+
```
76+
77+
## Mitigation status under `libkrun`
78+
79+
| Mitigation | Under `libkrun` | Status |
80+
|---|---|---|
81+
| vfkit/krunkit watchdog (Issue #303) | Provider-agnostic — matches both `vfkit` and `krunkit` process names | Active |
82+
| Sleep prevention (`caffeinate`, Issue #276) | Root-cause trigger (AVF) doesn't apply, but unvalidated across sleep/wake under `libkrun` | Active (not yet a retirement candidate) |
83+
| Status volume mirroring (Issue #276) | Exists solely for AVF bind-mount drops | Active (not yet a retirement candidate) |
84+
| Pre-launch/entrypoint/liveness mount probes | Provider-agnostic | Active |
85+
86+
None of these are gated on provider today — do not assume a `libkrun` host is exempt from a
87+
mount-failure exit code (4) until there is field evidence, not just theory, that it can't
88+
recur under `libkrun`.
89+
90+
## Known open issues (tracked upstream, not by Kapsis)
91+
92+
- FS-heavy workload performance regression after upgrading to Podman 6.0's `libkrun` default:
93+
[podman#29087](https://github.com/containers/podman/issues/29087)
94+
- Only one Podman machine runs at a time, complicating A/B testing:
95+
[podman#26281](https://github.com/containers/podman/issues/26281)
96+
97+
## Recommendation
98+
99+
Treat this as an **opt-in experiment**, not a default. If you try it, watch for the same
100+
symptoms the existing mitigations were built for (spurious `EACCES`/`ENOENT` under load, mount
101+
drops after sleep/wake) and report back on Issue #409 — a multi-week clean run is the bar for
102+
Kapsis to consider retiring any `applehv`-specific mitigation or recommending `libkrun` as the
103+
default on eligible hosts.

docs/STATUS-TRACKING.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,10 @@ Configuration is stored in `configs/tool-phase-mapping.yaml`.
180180
| 137 | Killed by liveness monitor (SIGKILL) | Check agent for hangs |
181181
| 143 | Killed by liveness monitor (SIGTERM) | Check agent for hangs |
182182

183+
### Podman Machine Provider (macOS)
184+
185+
`machine_provider` records which Podman machine hypervisor backend the launch detected: `"applehv"` (Apple Virtualization.framework, the `vfkit` process) or `"libkrun"` (Hypervisor.framework, the `krunkit` process — Podman's default since v6.0). It's `null` on Linux, for non-Podman backends, or if detection failed. This field is informational only — it doesn't change Kapsis's behavior — but it explains, for example, why a host on `libkrun` may not need the sleep-prevention/status-mirroring mitigations that exist specifically for `applehv`'s AVF virtio-fs issue (see [Issue #409](https://github.com/aviadshiber/kapsis/issues/409)). See `docs/KRUNKIT-PROVIDER.md` for the opt-in adoption path.
186+
183187
## Agent Gist (Live Activity Summary)
184188

185189
During long "thinking" periods, the standard status message may become stale. The **gist** feature provides a signaling file that agents can update in real-time to communicate what they're currently working on.

scripts/launch-agent.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3028,6 +3028,19 @@ main() {
30283028

30293029
# Update status with sandbox mode and worktree path now that we know them
30303030
status_init "$project_name" "$AGENT_ID" "$BRANCH" "$SANDBOX_MODE" "${WORKTREE_PATH:-}" || true
3031+
3032+
# Detect Podman machine provider (Issue #409): applehv vs libkrun affects
3033+
# which virtio-fs mitigations are structurally relevant to this host.
3034+
# Informational only — logs + status.json field, no behavioral branch.
3035+
if [[ "$BACKEND" == "podman" ]] && is_macos; then
3036+
local _machine_provider
3037+
_machine_provider=$(get_podman_machine_provider 2>/dev/null || true)
3038+
if [[ -n "$_machine_provider" ]]; then
3039+
log_info "Podman machine provider: $_machine_provider"
3040+
status_set_machine_provider "$_machine_provider"
3041+
fi
3042+
fi
3043+
30313044
status_phase "preparing" 18 "Sandbox ready" || true
30323045

30333046
# Podman-specific: generate volume mounts, env vars, secrets env-file

0 commit comments

Comments
 (0)