diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..c29bd82 --- /dev/null +++ b/.cargo/config.toml @@ -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 } diff --git a/crates/gatewayd/src/secrets.rs b/crates/gatewayd/src/secrets.rs index a01ce5d..8e97d5a 100644 --- a/crates/gatewayd/src/secrets.rs +++ b/crates/gatewayd/src/secrets.rs @@ -163,7 +163,7 @@ pub fn read_checked(path: &Path, between: &dyn Fn()) -> Result // 5. `if (opened.dev(), opened.ino()) != (named.dev(), named.ino())`: // " changed while it was read". // 6. The owner and mode checks from the original file check, word for word, on `opened` (not - // on `named`): the uid of "/proc/self"; " is not owned by the user gatewayd runs as"; + // on `named`): the uid of `user_id()`; " is not owned by the user gatewayd runs as"; // " has mode ; 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 // Err(e) is "cannot read : ". Ok(bytes). @@ -187,9 +187,7 @@ pub fn read_checked(path: &Path, between: &dyn Fn()) -> Result if (opened.dev(), opened.ino()) != (named.dev(), named.ino()) { return Err(format!("{} changed while it was read", path.display())); } - let owner = std::fs::metadata("/proc/self") - .map_err(|e| format!("cannot read /proc/self: {}", e))? - .uid(); + let owner = user_id()?; if opened.uid() != owner { return Err(format!( "{} is not owned by the user gatewayd runs as", @@ -210,6 +208,17 @@ pub fn read_checked(path: &Path, between: &dyn Fn()) -> Result 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 { + 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. fn value(bytes: Vec) -> Result { // UTF-8 (else an error), one trailing newline removed, not empty. Keep the bytes in `Zeroizing` diff --git a/crates/toolkit/tests/shell.rs b/crates/toolkit/tests/shell.rs index 7f821eb..4d6dd72 100644 --- a/crates/toolkit/tests/shell.rs +++ b/crates/toolkit/tests/shell.rs @@ -42,10 +42,12 @@ fn a_command_killed_by_a_signal_says_so() { fn the_command_runs_in_cwd_or_in_tmp() { let dir = TempDir::new("cwd"); 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)); - 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); - assert_eq!(ran.stdout, "/tmp\n\n[exit 0]"); + assert_eq!(ran.stdout, format!("{}\n\n[exit 0]", real("/tmp"))); } #[test] diff --git a/docs/implementer-log.md b/docs/implementer-log.md index db56d66..a0ddfb4 100644 --- a/docs/implementer-log.md +++ b/docs/implementer-log.md @@ -6,6 +6,7 @@ reviewer adds findings under "Reviews" once per milestone. | 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/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 |