Skip to content

Commit cc8624b

Browse files
authored
fix: restore fresh runtime and resident outcomes (#52)
Co-authored-by: Joseph Yaksich <gitcommit90@users.noreply.github.com>
1 parent 5437442 commit cc8624b

22 files changed

Lines changed: 282 additions & 39 deletions

CHANGELOG.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.0.35] - 2026-08-01
11+
12+
### Fixed
13+
14+
- Preserved the elevated Windows setup transaction's restart-required result
15+
before the signed-in process probes WSL, including when Windows returns an
16+
unreliable zero child-process exit code, and kept terminal setup state
17+
visible while the tracked parent process closes.
18+
- Restored resident internet on nested Linux hosts by installing the narrow
19+
AppArmor address-family permissions required by `crun`, bypassing
20+
netavark's unavailable user-bus DNS scope, and requiring socket creation,
21+
public DNS, and TCP egress before a resident computer becomes ready.
22+
- Made the essential outcome, blocker-resolution, and Skipper-escalation
23+
playbooks active in every resident turn so imperative setup requests are
24+
executed instead of answered with tutorials, and evidenced machine-wide
25+
network failures are escalated directly for repair.
26+
1027
## [0.0.34] - 2026-08-01
1128

1229
### Fixed
@@ -968,7 +985,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
968985
notarization, stapled tickets, Gatekeeper verification, persistent
969986
Application Support, and isolated Apple container machines.
970987

971-
[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.34...HEAD
988+
[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.35...HEAD
989+
[0.0.35]: https://github.com/gitcommit90/1Helm/compare/v0.0.34...v0.0.35
972990
[0.0.34]: https://github.com/gitcommit90/1Helm/compare/v0.0.33...v0.0.34
973991
[0.0.33]: https://github.com/gitcommit90/1Helm/compare/v0.0.32...v0.0.33
974992
[0.0.32]: https://github.com/gitcommit90/1Helm/compare/v0.0.31...v0.0.32

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ A fresh data directory opens first-run setup. The source runtime defaults to
313313
| `PORT` | `8123` | HTTP/WebSocket control-plane port. |
314314
| `CTRL_DATA_DIR` | `./data` | Databases, routing state, uploads, and non-OCI development/Apple workspace mirrors. |
315315
| `HELM_CHANNEL_COMPUTER_BACKEND` | `apple` on macOS, `oci` on Linux and Windows | Host isolation backend; `native` and `mock` are explicit development/test overrides. |
316-
| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.34` | Versioned channel-machine image contract. |
316+
| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.35` | Versioned channel-machine image contract. |
317317

318318
### Agent-first JSON CLI
319319

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "1helm",
33
"productName": "1Helm",
4-
"version": "0.0.34",
4+
"version": "0.0.35",
55
"private": true,
66
"type": "module",
77
"license": "AGPL-3.0-only",

scripts/1helm-oci-runtime

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,10 @@ print(bridge, subnet)
270270
}
271271

272272
ensure_network() {
273-
local owner="$1" installation network labels bridge_name
273+
local owner="$1" installation network labels bridge_name network_path
274274
installation="$(owner_installation "$owner")"
275275
network="1helm-$installation"
276+
network_path="$NETWORKS_ROOT/$network.json"
276277
# Linux IFNAMSIZ is 15 chars; keep a stable, installation-scoped bridge name.
277278
bridge_name="1h${installation:0:12}"
278279
if "${PODMAN[@]}" network exists "$network"; then
@@ -283,17 +284,43 @@ labels=json.loads(sys.argv[1] or "{}")
283284
if labels.get("com.1helm.managed") != "true" or labels.get("com.1helm.installation") != sys.argv[2]:
284285
raise SystemExit("network ownership labels do not match")
285286
PY
287+
# Netavark 1.4 starts aardvark-dns through a user systemd scope. A root
288+
# helper invoked by the system service has no user bus, so the advertised
289+
# gateway DNS listener never exists and every resident lookup times out.
290+
# These are 1Helm-owned JSON network definitions; disable only that broken
291+
# plugin and let containers inherit the host's working resolvers instead.
292+
if [[ -f "$network_path" && ! -L "$network_path" ]] \
293+
&& grep -q '"dns_enabled"[[:space:]]*:[[:space:]]*true' "$network_path"; then
294+
python3 - "$network_path" <<'PY'
295+
import json, os, stat, sys, tempfile
296+
path=sys.argv[1]
297+
info=os.lstat(path)
298+
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode) or info.st_uid != 0 or info.st_gid != 0:
299+
raise SystemExit("owned network definition is not a safe root-owned file")
300+
with open(path, encoding="utf-8") as source: network=json.load(source)
301+
labels=network.get("labels") or network.get("Labels") or {}
302+
if labels.get("com.1helm.managed") != "true": raise SystemExit("refusing to change an unmanaged network")
303+
network["dns_enabled"]=False
304+
descriptor,candidate=tempfile.mkstemp(prefix=".1helm-network-",dir=os.path.dirname(path))
305+
try:
306+
with os.fdopen(descriptor,"w",encoding="utf-8") as destination:
307+
json.dump(network,destination,indent=5); destination.write("\n"); destination.flush(); os.fsync(destination.fileno())
308+
os.chmod(candidate,stat.S_IMODE(info.st_mode)); os.chown(candidate,0,0); os.replace(candidate,path)
309+
finally:
310+
if os.path.exists(candidate): os.unlink(candidate)
311+
PY
312+
fi
286313
else
287314
# Prefer a stable IFNAMSIZ-safe bridge name when the backend supports Docker-
288315
# style bridge options (netavark). Debian bookworm's CNI backend rejects
289316
# com.docker.network.bridge.name; fall back to a labeled default bridge and
290317
# let ensure_guest_egress read the real interface from network inspect.
291318
create_out=""
292-
if ! create_out="$("${PODMAN[@]}" network create --disable-dns=false \
319+
if ! create_out="$("${PODMAN[@]}" network create --disable-dns \
293320
--opt "com.docker.network.bridge.name=${bridge_name}" \
294321
--label com.1helm.managed=true --label "com.1helm.installation=$installation" "$network" 2>&1)"; then
295322
if grep -qiE 'unsupported (bridge )?network option|unknown network option' <<<"$create_out"; then
296-
"${PODMAN[@]}" network create --disable-dns=false \
323+
"${PODMAN[@]}" network create --disable-dns \
297324
--label com.1helm.managed=true --label "com.1helm.installation=$installation" "$network" >/dev/null \
298325
|| die "channel network could not be created"
299326
else
@@ -671,27 +698,51 @@ create_container() {
671698
}
672699

673700
start_container() {
674-
local name="$1" owner="$2" state network
701+
local name="$1" owner="$2" state network root network_ready container_ok=0 network_ok=0
675702
verify_container "$name" "$owner"
676-
network="$(network_name "$owner")"
703+
root="$(channel_root "$name")"
704+
network_ready="$root/network-ready-v1"
705+
network="$(ensure_network "$owner")"
677706
# Re-assert egress on every start: Docker and host firewall reloads can drop
678707
# our FORWARD/NAT inserts after boot or package updates.
679708
if "${PODMAN[@]}" network exists "$network"; then ensure_guest_egress "$network"; fi
680709
state="$(container_state "$name")"
710+
# An update from the earlier aardvark-backed contract must restart a running
711+
# retained container once so Podman regenerates /etc/resolv.conf from the
712+
# now host-inherited network definition.
713+
if [[ "$state" == running && ! -f "$network_ready" ]]; then
714+
"${PODMAN[@]}" stop --time 90 "$name" >/dev/null
715+
state="stopped"
716+
fi
681717
if [[ "$state" != running ]]; then "${PODMAN[@]}" start "$name" >/dev/null; fi
682718
for _ in {1..100}; do
683719
# Podman can briefly fail name-based --user lookup while it reconstructs
684720
# its ephemeral runroot after a host reboot even though the persisted
685721
# image passwd database is intact. These identities are pinned by the
686722
# installed runtime manifest and image contract, so use their numeric form
687723
# for the readiness probe and every subsequent exec boundary.
688-
if "${PODMAN[@]}" exec --user 0:0 "$name" /bin/sh -c 'test "$(cat /var/lib/1helm/owner)" = "$1" && test -d /workspace && test -d /home/agent' 1helm-start "$owner" >/dev/null 2>&1; then
724+
if "${PODMAN[@]}" exec --user 0:0 "$name" /bin/sh -c \
725+
'test "$(cat /var/lib/1helm/owner)" = "$1" && test -d /workspace && test -d /home/agent' \
726+
1helm-start "$owner" >/dev/null 2>&1; then
689727
verify_container "$name" "$owner"
690-
return
728+
container_ok=1
729+
break
691730
fi
692731
sleep 0.1
693732
done
694-
die "container did not pass its ownership and storage health check"
733+
[[ "$container_ok" -eq 1 ]] || die "container did not pass its ownership and storage health check"
734+
if [[ ! -f "$network_ready" ]]; then
735+
for _ in {1..5}; do
736+
if timeout 7 "${PODMAN[@]}" exec --user "$AGENT_UID:$AGENT_GID" "$name" /usr/bin/python3 -c \
737+
'import socket; s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.close(); socket.getaddrinfo("example.com",443,type=socket.SOCK_STREAM); c=socket.create_connection(("example.com",443),3); c.close()' \
738+
>/dev/null 2>&1; then network_ok=1; break; fi
739+
sleep 0.2
740+
done
741+
[[ "$network_ok" -eq 1 ]] || die "container did not pass its network socket, public DNS, and TCP egress health check"
742+
printf '%s\n' 'host-dns-and-public-tcp-v1' >"$network_ready"
743+
chown root:root "$network_ready"
744+
chmod 0600 "$network_ready"
745+
fi
695746
}
696747

697748
stop_container() {
@@ -902,7 +953,7 @@ case "$operation" in
902953
;;
903954
ready)
904955
(($# == 0)) || die "ready takes no arguments"
905-
for command in find flock getfacl iptables podman python3 setfacl sha256sum stat tar; do command -v "$command" >/dev/null || die "missing $command"; done
956+
for command in find flock getfacl iptables podman python3 setfacl sha256sum stat tar timeout; do command -v "$command" >/dev/null || die "missing $command"; done
906957
[[ -r "$CONTAINERFILE" ]] || die "installed OCI image recipe is missing"
907958
"${PODMAN[@]}" info >/dev/null
908959
printf '{"ready":true,"version":"%s","engine":"podman"}\n' "$RUNTIME_VERSION"

scripts/install-wsl-runtime.ps1

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,50 @@ function Fail-Setup {
125125
exit $Code
126126
}
127127

128+
function Read-ReportedSetupStatus {
129+
$path = $env:HELM_WSL_SETUP_STATUS
130+
if (-not $path -or -not (Test-Path -LiteralPath $path -PathType Leaf)) { return $null }
131+
try {
132+
return Get-Content -LiteralPath $path -Raw | ConvertFrom-Json
133+
} catch {
134+
return $null
135+
}
136+
}
137+
138+
# The elevated process exit code is not authoritative across every Windows
139+
# PowerShell/UAC host. The shared status file is the transaction record: in
140+
# particular, never continue into the signed-in-user WSL probe after the
141+
# elevated pass has declared that a reboot is required.
142+
function Get-HostSetupOutcome {
143+
param([Nullable[int]]$ExitCode)
144+
$reported = Read-ReportedSetupStatus
145+
if ($null -ne $reported -and $reported.status -eq "restart_required") {
146+
$step = if ($reported.step) { [string]$reported.step } else { "WSL 2 features are enabled. Restart Windows once, then retry 1Helm computer setup." }
147+
$errorMessage = if ($reported.error) { [string]$reported.error } else { "Windows restart required to finish enabling WSL 2." }
148+
return [pscustomobject]@{ Status = "restart_required"; Step = $step; Detail = $errorMessage }
149+
}
150+
if ($null -ne $reported -and $reported.status -eq "failed") {
151+
$detail = if ($reported.error) { [string]$reported.error } elseif ($reported.step) { [string]$reported.step } else { "The administrator-approved WSL host setup failed." }
152+
return [pscustomobject]@{ Status = "failed"; Step = $detail; Detail = $detail }
153+
}
154+
if ($null -eq $ExitCode) {
155+
$detail = "Administrator approval was cancelled or Windows did not start the elevated WSL host setup."
156+
return [pscustomobject]@{ Status = "failed"; Step = $detail; Detail = $detail }
157+
}
158+
if ($ExitCode -eq 10) {
159+
return [pscustomobject]@{
160+
Status = "restart_required"
161+
Step = "WSL 2 features are enabled. Restart Windows once, then retry 1Helm computer setup."
162+
Detail = "Windows restart required to finish enabling WSL 2."
163+
}
164+
}
165+
if ($ExitCode -ne 0) {
166+
$detail = "The administrator-approved WSL host setup failed with exit code $ExitCode."
167+
return [pscustomobject]@{ Status = "failed"; Step = $detail; Detail = $detail }
168+
}
169+
return [pscustomobject]@{ Status = "continue"; Step = ""; Detail = "" }
170+
}
171+
128172
if ($HostSetup) {
129173
try {
130174
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
@@ -217,25 +261,14 @@ try {
217261
$hostArguments += @("-StatusPath", ('"{0}"' -f $statusArg))
218262
}
219263
$hostProcess = Start-Process -FilePath "powershell.exe" -ArgumentList ($hostArguments -join " ") -Verb RunAs -Wait -PassThru
220-
if ($null -eq $hostProcess -or $null -eq $hostProcess.ExitCode) {
221-
Fail-Setup "Administrator approval was cancelled or Windows did not start the elevated WSL host setup."
222-
}
223-
if ($hostProcess.ExitCode -eq 10) {
224-
Write-SetupStatus -Status "restart_required" -Step "WSL 2 features are enabled. Restart Windows once, then retry 1Helm computer setup." -Progress 20
225-
Write-Host "WSL 2 features are enabled. Restart Windows once, then retry 1Helm computer setup."
264+
$hostExitCode = if ($null -eq $hostProcess) { $null } else { $hostProcess.ExitCode }
265+
$hostOutcome = Get-HostSetupOutcome -ExitCode $hostExitCode
266+
if ($hostOutcome.Status -eq "restart_required") {
267+
Write-SetupStatus -Status "restart_required" -Step $hostOutcome.Step -Progress 20 -ErrorMessage $hostOutcome.Detail
268+
Write-Host $hostOutcome.Step
226269
exit 10
227270
}
228-
if ($hostProcess.ExitCode -ne 0) {
229-
$detail = "The administrator-approved WSL host setup failed with exit code $($hostProcess.ExitCode)."
230-
if ($env:HELM_WSL_SETUP_STATUS -and (Test-Path -LiteralPath $env:HELM_WSL_SETUP_STATUS)) {
231-
try {
232-
$reported = Get-Content -LiteralPath $env:HELM_WSL_SETUP_STATUS -Raw | ConvertFrom-Json
233-
if ($reported.error) { $detail = [string]$reported.error }
234-
elseif ($reported.step -and $reported.status -eq "failed") { $detail = [string]$reported.step }
235-
} catch { }
236-
}
237-
Fail-Setup $detail
238-
}
271+
if ($hostOutcome.Status -eq "failed") { Fail-Setup $hostOutcome.Detail }
239272
if (-not (Test-PinnedWslRuntime)) {
240273
Fail-Setup "Microsoft WSL $wslVersion is not ready in the signed-in user's session."
241274
}

site/public/apply-linux-release.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ HOST_CONTRACT_PATHS=(
2020
/usr/libexec/1helm-oci-runtime
2121
/etc/1helm/oci-runtime-v1.conf
2222
/etc/sudoers.d/1helm-oci-runtime
23+
/etc/apparmor.d/local/crun
2324
/etc/tmpfiles.d/1helm-oci.conf
2425
/usr/lib/1helm-oci/Containerfile.oci
2526
/usr/lib/1helm-oci/channel-machine.oci.tar

site/public/install-oci-runtime.sh

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,32 @@ fi
3030
for command in crun find flock getfacl iptables podman python3 setfacl sha256sum stat sudo tar visudo; do command -v "$command" >/dev/null || { echo "Missing OCI prerequisite after setup: $command" >&2; exit 1; }; done
3131
[[ "$(stat -fc %T /sys/fs/cgroup)" == cgroup2fs ]] || { echo "1Helm OCI resource controls require cgroup v2." >&2; exit 1; }
3232

33+
# Ubuntu ships an AppArmor attachment for /usr/bin/crun whose nominally
34+
# unconfined profile can still inherit the outer container host's address-family
35+
# restrictions. On a nested systemd host that manifests inside every resident
36+
# as EPERM from socket(2), even though the 1Helm host itself has internet. Add
37+
# only the missing address-family grants to crun's supported local include; the
38+
# outer host/container profile remains the isolation boundary.
39+
NESTED_CRUN_PROFILE="/etc/apparmor.d/local/crun"
40+
NESTED_CRUN_MARKER="# Managed by 1Helm: allow resident OCI network sockets on nested hosts."
41+
container_virt="$(systemd-detect-virt --container 2>/dev/null || true)"
42+
apparmor_enabled="$(cat /sys/module/apparmor/parameters/enabled 2>/dev/null || true)"
43+
if [[ -n "$container_virt" && "$container_virt" != none && "$apparmor_enabled" =~ ^[Yy]$ && -r /etc/apparmor.d/crun ]]; then
44+
command -v apparmor_parser >/dev/null || { echo "Nested AppArmor OCI setup requires apparmor_parser." >&2; exit 1; }
45+
if [[ ! -e "$NESTED_CRUN_PROFILE" ]]; then
46+
install -d -o root -g root -m 0755 "$(dirname "$NESTED_CRUN_PROFILE")"
47+
profile_candidate="$(mktemp)"
48+
printf '%s\nnetwork inet,\nnetwork inet6,\n' "$NESTED_CRUN_MARKER" >"$profile_candidate"
49+
install -o root -g root -m 0644 "$profile_candidate" "$NESTED_CRUN_PROFILE"
50+
rm -f -- "$profile_candidate"
51+
elif ! grep -Eq '^[[:space:]]*network[[:space:]]+inet[[:space:]]*,' "$NESTED_CRUN_PROFILE" \
52+
|| ! grep -Eq '^[[:space:]]*network[[:space:]]+inet6[[:space:]]*,' "$NESTED_CRUN_PROFILE"; then
53+
echo "The existing AppArmor local/crun policy does not permit resident IPv4 and IPv6 sockets; 1Helm left the custom policy unchanged." >&2
54+
exit 1
55+
fi
56+
apparmor_parser -r /etc/apparmor.d/crun
57+
fi
58+
3359
install -d -o root -g root -m 0755 /etc/1helm "$RECIPE_ROOT" /usr/libexec
3460
install -d -o root -g root -m 0711 "$STATE_ROOT/runtime/oci" "$STATE_ROOT/runtime/oci/channels"
3561
install -d -o root -g root -m 0700 "$STATE_ROOT/runtime/oci/storage" "$STATE_ROOT/runtime/oci/backups" "$STATE_ROOT/runtime/oci/networks"

site/public/install.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ HOST_CONTRACT_PATHS=(
1414
/usr/libexec/1helm-oci-runtime
1515
/etc/1helm/oci-runtime-v1.conf
1616
/etc/sudoers.d/1helm-oci-runtime
17+
/etc/apparmor.d/local/crun
1718
/etc/tmpfiles.d/1helm-oci.conf
1819
/usr/lib/1helm-oci/Containerfile.oci
1920
/usr/lib/1helm-oci/channel-machine.oci.tar

site/public/uninstall-host.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ done
3232
systemctl disable --now 1helm-update.path 2>/dev/null || true
3333
rm -f -- /etc/systemd/system/1helm.service /etc/systemd/system/1helm-update.service /etc/systemd/system/1helm-update.path
3434
rm -f -- /etc/sudoers.d/1helm-oci-runtime /etc/1helm/oci-runtime-v1.conf /usr/libexec/1helm-oci-runtime /usr/lib/1helm-oci/Containerfile.oci /usr/lib/1helm-oci/channel-machine.oci.tar /usr/lib/1helm-oci/channel-machine.oci.sha256 /usr/lib/1helm-oci/channel-machine.oci.json
35+
managed_crun_profile="$(mktemp)"
36+
printf '%s\nnetwork inet,\nnetwork inet6,\n' '# Managed by 1Helm: allow resident OCI network sockets on nested hosts.' >"$managed_crun_profile"
37+
if [[ -f /etc/apparmor.d/local/crun ]] && cmp -s "$managed_crun_profile" /etc/apparmor.d/local/crun; then
38+
rm -f -- /etc/apparmor.d/local/crun
39+
command -v apparmor_parser >/dev/null 2>&1 && [[ -r /etc/apparmor.d/crun ]] && apparmor_parser -r /etc/apparmor.d/crun || true
40+
fi
41+
rm -f -- "$managed_crun_profile"
3542
rm -f -- "$INSTALL_ROOT/update-host.sh" "$INSTALL_ROOT/uninstall-host.sh"
3643
systemctl daemon-reload
3744
printf 'Removed the 1Helm services and %s owned channel container(s). Preserved %s and versioned release files for recovery.\n' "${#MACHINES[@]}" "$STATE_ROOT"

0 commit comments

Comments
 (0)