Skip to content

pm-dispatch(sessions): directory confinement is lexical only — committed symlinks in the clone (nixpkgs, worktrees) escape it (residual of #2903) #3999

Closed

Description

@dezren39

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

Related: #2903 (original escape, fixed lexically), #2664 (directory
validation), #2668 (directory-scope design).

Activity

  1. added
    kind/securitySecurity issue or security hardening
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    effort/sSmall: expected under 2 hours of work
    on Nov 1, 2026
  2. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-b)
    • Session / ID: new-issue-creator-code-b run 2026-11-01 15:20 CST (America/Chicago)
    • Found via: filed this run
    • Task: Triage the pm-dispatch session-directory symlink escape: resolveSessionDirectory confines lexically only, and the clone's committed nixpkgs/worktrees symlinks resolve outside it (residual of fix(pm-dispatch): POST /v1/sessions accepts an arbitrary directory — OIDC callers escape their repo clone #2903).
    • Expectation: effort/s — canonicalize (realpath) the candidate and root before the prefix check, with regression tests for symlinked components; done when an outside-resolving directory is rejected like other out-of-scope paths.
    • Links: Evidence in the issue body; laptop probe ~/git/fleet/probe-2026-11-01-laneB-dispatchdir/ (verbatim validate.ts, sha256 c98ce808, main 83709fa).
  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  4. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  5. 16 remaining items

  6. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    agent-note:v1 — picked up — OpenChamber scheduled lane

  7. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor
  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  9. 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
  10. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

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/sSmall: expected under 2 hours of workkind/securitySecurity issue or security hardeningpriority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/fixhas a fix task queuedtask/small-fixQueue packet kind: bounded fix with reproduction and targeted tests

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions