fix(ssh): don't leak session-scoped metadata across sibling sessions

reuseExistingEnvironment copies persistent metadata from the previous
ExecutorRunning row into the new launch request so executors can pick up
worktree/container/sandbox IDs. For SSH that swept in session-scoped
keys too — agentctl PID/port, session dir, local forward port — which
made the second session's ResumeRemoteInstance reattach to the first
session's agentctl process. Mark those keys as session-scoped and skip
them in applyExecutorRunningMetadata; the same-session resume path
(applyRunningRecordToResumeRequest) still copies them because it works
off the session's own ExecutorRunning record.
This commit is contained in:
José Almeida
2026-05-18 02:34:27 +01:00
parent c4e5f593b6
commit 044b76bbd5
2 changed files with 36 additions and 2 deletions
@@ -152,6 +152,21 @@ var persistentMetadataPrefixes = []string{
"env_secret_id_", // Secret store UUIDs for profile env vars
}
// sessionScopedMetadataKeys lists metadata keys that point to per-session
// runtime resources (process PIDs, allocated ports, session directories on
// the remote). These keys ARE persisted across a SAME-session resume — that's
// how a backend restart reattaches to a still-running remote agent — but they
// MUST NOT be carried across SIBLING sessions on the same task. If they were,
// the second session would try to attach to the first session's agentctl
// process and end up sharing its ACP session and instance port.
var sessionScopedMetadataKeys = map[string]bool{
MetadataKeySSHRemoteSessionDir: true,
MetadataKeySSHRemoteAgentctlPort: true,
MetadataKeySSHRemoteAgentctlPID: true,
MetadataKeySSHLocalForwardPort: true,
MetadataKeySSHRemoteAgentctlURL: true,
}
// ShouldPersistMetadataKey returns true if the given metadata key should
// be carried forward when resuming a session from an ExecutorRunning record.
func ShouldPersistMetadataKey(key string) bool {
@@ -166,6 +181,13 @@ func ShouldPersistMetadataKey(key string) bool {
return false
}
// IsSessionScopedMetadataKey reports whether key references per-session
// runtime resources that must not leak across sibling sessions on the same
// task environment.
func IsSessionScopedMetadataKey(key string) bool {
return sessionScopedMetadataKeys[key]
}
// FilterPersistentMetadata returns a copy of src containing only keys that
// should be carried forward across session resumes. Returns nil if no keys match.
func FilterPersistentMetadata(src map[string]interface{}) map[string]interface{} {
@@ -133,9 +133,21 @@ func applyExecutorRunningMetadata(req *LaunchAgentRequest, running *models.Execu
if metadata == nil {
metadata = ensureLaunchMetadata(req)
}
if _, exists := metadata[k]; !exists && lifecycle.ShouldPersistMetadataKey(k) {
metadata[k] = v
if _, exists := metadata[k]; exists {
continue
}
if !lifecycle.ShouldPersistMetadataKey(k) {
continue
}
// Skip session-scoped runtime resources (PIDs, ports, session dirs).
// Carrying these across sibling sessions on the same task makes a
// fresh launch look like a same-session resume — see SSH executor's
// ResumeRemoteInstance — and the new session ends up sharing the
// previous session's agentctl process.
if lifecycle.IsSessionScopedMetadataKey(k) {
continue
}
metadata[k] = v
}
}