Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 104 additions & 133 deletions .dev-loop/INGEST_REPORT.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions log.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,7 @@ Append-only. Format: `## [YYYY-MM-DD] <ingest|revise|lint|gap|contradiction|drif
## [2026-09-28] ingest | testing-strategy-agent-tool-shared-handler-tests — a UI action that is also a registered tool is tested once at the shared function plus two entry-point tests per tool (Registration incl. AbortSignal teardown, Wiring via spy) against a `document.modelContext` stub; a bug fix that changes the handler's contract changes the `inputSchema` assertion in the same commit; one DevTools Run-tool pass per release.

## [2026-09-28] revise | WebMCP adopted as the development standard (owner decision 2026-09-28): frontend/agent-interfaces/agent-facing-tool-surfaces trigger widened to any new or changed user action in a web UI + bug-fix and exclusion-list edge cases; AGENTS.md routing step 7 gains a web-UI-action row → frontend agent-interfaces then qa parity gate; INDEX.md frontend/qa/testing route lines and the frontend/qa/testing domain indexes updated; related links added both ways (release-gates, cross-layer-effect-tests, in-session-tool-exposure). The standard keeps the human UI primary and the tool layer additive (CG draft; Chrome origin trial + ChatGPT desktop runtimes).

## [2026-09-28] ingest | testing-async-teardown-after-aborted-tasks — knowledge-flush: a test that spawned tokio tasks deletes their directory from a `Drop` guard or a post-assertion cleanup; `abort()` only schedules cancellation and a running task recreates the path, and a failing assertion skips the trailing cleanup. Record the outcome, abort and await every `JoinHandle`, delete, then assert; `Drop` stays the net for pre-runtime failures. Verified against tokio task/JoinHandle/Runtime docs and a local reproduction (tokio 1.53.1: abort-then-delete leaked 148/200, abort-await-delete 0/200). Edge rows added on async-testing and artifact-leakage-from-a-suite; back-links on artifact-leakage and test-data-and-isolation (async-testing's related line is left to open PR #226, which rewrites it)

## [2026-09-28] ingest | platforms-filesystems-trailing-separator-under-realpath — knowledge-flush: `realpath(3)` on `file/` (and `file/.`) is `ENOTDIR` under POSIX, glibc 2.41 and musl but resolves to the file on macOS, so a containment or file-vs-directory check that relies on the resolver's error gives a platform-dependent verdict; Rust `Path::join`/`==` discard the separator while the bytes reach `canonicalize`. Check the raw string first and decide by intent (reject, or require `is_dir()` after resolving); pin the `file/` case on both platforms. Reproduced via libc `realpath` under ctypes on Darwin 25.1 / python:3-slim / python:3-alpine and with Rust 1.98 `std::fs::canonicalize`. Edge rows on validation-at-trust-boundaries and bsd-vs-gnu-cli; back-links on those two, paths-case-and-line-endings and path-valued-config
2 changes: 1 addition & 1 deletion wiki/infrastructure/config/path-valued-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ sources:
- https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html
- https://12factor.net/config
last_verified: 2026-09-03
related: [infrastructure-config-environment-config, platforms-processes-background-services, platforms-environment-path-resolution, backend-python-boundaries-runtime-validation, backend-node-boundaries-runtime-validation, infrastructure-agent-orchestration-gate-evidence-exit-code-class]
related: [infrastructure-config-environment-config, platforms-processes-background-services, platforms-environment-path-resolution, backend-python-boundaries-runtime-validation, backend-node-boundaries-runtime-validation, infrastructure-agent-orchestration-gate-evidence-exit-code-class, platforms-filesystems-trailing-separator-under-realpath]
---

# A Config Value That Is a Filesystem Path
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ sources:
- https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
- https://git-scm.com/docs/gitattributes
last_verified: 2026-07-10
related: [platforms-shells-portable-shell-scripts, platforms-environment-unicode-text-matching, platforms-filesystems-unix-domain-socket-path-length]
related: [platforms-shells-portable-shell-scripts, platforms-environment-unicode-text-matching, platforms-filesystems-unix-domain-socket-path-length, platforms-filesystems-trailing-separator-under-realpath]
---

# Files That Break When a Repo Moves Between macOS, Windows, and Linux
Expand Down
90 changes: 90 additions & 0 deletions wiki/platforms/filesystems/trailing-separator-under-realpath.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
id: platforms-filesystems-trailing-separator-under-realpath
domain: platforms
category: filesystems
applies_to: [general, rust]
confidence: verified
sources:
- https://pubs.opengroup.org/onlinepubs/9699919799/functions/realpath.html
- https://doc.rust-lang.org/std/fs/fn.canonicalize.html
- https://doc.rust-lang.org/std/path/index.html
- https://www.gnu.org/software/coreutils/realpath
last_verified: 2026-09-28
related: [security-input-validation-at-trust-boundaries, platforms-tools-bsd-vs-gnu-cli, platforms-filesystems-paths-case-and-line-endings, infrastructure-config-path-valued-config]
---

# A Path Ending in a Separator Passed to realpath / canonicalize

## When this applies

Code resolves a path that may end in `/` (or `/.`) through `realpath(3)` —
Rust `std::fs::canonicalize`, C `realpath`, any binding over it — and the
result decides something: whether an untrusted relative path stays inside a
base directory, whether the entry is a file or a directory, whether it exists.
Also when such a check accepts on a developer's Mac and rejects on Linux CI, or
the reverse.

## Do this

1. **Decide the trailing-separator rule in your own code, before resolving.**
POSIX says `realpath` "shall fail" with `ENOTDIR` when the name "ends with
one or more trailing `<slash>` characters and the last pathname component
names an existing file that is neither a directory nor a symbolic link to a
directory"; glibc and musl do, macOS resolves `file/` to `file` and returns
success. A verdict that depends on that error is a verdict that depends on
the OS:

| libc / platform | `realpath("…/file/")` | `realpath("…/file/.")` |
|-----------------|-----------------------|------------------------|
| POSIX.1-2017 | shall fail, `ENOTDIR` | shall fail, `ENOTDIR` |
| glibc 2.41 (Linux) | `ENOTDIR` | `ENOTDIR` |
| musl (Alpine Linux) | `ENOTDIR` | `ENOTDIR` |
| macOS (Darwin 25.1) | `Ok("…/file")` | `Ok("…/file")` |
| GNU coreutils `realpath` command | "It ignores trailing slashes" (documented) | — |

2. **Check the raw string, not the path object.** Rust's `Path::join`,
`PathBuf::push`, `components()` and `==` all "disregard trailing
separators", yet the joined value handed to `canonicalize` still carries the
bytes — `Path::new("a/b/") == Path::new("a/b")` is `true` while
`canonicalize` receives `a/b/`. Test the string:
`rel.ends_with(std::path::is_separator)` or `rel.ends_with("/.")`.
3. **Pick the action by what the input is allowed to name:**

| The input must name | Do |
|---------------------|----|
| A file | Reject a trailing separator (or `/.`) before resolving; report it the same way as a missing file |
| A file or a directory | Resolve, then when the raw input ended with a separator require `metadata(&resolved)?.is_dir()` — your check, not the resolver's error |
| A directory only | Resolve, then require `is_dir()` regardless of the suffix |

4. **Pin the `file/` case with a test and run it on both platforms.** The
guard is invisible on Linux (the OS rejects anyway) and load-bearing on
macOS; a mutation that removes the guard must turn the test red on macOS,
and a CI matrix keeps the Linux verdict identical.

## Edge cases

| Case | Then |
|------|------|
| The last component is a symlink to a regular file, with a trailing slash | Same divergence — POSIX names "a symbolic link to a directory" as the only symlink case that resolves; apply the same string check |
| The suffix is `/.` rather than `/` | `ends_with(is_separator)` misses it; check `ends_with("/.")` too (macOS resolves it, glibc/musl return `ENOTDIR`) |
| The trailing slash arrives through a directory-only input (`out/`) | Legitimate on every platform: resolves to the directory. The rule fires only on a non-directory leaf |
| Containment is checked as `resolved.starts_with(base)` | Run the string check first; on macOS `base/report.md/` resolves inside `base` and passes containment while the caller asked for a directory that does not exist |
| Windows | `canonicalize` maps to `GetFinalPathNameByHandle`, not `realpath`; measure separately before assuming either row above |

## Instead of

| If you are about to | Do this instead | Why |
|---------------------|-----------------|-----|
| Let `canonicalize`/`realpath` reject `file/` for you | Check the raw string and decide by intent (step 3) | macOS returns success on `file/`; only glibc/musl return `ENOTDIR` |
| Strip the trailing slash before resolving so both platforms agree | Reject it, or require `is_dir()` after resolving | Stripping accepts `report.md/` as `report.md`, which is exactly the macOS behavior you are trying not to depend on |
| Compare `Path` values to detect the slash | Compare the string (`ends_with(is_separator)`) | `Path` equality and `components()` normalize the separator away |
| Verify the containment check on the developer's Mac only | Add the `file/` case and run it on Linux CI too | The same input accepts on macOS and rejects on Linux; a single platform sees one verdict |

## Sources

- https://pubs.opengroup.org/onlinepubs/9699919799/functions/realpath.html — ERRORS, "shall fail": `[ENOTDIR]` "the file_name argument contains at least one non-<slash> character and ends with one or more trailing <slash> characters and the last pathname component names an existing file that is neither a directory nor a symbolic link to a directory"
- https://doc.rust-lang.org/std/fs/fn.canonicalize.html — "corresponds to the `realpath` function on Unix and the `CreateFile` and `GetFinalPathNameByHandle` functions on Windows"
- https://doc.rust-lang.org/std/path/index.html — "Several methods in this module perform basic path normalization by disregarding repeated separators, non-leading `.` components, and trailing separators"; "`Path::join` and `PathBuf::push` also disregard trailing slashes"
- https://www.gnu.org/software/coreutils/realpath — the coreutils command "ignores trailing slashes"
- Local reproduction 2026-09-28, libc `realpath` called through Python `ctypes` on a regular file `out/report.md`: macOS Darwin 25.1.0 → `Ok(out/report.md)` for both `out/report.md/` and `out/report.md/.`; `python:3-slim` (glibc 2.41) and `python:3-alpine` (musl) → `errno 20 Not a directory` for both. Rust (cargo 1.98.0, macOS): `base.join("out/report.md/")` keeps the slash (`ends_with(is_separator) == true`) and `std::fs::canonicalize` returns `Ok(…/out/report.md)`; `Path::new("out/report.md/") == Path::new("out/report.md")` is `true`
- Field evidence 2026-09-28 (a Rust CLI containing untrusted relative paths under an output directory, crew-run worktree t3-swap-flake; recorded by the originating session): the containment check accepted `…/out/report.md/` on macOS; a string guard on the trailing separator was pinned by a test named `path_entry_with_trailing_slash_is_missing_file_not_found`, and an independent auditor's mutation removing the guard turned it red
1 change: 1 addition & 0 deletions wiki/platforms/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Match your situation to a "load when" line; load only matching pages.
| Page | Load when |
|------|-----------|
| [paths-case-and-line-endings](filesystems/paths-case-and-line-endings.md) | A repo moves between macOS/Windows/Linux and files disappear or collide; an import resolves locally but fails on Linux CI (casing); renaming only the case of a file; diffs show every line changed or a script dies with `bad interpreter: ^M` (CRLF); setting up `.gitattributes` line-ending policy; generating file names or paths that must be valid on Windows (reserved names, path length) |
| [trailing-separator-under-realpath](filesystems/trailing-separator-under-realpath.md) | Resolving a path that may end in `/` or `/.` through `realpath(3)` (Rust `std::fs::canonicalize`, C `realpath`, a binding) when the result decides containment under a base directory, file-vs-directory, or existence; a path check accepts on macOS and rejects on Linux CI (`ENOTDIR`) or the reverse; deciding whether to reject a trailing separator or require `is_dir()` after resolving, and why `Path` equality cannot detect it |
| [unix-domain-socket-path-length](filesystems/unix-domain-socket-path-length.md) | A unix-domain-socket bind/listen fails with "Failed to listen", `listen EINVAL`, `ENAMETOOLONG`, or "AF_UNIX path too long" only inside a deep path (git worktree, nested cache dir) and passes from a shorter path; choosing where to place a socket file for a test suite or IPC channel; verifying whether a suite failure is the fixed `sun_path` buffer limit (104 bytes macOS, 108 Linux) or a real regression |
| [permissions-and-exec-bits](filesystems/permissions-and-exec-bits.md) | "Permission denied" running a script that exists; a script loses its executable bit through git/Windows/zip/CI artifacts; surprise file-mode diffs in git (`core.fileMode`); docker bind-mount files root-owned or unreadable (host/container uid mismatch); pipeline stages can't read each other's artifacts (umask); setting up a shared directory for several users/daemons; reviewing file-permission handling in a repo or pipeline |
| [deleted-file-recovery-on-apfs](filesystems/deleted-file-recovery-on-apfs.md) | Someone asks you to recover a file or folder deleted on macOS and you are about to recommend a recovery tool; deciding whether free-space carving is available at all (TRIM/APFS) before spending time on it; ordering the copy sources (Trash, APFS local snapshot, Time Machine, cloud trash); recovering an exact path from an app's stored bookmark data when the remembered name is wrong |
Expand Down
3 changes: 2 additions & 1 deletion wiki/platforms/tools/bsd-vs-gnu-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ sources:
- https://man.freebsd.org/cgi/man.cgi?sed(1)
- https://man.freebsd.org/cgi/man.cgi?seq(1)
last_verified: 2026-07-10
related: [platforms-shells-portable-shell-scripts, platforms-environment-unicode-text-matching]
related: [platforms-shells-portable-shell-scripts, platforms-environment-unicode-text-matching, platforms-filesystems-trailing-separator-under-realpath]
---

# Same Command Name, Different Userland: BSD (macOS) vs GNU (Linux) Flags
Expand Down Expand Up @@ -50,6 +50,7 @@ General strategy by situation:
|------|------|
| `command -v timeout` succeeds on macOS | Someone installed coreutils unprefixed — confirm `timeout --version` reports GNU coreutils before relying on GNU exit-code semantics (124 on timeout) |
| Any flags passed to `echo` (`-e`, `-n`) | `echo` flag handling differs across shells and userlands — use `printf` for anything beyond a bare literal string |
| A program (not a script) resolves `file/` through `realpath(3)`/`canonicalize` and the check passes on macOS but fails on Linux with `ENOTDIR` | The libc divergence, not the coreutils one: macOS `realpath` resolves a trailing slash on a regular file, glibc/musl reject it — decide the trailing-separator rule in code ([platforms-filesystems-trailing-separator-under-realpath]) |
| Script needs bash 4+ features on macOS | Stock `/bin/bash` on macOS is 3.2 — use `#!/usr/bin/env bash` so a brew-installed bash is picked up, and state the required bash version in the script header |

## Instead of
Expand Down
3 changes: 2 additions & 1 deletion wiki/security/input/validation-at-trust-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ sources:
- https://cmu-sei.github.io/secure-coding-standards/sei-cert-oracle-coding-standard-for-java/rules/input-output-fio/fio16-j/
- https://zod.dev/api
last_verified: 2026-09-03
related: [security-authz-resource-level-checks, frontend-security-xss-safe-rendering, security-agent-exposure-in-session-tool-exposure]
related: [security-authz-resource-level-checks, frontend-security-xss-safe-rendering, security-agent-exposure-in-session-tool-exposure, platforms-filesystems-trailing-separator-under-realpath]
---

# Validating Data at a Trust Boundary
Expand Down Expand Up @@ -56,6 +56,7 @@ from other services/queues.
| Message from your own internal service ("we trust our services") | Validate the shape at the consumer boundary anyway — the sender can be buggy or compromised; a trust boundary is wherever data enters code that acts on it |
| Header value used in logic (`X-Forwarded-For`, `Host`) | Client-settable: validate format and accept forwarding headers only from your configured trusted proxy before using them |
| A persisted numeric value positions or sizes an entity in a shared space (placement coordinates, scale, canvas/map position in a game or collaborative board) | The valid range is a domain rule (playfield rectangle, min/max scale), not a type limit — OWASP's semantic validation. Clamp to those bounds on the server at the write, or reject with the bound in the error; a shape-only schema (`z.number()`) accepts `x=-9999` and `scale=0.01`, which place the entity off-screen or invisible and break the rules the space enforces |
| The user-supplied path may end in `/` or `/.` and the containment check relies on canonicalize/`realpath` rejecting `file/` | Check the raw string before resolving and decide by intent (reject, or require `is_dir()` after resolving): macOS resolves `file/` to `file`, glibc/musl return `ENOTDIR`, so the verdict otherwise differs between a Mac and Linux CI ([platforms-filesystems-trailing-separator-under-realpath]) |
| Webhook provider offers no signature | Require a shared-secret token in the URL/header, and act on provider state re-fetched from the provider's API rather than on payload fields |

