Skip to content

status reports sandbox present while every live run booted with it off — preflight runs in the caller's environment, not the daemon's #19

Description

@ascalva

Observed

mind-palace status (owner's shell, 2026-07-28, run #38) prints:

  ✓ sandbox: podman present

data/logs/palace.out.log, the preflight block the daemon actually booted with — for runs
#35, #36, #37 and #38, i.e. every run for the last three days:

  ⚠ sandbox: podman not found (sandbox off)

Both lines come from the same function (ops/lifecycle/preflight.py:84-85). They disagree
because status re-runs preflight in the invoking shell's environment and presents the
result as the running system's state.
It is not reading anything the daemon recorded.

Root cause

podman lives at /opt/homebrew/bin/podman. ops/lifecycle/com.mind-palace.palace.plist
declares only:

<key>EnvironmentVariables</key>
<dict>
    <key>PYTHONUNBUFFERED</key>
    <string>1</string>
</dict>

launchd's default PATH is /usr/bin:/bin:/usr/sbin:/sbin — Homebrew's prefix is not on it,
so shutil.which("podman") returns None inside the daemon and finds the binary in the
owner's interactive shell. The plist already solves this exact problem for uv by using
an absolute path (/opt/homebrew/bin/uv, with the comment "launchd PATH is minimal") —
the lesson was learned there and not generalised.

Severity: not an invariant breach today, but a live trap for Track G

I checked before escalating. core.sandbox has no consumer anywhere outside itself
grep for build_runner / from core.sandbox across the tree returns only intra-module
imports and tests. Nothing in scheduler/, ops/ or the job kinds executes code through
it. So NN-4 ("executed code is powerless") is not currently violated: there is no
execution path to be unsandboxed. This is consistent with the standing posture that
Track G is unwired.

The hazard is deferred, not absent. PodmanRunner.available()
(core/sandbox/runner.py:53-62) gates on the same shutil.which(self.binary). The moment
code execution is wired into the daemon, the runner will report itself unavailable inside
launchd for a reason that has nothing to do with podman's actual state — while status
continues to tell the owner the sandbox is present. Fail-closed is the good outcome there;
the bad one is a fallback path that treats "no sandbox" as "skip sandboxing".

Two defects, one of them structural

  1. The plist does not give the daemon a usable PATH. Mechanical: add /opt/homebrew/bin
    to EnvironmentVariables, or pass the podman binary by absolute path the way uv
    already is.

  2. status sources a capability claim from the wrong process. This is the one worth
    design attention. Every line under preflight: in status output describes the
    invoking shell, not the run it prints two lines below under recent runs:. For
    data_dir and constitution that happens to coincide; for anything environment-derived
    it can silently diverge, and it diverged here on a safety-relevant capability for three
    days without anyone noticing. A green that cannot be false is worth more than a green
    that is usually true — the run row should carry the preflight verdict the daemon
    actually booted with, and status should print that for the live run, reserving a
    freshly-computed preflight for start.

Proposed direction

Fix (1) mechanically. For (2), record the boot preflight into the run row at start and
have status display the live run's recorded verdict, visibly distinguishing "what this
run booted with" from "what a new run would boot with". Related: #12 (the scheduler cannot
see the battery) is the same shape — an unmodelled environment fact the system cannot
observe about itself.

Evidence: data/logs/palace.out.log, ops/lifecycle/com.mind-palace.palace.plist:38-42,
ops/lifecycle/preflight.py:84-85, core/sandbox/runner.py:53-62. Read-only throughout.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    route:orchestratordesign/direction — orchestrator settles or escalatestrack:opsoperational/lifecycle tracktype:defectthe record or the code is wrong

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions