fix(egress,ctl): survive a dead hostname, name the right workspace #11

Merged
troed merged 2 commits from fix/feedback-notes-triage into main 2026-10-28 22:32:32 +01:00
Owner

Why

A triage of the agent feedback log (62 entries, all projects, Oct 5-28). Three
failure classes were worth fixing in code; the rest were stale (the MCP-abort
saga was already fixed by deferring the attach, and remote allowlist refusal is
already loud) or belong to a machine's host-action policy.

Egress: one dead hostname no longer takes the control plane down

A [[egress.tcp]] rule whose host stops resolving aborted run.py/agent.py
startup (exit 3). That took every umwelt tool with it - including the
umwelt_egress remove call that would have fixed the rule - so the only way out
was hand-editing .umwelt.toml. Recorded three times, in three workspaces.

  • resolve_tcp_hosts_tolerant resolves each rule independently and returns
    (pairs, unresolved); the strict resolve_tcp_hosts keeps its
    "resolve or raise" contract for callers that want it.
  • prepare_egress_hooks is the startup path (both front-ends plus recreate):
    an unresolvable rule is skipped with one stderr warning and recorded on
    EgressState.unresolved.
  • umwelt_egress list reports unresolved, and a rule absent from the
    rendered ruleset is no longer advertised as direct (only what the kernel
    actually permits is direct).
  • umwelt_egress add resolves the host at write time and returns a warning
    when it does not resolve: the rule is saved, but the agent learns it is inert
    before the next start rather than after.

The trade is explicit: a typo now degrades to "no access, loudly reported"
instead of "no control plane, hand-edit to recover".

umwelt-ctl stop: name the right workspace

umwelt-ctl stop reflect, run from a checkout whose root is ~/dev/reflect,
resolved the package subdir ~/dev/reflect/reflect, stopped nothing, and
reported a misleading "control plane unreachable; try --hard".

A directory target with neither a socket nor a container is now refused with
the nearest parent sandbox named - for --hard too, which previously printed
nothing and "succeeded". A nested workspace that has its own sandbox still
stops normally (pinned by a test), so the hint cannot fire on a real target.

Exec env: writable build caches

$HOME/.cache is a read-only mount, so uv and pip failed with
"Failed to initialize cache ... Read-only file system" in project after
project. Every exec now carries XDG_CACHE_HOME=/tmp/umwelt-cache plus
UV_CACHE_DIR and PIP_CACHE_DIR under it. Container-local by design: it
removes the failure, not the re-download after a recreate, and a command's own
export still wins.

Docs

The egress section, the quirk list (uv/pip cache defaults, curl --noproxy '*'
for a direct rule - LAN rules want an IP, the gateway does not resolve LAN
names, pkill -f matching its own wrapper shell, $HOME inside the sandbox
being synthetic), the skill's network reference, and the AGENTS.md hygiene
scan (wrapped in bash -c: dash has no source, so the bare form exited 127
and read as clean).

Verification

  • python3 -m pytest -m "not integration and not root and not mitmproxy":
    1187 passed, 44 deselected.
  • Every new test was watched failing first (AttributeError / KeyError /
    ConfigError / misleading-stop) before the code that makes it pass.
  • Tracked-tree hygiene scan: clean.
  • Not covered here: the image tooling the same triage asked for (xxd,
    pngquant, python3-pil) went into a machine-local image overlay rather
    than this repo, so the public image is unchanged.
## Why A triage of the agent feedback log (62 entries, all projects, Oct 5-28). Three failure classes were worth fixing in code; the rest were stale (the MCP-abort saga was already fixed by deferring the attach, and remote allowlist refusal is already loud) or belong to a machine's host-action policy. ## Egress: one dead hostname no longer takes the control plane down A `[[egress.tcp]]` rule whose host stops resolving aborted `run.py`/`agent.py` startup (exit 3). That took every umwelt tool with it - including the `umwelt_egress remove` call that would have fixed the rule - so the only way out was hand-editing `.umwelt.toml`. Recorded three times, in three workspaces. - `resolve_tcp_hosts_tolerant` resolves each rule independently and returns `(pairs, unresolved)`; the strict `resolve_tcp_hosts` keeps its "resolve or raise" contract for callers that want it. - `prepare_egress_hooks` is the startup path (both front-ends plus `recreate`): an unresolvable rule is skipped with one stderr warning and recorded on `EgressState.unresolved`. - `umwelt_egress list` reports `unresolved`, and a rule absent from the rendered ruleset is no longer advertised as `direct` (only what the kernel actually permits is direct). - `umwelt_egress add` resolves the host at write time and returns a `warning` when it does not resolve: the rule is saved, but the agent learns it is inert before the next start rather than after. The trade is explicit: a typo now degrades to "no access, loudly reported" instead of "no control plane, hand-edit to recover". ## umwelt-ctl stop: name the right workspace `umwelt-ctl stop reflect`, run from a checkout whose root is `~/dev/reflect`, resolved the package subdir `~/dev/reflect/reflect`, stopped nothing, and reported a misleading "control plane unreachable; try --hard". A directory target with neither a socket nor a container is now refused with the nearest parent sandbox named - for `--hard` too, which previously printed nothing and "succeeded". A nested workspace that has its own sandbox still stops normally (pinned by a test), so the hint cannot fire on a real target. ## Exec env: writable build caches `$HOME/.cache` is a read-only mount, so `uv` and `pip` failed with "Failed to initialize cache ... Read-only file system" in project after project. Every exec now carries `XDG_CACHE_HOME=/tmp/umwelt-cache` plus `UV_CACHE_DIR` and `PIP_CACHE_DIR` under it. Container-local by design: it removes the failure, not the re-download after a recreate, and a command's own `export` still wins. ## Docs The egress section, the quirk list (uv/pip cache defaults, `curl --noproxy '*'` for a direct rule - LAN rules want an IP, the gateway does not resolve LAN names, `pkill -f` matching its own wrapper shell, `$HOME` inside the sandbox being synthetic), the skill's network reference, and the `AGENTS.md` hygiene scan (wrapped in `bash -c`: dash has no `source`, so the bare form exited 127 and read as clean). ## Verification - `python3 -m pytest -m "not integration and not root and not mitmproxy"`: 1187 passed, 44 deselected. - Every new test was watched failing first (`AttributeError` / `KeyError` / `ConfigError` / misleading-stop) before the code that makes it pass. - Tracked-tree hygiene scan: clean. - Not covered here: the image tooling the same triage asked for (`xxd`, `pngquant`, `python3-pil`) went into a machine-local image overlay rather than this repo, so the public image is unchanged.
Three failure classes recorded by agents in the feedback log:

Egress. A [[egress.tcp]] rule whose host no longer resolves aborted
run.py/agent.py startup (exit 3), which took every umwelt tool with it -
including the `umwelt_egress remove` call that would have fixed the rule,
so a hand edit of .umwelt.toml was the only way out. Such a rule is now
skipped at create/recreate with one stderr warning, reported by
`umwelt_egress list` under `unresolved`, and left out of the rendered
ruleset (a skipped rule is no longer advertised as `direct`). `add` warns
when the host does not resolve at write time. resolve_tcp_hosts keeps its
strict "resolve or raise" contract; the tolerant variant is the startup
path (run.py, agent.py, recreate).

umwelt-ctl stop. `stop reflect` from ~/dev/reflect resolves the package
subdir, stops nothing, and reported a misleading "control plane
unreachable; try --hard". A directory target with neither socket nor
container is now refused with the nearest parent sandbox named, for
--hard as well (which used to "succeed" silently). A nested workspace
with its own sandbox still stops normally.

Exec env. $HOME/.cache is a read-only mount, so uv and pip failed with
"Read-only file system" in project after project. Every exec now carries
XDG_CACHE_HOME=/tmp/umwelt-cache plus UV_CACHE_DIR and PIP_CACHE_DIR
under it; a command's own export still wins.

Docs: the egress section, the quirk list (uv cache default, curl
--noproxy for a direct rule, pkill -f killing the wrapper), the skill's
network reference, and AGENTS.md's hygiene scan command (dash has no
`source`, so the bare form exited 127 and read as clean).
docs(quirks): $HOME inside the sandbox is synthetic, not the host's
All checks were successful
ci / unit (pull_request) Successful in 57s
00e6668728
troed merged commit 9ea785ae31 into main 2026-10-28 22:32:32 +01:00
troed deleted branch fix/feedback-notes-triage 2026-10-28 22:32:32 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
troed/umwelt!11
No description provided.