Repository navigation
Description
pm-dispatch(sessions): directory confinement is lexical only — committed symlinks in the clone escape it (residual of #2903)
Summary
resolveSessionDirectory (pm-stack/dispatch/src/validate.ts:159-176) confines a
POST /v1/sessions directory to the target repo's clone using only lexical
normalization (normalizeDirectory, validate.ts:182-199): it collapses ./..
segments and checks the root/ prefix. Nothing canonicalizes the path
(no realpath anywhere in pm-stack/dispatch/src), so a path that is lexically
inside the clone but passes through a symlink pointing outside is accepted —
and the directory is handed verbatim to the OpenCode session create
(server.ts:191, opencode.ts:53-59 location: { directory }).
The code repo itself commits two such symlinks at its root (both mode 120000
in git ls-files on main 83709fa):
nixpkgs→../nixpkgs(a sibling path outside the clone)worktrees→/home/user/.local/share/opencode/worktree/code(absolute,
outside the clone)
Verified (laptop probe, this run)
Probe: ~/git/fleet/probe-2026-11-01-laneB-dispatchdir/ — verbatim copy of
validate.ts (sha256 c98ce808…, identical to main), REPO_DIRS value for
developing-today/code from config.ts:37 (/home/user/git/code), run with bun:
- control
/home/user/git/code/pm-stack→ ACCEPTED, realpath inside clone ✓ - control
/home/user/git/nix→ REJECTED ✓ - control
/home/user/git/code/../nix→ REJECTED ✓ /home/user/git/code/worktrees→ ACCEPTED as inside the clone, but
realpath=/home/user/.local/share/opencode/worktree/code— outside the
clone, and the target directory exists on the laptop, so a session can
actually be rooted there/home/user/git/code/nixpkgs→ ACCEPTED, symlink target../nixpkgs
resolves outside the clone (currently dangling on this laptop; the checker's
verdict is wrong regardless of whether the target exists today)
Why it matters
The directory scope is the confinement boundary the #2903 fix (priority/high,
kind/security) and #2664 established: "OIDC callers may only run inside the
target repo clone". A caller whose own repo is allowlisted (or a bearer
caller) can name a directory in a different repo's clone area that a
committed symlink reaches — e.g. dispatching against developing-today/code
with directory /home/user/git/code/worktrees starts the session in the
OpenCode worktree store, outside the code clone entirely. The same lexical
gap applies to any symlink a repo gains in the future, and to .. segments
that follow a symlinked component (lexical collapse happens before the OS
resolves the symlink, so the two disagree).
Acceptance criteria
- Session-directory resolution canonicalizes the candidate (and the
configured root) with realpath semantics before the prefix check, so a
lexically-inside path whose real location is outside the clone is
rejected with the same ValidationError as other out-of-scope paths. - Nonexistent candidate directories keep working or fail with a clear
validation error (today a not-yet-created subdirectory of the clone is
accepted lexically; the fix must state and test what happens for them —
e.g. canonicalize the nearest existing ancestor). - Regression tests cover: a symlinked component inside the clone pointing
outside (rejected), a symlinked component pointing elsewhere inside the
same clone (accepted),..after a symlinked component, and the two
committed cases (nixpkgs,worktrees) by name or equivalent fixture. - Existing directory tests still pass: missing/blank → clone root,
plain outside path and plain..escape → rejected (pm-dispatch(sessions): directory is never validated — empty string defeats the repo default, and any absolute path lets an OIDC caller launch a session outside its own repo #2664/fix(pm-dispatch): POST /v1/sessions accepts an arbitrary directory — OIDC callers escape their repo clone #2903 cases).
Related: #2903 (original escape, fixed lexically), #2664 (directory
validation), #2668 (directory-scope design).
Activity
dezren39 commentedon Nov 1, 2026
🤖 Agent note · filed
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
16 remaining items
dezren39 commentedon Nov 1, 2026
agent-note:v1 — picked up — OpenChamber scheduled lane
dezren39 commentedon Nov 1, 2026
agent-note:v1 — parked — OpenChamber scheduled lane
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
held/user/owned/do-not-merge/do-not-auto-editlabels. PR fix(pm-dispatch): constrain session directories for every caller #3483 is not Drew-personal either — its branch is queue/5418-2664-session-dir-validation, a stale duplicate of task 5418's canonical PR fix(dispatch): session directory containment + blank title/directory handling (#2664) #3507 (merged 2026-11-01T10:42Z, closing pm-dispatch(sessions): directory is never validated — empty string defeats the repo default, and any absolute path lets an OIDC caller launch a session outside its own repo #2664). Per the packet, PR fix(pm-dispatch): constrain session directories for every caller #3483 stays untouched; it does not own issue pm-dispatch(sessions): directory confinement is lexical only — committed symlinks in the clone (nixpkgs, worktrees) escape it (residual of #2903) #3999 (no PR implements pm-dispatch(sessions): directory confinement is lexical only — committed symlinks in the clone (nixpkgs, worktrees) escape it (residual of #2903) #3999).fix(pm-dispatch): propagate symlink-depth ValidationError in director…
fix(pm-dispatch): bound total symlink expansions across a directory r…
Merge pull request #4032 from developing-today/queue/5935-3999-symlin…
dezren39 commentedon Nov 1, 2026
🤖 Agent note · done
..pops the resolved parent, nonexistent tails append lexically) and judges containment on the resolved path. Codex reviewed 3 rounds: round-1 P2 (depth-limit ValidationError swallowed by the fallback catch) fixed in e52d599, round-2 P2 (per-recursion depth cap allowed exponential dangling-link expansion) fixed in 8789ace with a resolution-wide 40-expansion budget; round 3 completed with no unresolved findings. Full dispatch suite 100/100 on the laptop; live probe against the real clone rejects both committed escape links (worktrees, nixpkgs) and accepts normal in-clone paths unchanged.