Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions internal/derive/buckets.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func PlaceProcesses(procs []model.Proc, sessions []model.Session, sigs *signatur
}
placement := ProcPlacement{PID: p.PID, Bucket: BucketOther}
placement.Memory, placement.Measure = ProcessMemory(p)
agent, likely, appBoundary := false, false, false
agent, likely, appBoundary, hostPending := false, false, false, false
var shared int32
bundle := ""
for cur := range processChain(byPID, p.PID) {
Expand All @@ -97,9 +97,13 @@ func PlaceProcesses(procs []model.Proc, sessions []model.Session, sigs *signatur
if !appBoundary {
// MCP evidence wins over the bundle: editors may run MCP servers
// with their own bundled runtime.
if mcp[cur.PID] && shared == 0 {
shared = cur.PPID
likely = true
if hostPending {
// The host is the next process the chain walked, so it is
// a sampled, valid parent and never a reused or dead PID.
shared, likely, hostPending = cur.PID, true, false
}
if mcp[cur.PID] && shared == 0 && !likely {
hostPending = true
} else if AppBundle(cur.Exe) != "" && !proven[cur.PID] {
appBoundary = true
}
Expand Down
27 changes: 27 additions & 0 deletions internal/derive/buckets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,33 @@ func TestBucketsEditorExtensionHostMCP(t *testing.T) {
t.Fatal(got)
}
}

// An MCP server's shared host is the parent its chain walked: never a process
// that reused the PID of its exited parent, and never a parent missing from
// the sample. Without a host the server is not shared with an editor.
func TestBucketsMCPHostIsAWalkedParent(t *testing.T) {
now := time.Now()
for _, tc := range []struct {
name string
mutate func([]model.Proc) []model.Proc
host int32
bucket Bucket
}{
{"live parent", func(ps []model.Proc) []model.Proc { return ps }, 1, BucketLikely},
{"reused parent PID", func(ps []model.Proc) []model.Proc {
ps[0].StartTime, ps[1].StartTime = now, now.Add(-time.Minute)
return ps
}, 0, BucketOther},
{"parent not sampled", func(ps []model.Proc) []model.Proc { return ps[1:] }, 0, BucketOther},
} {
t.Run(tc.name, func(t *testing.T) {
got := PlaceProcesses(tc.mutate(editorBucketFixture()), nil, nil)[2]
if got.Bucket != tc.bucket || got.SharedHost != tc.host {
t.Fatal(got)
}
})
}
}
func TestBucketsEditorWorkerIsOther(t *testing.T) {
ps := editorBucketFixture()
ps[1].Cmdline = "node /x/tsserver.js"
Expand Down
Loading