Skip to content

mcpx testing: e2e inherits the caller's environment -- build it from an allowlist so tests never reach the user's daemon #1992

Open

Description

@dezren39

Relocated from dezren39/darwin-nix#138 on 2026-10-28 (placement audit): mcpx's single source lives in developing-today/code (pkgs/mcpx); re-home tracked at #309. Original body below; issue references inside it are historical (dezren39/nix numbering).

Parent: #97

Summary

The e2e harness starts every test's environment from the developer's own os.Environ(), then overrides four variables. Anything else set in the caller's shell reaches the test binary. That includes MCPX_DAEMON_ENDPOINT, which the opencode mcpx plugin injects into every shell it opens. So an agent running go test ./internal/e2e inside opencode can point the suite at the user's real daemon. Separately, several code paths read $HOME/XDG_* directly, and the harness does not isolate those.

The fix: build the child environment from an allowlist, not by inheriting and overriding, and add a guard that catches new MCPX_* leaks. (The short autostart idle-exit this issue also asked for landed in #182, which fixed #52; the inheritance itself is untouched and is what remains.)

This is the precondition for "very few shouldn't work if user has own real daemon running" (#134): isolation has to be strong enough that "very few" is true.

Original notes

  • testing
    • a general goal is to ensure all features have one or more integration tests
      • for both binary && app entrypoint (consider binary a superset)
        • all tests should pass for nix flake & container
          • all local also but may need impractical setup
            • very few shouldn't work if user has own real daemon running

Current state (written on d0b7e01; re-checked at 05c78b2)

newEnv (internal/e2e/e2e_test.go:35-87 at 05c78b2) sets envVars: append(os.Environ(), MCPX_REGISTRY_URL=..., MCPX_STATE_DIR=..., MCPX_CACHE_DIR=..., MCPX_CONFIG=..., MCPX_AUTOSTART_IDLE_EXIT=60s) (:52-69). os/exec keeps the last value for a duplicated key, so these five win. Everything else is still inherited -- #182 added the fifth variable and changed the cleanup to stop --all with a logged error (:71-85), but did not touch the os.Environ() base. A developer's or agent's MCPX_DAEMON_ENDPOINT, MCPX_SOCKET or any other MCPX_* still reaches every test.

The table below comes from reading the code; the suite was not run for this issue. The first implementation step confirms these reads with a test.

Inherited from the caller Effect in a test Where read
MCPX_DAEMON_ENDPOINT every client command targets that daemon and refuses to start a local one daemon.endpoint, internal/settings/registry.go:350. The plugin injects it into every opencode shell: plugin/opencode/mcpx-session.ts:696-701
MCPX_SOCKET replaces the socket path outright, for both bind and connect (its precedence over the info file is #88's to decide, #181 collision 8) internal/daemon/paths.go:103-105
any MCPX_<SETTING> (derived from each setting's path, internal/settings/schema.go:220-224), e.g. MCPX_SCRIPT_RUNTIME, MCPX_DAEMON_INLINE the test runs under the developer's settings, not the documented defaults settings.ApplyEnv, bind.go:163
MCPX_SESSION_ID, MCPX_PARENT_SESSION_ID, MCPX_HARNESS_* session scoping and lease keys come from the outer agent session several internal/*
HOME, XDG_CONFIG_HOME recipe dirs include the user's (internal/cli/scripts.go:71-72, internal/daemon/consumer.go:141-142, internal/recipes/dirs.go:28), so the user's real recipes can change TestRecipesAreMatchedDeterministically-style results. A --persist user or servers add --scope user test would write the user's real config (internal/config/write.go:33-40) as listed
XDG_DATA_HOME, OPENCODE_DB the opencode v1 DB lookup finds the user's real opencode.db internal/opencode/opencode.go:35-50
XDG_RUNTIME_DIR the fallback socket dir is shared with the user's daemons. The name is hashed per state dir (paths.go:115-116), so there is no collision; leaked test daemons used to accumulate there for hours (#52), and since #182 they idle out after 60 s paths.go:141-148
XDG_STATE_HOME/XDG_CACHE_HOME masked, because MCPX_STATE_DIR/MCPX_CACHE_DIR win (paths.go:36-45) --

Related, from #52 (closed, fixed in #182): newEnv now sets autostart.idleExit (internal/settings/wire.go:135) to 60 s through MCPX_AUTOSTART_IDLE_EXIT (e2e_test.go:68, testIdleExit at :92), and cleanup runs mcpx stop --all through the daemon-<key>.json info files, logging rather than discarding its error (:71-85). That bounds a leak; it does not stop a test from reaching the caller's daemon, which is this issue.

The in-process runner (#133) inherits all of this through os.Getenv in the same process. The fix below has to apply to both runners.

Approaches

A -- denylist: unset known-dangerous variables (least recommended)

Strip MCPX_DAEMON_ENDPOINT and MCPX_SOCKET after os.Environ(). It is the smallest diff, and it fixes today's concrete hole. It fails open: every future MCPX_* setting, and every new XDG_*/HOME read, leaks again until someone notices. That is the pattern the history shows (#65 registry, #52 leaks). Not recommended as the end state.

B -- strip every MCPX_* and override HOME/XDG_*

Drop every inherited variable that starts with MCPX_, and set HOME, XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_STATE_HOME, XDG_CACHE_HOME and XDG_RUNTIME_DIR under e.dir (short names, for the 100-byte socket cap, paths.go:75). This is robust against new settings. It still inherits unrelated variables: DENO_DIR, BUN_INSTALL, NODE_OPTIONS, OPENCODE_*, proxies. Some of those are wanted (runtime caches), some are not (NODE_OPTIONS=--require ...).

C -- allowlist plus a guard (recommended)

Recommended because it fails closed. A new setting, or a new read of $HOME, is isolated by default, and the guard catches the regression if someone widens the allowlist.

Implementation steps

  1. Write the guard test first, expecting it to fail on main. That confirms the table above.
  2. Replace append(os.Environ(), ...) with the allowlist builder in internal/testsupport (shared with the plugin harness in Update dependency @chenglou/pretext to ^0.0.9 [SECURITY] #132 and the in-process runner in Update Rust crate bytes to v1.11.1 [SECURITY] #133).
  3. Add MCPX_AUTOSTART_IDLE_EXIT=30s, and record the stop error rather than discarding it (chore(deps): bump build/buck/preludes/buck2-prelude from d956689 to e7457f6 #52 fixes 1 and 2). Done in Phase 5: tag event bodies must travel with the event #182 (60 s; stop --all, error logged).
  4. Add the TestMain backstop.

Open questions

  • Should a caller's DENO_DIR/bun cache be shared with tests (fast) or isolated (hermetic)? Proposal: shared on laptops, isolated in the container entry.
  • Does anything need SSH_AUTH_SOCK/git credentials? The repo/worktree scopes use git (package.nix comment), but only locally. (The GIT architect is replacing the git subprocess with native resolution in internal/config/scope.go; if that lands, git may drop out of the allowlist question entirely.)
  • Should the plugin's shell.env injection skip go test commands? No: the harness must be robust against any caller, and special-casing a command name in the plugin is the wrong layer.

Ease · Impact · Related

Ease S-M · Impact high (a correctness hole in every e2e run started from opencode with the plugin, and the foundation for the "user's daemon" subset)
Depends on / related: #52 (closed, fixed in #182 -- step 3 is done), #139 (this is the remaining precondition for t.Parallel in e2e), #88 (owns MCPX_SOCKET precedence, #181 collision 8), #172 (the undeclared MCPX_* allowlist the guard should reuse), #134 (the labelled subset; the inline/spawn/remote run modes), #133 (the in-process runner needs the same env), #132 (the plugin harness), #99 (guard-per-bug-class), #65 (the registry precedent for "never the real thing").

Security/auth deferred; see tracker.

Acceptance criteria

  • The e2e harness builds the child environment from an allowlist (no inherited MCPX_* or caller daemon endpoints), a guard test catches new MCPX_* leaks, and CI is green.
  • The work described in the Summary/Scope above is delivered via a branch + PR (no direct push to main); verification runs in GitHub Actions CI, not on the sandbox.
  • Evidence (CI links, timings, or probe output as applicable) is recorded on this issue before it is closed.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    History carried over from dezren39/darwin-nix#138 (verbatim, oldest first):


    @dezren39 · 2026-09-30T06:51:51Z

    Overlaps #162, written concurrently by a second worker on the same slice. Both are kept until reconciled; see the tracker (#97) for the differences and which one to keep.


    @dezren39 · 2026-09-30T07:21:13Z

    Integration pass. Placed in the DAG rooted at #181.


    @dezren39 · 2026-09-30T12:03:31Z

    Issues pass (2026-09-30, at 05c78b2).

  3. added
    bugSomething isn't working
    queue/queuedHas a queued task in the Supabase agent queue
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    priority/highImportant and time-sensitive
    effort/sSmall: expected under 2 hours of work
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Oct 28, 2026
  4. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    Intake review

    • Related: part of the mcpx testing relocation batch (parent/epic references in the body; re-home tracked at mcpx: re-home the open mcpx backlog from dezren39/nix, with this repo as the single source #309). Queued triage task is live in repo-health-code-pilot — this issue is not ownerless.
    • Live check: filed 2026-10-28 ~22:5xZ as a relocation; no open or merged PR implements it yet (checked against the open-PR list this run), and no duplicate open issue covers it.
    • Notes: body is well-scoped with evidence from the original filing; the acceptance criteria appended this pass are drawn from its own Summary/Scope.
    • Risks: historical issue references inside the body use dezren39/nix numbering — a worker must not chase those numbers in this repo. CI-first verification applies (GitHub Actions, never sandbox builds).
    • Suggested approach: triage task confirms the current-state claims on current main, then routes a scoped follow-up packet per the body's scope.

    🤖 Agent note · state: filed

    • Agent: fleet monitor (repo-fleet-monitor)
    • Session / ID: repo-fleet-monitor run 2026-10-28 18:19 CDT
    • Found via: monitor reconciliation (intake enrichment, protocol §5)
    • Task: Intake enrichment — acceptance criteria appended, labels normalized (kind/bug, priority/high, effort/s, app/mcpx), meta/enriched applied.
    • Expectation: effort/s class per the appended acceptance criteria.
  5. added
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  6. 81 remaining items

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

    @developing-today
    Contributor

    🤖 Agent note · handed off

  10. 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 5, 2026
  11. developing-today commented on Nov 5, 2026

    @developing-today
    Contributor

    🤖 Agent note · handed off

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

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)bugSomething isn't workingeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)meta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/highImportant and time-sensitivequeue/parkedQueue task is parked (waiting on CI, time, or an event)source/creatorFiled by the new-issue-creator jobstatus/parkedDeliberately paused with a recorded resume conditiontask/triageQueue packet kind: verify, reproduce, and route this itemwork/assignedQueue worker lifecycle state

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions