From 346f6c734790417276ddcf94f52b5d00123759be Mon Sep 17 00:00:00 2001 From: Joseph Yaksich Date: Sat, 1 Aug 2026 22:13:41 +0000 Subject: [PATCH] fix: restore fresh runtime and resident outcomes --- CHANGELOG.md | 20 ++++++++- README.md | 2 +- package-lock.json | 4 +- package.json | 2 +- scripts/1helm-oci-runtime | 69 ++++++++++++++++++++++++++---- scripts/install-wsl-runtime.ps1 | 67 +++++++++++++++++++++-------- site/public/apply-linux-release.sh | 1 + site/public/install-oci-runtime.sh | 26 +++++++++++ site/public/install.sh | 1 + site/public/uninstall-host.sh | 7 +++ site/public/update-host.sh | 1 + src/server/bots.ts | 5 ++- src/server/channel-computers.ts | 7 ++- src/server/db.ts | 2 +- src/server/skills.ts | 15 +++++++ test/autonomy-platform.mjs | 7 ++- test/channel-computers.mjs | 2 +- test/desktop.mjs | 6 ++- test/mock-openai.mjs | 17 ++++++++ test/native-world.mjs | 14 ++++++ test/site.mjs | 5 +++ test/windows-wsl-status.ps1 | 41 ++++++++++++++++++ 22 files changed, 282 insertions(+), 39 deletions(-) create mode 100644 test/windows-wsl-status.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e006b6..e131e7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.35] - 2026-08-01 + +### Fixed + +- Preserved the elevated Windows setup transaction's restart-required result + before the signed-in process probes WSL, including when Windows returns an + unreliable zero child-process exit code, and kept terminal setup state + visible while the tracked parent process closes. +- Restored resident internet on nested Linux hosts by installing the narrow + AppArmor address-family permissions required by `crun`, bypassing + netavark's unavailable user-bus DNS scope, and requiring socket creation, + public DNS, and TCP egress before a resident computer becomes ready. +- Made the essential outcome, blocker-resolution, and Skipper-escalation + playbooks active in every resident turn so imperative setup requests are + executed instead of answered with tutorials, and evidenced machine-wide + network failures are escalated directly for repair. + ## [0.0.34] - 2026-08-01 ### Fixed @@ -968,7 +985,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 notarization, stapled tickets, Gatekeeper verification, persistent Application Support, and isolated Apple container machines. -[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.34...HEAD +[Unreleased]: https://github.com/gitcommit90/1Helm/compare/v0.0.35...HEAD +[0.0.35]: https://github.com/gitcommit90/1Helm/compare/v0.0.34...v0.0.35 [0.0.34]: https://github.com/gitcommit90/1Helm/compare/v0.0.33...v0.0.34 [0.0.33]: https://github.com/gitcommit90/1Helm/compare/v0.0.32...v0.0.33 [0.0.32]: https://github.com/gitcommit90/1Helm/compare/v0.0.31...v0.0.32 diff --git a/README.md b/README.md index e25a61c..dc61b41 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,7 @@ A fresh data directory opens first-run setup. The source runtime defaults to | `PORT` | `8123` | HTTP/WebSocket control-plane port. | | `CTRL_DATA_DIR` | `./data` | Databases, routing state, uploads, and non-OCI development/Apple workspace mirrors. | | `HELM_CHANNEL_COMPUTER_BACKEND` | `apple` on macOS, `oci` on Linux and Windows | Host isolation backend; `native` and `mock` are explicit development/test overrides. | -| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.34` | Versioned channel-machine image contract. | +| `HELM_CHANNEL_MACHINE_IMAGE` | `local/1helm-channel-machine:0.0.35` | Versioned channel-machine image contract. | ### Agent-first JSON CLI diff --git a/package-lock.json b/package-lock.json index 30104e5..6977b4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "1helm", - "version": "0.0.34", + "version": "0.0.35", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "1helm", - "version": "0.0.34", + "version": "0.0.35", "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { diff --git a/package.json b/package.json index 7bee699..7c454bd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "1helm", "productName": "1Helm", - "version": "0.0.34", + "version": "0.0.35", "private": true, "type": "module", "license": "AGPL-3.0-only", diff --git a/scripts/1helm-oci-runtime b/scripts/1helm-oci-runtime index 1158f7d..266b4d1 100755 --- a/scripts/1helm-oci-runtime +++ b/scripts/1helm-oci-runtime @@ -270,9 +270,10 @@ print(bridge, subnet) } ensure_network() { - local owner="$1" installation network labels bridge_name + local owner="$1" installation network labels bridge_name network_path installation="$(owner_installation "$owner")" network="1helm-$installation" + network_path="$NETWORKS_ROOT/$network.json" # Linux IFNAMSIZ is 15 chars; keep a stable, installation-scoped bridge name. bridge_name="1h${installation:0:12}" if "${PODMAN[@]}" network exists "$network"; then @@ -283,17 +284,43 @@ labels=json.loads(sys.argv[1] or "{}") if labels.get("com.1helm.managed") != "true" or labels.get("com.1helm.installation") != sys.argv[2]: raise SystemExit("network ownership labels do not match") PY + # Netavark 1.4 starts aardvark-dns through a user systemd scope. A root + # helper invoked by the system service has no user bus, so the advertised + # gateway DNS listener never exists and every resident lookup times out. + # These are 1Helm-owned JSON network definitions; disable only that broken + # plugin and let containers inherit the host's working resolvers instead. + if [[ -f "$network_path" && ! -L "$network_path" ]] \ + && grep -q '"dns_enabled"[[:space:]]*:[[:space:]]*true' "$network_path"; then + python3 - "$network_path" <<'PY' +import json, os, stat, sys, tempfile +path=sys.argv[1] +info=os.lstat(path) +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: + raise SystemExit("owned network definition is not a safe root-owned file") +with open(path, encoding="utf-8") as source: network=json.load(source) +labels=network.get("labels") or network.get("Labels") or {} +if labels.get("com.1helm.managed") != "true": raise SystemExit("refusing to change an unmanaged network") +network["dns_enabled"]=False +descriptor,candidate=tempfile.mkstemp(prefix=".1helm-network-",dir=os.path.dirname(path)) +try: + with os.fdopen(descriptor,"w",encoding="utf-8") as destination: + json.dump(network,destination,indent=5); destination.write("\n"); destination.flush(); os.fsync(destination.fileno()) + os.chmod(candidate,stat.S_IMODE(info.st_mode)); os.chown(candidate,0,0); os.replace(candidate,path) +finally: + if os.path.exists(candidate): os.unlink(candidate) +PY + fi else # Prefer a stable IFNAMSIZ-safe bridge name when the backend supports Docker- # style bridge options (netavark). Debian bookworm's CNI backend rejects # com.docker.network.bridge.name; fall back to a labeled default bridge and # let ensure_guest_egress read the real interface from network inspect. create_out="" - if ! create_out="$("${PODMAN[@]}" network create --disable-dns=false \ + if ! create_out="$("${PODMAN[@]}" network create --disable-dns \ --opt "com.docker.network.bridge.name=${bridge_name}" \ --label com.1helm.managed=true --label "com.1helm.installation=$installation" "$network" 2>&1)"; then if grep -qiE 'unsupported (bridge )?network option|unknown network option' <<<"$create_out"; then - "${PODMAN[@]}" network create --disable-dns=false \ + "${PODMAN[@]}" network create --disable-dns \ --label com.1helm.managed=true --label "com.1helm.installation=$installation" "$network" >/dev/null \ || die "channel network could not be created" else @@ -671,13 +698,22 @@ create_container() { } start_container() { - local name="$1" owner="$2" state network + local name="$1" owner="$2" state network root network_ready container_ok=0 network_ok=0 verify_container "$name" "$owner" - network="$(network_name "$owner")" + root="$(channel_root "$name")" + network_ready="$root/network-ready-v1" + network="$(ensure_network "$owner")" # Re-assert egress on every start: Docker and host firewall reloads can drop # our FORWARD/NAT inserts after boot or package updates. if "${PODMAN[@]}" network exists "$network"; then ensure_guest_egress "$network"; fi state="$(container_state "$name")" + # An update from the earlier aardvark-backed contract must restart a running + # retained container once so Podman regenerates /etc/resolv.conf from the + # now host-inherited network definition. + if [[ "$state" == running && ! -f "$network_ready" ]]; then + "${PODMAN[@]}" stop --time 90 "$name" >/dev/null + state="stopped" + fi if [[ "$state" != running ]]; then "${PODMAN[@]}" start "$name" >/dev/null; fi for _ in {1..100}; do # Podman can briefly fail name-based --user lookup while it reconstructs @@ -685,13 +721,28 @@ start_container() { # image passwd database is intact. These identities are pinned by the # installed runtime manifest and image contract, so use their numeric form # for the readiness probe and every subsequent exec boundary. - 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 + 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 verify_container "$name" "$owner" - return + container_ok=1 + break fi sleep 0.1 done - die "container did not pass its ownership and storage health check" + [[ "$container_ok" -eq 1 ]] || die "container did not pass its ownership and storage health check" + if [[ ! -f "$network_ready" ]]; then + for _ in {1..5}; do + if timeout 7 "${PODMAN[@]}" exec --user "$AGENT_UID:$AGENT_GID" "$name" /usr/bin/python3 -c \ + '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()' \ + >/dev/null 2>&1; then network_ok=1; break; fi + sleep 0.2 + done + [[ "$network_ok" -eq 1 ]] || die "container did not pass its network socket, public DNS, and TCP egress health check" + printf '%s\n' 'host-dns-and-public-tcp-v1' >"$network_ready" + chown root:root "$network_ready" + chmod 0600 "$network_ready" + fi } stop_container() { @@ -902,7 +953,7 @@ case "$operation" in ;; ready) (($# == 0)) || die "ready takes no arguments" - for command in find flock getfacl iptables podman python3 setfacl sha256sum stat tar; do command -v "$command" >/dev/null || die "missing $command"; done + for command in find flock getfacl iptables podman python3 setfacl sha256sum stat tar timeout; do command -v "$command" >/dev/null || die "missing $command"; done [[ -r "$CONTAINERFILE" ]] || die "installed OCI image recipe is missing" "${PODMAN[@]}" info >/dev/null printf '{"ready":true,"version":"%s","engine":"podman"}\n' "$RUNTIME_VERSION" diff --git a/scripts/install-wsl-runtime.ps1 b/scripts/install-wsl-runtime.ps1 index f75633e..7bc5343 100644 --- a/scripts/install-wsl-runtime.ps1 +++ b/scripts/install-wsl-runtime.ps1 @@ -125,6 +125,50 @@ function Fail-Setup { exit $Code } +function Read-ReportedSetupStatus { + $path = $env:HELM_WSL_SETUP_STATUS + if (-not $path -or -not (Test-Path -LiteralPath $path -PathType Leaf)) { return $null } + try { + return Get-Content -LiteralPath $path -Raw | ConvertFrom-Json + } catch { + return $null + } +} + +# The elevated process exit code is not authoritative across every Windows +# PowerShell/UAC host. The shared status file is the transaction record: in +# particular, never continue into the signed-in-user WSL probe after the +# elevated pass has declared that a reboot is required. +function Get-HostSetupOutcome { + param([Nullable[int]]$ExitCode) + $reported = Read-ReportedSetupStatus + if ($null -ne $reported -and $reported.status -eq "restart_required") { + $step = if ($reported.step) { [string]$reported.step } else { "WSL 2 features are enabled. Restart Windows once, then retry 1Helm computer setup." } + $errorMessage = if ($reported.error) { [string]$reported.error } else { "Windows restart required to finish enabling WSL 2." } + return [pscustomobject]@{ Status = "restart_required"; Step = $step; Detail = $errorMessage } + } + if ($null -ne $reported -and $reported.status -eq "failed") { + $detail = if ($reported.error) { [string]$reported.error } elseif ($reported.step) { [string]$reported.step } else { "The administrator-approved WSL host setup failed." } + return [pscustomobject]@{ Status = "failed"; Step = $detail; Detail = $detail } + } + if ($null -eq $ExitCode) { + $detail = "Administrator approval was cancelled or Windows did not start the elevated WSL host setup." + return [pscustomobject]@{ Status = "failed"; Step = $detail; Detail = $detail } + } + if ($ExitCode -eq 10) { + return [pscustomobject]@{ + Status = "restart_required" + Step = "WSL 2 features are enabled. Restart Windows once, then retry 1Helm computer setup." + Detail = "Windows restart required to finish enabling WSL 2." + } + } + if ($ExitCode -ne 0) { + $detail = "The administrator-approved WSL host setup failed with exit code $ExitCode." + return [pscustomobject]@{ Status = "failed"; Step = $detail; Detail = $detail } + } + return [pscustomobject]@{ Status = "continue"; Step = ""; Detail = "" } +} + if ($HostSetup) { try { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() @@ -217,25 +261,14 @@ try { $hostArguments += @("-StatusPath", ('"{0}"' -f $statusArg)) } $hostProcess = Start-Process -FilePath "powershell.exe" -ArgumentList ($hostArguments -join " ") -Verb RunAs -Wait -PassThru - if ($null -eq $hostProcess -or $null -eq $hostProcess.ExitCode) { - Fail-Setup "Administrator approval was cancelled or Windows did not start the elevated WSL host setup." - } - if ($hostProcess.ExitCode -eq 10) { - Write-SetupStatus -Status "restart_required" -Step "WSL 2 features are enabled. Restart Windows once, then retry 1Helm computer setup." -Progress 20 - Write-Host "WSL 2 features are enabled. Restart Windows once, then retry 1Helm computer setup." + $hostExitCode = if ($null -eq $hostProcess) { $null } else { $hostProcess.ExitCode } + $hostOutcome = Get-HostSetupOutcome -ExitCode $hostExitCode + if ($hostOutcome.Status -eq "restart_required") { + Write-SetupStatus -Status "restart_required" -Step $hostOutcome.Step -Progress 20 -ErrorMessage $hostOutcome.Detail + Write-Host $hostOutcome.Step exit 10 } - if ($hostProcess.ExitCode -ne 0) { - $detail = "The administrator-approved WSL host setup failed with exit code $($hostProcess.ExitCode)." - if ($env:HELM_WSL_SETUP_STATUS -and (Test-Path -LiteralPath $env:HELM_WSL_SETUP_STATUS)) { - try { - $reported = Get-Content -LiteralPath $env:HELM_WSL_SETUP_STATUS -Raw | ConvertFrom-Json - if ($reported.error) { $detail = [string]$reported.error } - elseif ($reported.step -and $reported.status -eq "failed") { $detail = [string]$reported.step } - } catch { } - } - Fail-Setup $detail - } + if ($hostOutcome.Status -eq "failed") { Fail-Setup $hostOutcome.Detail } if (-not (Test-PinnedWslRuntime)) { Fail-Setup "Microsoft WSL $wslVersion is not ready in the signed-in user's session." } diff --git a/site/public/apply-linux-release.sh b/site/public/apply-linux-release.sh index 5017760..ae7ff7c 100755 --- a/site/public/apply-linux-release.sh +++ b/site/public/apply-linux-release.sh @@ -20,6 +20,7 @@ HOST_CONTRACT_PATHS=( /usr/libexec/1helm-oci-runtime /etc/1helm/oci-runtime-v1.conf /etc/sudoers.d/1helm-oci-runtime + /etc/apparmor.d/local/crun /etc/tmpfiles.d/1helm-oci.conf /usr/lib/1helm-oci/Containerfile.oci /usr/lib/1helm-oci/channel-machine.oci.tar diff --git a/site/public/install-oci-runtime.sh b/site/public/install-oci-runtime.sh index 17e501d..872b296 100755 --- a/site/public/install-oci-runtime.sh +++ b/site/public/install-oci-runtime.sh @@ -30,6 +30,32 @@ fi 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 [[ "$(stat -fc %T /sys/fs/cgroup)" == cgroup2fs ]] || { echo "1Helm OCI resource controls require cgroup v2." >&2; exit 1; } +# Ubuntu ships an AppArmor attachment for /usr/bin/crun whose nominally +# unconfined profile can still inherit the outer container host's address-family +# restrictions. On a nested systemd host that manifests inside every resident +# as EPERM from socket(2), even though the 1Helm host itself has internet. Add +# only the missing address-family grants to crun's supported local include; the +# outer host/container profile remains the isolation boundary. +NESTED_CRUN_PROFILE="/etc/apparmor.d/local/crun" +NESTED_CRUN_MARKER="# Managed by 1Helm: allow resident OCI network sockets on nested hosts." +container_virt="$(systemd-detect-virt --container 2>/dev/null || true)" +apparmor_enabled="$(cat /sys/module/apparmor/parameters/enabled 2>/dev/null || true)" +if [[ -n "$container_virt" && "$container_virt" != none && "$apparmor_enabled" =~ ^[Yy]$ && -r /etc/apparmor.d/crun ]]; then + command -v apparmor_parser >/dev/null || { echo "Nested AppArmor OCI setup requires apparmor_parser." >&2; exit 1; } + if [[ ! -e "$NESTED_CRUN_PROFILE" ]]; then + install -d -o root -g root -m 0755 "$(dirname "$NESTED_CRUN_PROFILE")" + profile_candidate="$(mktemp)" + printf '%s\nnetwork inet,\nnetwork inet6,\n' "$NESTED_CRUN_MARKER" >"$profile_candidate" + install -o root -g root -m 0644 "$profile_candidate" "$NESTED_CRUN_PROFILE" + rm -f -- "$profile_candidate" + elif ! grep -Eq '^[[:space:]]*network[[:space:]]+inet[[:space:]]*,' "$NESTED_CRUN_PROFILE" \ + || ! grep -Eq '^[[:space:]]*network[[:space:]]+inet6[[:space:]]*,' "$NESTED_CRUN_PROFILE"; then + echo "The existing AppArmor local/crun policy does not permit resident IPv4 and IPv6 sockets; 1Helm left the custom policy unchanged." >&2 + exit 1 + fi + apparmor_parser -r /etc/apparmor.d/crun +fi + install -d -o root -g root -m 0755 /etc/1helm "$RECIPE_ROOT" /usr/libexec install -d -o root -g root -m 0711 "$STATE_ROOT/runtime/oci" "$STATE_ROOT/runtime/oci/channels" install -d -o root -g root -m 0700 "$STATE_ROOT/runtime/oci/storage" "$STATE_ROOT/runtime/oci/backups" "$STATE_ROOT/runtime/oci/networks" diff --git a/site/public/install.sh b/site/public/install.sh index ef4f3ca..62b13dd 100644 --- a/site/public/install.sh +++ b/site/public/install.sh @@ -14,6 +14,7 @@ HOST_CONTRACT_PATHS=( /usr/libexec/1helm-oci-runtime /etc/1helm/oci-runtime-v1.conf /etc/sudoers.d/1helm-oci-runtime + /etc/apparmor.d/local/crun /etc/tmpfiles.d/1helm-oci.conf /usr/lib/1helm-oci/Containerfile.oci /usr/lib/1helm-oci/channel-machine.oci.tar diff --git a/site/public/uninstall-host.sh b/site/public/uninstall-host.sh index 9171171..bab8cad 100755 --- a/site/public/uninstall-host.sh +++ b/site/public/uninstall-host.sh @@ -32,6 +32,13 @@ done systemctl disable --now 1helm-update.path 2>/dev/null || true rm -f -- /etc/systemd/system/1helm.service /etc/systemd/system/1helm-update.service /etc/systemd/system/1helm-update.path 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 +managed_crun_profile="$(mktemp)" +printf '%s\nnetwork inet,\nnetwork inet6,\n' '# Managed by 1Helm: allow resident OCI network sockets on nested hosts.' >"$managed_crun_profile" +if [[ -f /etc/apparmor.d/local/crun ]] && cmp -s "$managed_crun_profile" /etc/apparmor.d/local/crun; then + rm -f -- /etc/apparmor.d/local/crun + command -v apparmor_parser >/dev/null 2>&1 && [[ -r /etc/apparmor.d/crun ]] && apparmor_parser -r /etc/apparmor.d/crun || true +fi +rm -f -- "$managed_crun_profile" rm -f -- "$INSTALL_ROOT/update-host.sh" "$INSTALL_ROOT/uninstall-host.sh" systemctl daemon-reload printf 'Removed the 1Helm services and %s owned channel container(s). Preserved %s and versioned release files for recovery.\n' "${#MACHINES[@]}" "$STATE_ROOT" diff --git a/site/public/update-host.sh b/site/public/update-host.sh index 30fb4d7..4f137aa 100755 --- a/site/public/update-host.sh +++ b/site/public/update-host.sh @@ -22,6 +22,7 @@ HOST_CONTRACT_PATHS=( /usr/libexec/1helm-oci-runtime /etc/1helm/oci-runtime-v1.conf /etc/sudoers.d/1helm-oci-runtime + /etc/apparmor.d/local/crun /etc/tmpfiles.d/1helm-oci.conf /usr/lib/1helm-oci/Containerfile.oci /usr/lib/1helm-oci/channel-machine.oci.tar diff --git a/src/server/bots.ts b/src/server/bots.ts index 6914fe2..ff831b1 100644 --- a/src/server/bots.ts +++ b/src/server/bots.ts @@ -6,7 +6,7 @@ import { isChatGPTProvider, streamChatGPTCompletion } from "./chatgpt.ts"; import { generateRoutingChatGPTImage, isInternalRoutingProvider, routingEndpointForUser } from "./routing.ts"; import { availableGoogleAccounts, createGmailDraft, getGmailMessage, gmailConnectionStatus, normalizeMailConfig, searchGmail, startGmailConnection } from "./gmail.ts"; import { recallForAgent, rememberForAgent } from "./memory.ts"; -import { agentSkillContext, createSkill, imageGenerationAvailable, listSkills, proposeSkill, provisionSkill, readAgentSkill, requestSkill, skillsForAgent } from "./skills.ts"; +import { agentSkillContext, createSkill, essentialResidentSkillContext, imageGenerationAvailable, listSkills, proposeSkill, provisionSkill, readAgentSkill, requestSkill, skillsForAgent } from "./skills.ts"; import { inspectCatalogSkill, installCatalogSkill, searchSkillCatalog } from "./skill-catalog.ts"; import { createWorkflow, listWorkflows, setWorkflowStatus } from "./workflows.ts"; import { runThreadAuditPass } from "./thread-audit.ts"; @@ -237,15 +237,18 @@ function systemPromptTiers(bot: Row, agent: RuntimeAgent | undefined, channelId: ? "Contribute only the expertise requested in this thread. No shell, workspace, or durable-memory capability is attached to this invitation." : "You own an isolated persistent Linux computer for this channel. Its durable workspace is /workspace. You have direct shell, internet, SSH, package-install, file, memory, and listed-tool autonomy inside it. Routine commands, downloads, browsing, installations, and network access do not require Skipper or user approval.", visiting ? "" : "Own the requested outcome. Inspect and act instead of returning tutorials, interviews, rationalizations, or plans for work you can perform. If you make the wrong artifact or implementation choice, acknowledge it briefly and fix it; do not defend the mistake.", + visiting ? "" : "An imperative such as install, set up, make, fix, or can YOU do it authorizes you to perform the work with your tools. Never replace that request with commands for the user to run. Never say you are checking, fetching, creating, or continuing unless you actually invoke the relevant tool in this turn.", visiting ? "" : "Workspace file contracts: Markdown for notes and documents; plain text source files for code. /workspace/whiteboards holds `.whiteboard.json` Excalidraw scenes and /workspace/presentations holds `.slides.json` decks — these two folders are rendered by exact schemas, so never place .html decks or invented JSON formats there. When a request supplies a format contract for a folder, follow it exactly.", "Treat tool results as evidence. Retry transient failures with a bounded changed strategy, stop repeating unchanged failures, and verify before claiming success.", "If work truly crosses the resident boundary—host/native state, credentials, another channel, fleet lifecycle, or a missing capability—call Skipper once with the exact operation and evidence. Do not escalate routine shell, SSH, internet, download, or resident-computer work.", + visiting ? "" : "If resident commands cannot create network sockets or consistently fail with Permission denied, No route to host, DNS resolution failures, or equivalent machine-wide egress evidence, treat that as a broken resident-computer boundary: call Skipper directly with the failed probe and original outcome, then resume after hand-back. Do not answer with a Docker, curl, SSH, or package-install tutorial.", visiting ? "" : "Use ask_user only for consequential human judgment, missing credentials the human must supply, external authority, or an irreversible commitment. Difficulty and harmless implementation choices are not blockers.", "Use Markdown. Keep answers focused and attach user-facing artifacts rather than only naming a path.", "The callable tools below are your current capabilities. Their implementations enforce authority and isolation boundaries.", ].filter(Boolean).join("\n\n"); const context = [ ``, + !visiting && agent?.id ? essentialResidentSkillContext(Number(agent.id)) : "", agent?.id ? agentSkillContext(Number(agent.id), task) : "", ].filter(Boolean).join("\n\n"); return { identity, operating, context }; diff --git a/src/server/channel-computers.ts b/src/server/channel-computers.ts index d83d5fd..e990914 100644 --- a/src/server/channel-computers.ts +++ b/src/server/channel-computers.ts @@ -68,7 +68,7 @@ const APPLE_RUNTIME_VERSION = "1.1.0"; export const APPLE_RUNTIME_PACKAGE = `container-${APPLE_RUNTIME_VERSION}-installer-signed.pkg`; export const APPLE_RUNTIME_URL = `https://github.com/apple/container/releases/download/${APPLE_RUNTIME_VERSION}/${APPLE_RUNTIME_PACKAGE}`; export const APPLE_RUNTIME_SHA256 = "0ca1c42a2269c2557efb1d82b1b38ac553e6a3a3da1b1179c439bcee1e7d6714"; -export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.34"; +export const DEFAULT_CHANNEL_IMAGE = process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.35"; const CONTAINER_CANDIDATES = [process.env.HELM_CONTAINER_CLI, "/usr/local/bin/container", "/opt/homebrew/bin/container", "container"].filter(Boolean) as string[]; const OCI_RUNTIME_VERSION = "1helm-oci-runtime-v1"; const OCI_HELPER_CANDIDATES = [ @@ -1659,7 +1659,10 @@ export function windowsWslSetupStatus(): WindowsWslSetupStatus { ? parsed.status as WindowsWslSetupStatus["status"] : "running"; setWindowsWslSetupState({ - status: windowsWslSetupChild ? "running" : status, + // The elevated PowerShell pass reports terminal outcomes through the + // shared file before its parent process necessarily closes. Do not + // hide a required reboot (or a real failure) behind the live child. + status, step: String(parsed.step), progress: Math.max(0, Math.min(100, Number(parsed.progress) || windowsWslSetupState.progress)), error: String(parsed.error || windowsWslSetupState.error || ""), diff --git a/src/server/db.ts b/src/server/db.ts index a28d589..0e8d30d 100644 --- a/src/server/db.ts +++ b/src/server/db.ts @@ -939,7 +939,7 @@ export function migrate(): void { const platformBackend = process.platform === "darwin" ? "apple" : "oci"; const configuredBackend = String(process.env.HELM_CHANNEL_COMPUTER_BACKEND || platformBackend); const backend = ["apple", "oci", "native", "mock"].includes(configuredBackend) ? configuredBackend : platformBackend; - const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.34"); + const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.35"); for (const channel of q(`SELECT c.id FROM channels c JOIN agent_channels ac ON ac.channel_id=c.id WHERE c.kind='channel' AND c.status<>'deleted'`)) { const channelId = Number(channel.id); diff --git a/src/server/skills.ts b/src/server/skills.ts index 295bc92..d38cfbc 100644 --- a/src/server/skills.ts +++ b/src/server/skills.ts @@ -182,6 +182,21 @@ export function agentSkillContext(agentId: number, _task = ""): string { ].join("\n"); } +/** These three playbooks are the resident execution kernel, not optional + * reference material. Models were sometimes reading only skill metadata and + * then returning tutorials or failing to escalate a broken resident computer. + * Inject the full, compact contracts on every home-resident turn. */ +export function essentialResidentSkillContext(agentId: number): string { + const essential = new Set(["outcome-ownership", "blocker-resolution", "skipper-escalation"]); + const available = skillsForAgent(agentId).filter((entry) => essential.has(String(entry.slug)) && !entry.arsenal_locked); + return [ + '', + ...available.map((entry) => `\n${String(entry.instructions || "").trim()}\n`), + "These playbooks are active instructions for this turn; do not wait for read_skill before following them.", + "", + ].join("\n"); +} + export function templateForSlug(slug: string): Row | undefined { return q1("SELECT * FROM agent_templates WHERE slug=? AND status='active'", skillSlug(slug)); } diff --git a/test/autonomy-platform.mjs b/test/autonomy-platform.mjs index 74961dd..236f4b3 100644 --- a/test/autonomy-platform.mjs +++ b/test/autonomy-platform.mjs @@ -130,7 +130,7 @@ test("a finalized turn is immutable to stale stream writers", () => { assert.equal(q1("SELECT status FROM agent_progress WHERE id=?", progressId).status, "complete"); }); -test("runtime exposes usable skill metadata and durable operating identity without injecting full playbooks", () => { +test("runtime injects the essential resident operating playbooks and keeps the remaining arsenal metadata-bounded", () => { seed(); const channelId = run("INSERT INTO channels (name,slug,kind,topic,purpose,status,created) VALUES ('prompt-tiers','prompt-tiers','channel','','First purpose','active',?)", now()).lastInsertRowid; const botId = run("INSERT INTO bots (name,model,prompt,created) VALUES ('prompt-agent','mock','Patient domain partner.',?)", now()).lastInsertRowid; @@ -148,7 +148,10 @@ test("runtime exposes usable skill metadata and durable operating identity witho assert.match(first.operating, /\/workspace/); assert.match(first.context, /skill-arsenal count=/i); assert.match(first.context, /outcome-ownership: Outcome ownership/i); - assert.doesNotMatch(first.context, /active-skill-playbooks|workspace-skill-catalog|### /i); + assert.match(first.context, /essential-resident-operations[\s\S]*outcome-ownership[\s\S]*Carry an outcome from request to verified completion/i); + assert.match(first.operating, /cannot create network sockets[\s\S]*call Skipper directly[\s\S]*Do not answer with a Docker/i); + assert.match(first.operating, /Never say you are checking[\s\S]*unless you actually invoke/i); + assert.doesNotMatch(first.context, /workspace-skill-catalog|### /i); assert(first.identity.length + first.operating.length + first.context.length < 15_000, "capability map remains metadata-bounded rather than injecting full procedures"); assert.match(first.context, /Own prompt testing/); assert.match(second.context, /Changed volatile purpose/); diff --git a/test/channel-computers.mjs b/test/channel-computers.mjs index 5e6e553..d6f096e 100644 --- a/test/channel-computers.mjs +++ b/test/channel-computers.mjs @@ -181,7 +181,7 @@ test("Apple channel-computer contract preserves isolation, files, wakes, archive test("runtime digest and packaged image recipe stay pinned", async () => { assert.equal(computers.APPLE_RUNTIME_SHA256, "0ca1c42a2269c2557efb1d82b1b38ac553e6a3a3da1b1179c439bcee1e7d6714"); assert.match(computers.APPLE_RUNTIME_URL, /\/1\.1\.0\/container-1\.1\.0-installer-signed\.pkg$/); - assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.34"); + assert.equal(computers.DEFAULT_CHANNEL_IMAGE, "local/1helm-channel-machine:0.0.35"); const packaging = await readFile(join(root, "scripts", "package-mac-dmg.cjs"), "utf8"); assert.match(packaging, /container\(\?:\$\|\\\/\)/, "release packaging includes container/ image assets"); const image = await readFile(join(root, "container", "Containerfile"), "utf8"); diff --git a/test/desktop.mjs b/test/desktop.mjs index c8322a9..5c1c696 100644 --- a/test/desktop.mjs +++ b/test/desktop.mjs @@ -146,6 +146,7 @@ test("desktop entrypoint keeps the renderer sandboxed and data on the Mac", asyn const macPackager = await readFile(join(root, "scripts", "package-mac-dmg.cjs"), "utf8"); const windowsRemoval = await readFile(join(root, "scripts", "windows-removal.cjs"), "utf8"); const windowsRuntime = await readFile(join(root, "scripts", "install-wsl-runtime.ps1"), "utf8"); + const windowsRuntimeStatusTest = await readFile(join(root, "test", "windows-wsl-status.ps1"), "utf8"); assert.match(windowsPackager, /HELM_REQUIRE_WINDOWS_SIGNATURE/); assert.match(windowsPackager, /require\("png-to-ico"\)\.default/, "Windows packaging uses the module's CommonJS default export"); assert.match(windowsPackager, /1Helm-\$\{VERSION\}-windows-x64-setup\.exe/); @@ -187,7 +188,7 @@ test("desktop entrypoint keeps the renderer sandboxed and data on the Mac", asyn assert.match(windowsRuntime, /-HostSetup[\s\S]*-Verb RunAs[\s\S]*Get-WslDistributionNames/, "only WSL host components cross UAC while the signed-in owner imports the shared distribution"); assert.match(windowsRuntime, /function Fetch-File[\s\S]*if \(\$HostSetup\)/, "Fetch-File is script-scoped so the signed-in owner path can download the Ubuntu rootfs after elevation"); assert.match(windowsRuntime, /Write-SetupStatus/, "Windows setup writes machine-readable progress for the app UI"); - assert.match(windowsRuntime, /\$null -eq \$hostProcess -or \$null -eq \$hostProcess\.ExitCode/, "cancelled UAC is reported as setup failure instead of a silent no-op"); + assert.match(windowsRuntime, /\$hostExitCode = if \(\$null -eq \$hostProcess\)[\s\S]*Get-HostSetupOutcome/, "cancelled UAC is reported through the shared host-outcome transaction instead of a silent no-op"); assert.match(windowsRuntime, /Get-WslText|replace \[char\]0/, "WSL UTF-16 NUL output is normalized before version and distro matching"); assert.match(windowsRuntime, /function Get-WslText[\s\S]*ErrorActionPreference = "Continue"[\s\S]*\$LASTEXITCODE[\s\S]*ErrorActionPreference = \$previousErrorAction/, "a fresh host's expected failing WSL probe cannot terminate setup before the pinned runtime is installed"); assert.match(windowsRuntime, /Test-PinnedWslRuntime/, "host setup verifies the pinned Microsoft WSL build"); @@ -197,6 +198,9 @@ test("desktop entrypoint keeps the renderer sandboxed and data on the Mac", asyn assert.match(windowsRuntime, /\$enabledWslFeatureNow[\s\S]*\$enabledVmFeatureNow[\s\S]*\$restartRequired/, "features enabled in the current pass force a reboot before WSL import regardless of DISM enum formatting"); assert.match(windowsRuntime, /Get-Service -Name vmcompute[\s\S]*\$restartRequired = \$true/, "an enabled-but-not-registered WSL VM compute service stops setup at the reboot boundary"); assert.match(windowsRuntime, /HCS_E_SERVICE_NOT_AVAILABLE[\s\S]*Require-WindowsRestart|Test-WslRestartFailure[\s\S]*Require-WindowsRestart/, "an unavailable VM compute service is reported as restart-required instead of a broken runtime"); + assert.match(windowsRuntime, /Get-HostSetupOutcome[\s\S]*Status -eq "restart_required"[\s\S]*Test-PinnedWslRuntime/, "the signed-in setup honors the elevated shared reboot result before probing WSL"); + assert.doesNotMatch(channelComputers, /status: windowsWslSetupChild \? "running" : status/, "a live parent process cannot mask a terminal shared WSL setup status"); + assert.match(windowsRuntimeStatusTest, /Get-HostSetupOutcome -ExitCode 0[\s\S]*restart_required[\s\S]*Get-HostSetupOutcome -ExitCode 10/, "the executable PowerShell regression proves shared restart status wins over an unreliable zero exit code"); assert.match(windowsRuntime, /\.1helm-partial-import[\s\S]*\$ownedPartial[\s\S]*Remove-Item -LiteralPath \$installDirectory/, "an app-owned partial WSL import can recover safely after reboot"); assert.match(windowsRuntime, /\[automount\][\s\S]*enabled=false[\s\S]*\[interop\][\s\S]*enabled=false/, "the shared runtime exposes neither Windows drives nor process interop"); assert.doesNotMatch(windowsRuntime, /--update/); diff --git a/test/mock-openai.mjs b/test/mock-openai.mjs index a75186b..9dafee5 100644 --- a/test/mock-openai.mjs +++ b/test/mock-openai.mjs @@ -83,6 +83,15 @@ createServer(async (req, res) => { const wantsAutonomousInstall = reqBody.tools?.some((tool) => tool.function?.name === "run_command") && /(?:download|install|set up) (?:openai )?codex/i.test(latestUser) && !hasToolResult; + const residentNetworkFailure = [...reqBody.messages].reverse().find((message) => message.role === "tool" && message.name === "run_command" && /Permission denied.*socket|socket.*Permission denied/is.test(String(message.content || ""))); + const wantsResidentNetworkSetup = reqBody.tools?.some((tool) => tool.function?.name === "run_command") + && /set up jellyfin|install jellyfin/i.test(latestUser) + && !hasToolResult + && /You are @\S+-agent/.test(serialized); + const wantsResidentNetworkEscalation = reqBody.tools?.some((tool) => tool.function?.name === "call_skipper") + && Boolean(residentNetworkFailure) + && !reqBody.messages.some((message) => message.role === "tool" && message.name === "call_skipper") + && /You are @\S+-agent/.test(serialized); const wantsLearnSkill = reqBody.tools?.some((tool) => tool.function?.name === "create_skill") && /Learn one new reusable workspace skill/i.test(latestUser) && !hasToolResult; @@ -192,6 +201,14 @@ createServer(async (req, res) => { const args = { delay_seconds: /browser follow-up countdown/i.test(latestUser) ? 300 : 30, reason: "Check whether the async download finished and report Downloaded or Blocked." }; sse(res, { choices: [{ delta: { tool_calls: [{ index: 0, id: "schedule_followup_1", type: "function", function: { name: "schedule_followup", arguments: JSON.stringify(args) } }] } }] }); sse(res, { choices: [{ delta: {}, finish_reason: "tool_calls" }] }); + } else if (wantsResidentNetworkSetup) { + const args = { command: "printf 'socket Permission denied\\n' >&2; exit 1" }; + sse(res, { choices: [{ delta: { tool_calls: [{ index: 0, id: "resident_network_probe_1", type: "function", function: { name: "run_command", arguments: JSON.stringify(args) } }] } }] }); + sse(res, { choices: [{ delta: {}, finish_reason: "tool_calls" }] }); + } else if (wantsResidentNetworkEscalation) { + const args = { reason: "Original outcome: set up Jellyfin locally. Resident network probe failed at socket creation with Permission denied, a machine boundary. Inspect and repair resident-computer networking, then return the verified boundary result so I can finish the setup." }; + sse(res, { choices: [{ delta: { tool_calls: [{ index: 0, id: "resident_network_skipper_1", type: "function", function: { name: "call_skipper", arguments: JSON.stringify(args) } }] } }] }); + sse(res, { choices: [{ delta: {}, finish_reason: "tool_calls" }] }); } else if (wantsAutonomousInstall) { const args = { command: "printf 'codex installed\\n' > codex-install.txt" }; sse(res, { choices: [{ delta: { tool_calls: [{ index: 0, id: "install_codex_1", type: "function", function: { name: "run_command", arguments: JSON.stringify(args) } }] } }] }); diff --git a/test/native-world.mjs b/test/native-world.mjs index f7b81da..aff98ec 100644 --- a/test/native-world.mjs +++ b/test/native-world.mjs @@ -818,6 +818,20 @@ try { return residentReplies.length >= 2 && handoff ? { residentReplies: residentReplies.length, handoff } : null; }, "automatic Skipper-to-resident return", 20_000); ok(Boolean(automaticReturn), "runtime automatically returns a resident escalation after Skipper unblocks it even when the Skipper model omits call_agent"); + const networkSetupRoot = await api(`/api/channels/${launch.id}/messages`, { body: { body: `@${afterRestart.agent.name} set up Jellyfin locally; can YOU set it up` } }, captain); + await waitForAgentReply(networkSetupRoot.body.message.id, captain, afterRestart.agent.name); + await waitForAgentReply(networkSetupRoot.body.message.id, captain, "skipper"); + const networkSetupActivity = await waitFor(async () => { + const result = await api(`/api/channels/${launch.id}/activity`, {}, captain); + const thread = await api(`/api/messages/${networkSetupRoot.body.message.id}/thread`, {}, captain); + const actions = result.body.actions || []; + const escalated = (result.body.escalations || []).some((item) => /socket creation with Permission denied/i.test(String(item.reason || ""))); + const residentFailed = actions.some((item) => item.tool === "run_command" && item.status === "failed" && /socket Permission denied/i.test(String(item.result_summary || ""))); + const askedHuman = actions.some((item) => item.tool === "ask_user" && item.thread_id === thread.body.thread?.id); + const tutorial = (thread.body.replies || []).some((item) => item.author?.name === afterRestart.agent.name && /(?:run|try|use) (?:this )?(?:docker|curl|sudo) command/i.test(String(item.body || ""))); + return escalated && residentFailed && !askedHuman && !tutorial ? true : null; + }, "resident network-boundary escalation", 20_000); + ok(Boolean(networkSetupActivity), "an imperative resident install with machine-wide socket denial calls Skipper with evidence instead of returning another tutorial or asking the human"); const escalationLoopDb = new DatabaseSync(join(dataDir, "ctrl-pane.db")); const escalationThread = escalationLoopDb.prepare("SELECT id FROM threads WHERE root_message_id=?").get(escalationRoot.body.message.id); const openEscalation = escalationLoopDb.prepare("SELECT id FROM escalations WHERE thread_id=? AND from_agent_id=? ORDER BY id DESC LIMIT 1").get(escalationThread.id, afterRestart.agent.id); diff --git a/test/site.mjs b/test/site.mjs index 18a2112..27e04c4 100644 --- a/test/site.mjs +++ b/test/site.mjs @@ -233,6 +233,7 @@ test("installer assets are explicit and syntax-valid", () => { assert.match(ociInstaller, /acl[\s\S]*crun[\s\S]*fuse-overlayfs[\s\S]*podman/, "the installer supplies the complete OCI and direct-access prerequisites"); assert.match(ociInstaller, /visudo -cf/, "the minimal helper-only sudo policy is validated before installation"); assert.match(ociInstaller, /cgroup2fs/, "live CPU and memory controls require cgroup v2"); + assert.match(ociInstaller, /\/etc\/apparmor\.d\/local\/crun[\s\S]*systemd-detect-virt --container[\s\S]*network inet,[\s\S]*network inet6,[\s\S]*apparmor_parser -r/, "nested Linux hosts install the narrow crun address-family grants needed for resident sockets"); assert.match(ociHelper, /com\.1helm\.managed[\s\S]*com\.1helm\.owner[\s\S]*com\.1helm\.machine/, "every container has exact ownership labels"); assert.match(ociHelper, /actual != expected[\s\S]*container storage mounts do not match/, "runtime adoption verifies the complete authoritative mount set"); assert.match(ociHelper, /ONEHELM_OCI_SERVICE_USER/, "the installed service identity participates in the direct storage contract"); @@ -243,6 +244,9 @@ test("installer assets are explicit and syntax-valid", () => { assert.match(ociHelper, /network\.json[\s\S]*--ip "\$ip" --mac-address "\$mac"/, "each channel receives a persistent static IP and locally administered MAC"); assert.match(ociHelper, /container static network creation contract does not match[\s\S]*container network ID does not match[\s\S]*running container network identity does not match/, "runtime adoption verifies create-time and live network identity"); assert.match(ociHelper, /network_ids_compatible/, "network ID checks tolerate short/full ids and pre-start NetworkID gaps"); + assert.match(ociHelper, /Netavark 1\.4 starts aardvark-dns[\s\S]*"dns_enabled"[\s\S]*False[\s\S]*--disable-dns/, "system-service OCI networks bypass netavark's unavailable user-bus DNS scope and inherit working host resolvers"); + assert.doesNotMatch(ociHelper, /network create --disable-dns=false/, "fresh resident networks never point at a gateway DNS listener that cannot start from the system service"); + assert.match(ociHelper, /network-ready-v1[\s\S]*timeout 7[\s\S]*socket\.socket\(socket\.AF_INET,socket\.SOCK_STREAM\)[\s\S]*socket\.getaddrinfo\("example\.com",443[\s\S]*socket\.create_connection/, "a bounded first-start check requires resident socket creation, public DNS, and TCP egress before the network contract becomes ready"); assert.match(ociHelper, /backup_container[\s\S]*sha256sum[\s\S]*restore_container[\s\S]*backup digest does not match/, "backup and recovery are digest-qualified and ownership-gated"); assert.match(ociHelper, /source\.extractall\(destination, filter="data"\)/, "backup extraction rejects unsafe archive paths"); assert.match(ociRecipe, /docker\.io\/library\/ubuntu:24\.04@sha256:[a-f0-9]{64}/, "the OCI guest base is fully qualified and digest-pinned without mutable short-name state"); @@ -271,6 +275,7 @@ test("installer assets are explicit and syntax-valid", () => { } assert.match(uninstaller, /"\$HELPER" delete "\$name" "\$INSTALLATION_ID:\$channel_id"/, "uninstall deletes only exact installation-owned containers"); assert.match(uninstaller, /Preserved %s/, "uninstall preserves durable workspace state"); + assert.match(uninstaller, /cmp -s[\s\S]*\/etc\/apparmor\.d\/local\/crun[\s\S]*apparmor_parser -r/, "uninstall removes and reloads only the exact app-managed crun profile"); }); test("standalone deployment runs the website and tunnel without root process authority", () => { diff --git a/test/windows-wsl-status.ps1 b/test/windows-wsl-status.ps1 new file mode 100644 index 0000000..8363583 --- /dev/null +++ b/test/windows-wsl-status.ps1 @@ -0,0 +1,41 @@ +$ErrorActionPreference = "Stop" +$installerPath = Join-Path (Split-Path -Parent $PSScriptRoot) "scripts\install-wsl-runtime.ps1" +$tokens = $null +$errors = $null +$ast = [System.Management.Automation.Language.Parser]::ParseFile($installerPath, [ref]$tokens, [ref]$errors) +if ($errors.Count) { throw "install-wsl-runtime.ps1 did not parse: $($errors[0].Message)" } + +foreach ($name in @("Read-ReportedSetupStatus", "Get-HostSetupOutcome")) { + $definition = $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name }, $true) | Select-Object -First 1 + if ($null -eq $definition) { throw "Missing $name in the Windows runtime installer." } + . ([scriptblock]::Create($definition.Extent.Text)) +} + +$temporary = Join-Path ([System.IO.Path]::GetTempPath()) ("1helm-wsl-status-test-" + [Guid]::NewGuid().ToString("N")) +New-Item -ItemType Directory -Path $temporary | Out-Null +try { + $statusPath = Join-Path $temporary "setup-status.json" + $env:HELM_WSL_SETUP_STATUS = $statusPath + [System.IO.File]::WriteAllText($statusPath, '{"status":"restart_required","step":"Restart from elevated pass","error":"Reboot boundary","progress":20}', [System.Text.UTF8Encoding]::new($false)) + $restart = Get-HostSetupOutcome -ExitCode 0 + if ($restart.Status -ne "restart_required" -or $restart.Step -ne "Restart from elevated pass" -or $restart.Detail -ne "Reboot boundary") { + throw "A shared restart_required result was not authoritative over elevated exit code 0." + } + + [System.IO.File]::WriteAllText($statusPath, '{"status":"failed","step":"Elevated failure","error":"Exact elevated error","progress":0}', [System.Text.UTF8Encoding]::new($false)) + $failed = Get-HostSetupOutcome -ExitCode 0 + if ($failed.Status -ne "failed" -or $failed.Detail -ne "Exact elevated error") { + throw "A shared elevated failure was not preserved." + } + + Remove-Item -LiteralPath $statusPath -Force + $zero = Get-HostSetupOutcome -ExitCode 0 + $ten = Get-HostSetupOutcome -ExitCode 10 + if ($zero.Status -ne "continue" -or $ten.Status -ne "restart_required") { + throw "Exit-code fallback behavior is invalid." + } + Write-Host "Windows WSL shared-status transaction regression passed." +} finally { + Remove-Item Env:HELM_WSL_SETUP_STATUS -ErrorAction SilentlyContinue + if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Recurse -Force } +}