Repository navigation
Description
Problem
pm-dispatch's POST /v1/sessions accepts a caller-supplied directory for the OpenCode session that is never validated — not for emptiness, and not against the repo the session is supposedly for (pm-stack/dispatch/, main 732ca26):
optString(src/validate.ts:93-98) returns string values verbatim: no trim, no non-empty check (contrastreqStringat:85-91, which trims and requires non-empty). Sodirectory: ""anddirectory: " "pass validation.handleSessionsresolves the directory asbody.directory ?? REPO_DIRS[body.repo]!(src/server.ts:180) —??only catchesnull/undefined, so""defeats the per-repo default and is passed verbatim toOpenCodeClient.createSessionaslocation.directory(src/opencode.ts:53-61).- Any other absolute path is equally accepted:
directory: "/etc"was accepted byparseSessionRequestin verification (below). Nothing ties the directory to the request's repo — no check againstREPO_DIRS[repo], no subtree/allowlist check. - Authorization does not close this:
assertCallerRepo(src/auth.ts:190-201) binds an OIDC caller to its own repo via the body'srepofield only. A GitHub Actions workflow in repo X can therefore setrepo: X(passing the check) withdirectory: /home/user/git/<another repo>— or any other directory on the laptop — and pm-dispatch will launch an OpenCode session there, with the caller's prompt, on the laptop's standing server. The static-bearer caller is Drew-shaped and trusted; the OIDC route is the one whose blast radius the repo binding was designed to limit, and the directory field escapes it.
Evidence (verified this run)
Laptop probe (bun, importing the real validate.ts from the laptop primary at main 732ca26, md5-identical to the sandbox clone; probe at ~/git/fleet/probe-2026-10-30-laneB-dispatch/ on the laptop):
C empty title/directory accepted: {"title":"","directory":""}
C whitespace title + /etc directory accepted: {"title":" ","directory":"/etc"}
(The same optString gap makes title: "" defeat the titleFromPrompt fallback in handleSessions/launchSession, which also use ??.)
Why it matters
The directory is where the launched agent session reads and writes. Letting the least-trusted caller class (any workflow in an allowlisted repo) point a session at an arbitrary laptop directory — including other repos' primaries or the home directory — silently widens the OIDC route from "launch work on your own repo" to "run an agent anywhere on the laptop". The empty-string case is the same gate failing at the other end: a malformed request produces a session with an unintended working directory instead of a 400.
Acceptance criteria
-
directoryis validated: empty/whitespace is a 400 (or normalized to the repo default); the accepted value must be the repo'sREPO_DIRSentry or a directory under it (or an explicit per-repo allowlist triage pins) — anything else is a 400. - The binding holds for OIDC callers in particular: a token for repo X cannot launch a session outside repo X's directory tree.
-
titlegets the same empty/whitespace treatment as other optional strings, so the prompt-derived fallback title applies. - Tests pin:
""," ","/etc", and another repo's directory are rejected for a session request; the repo default and a subdirectory of it are accepted. - Existing dispatch tests still pass.
Activity
dezren39 commentedon Oct 30, 2026
🤖 Agent note · filed
dezren39 commentedon Oct 30, 2026
agent-note:v1 — design-architect adoption (pass 19, 2026-10-30): this issue is Phase 1 (boundary) of design #2668, "pm-dispatch launch pipeline, staged", together with #2665. Design doc:
files/designs/code-pm-dispatch-launch-pipeline.mdin the keep-the-four-repos-healthy goal workspace.Recorded decision for triage (do not re-file, re-scope, or split this issue; record the phase structure instead): the directory rule is — absent/empty/whitespace →
REPO_DIRS[repo]; otherwise the value must be an absolute path that, after normalization, is the repo's primary or lies inside it; anything else is aValidationError(400). Relative paths rejected. The rule applies to all caller types uniformly (no bearer exemption); an extra-roots allowlist is a recorded open question on #2668, not part of this phase. Timing: this phase is the gate for adding the publicdispatch.security.cabtunnel hostname — it should land before the service becomes internet-reachable.This issue already carries a queued triage packet; no new packet was enqueued by the architect.
14 remaining items
dezren39 commentedon Nov 1, 2026
🤖 Agent note · parked
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
Merge pull request #3507 from developing-today/queue/5418-2664-sessio…
dezren39 commentedon Nov 1, 2026
🤖 Agent note · done
One containment rule for OIDC and bearer alike in
resolveSessionDirectory; blank title/directory parse as omitted inparseSessionRequest; README updated. Tests: validate.test.ts 33/33; dispatch suite 85 pass with the two pre-existing borderline server.test.ts timeouts (identical on unmodified main — see PR). Codex review: completed, no findings.