## Instead of
Expand Down
1 change: 1 addition & 0 deletions wiki/testing/async/async-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ un-awaited promises; or an async test intermittently interferes with the next te
| Runner reports an unhandled rejection after the suite passes | A promise was created without `await`/`return` — find it and await it; do not silence the warning |
| Assertions run inside a `.then`/callback the test never awaits | Add `expect.assertions(n)` / `expect.hasAssertions()` so the test fails when the callback is skipped, then restructure to await-then-assert |
| A stream-fed test hangs after consuming the first record, with the later records never delivered | The records arrived in one chunk: a readable concatenates buffered writes, and a line-oriented consumer walks every delimiter in that chunk synchronously, discarding the lines no reader is waiting for. Write one record per turn (table row above) and re-run |
| Teardown deletes a directory or closes a socket that spawned runtime tasks (tokio and similar) still use, from a `Drop`/destructor or a cleanup call placed after the assertions | Abort *and await* every task handle before the delete, on the single path every outcome takes — `abort()` returns before the task stops, and a failing assertion skips a trailing cleanup → [testing-async-teardown-after-aborted-tasks] |
| The consumer is rebuilt per prompt (a new interface inside a retry loop) | Construct it once per interaction and reuse it — a second instance attached to the same stream competes for the same buffered data, so records land in whichever instance reads first |

## Instead of
Expand Down
Loading
Loading