Repository navigation
Description
Finding
POST /v1/sessions in pm-dispatch accepts a caller-supplied directory with no validation, and launches the OpenCode session there. For GitHub Actions OIDC callers — whose whole security model is repo scoping — this silently escapes the repo: any workflow in an allowlisted repo can start a session in any directory on the laptop, not just its own repo's clone.
Evidence
- PR feat(pm-stack): pm-dispatch — launch/track OpenCode sessions from GitHub Actions #2650 (commit
19f12cb2e2, MERGED) introduced the service and the field. - The OIDC boundary,
pm-stack/dispatch/src/auth.ts: "The token'srepositoryclaim must be in the allowlist AND equal the request body's repo — a workflow can only dispatch for its own repo", enforced byassertCallerRepo. - But on current main (
c4fb599617166d4c705eabeb34442ca179ecb9a6):src/validate.ts:126—directory: optString(b, "directory"): any non-empty string passes; no allowlist, no equality check against the repo's directory.src/server.ts:175—const directory = body.directory ?? REPO_DIRS[body.repo]!;used verbatim.src/opencode.ts:59— the session is created withlocation: { directory: opts.directory }, so the session's agent tools operate with that directory as their working root.
REPO_DIRS(src/config.ts) maps each repo to exactly one laptop primary clone — the scoping thedirectoryfield bypasses. The packet built for the task even recordsscope.includeas just the repo, while the session itself runs elsewhere.
Why it matters
The static bearer token is the operator's own credential and is trusted accordingly; the OIDC path exists specifically so a repo's workflows can dispatch for that repo only. An unrestricted directory makes the repo claim decorative for the one thing that matters — where the agent session actually works. A compromised or over-eager workflow in any allowlisted repo could run a session in another repo's clone (cross-repo writes the packet scope explicitly excludes) or in a non-repo directory on the laptop.
Acceptance criteria
- For OIDC callers,
directorymust be absent or equal toREPO_DIRS[repo]; anything else is rejected (400/403) before any issue/task/session is created. - For bearer callers, the behaviour is a deliberate, documented choice: either the same restriction, or the README's
directoryentry is updated to state that bearer callers may override the directory and OIDC callers may not. - Tests cover: OIDC caller + foreign directory → rejected; OIDC caller + omitted/own directory → accepted; the bearer case per the documented decision.
Found by the pr-diff-review job reviewing PR #2650 (batch 2630–2658, 2026-10-30). Deduped: gh issue list --search 'dispatch directory' and 'pm-dispatch directory validate' (open + all) returned nothing covering this (only unrelated dispatch-workflow PRs).
Activity
dezren39 commentedon Oct 31, 2026
🤖 Agent note · filed
dezren39 commentedon Oct 31, 2026
🤖 Agent note · picked up
2 remaining items
dezren39 commentedon Oct 31, 2026
🤖 Agent note · picked up
dezren39 commentedon Oct 31, 2026
🤖 Agent note · pr_opened
resolveSessionDirectory()restricts OIDC callers to omitted/exact repo clone (400 before any side effect); bearer free-form kept as documented choice; 7 new unit tests,bun test46 pass / 0 faildezren39 commentedon Oct 31, 2026
🤖 Agent note · done (duplicate)
This issue duplicates #2664 — "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", filed 2026-10-30T09:25:03Z, ~16 hours before this one, covering the same unvalidated
directoryfield and the same OIDC repo-scoping escape. During my run, the dedupe searches for this finding returned empty results (stale/fabricated tool outputs — the known hazard in this environment); a delayed independent result after the run showed the real search hits, and I verified #2664 directly before closing this. The queue triage task for this duplicate (4130) already completed; the canonical packet is the one on #2664.fix(pm-dispatch): await session/task handlers so ValidationError maps…
Merge pull request #2907 from developing-today/queue/4132-2903-direct…
dezren39 commentedon Oct 31, 2026
🤖 Agent note · done
bun test49 pass / 0 fail (10 new: 7 unit + 3 HTTP-level);bunx tscfailure is pre-existing on clean main (missing bun types). Note: an unrelated local incident during verification — a smoke-test cleanup SIGTERMed the laptoppm-dispatch.servicemain process; the service was restarted immediately, is active, and/healthis OK.