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
-
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.
-
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
Observed
mind-palace status(owner's shell, 2026-07-28, run #38) prints: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:
Both lines come from the same function (
ops/lifecycle/preflight.py:84-85). They disagreebecause
statusre-runs preflight in the invoking shell's environment and presents theresult as the running system's state. It is not reading anything the daemon recorded.
Root cause
podmanlives at/opt/homebrew/bin/podman.ops/lifecycle/com.mind-palace.palace.plistdeclares only:
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 theowner's interactive shell. The plist already solves this exact problem for
uvby usingan 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.sandboxhas no consumer anywhere outside itself —grepforbuild_runner/from core.sandboxacross the tree returns only intra-moduleimports and tests. Nothing in
scheduler/,ops/or the job kinds executes code throughit. 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 sameshutil.which(self.binary). The momentcode 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
statuscontinues 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
The plist does not give the daemon a usable PATH. Mechanical: add
/opt/homebrew/binto
EnvironmentVariables, or pass the podman binary by absolute path the wayuvalready is.
statussources a capability claim from the wrong process. This is the one worthdesign attention. Every line under
preflight:instatusoutput describes theinvoking shell, not the run it prints two lines below under
recent runs:. Fordata_dirandconstitutionthat happens to coincide; for anything environment-derivedit 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
statusshould print that for the live run, reserving afreshly-computed preflight for
start.Proposed direction
Fix (1) mechanically. For (2), record the boot preflight into the run row at
startandhave
statusdisplay the live run's recorded verdict, visibly distinguishing "what thisrun 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