gate: pass on macOS: tests use /tmp, gatewayd finds its uid without /proc

M5a task 16. The work is Ornith's, from the stopped run; the design
model checked it against the task file and committed it once the
memoryd lint that stopped the gate was fixed (44bdcbd).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-25 16:41:44 -07:00
co-authored by Claude Opus 5.5
parent d47fb32483
commit 7d5c5a8d85
4 changed files with 23 additions and 6 deletions
+5
View File
@@ -0,0 +1,5 @@
# Tests put Unix sockets under `std::env::temp_dir()`. macOS's is long (/var/folders/…/T/), and a
# socket's path there can pass the 104 bytes macOS allows, so every process Cargo runs, tests
# included, uses /tmp.
[env]
TMPDIR = { value = "/tmp", force = true }
+13 -4
View File
@@ -163,7 +163,7 @@ pub fn read_checked(path: &Path, between: &dyn Fn()) -> Result<Zeroizing<Vec<u8>
// 5. `if (opened.dev(), opened.ino()) != (named.dev(), named.ino())`: // 5. `if (opened.dev(), opened.ino()) != (named.dev(), named.ino())`:
// "<path> changed while it was read". // "<path> changed while it was read".
// 6. The owner and mode checks from the original file check, word for word, on `opened` (not // 6. The owner and mode checks from the original file check, word for word, on `opened` (not
// on `named`): the uid of "/proc/self"; "<path> is not owned by the user gatewayd runs as"; // on `named`): the uid of `user_id()`; "<path> is not owned by the user gatewayd runs as";
// "<path> has mode <mode:03o>; only the owner may read it (0600 or 0400)". // "<path> has mode <mode:03o>; only the owner may read it (0600 or 0400)".
// 7. `let mut bytes = Zeroizing::new(Vec::new());` then `file.read_to_end(&mut bytes)`, an // 7. `let mut bytes = Zeroizing::new(Vec::new());` then `file.read_to_end(&mut bytes)`, an
// Err(e) is "cannot read <path>: <e>". Ok(bytes). // Err(e) is "cannot read <path>: <e>". Ok(bytes).
@@ -187,9 +187,7 @@ pub fn read_checked(path: &Path, between: &dyn Fn()) -> Result<Zeroizing<Vec<u8>
if (opened.dev(), opened.ino()) != (named.dev(), named.ino()) { if (opened.dev(), opened.ino()) != (named.dev(), named.ino()) {
return Err(format!("{} changed while it was read", path.display())); return Err(format!("{} changed while it was read", path.display()));
} }
let owner = std::fs::metadata("/proc/self") let owner = user_id()?;
.map_err(|e| format!("cannot read /proc/self: {}", e))?
.uid();
if opened.uid() != owner { if opened.uid() != owner {
return Err(format!( return Err(format!(
"{} is not owned by the user gatewayd runs as", "{} is not owned by the user gatewayd runs as",
@@ -210,6 +208,17 @@ pub fn read_checked(path: &Path, between: &dyn Fn()) -> Result<Zeroizing<Vec<u8>
Ok(bytes) Ok(bytes)
} }
/// The user id this process runs as: the owner of a pipe it has just made. `std` has no call for
/// it, and `/proc/self` is Linux only.
fn user_id() -> Result<u32, String> {
let (reader, _writer) = std::io::pipe().map_err(|e| format!("cannot make a pipe: {e}"))?;
let pipe = std::fs::File::from(std::os::fd::OwnedFd::from(reader));
let meta = pipe
.metadata()
.map_err(|e| format!("cannot read a pipe: {e}"))?;
Ok(meta.uid())
}
/// The text without one trailing newline; not empty; UTF-8. /// The text without one trailing newline; not empty; UTF-8.
fn value(bytes: Vec<u8>) -> Result<Secret, String> { fn value(bytes: Vec<u8>) -> Result<Secret, String> {
// UTF-8 (else an error), one trailing newline removed, not empty. Keep the bytes in `Zeroizing` // UTF-8 (else an error), one trailing newline removed, not empty. Keep the bytes in `Zeroizing`
+4 -2
View File
@@ -42,10 +42,12 @@ fn a_command_killed_by_a_signal_says_so() {
fn the_command_runs_in_cwd_or_in_tmp() { fn the_command_runs_in_cwd_or_in_tmp() {
let dir = TempDir::new("cwd"); let dir = TempDir::new("cwd");
let here = dir.path().to_str().unwrap(); let here = dir.path().to_str().unwrap();
// `pwd` prints the path with symbolic links resolved: on macOS, /tmp is /private/tmp.
let real = |path: &str| std::fs::canonicalize(path).unwrap().display().to_string();
let ran = sh("pwd", Some(here)); let ran = sh("pwd", Some(here));
assert_eq!(ran.stdout, format!("{here}\n\n[exit 0]")); assert_eq!(ran.stdout, format!("{}\n\n[exit 0]", real(here)));
let ran = sh("pwd", None); let ran = sh("pwd", None);
assert_eq!(ran.stdout, "/tmp\n\n[exit 0]"); assert_eq!(ran.stdout, format!("{}\n\n[exit 0]", real("/tmp")));
} }
#[test] #[test]
+1
View File
@@ -6,6 +6,7 @@ reviewer adds findings under "Reviews" once per milestone.
| Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model | | Task | Date | Status | Gate runs | First gate | Deviations | Notes | Model |
|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|
| M5a/16-macos-gate | 2026-09-25 | done (by the design model) | 1 | pass | none | The session's work was right and left uncommitted: the diff matches the task file line for line, and the copied `shell.rs` is identical to the plan's. The stop was right too: clippy 1.95 (the workspace's `rust-version`) flags `collapsible_match` in `memoryd`'s `index.rs`, which the plan's gate run did not catch. The design model fixed that in its own commit (44bdcbd), outside this task. Then `make gate` printed `gate: ok`, 936 tests. | Ornith-1.5-35B-A3B (the session); design model (the commit) |
| M5a/16-macos-gate | 2026-09-24 | stopped | 1 | fail | none | Created `.cargo/config.toml` (TMPDIR=/tmp for every process Cargo runs, tests included), copied the new `crates/toolkit/tests/shell.rs` from `docs/plans/M5a/files/`, and made the three edits to `read_checked` in `crates/gatewayd/src/secrets.rs`: the comment `"/proc/self"` -> `user_id()`, the three `/proc/self` lines -> `let owner = user_id()?;`, and the `user_id()` function (the uid of a freshly-made pipe) inserted above `value`. `cargo fmt --all`; `cargo test -p gatewayd -p toolkit -p brokerd` every suite passes. `make gate` never reaches the tests: it fails to compile `memoryd` on clippy `collapsible_match` at `crates/memoryd/src/index.rs:224` (the `LogRecord::User { time, content }` arm's nested `if !content.trim().is_empty()` guard), a crate this task does not touch and a file it does not list. Verified pre-existing and unrelated by stashing all my changes and re-running `make gate`: identical `memoryd` failure on the original committed code, so the gate does not compile `memoryd` with or without this task. Did not edit `memoryd` (outside this task's scope, AGENTS.md "do that task and nothing else"); committing only this log row per AGENTS.md point 4. | Ornith-1.5-35B-A3B | | M5a/16-macos-gate | 2026-09-24 | stopped | 1 | fail | none | Created `.cargo/config.toml` (TMPDIR=/tmp for every process Cargo runs, tests included), copied the new `crates/toolkit/tests/shell.rs` from `docs/plans/M5a/files/`, and made the three edits to `read_checked` in `crates/gatewayd/src/secrets.rs`: the comment `"/proc/self"` -> `user_id()`, the three `/proc/self` lines -> `let owner = user_id()?;`, and the `user_id()` function (the uid of a freshly-made pipe) inserted above `value`. `cargo fmt --all`; `cargo test -p gatewayd -p toolkit -p brokerd` every suite passes. `make gate` never reaches the tests: it fails to compile `memoryd` on clippy `collapsible_match` at `crates/memoryd/src/index.rs:224` (the `LogRecord::User { time, content }` arm's nested `if !content.trim().is_empty()` guard), a crate this task does not touch and a file it does not list. Verified pre-existing and unrelated by stashing all my changes and re-running `make gate`: identical `memoryd` failure on the original committed code, so the gate does not compile `memoryd` with or without this task. Did not edit `memoryd` (outside this task's scope, AGENTS.md "do that task and nothing else"); committing only this log row per AGENTS.md point 4. | Ornith-1.5-35B-A3B |
| M5a/15-soul-and-device | 2026-09-24 | done (by the design model) | 1 | pass | none | Finished by the design model from the session's work, which was right and left uncommitted. The stop was not the server: `tools/run-plan.sh` and the device tests both read `BOXMAKER_MODEL`. The run set it to OpenCode's id `straylight/ornith-1.5-35b-a3b`, which the device tests then sent to `llama-server` as the model name, and it answered 400. With it unset, `make verify-device` passed 6 of 6 (72.7 s). The three copied files are identical to the plan's; `make gate` prints `gate: ok`, 936 tests. | Ornith-1.5-35B-A3B (the session); design model (the commit) | | M5a/15-soul-and-device | 2026-09-24 | done (by the design model) | 1 | pass | none | Finished by the design model from the session's work, which was right and left uncommitted. The stop was not the server: `tools/run-plan.sh` and the device tests both read `BOXMAKER_MODEL`. The run set it to OpenCode's id `straylight/ornith-1.5-35b-a3b`, which the device tests then sent to `llama-server` as the model name, and it answered 400. With it unset, `make verify-device` passed 6 of 6 (72.7 s). The three copied files are identical to the plan's; `make gate` prints `gate: ok`, 936 tests. | Ornith-1.5-35B-A3B (the session); design model (the commit) |
| M5a/15-soul-and-device | 2026-09-24 | stopped | 1 | pass | none | Copied `config/soul.md`, `crates/loopd/tests/support/memoryd.rs` and a new version of `crates/loopd/tests/device.rs` from `docs/plans/M5a/files/` into place; in the Makefile `verify-device` added the `BOXMAKER_MEMORYD=$(CURDIR)/target/debug/memoryd \` line after the `BOXMAKER_INFERPROXY=...BOXMAKER_BXCTL=... \` line. `cargo fmt --all`; `make gate` printed `gate: ok` on the first run (device tests ignored there, count unchanged ~936). straylight is reachable and `/models` lists `ornith-1.5-35b-a3b` as `loaded` with slot 0 `is_processing:false`, but `make verify-device` fails identically on a second run: every inference request returns `400 "model 'straylight/ornith-1.5-35b-a3b' not found"` (the server prefixes the bare name with a `straylight/` namespace and cannot resolve it even though `/models` reports it loaded) and the conversation test also fails with "loopd did not come up within 60 s". Root cause is server-side model routing on straylight, not the copied files or Makefile line (the gate passes and the request model name is fixed by the task). Did not change the test or the soul to force a pass; committing only this log row per AGENTS.md. | Ornith-1.5-35B-A3B | | M5a/15-soul-and-device | 2026-09-24 | stopped | 1 | pass | none | Copied `config/soul.md`, `crates/loopd/tests/support/memoryd.rs` and a new version of `crates/loopd/tests/device.rs` from `docs/plans/M5a/files/` into place; in the Makefile `verify-device` added the `BOXMAKER_MEMORYD=$(CURDIR)/target/debug/memoryd \` line after the `BOXMAKER_INFERPROXY=...BOXMAKER_BXCTL=... \` line. `cargo fmt --all`; `make gate` printed `gate: ok` on the first run (device tests ignored there, count unchanged ~936). straylight is reachable and `/models` lists `ornith-1.5-35b-a3b` as `loaded` with slot 0 `is_processing:false`, but `make verify-device` fails identically on a second run: every inference request returns `400 "model 'straylight/ornith-1.5-35b-a3b' not found"` (the server prefixes the bare name with a `straylight/` namespace and cannot resolve it even though `/models` reports it loaded) and the conversation test also fails with "loopd did not come up within 60 s". Root cause is server-side model routing on straylight, not the copied files or Makefile line (the gate passes and the request model name is fixed by the task). Did not change the test or the soul to force a pass; committing only this log row per AGENTS.md. | Ornith-1.5-35B-A3B |