Repository navigation
mcpx: program-spec resolver — nix:<ref>#<attr> resolves once to a store path (#1921 Part 1) #2585
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b226e14
mcpx: program-spec resolver — nix:<ref>#<attr> resolves once to a sto…
dezren39 0cf988a
mcpx: disposition Codex findings on the program-spec resolver (#1921)
dezren39 7dfeac2
mcpx: disposition #2585 review findings — attach before resolve, vali…
dezren39 10b3860
mcpx: hand the resolved store path to the spawned supervisor (Codex P…
dezren39 f12648f
Merge remote-tracking branch 'origin/main' into queue/3199-1921-nix-p…
dezren39 1918789
mcpx: progspec memo evicts dead store paths; doctor checks nix presen…
dezren39 01e8b72
mcpx: poll the reattach in the nix-spec test until the attach slot fr…
dezren39 fda3a0f
mcpx: regenerate parity doc after #2191 wording landed on main
dezren39 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| package mcpclient_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/dezren39/mcpx/internal/mcpclient" | ||
| ) | ||
|
|
||
| // stubNixForSpawn installs a fake nix binary on PATH whose build prints | ||
| // a fake store output holding one executable shell script. | ||
| func stubNixForSpawn(t *testing.T) { | ||
| t.Helper() | ||
| dir := t.TempDir() | ||
| bin := filepath.Join(dir, "store", "aaaa-fakepkg-1.0", "bin") | ||
| if err := os.MkdirAll(bin, 0o755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| tool := filepath.Join(bin, "faketool") | ||
| if err := os.WriteFile(tool, []byte("#!/bin/sh\nexec cat\n"), 0o755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| out := filepath.Dir(bin) | ||
| stub := "#!/bin/sh\n" + | ||
| "for a in \"$@\"; do case \"$a\" in *slow*) sleep 30;; esac; done\n" + | ||
| "echo \"" + out + "\"\n" | ||
| if err := os.WriteFile(filepath.Join(dir, "nix"), []byte(stub), 0o755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) | ||
| } | ||
|
|
||
| func TestNewStdioResolvesNixSpecAtSpawn(t *testing.T) { | ||
| stubNixForSpawn(t) | ||
| tr, err := mcpclient.NewStdio(mcpclient.StdioOptions{Command: "nix:nixpkgs#faketool", InheritEnv: true}) | ||
| if err != nil { | ||
| t.Fatalf("NewStdio with a nix: spec: %v", err) | ||
| } | ||
| _ = tr.Close() | ||
| } | ||
|
|
||
| func TestNewStdioNixResolutionHonoursTheStartupContext(t *testing.T) { | ||
| stubNixForSpawn(t) | ||
| ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) | ||
| defer cancel() | ||
| _, err := mcpclient.NewStdio(mcpclient.StdioOptions{ | ||
| Command: "nix:slowref#tool", InheritEnv: true, Context: ctx, | ||
| }) | ||
| if err == nil || !strings.Contains(err.Error(), "nix:slowref#tool") { | ||
| t.Fatalf("err = %v, want a context-bound resolution error naming the spec", err) | ||
| } | ||
| } | ||
|
|
||
| func TestSupervisedReattachDoesNotResolveNixSpec(t *testing.T) { | ||
| // A daemon reattaching to a live supervised nix server must not | ||
| // resolve the spec again: the child is already running, and a nix | ||
| // that has since left PATH (or a flake that no longer resolves) | ||
| // must not fail the reattach (Codex finding on #2585). | ||
| stubNixForSpawn(t) | ||
| opts := mcpclient.SupervisedOptions{ | ||
| Name: "nixreattach", StateDir: t.TempDir(), | ||
| Command: "nix:nixpkgs#echotool", InheritEnv: true, | ||
| } | ||
| tr, err := mcpclient.NewSupervised(opts) | ||
| if err != nil { | ||
| t.Fatalf("first attach: %v", err) | ||
| } | ||
| pid := tr.PID() | ||
| defer killChild(t, tr) | ||
| if err := tr.Close(); err != nil { | ||
| t.Fatalf("detach: %v", err) | ||
| } | ||
| // nix vanishes: with resolution on the attach path, the reattach | ||
| // would now fail before ever reaching the live supervisor. | ||
| t.Setenv("PATH", t.TempDir()) | ||
| // Close returns once the local connection is closed; the supervisor | ||
| // clears its attach slot when it notices the disconnect, which is | ||
| // asynchronous. Poll the reattach until the slot frees instead of | ||
| // assuming the release is instant (the #2609 flake, hit by this | ||
| // test in the PR stress run: under shuffle/load the immediate | ||
| // reattach was refused "already attached"). | ||
| var tr2 *mcpclient.SupervisedTransport | ||
| deadline := time.Now().Add(10 * time.Second) | ||
| for { | ||
| tr2, err = mcpclient.NewSupervised(opts) | ||
| if err == nil { | ||
| break | ||
| } | ||
| if !strings.Contains(err.Error(), "already attached") || time.Now().After(deadline) { | ||
| t.Fatalf("reattach without nix on PATH: %v", err) | ||
| } | ||
| time.Sleep(10 * time.Millisecond) | ||
| } | ||
| defer tr2.Close() | ||
| if tr2.PID() != pid { | ||
| t.Fatalf("child pid changed across reattach: %d -> %d", pid, tr2.PID()) | ||
| } | ||
| } | ||
|
|
||
| func TestSupervisedResolvesNixSpecForItsChild(t *testing.T) { | ||
| stubNixForSpawn(t) | ||
| tr, err := mcpclient.NewSupervised(mcpclient.SupervisedOptions{ | ||
| Name: "nixecho", StateDir: t.TempDir(), | ||
| Command: "nix:nixpkgs#echotool", InheritEnv: true, | ||
| }) | ||
| if err != nil { | ||
| t.Fatalf("NewSupervised with a nix: spec: %v", err) | ||
| } | ||
| defer killChild(t, tr) | ||
| defer tr.Close() | ||
| ctx := context.Background() | ||
| frame := []byte(`{"jsonrpc":"2.0","id":1,"method":"ping"}`) | ||
| if err := tr.Send(ctx, frame); err != nil { | ||
| t.Fatalf("send: %v", err) | ||
| } | ||
| got, err := tr.Recv() | ||
| if err != nil { | ||
| t.Fatalf("recv: %v", err) | ||
| } | ||
| if string(got) != string(frame) { | ||
| t.Fatalf("echo = %s, want %s", got, frame) | ||
| } | ||
| } | ||
|
|
||
| // Codex P2 (PR #2585): a supervised server whose own Env overrides | ||
| // PATH with one that excludes nix must still launch: the daemon | ||
| // resolves the spec with its own PATH and hands the resolved store | ||
| // path to the supervisor it spawns, instead of leaving the | ||
| // supervisor to resolve the spec again under the server's PATH. | ||
| // The stub tool here execs cat by absolute path, so the only thing | ||
| // the restricted PATH needs to hide is nix itself. | ||
| func TestSupervisedNixSpecWithServerPathExcludingNix(t *testing.T) { | ||
| cat, err := exec.LookPath("cat") | ||
| if err != nil { | ||
| t.Skip("no cat on PATH for the stub tool") | ||
| } | ||
| dir := t.TempDir() | ||
| bin := filepath.Join(dir, "store", "aaaa-fakepkg-1.0", "bin") | ||
| if err := os.MkdirAll(bin, 0o755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| tool := "#!/bin/sh\nexec " + cat + "\n" | ||
| if err := os.WriteFile(filepath.Join(bin, "faketool"), []byte(tool), 0o755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| stub := "#!/bin/sh\necho \"" + filepath.Dir(bin) + "\"\n" | ||
| if err := os.WriteFile(filepath.Join(dir, "nix"), []byte(stub), 0o755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) | ||
|
|
||
| tr, err := mcpclient.NewSupervised(mcpclient.SupervisedOptions{ | ||
| Name: "nixrestrictedpath", StateDir: t.TempDir(), | ||
| Command: "nix:nixpkgs#faketool", InheritEnv: true, | ||
| Env: map[string]string{"PATH": "/usr/bin:/bin"}, | ||
| }) | ||
| if err != nil { | ||
| t.Fatalf("NewSupervised with a nix: spec and a nix-less server PATH: %v", err) | ||
| } | ||
| defer killChild(t, tr) | ||
| defer tr.Close() | ||
| ctx := context.Background() | ||
| frame := []byte(`{"jsonrpc":"2.0","id":1,"method":"ping"}`) | ||
| if err := tr.Send(ctx, frame); err != nil { | ||
| t.Fatalf("send: %v", err) | ||
| } | ||
| got, err := tr.Recv() | ||
| if err != nil { | ||
| t.Fatalf("recv: %v", err) | ||
| } | ||
| if string(got) != string(frame) { | ||
| t.Fatalf("echo = %s, want %s", got, frame) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.