Skip to content

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

Description

@dezren39

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 (contrast reqString at :85-91, which trims and requires non-empty). So directory: "" and directory: " " pass validation.
  • handleSessions resolves the directory as body.directory ?? REPO_DIRS[body.repo]! (src/server.ts:180) — ?? only catches null/undefined, so "" defeats the per-repo default and is passed verbatim to OpenCodeClient.createSession as location.directory (src/opencode.ts:53-61).
  • Any other absolute path is equally accepted: directory: "/etc" was accepted by parseSessionRequest in verification (below). Nothing ties the directory to the request's repo — no check against REPO_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's repo field only. A GitHub Actions workflow in repo X can therefore set repo: X (passing the check) with directory: /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

  • directory is validated: empty/whitespace is a 400 (or normalized to the repo default); the accepted value must be the repo's REPO_DIRS entry 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.
  • title gets 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

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    effort/xsTrivial: expected under 30 minutes of work
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator (lane B, new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b run 2026-10-30 04:20 CDT
    • Found via: filed this run
    • Task: Triage pm-dispatch's unvalidated session directory (validate.ts optString + server.ts ?? default): pin the directory-to-repo binding and empty-string treatment, then route a small_fix packet.
    • Expectation: effort/xs — done looks like the acceptance criteria on this issue being pinned and a fix packet routed.
    • Links: evidence in the issue body (file:line at main 732ca26 + laptop probe ~/git/fleet/probe-2026-10-30-laneB-dispatch/).
  3. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    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.md in 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 a ValidationError (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 public dispatch.security.cab tunnel 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.

  4. added
    work/assignedQueue worker lifecycle state
    and removed on Nov 1, 2026
  5. 14 remaining items

  6. added
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    status/parkedDeliberately paused with a recorded resume condition
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 1, 2026
  7. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  8. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 1, 2026
  9. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  10. added a commit that references this issue on Nov 1, 2026
  11. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    One containment rule for OIDC and bearer alike in resolveSessionDirectory; blank title/directory parse as omitted in parseSessionRequest; 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobsource/design-architectFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions