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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user