Files
multi-agent-mux/.agents/reports/reviewer-grok-01/report-fe93e3f2.md
T
GodopuandClaude Opus 5.5 2e90c809d3 release(v4.8.1): bump framework and eight skills to 4.8.1 in lockstep; archive PR #43 reviews (two rounds)
Issue #26: one .mam.env parser for the wrapper and the Python tools,
decimal integer knobs, tests on a local MQTT broker. Merge-workspace
review: round 1 (@b58edac) claude agy PASS, codex NOT PASS (R1-R4), grok
no verdict (its late report, archived as report-c9ed5b14.md, found R1 too);
round 2 (@0eec795) 4/4 PASS. Reports and round-2 identity sidecars go to
.agents/reports/<session>/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
2026-10-02 00:23:22 +09:00

122 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Review: PR #43 (fix/issue-26) round 2 @ 0eec795
- Role: Reviewer (grok). Full range `e1eb22b..0eec795` (this reviewer had no recorded round-1 verdict on the previous job).
- Identity: base `e1eb22bd59ce825e3a22e86433c30f7ffd833810`, head `0eec795cd1b46f7a28b9a1cc87ef8577619a731f`, tree `584eac551f880232896b898cc43156f28ae1bacb`, 7 commits, clean worktree.
- Verified at `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261001T140030Z-34381f/1`. Main stayed at `e1eb22b`. No product file was edited.
- `git diff --check b58edac..HEAD` is clean. That commit is 7 files, +128/−19: `lib.sh`, `envfile.py`, `mqtt_common.py`, the delegate-job wrapper, `.mam.env.example`, and the two issue-26 test files.
`0eec795` closes the round-1 fail-open: a `.mam.env` that exists but cannot be read or decoded is `EnvFileError`, the `--shell` command exits 1, and the wrapper exits 1. NUL lines are skipped by the shared parser. `mam_int` uses ASCII whitespace and digits. An explicitly empty integer flag is refused.
## 1. Parser agreement
`mqtt_common._parse_env_file` (`mqtt_common.py:98-104`) and the wrapper both call `envfile.apply` / `envfile.parse`. One table, both loaders.
Rules in `envfile.py:3-12` and `.mam.env.example:14-25` match the code:
| Input | Result |
|---|---|
| Blank line, `#` comment | skipped (`envfile.py:64-65`) |
| Surrounding ASCII space, one matching quote pair | stripped; internal spaces kept (`envfile.py:80-83`) |
| Mismatched quotes | kept |
| Extra `=` | value keeps every `=` after the first (`envfile.py:69`) |
| Empty value | kept |
| `$(...)`, `${...}`, backticks | literal |
| `export KEY=...` | skipped, `invalid key` (the key is `export KEY`) |
| Reserved bash variables | skipped with the name, never the value (`envfile.py:77-79`) |
| No `=`, or a key outside `^[A-Za-z_][A-Za-z0-9_]*$` | skipped with the line number (`envfile.py:71-76`) |
| CRLF | `splitlines()` drops the CR |
| A line containing a NUL byte | skipped, `{path}:{n}: skipped (NUL byte)` (`envfile.py:66-68`). The warning has no value. |
| Whole-file `OSError` or `UnicodeDecodeError` | `EnvFileError: {path}: cannot read ({type name})` (`envfile.py:57-61`), `from None` so the message carries no bytes from the file |
| Duplicate keys | both pairs returned; `apply` (`envfile.py:91-94`) sets the first, because the key is then already in the environment |
`export KEY=` is what the old `source` accepted. Both loaders skip it, and the example says so (`.mam.env.example:21-23`). `deploy/install.sh:513-530` is an unquoted heredoc, so `$MAM_PORT`, `$MAM_CLIENT_PREFIX`, and `$HOME` expand before the parser sees the file. Uncommented example assignments contain no `$` or backticks (`test_a5`).
A leading BOM is outside the example. `open(..., encoding="utf-8")` does not strip it, so line 1 fails the key check and the next line loads. Both loaders do that together.
`tests/test_issue26_envfile.py` locks the two real loaders, including mode `000` and broken UTF-8 (`:154-178`, skipped only when euid is 0) and the NUL line (`:183-193`).
## 2. Wrapper eval
The wrapper runs `python -I envfile.py --shell "$TARGET_ENV"` and evals stdout (`multi-agent-mux-delegate-job:37-45`). The command substitution captures stdout only, so the parser's stderr stays visible. Every printed line is `export NAME=<shlex.quote(value)>` for a key that passed `_KEY.fullmatch` and `is_reserved` (`envfile.py:74-83`, `112-113`). A value such as `BASH_VERSINFO[$(touch marker)0]` stays quoted text; the marker is not created (`test_a6`).
Failure:
- Missing interpreter: the substitution fails, the wrapper prints `ERROR: cannot load <path>` and exits 1 (`test_a1_an_unreadable_parser_fails_loudly`, `:138-146`).
- Unreadable or undecodable file: `envfile.py` prints `ERROR: {path}: cannot read (PermissionError|UnicodeDecodeError)` and returns 1 (`envfile.py:107-109`); the wrapper then prints `ERROR: cannot load <path> (see the line above)` and exits 1 (`:43-44`). The test requires both lines and requires the value to be absent from stderr.
- A missing file is still success with no exports (`envfile.py:103-104`, `test_a5` `:294-302`). The wrapper only enters this path when `[ -f "$TARGET_ENV" ]`.
Python entry points take the same read failure. `_python_load` asserts a non-zero exit, no `LOADED`, and `EnvFileError` in stderr.
## 3. Environment wins over the file
`apply` sets a key only when it is absent from the mapping (`envfile.py:92-94`). The shell side passes the process environment in, so a variable the wrapper already set, or one the caller exported, stays. Documented in `envfile.py:10`, the wrapper comment at `:37-40`, `.mam.env.example:24`, and `MESSAGING.md:366-369`.
Effect of leaving `source` (which overwrote the environment):
- The loop runner starts the wrapper with `dict(os.environ)` and sets `DELEGATE_JOB_PYTHON` to `sys.executable` when unset (`runner.py:548-550`), so the file cannot replace the runner's broker or interpreter.
- The remote runner already used the Python loader, which had the same precedence.
- Tests set `MQTT_BROKER` / `MQTT_PORT` / `MQTT_TLS` in the environment (`conftest.py:1568-1575`). The file can no longer point those back at another broker.
- A stale shell export of `MQTT_*` is no longer overwritten on each wrapper start. That is the intended precedence. `install.sh` still expands its seed before the file exists.
`test_a3` (`:230-238`) checks file `50` against environment `70` on a real dry-run record and on the Python loader.
## 4. Integer rule
`mam_int` (`lib.sh:2345-2358`) builds its class from the six ASCII blanks `$ ' \t\n\r\v\f '` and the digits `0123456789`, strips leading zeros, forces base 10, and accepts `min <= n <= 999999999`. `runner._positive_int` (`runner.py:71-81`) strips the same six characters and requires `isascii()` and `isdigit()`. The expectation helper uses the same ASCII strip (`test_issue26_ints.py:88-96`).
Minimums in `INT_KNOBS` (`:35-54`):
- Minimum 1 (0 would close or idle out immediately): `MAM_JOB_TIMEOUT_S` 3600, `MAM_HEARTBEAT_SEC` 30, `MAM_HEARTBEAT_MISSES` 4, `MAM_ACK_TIMEOUT_S` 180, and the five flags `--timeout`, `--idle-timeout`, `--heartbeat`, `--heartbeat-misses`, `--ack-timeout`.
- Minimum 0 (0 means check once or do not wait): `MAM_INJECT_ACK_GRACE_S`, `delegate_wait_started`'s `ack_s` (`lib.sh:2384`), `SKS_DIALOG_TIMEOUT` 30, `SKS_BUSY_TIMEOUT` 120, `SKS_DELIVERY_GRACE_S` 15, `SKS_QUIESCENT_TRIES` 20.
An invalid environment value warns and uses the default (`mam_int_knob`, `lib.sh:2364-2369`). An invalid flag exits 1. `parse_opts` records that the flag was present (`:167-179`) and refuses an empty value (`:199-203`); the default applies only when the flag is absent. The space-padded token keeps `HEARTBEAT_SEC` distinct from `HEARTBEAT_MISSES`. `test_b2` includes the explicit empty string among the refused values (`:228-232`).
Locale: `test_b1_unicode_blanks_and_digits_are_invalid_in_every_locale` (`:188-204`) runs U+2003, U+00A0, U+3000, and Arabic and fullwidth digits under `C` and `en_US.UTF-8`, through `mam_int`, `_positive_int`, the wrapper record, and the runner. All four reject them and keep the 3600 default.
Three runner settings stay on their own Python conversions and do not reach bash arithmetic: `MAM_WAIT_GRACE_S` (`runner.py:627-628`, `isdigit` then `int`), `MAM_CHILD_WAIT_S` (`:748-749`, `float`), `MAM_JOB_TREE_MAX` (`:752`, `int`). They predate this integer rule. `NOT_ARITHMETIC` classifies the shell defaults that use `[ -ge ]` / `[ -le ]` / `sleep` / an argv, and on this host's bash 3.2 those `[` comparisons are decimal.
## 5. Local MQTT broker
`tests/_local_mqtt.py` is an in-process MQTT 3.1.1 broker on `127.0.0.1:0`, daemon accept and connection threads (`:103-104`, `:139`), stopped by closing the listen socket and the live connections (`:107-120`). Scope is what the repo's clients use: protocol level 4, QoS 0/1, retained messages. QoS 2 closes the connection (`:190-191`).
A PUBLISH is routed and, when retained, stored before PUBACK (`:196-200`). The session fixture (`conftest.py:1568-1584`) sets `MQTT_BROKER` / `MQTT_PORT` / `MQTT_TLS` and drops `MQTT_HOST` and the credential and certificate variables for the session, then restores them. `test_c0` waits for CONNACK, SUBACK, UNSUBACK, and PUBACK before it reads broker state, and it checks topic, payload, QoS, and the retain flag (`test_local_mqtt.py:100-130`). `test_c0_holds_when_the_broker_is_slow` (`:140-160`) delays CONNECT, UNSUBSCRIBE, and routing so a test that reads state before the ack fails. `test_c2` (`:297-310`) rejects the public-broker literal outside an allowlist and rejects a helper that drops `MQTT_BROKER` / `MQTT_HOST` without naming a replacement. `test_c0_publish_event_reaches_job_subscriber_over_the_local_broker` uses a `uuid4` topic prefix.
Commit `2826e70` lists the moved cases: issue 18 acceptance 1/2/3/5, issue 6 `t1_1/2/7/8/14/15/16/20`, issue 19's two MQTT-loop cases, issue 33 `b3/r3/b7`, issue 30 `s9/s10/r2`, and calls that set 22. Those harnesses inject `local_broker_env()` and the "broker unreachable" skips are gone. `0eec795` does not touch the broker or those tests. This round's required run includes `tests/test_local_mqtt.py` and `tests/test_issue18_liveness_contract.py`; both are in the 174 passes below. Issue 6, 19, 30, and 33 were outside this brief's command.
## 6. The baseline-failure claim
`tests/test_issue10_13_purge.py` is identical to `e1eb22b` (`git diff` empty). On this host, with `HOME` and `--basetemp` under `/tmp` and with MQTT variables unset:
`23 passed in 11.62s`
The claim that a full-suite run shows 13 failures of which 12 are this host's baseline in that file does not reproduce here. The tests use `tmp_path` sandboxes and `uuid4` conversation ids. Nothing under the real home was purged.
## 7. Design boundary
The shell diff adds numeric parsing for clocks that already existed: `mam_int` / `mam_int_knob` on the job timeout, heartbeat, ack, inject grace, and the SKS dialog, busy, delivery, and quiescent counts (`lib.sh` around the `$(( $(date +%s) + $(mam_int_knob SKS_...) ))` deadlines). That is the decimal-integer rule, so `0700` is 700 and `0900` does not abort arithmetic.
No new bash judgment of a timeout label, a budget extension, or a stall close landed in this range. `MAM_STALL_WATCH` stays a string compare (`NOT_ARITHMETIC`). Closing remains the existing clocks. Nothing here implements #30 or #33, and nothing needs to move back to the planner.
## Should Python entry points raise?
Yes. A file that exists and cannot be read must stop the process. Warning and continuing would load an empty configuration and, when `MQTT_*` are unset, the public default broker. That is the fail-open `0eec795` removes, and it has to be the same in both loaders. `publish_event.main` calls `ensure_env_loaded()` after argument parsing and does not catch the exception (`publish_event.py:337`); the same call is the first environment step in `registry.main`, `job_subscriber.main`, `cli.main`, `bench.main`, and reconcile's embedded subscriber (`mqtt_common.py:113-115`). An agent whose `.mam.env` is unreadable then cannot publish `started` or `completed`, and the job closes on the clocks. That is the same outcome as refusing to run unconfigured.
The wrapper prints one `ERROR` line and exits 1. The Python process exits with a traceback that names `EnvFileError` and the path and the exception type, and does not include the file body. That difference is acceptable. `_env_loaded = True` is set before `_load_dotenv` (`mqtt_common.py:123-124`), so a caller that caught the exception would not retry in-process. The entry points above do not catch it.
## Checks
From the review snapshot, MQTT variables unset, no `.mam.env` exported, `AGENT_PYTHON_BIN` and the interpreter the pr-43 venv, `--basetemp` under `/tmp`:
`174 passed in 208.02s` — `tests/test_issue26_envfile.py`, `tests/test_issue26_ints.py`, `tests/test_local_mqtt.py`, `tests/test_issue18_liveness_contract.py`, `tests/test_tier1_unit.py`.
The full suite is the orchestrator's.
## Notes (non-blocking)
- Duplicate keys in one file are first-wins (`envfile.py:91-94`). The old `source` was last-wins. Both loaders agree. The example states environment-over-file and does not mention a repeated key inside the file.
- A `#` comment that also contains a NUL is skipped as a comment (`envfile.py:64-65` runs before the NUL check). A value line with a NUL still warns and is skipped.
- `MESSAGING.md:369` still says a missing explicit `MAM_ENV_FILE` logs an error and connections fail closed with `RuntimeError`. `mqtt_common.py:51-54` returns without loading when that path is not a regular file, and this PR does not change that branch. A file that exists and cannot be read now raises, which is the contract under review.
- `MAM_WAIT_GRACE_S`, `MAM_CHILD_WAIT_S`, and `MAM_JOB_TREE_MAX` remain outside `_positive_int` (section 4). They do not reach bash arithmetic.
[VERDICT: PASS]