agent: reject option-like resume session ids · Entire

agent: reject option-like resume session ids

e8c185a→main · peyton-alt · 2w ago · 4 files · +37 added/-5 removed

Sessions

f68a8bbbdf1d View transcript

?\ Add Trail Resume Subcommand Codex · GPT-5.5 · 1 step

Changes

4

6 unmodified lines

7
8
9
10
11
12
13
10 unmodified lines

24
25
26
26
27
28
29
30
31
31
32
33
34
35
36
36
37
38
39
40
41
41
42
43
44
45
46
46
47
48
49
50
1 unmodified line

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

6 unmodified lines

"strings"

"github.com/entireio/cli/cmd/entire/cli/agent/types"
    "github.com/entireio/cli/cmd/entire/cli/validation"

// ForegroundCommandSpec describes a command Entire can launch in the caller's
10 unmodified lines

sessionID = strings.TrimSpace(sessionID)
switch name {
case AgentNameClaudeCode:
if sessionID == "" {
if !isLaunchableResumeSessionID(sessionID) {
return ForegroundCommandSpec{}, false
}
return ForegroundCommandSpec{Binary: "claude", Args: []string{"-r", sessionID}}, true
}
case AgentNameCodex:
if sessionID == "" {
if !isLaunchableResumeSessionID(sessionID) {
return ForegroundCommandSpec{}, false
}
return ForegroundCommandSpec{Binary: "codex", Args: []string{"resume", sessionID}}, true
}
case AgentNameCopilotCLI:
if sessionID == "" {
if !isLaunchableResumeSessionID(sessionID) {
return ForegroundCommandSpec{}, false
}
return ForegroundCommandSpec{Binary: "copilot", Args: []string{"--resume", sessionID}}, true
}
case AgentNameFactoryAIDroid:
if sessionID == "" {
if !isLaunchableResumeSessionID(sessionID) {
return ForegroundCommandSpec{}, false
}
return ForegroundCommandSpec{Binary: "droid", Args: []string{"--session-id", sessionID}}, true
}
case AgentNameGemini:
if sessionID == "" {
if !isLaunchableResumeSessionID(sessionID) {
return ForegroundCommandSpec{}, false
}
return ForegroundCommandSpec{Binary: "gemini", Args: []string{"--resume", sessionID}}, true
}
1 unmodified line

if sessionID == "" {
return ForegroundCommandSpec{Binary: "opencode"}, true
}
if !isLaunchableResumeSessionID(sessionID) {
return ForegroundCommandSpec{}, false
}
return ForegroundCommandSpec{Binary: "opencode", Args: []string{"-s", sessionID}}, true
case AgentNamePi:
if sessionID == "" {
return ForegroundCommandSpec{Binary: "pi", Args: []string{"--continue"}}, true
}
if !isLaunchableResumeSessionID(sessionID) {
return ForegroundCommandSpec{}, false
}
return ForegroundCommandSpec{Binary: "pi", Args: []string{"--session", sessionID}}, true
default:
return ForegroundCommandSpec{}, false
}
}

func isLaunchableResumeSessionID(sessionID string) bool {
return sessionID != "" && validation.ValidateSessionID(sessionID) == nil
}

// NewResumeForegroundCommand builds a foreground command for resuming a session,
// when the agent has a launchable resume command. ok=false means callers should
// print FormatResumeCommand for the user instead.

Mcmd/entire/cli/agent/resume_command.go +16/-5

65 unmodified lines

66
67
68
69
70
71
72
73
74
75
76
77

65 unmodified lines

want:      ForegroundCommandSpec{Binary: "pi", Args: []string{"--session", "session-123"}},
        wantOK:    true,
        },
        {
        name:      "leading dash session id is not launchable",
        agentName: AgentNameClaudeCode,
        sessionID: "--dangerously-skip-permissions",
        wantOK:    false,
        },
        {
        name:      "cursor is print only",
        agentName: AgentNameCursor,

Mcmd/entire/cli/agent/resume_command_test.go +6

20 unmodified lines

21
22
23
24
25
26
27
28
29
53 unmodified lines

83
84
85
86
87
88
89
90
91

20 unmodified lines

if strings.TrimSpace(id) == "" {
    return errors.New("session ID cannot be empty")
}
    if strings.HasPrefix(id, "-") {
    return fmt.Errorf("invalid session ID %q: starts with dash", id)
}
    if strings.ContainsAny(id, "/\\") {
    return fmt.Errorf("invalid session ID %q: contains path separators", id)
}
53 unmodified lines

if id == "" {
    return errors.New("agent session ID cannot be empty")
}
    if strings.HasPrefix(id, "-") {
    return fmt.Errorf("invalid agent session ID %q: starts with dash", id)
}
    if !pathSafeRegex.MatchString(id) {
    return fmt.Errorf("invalid agent session ID %q: must be alphanumeric with underscores/hyphens only", id)
}

Mcmd/entire/cli/validation/validators.go +6

40 unmodified lines

41
42
43
44
45
46
47
48
49
50
51
52
53
223 unmodified lines

277
278
279
280
281
282
283
284

40 unmodified lines

wantErr:   true,
        errMsg:    "session ID cannot be empty",
        },
        // Leading dash (security-critical - option injection prevention)
        {
        name:      "leading dash",
        sessionID: "--dangerously-skip-permissions",
        wantErr:   true,
        errMsg:    "starts with dash",
        },
        // Path separators (security-critical - path traversal prevention)
        {
        name:      "session ID with forward slash",
223 unmodified lines

{name: "test session id", id: "test-session-1", wantErr: false},
        {name: "alphanumeric", id: "session123", wantErr: false},
        {name: "with underscores", id: "test_session_1", wantErr: false},
        // Invalid - option injection
        {name: "leading dash", id: "--dangerously-skip-permissions", wantErr: true},
        // Invalid - empty (required field)
        {name: "empty rejected", id: "", wantErr: true},
        // Invalid - path traversal