Skip to content

Commit 8bf10e4

Browse files
fix(runtime): bootstrap validation and WorkspaceDir on Runtime
Validate agent path in Bootstrap, skip empty skill/plugin dirs, use ConfigDir/WorkspaceDir methods instead of sandbox constants, and unexport sanitizeOutput. Signed-off-by: Barak Korren <bkorren@redhat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent d895415 commit 8bf10e4

7 files changed

Lines changed: 54 additions & 21 deletions

File tree

internal/runtime/claude.go

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,18 @@ func (ClaudeRuntime) Name() string { return "claude" }
2323

2424
func (ClaudeRuntime) ConfigDir() string { return sandbox.SandboxClaudeConfig }
2525

26-
func (ClaudeRuntime) EnvExports() []string {
27-
return []string{fmt.Sprintf("export CLAUDE_CONFIG_DIR=%s", sandbox.SandboxClaudeConfig)}
26+
func (ClaudeRuntime) WorkspaceDir() string { return sandbox.SandboxWorkspace }
27+
28+
func (r ClaudeRuntime) EnvExports() []string {
29+
return []string{fmt.Sprintf("export CLAUDE_CONFIG_DIR=%s", r.ConfigDir())}
2830
}
2931

3032
func (r ClaudeRuntime) Bootstrap(input BootstrapInput) error {
33+
agentPath := input.AgentPath()
34+
if agentPath == "" {
35+
return fmt.Errorf("agent path is required")
36+
}
37+
3138
sandboxName := input.SandboxName()
3239
configDir := r.ConfigDir()
3340

@@ -37,21 +44,30 @@ func (r ClaudeRuntime) Bootstrap(input BootstrapInput) error {
3744
return fmt.Errorf("creating runtime config dirs: %w", err)
3845
}
3946

40-
if err := sandbox.Upload(sandboxName, input.AgentPath(),
47+
if err := sandbox.Upload(sandboxName, agentPath,
4148
fmt.Sprintf("%s/agents/", configDir)); err != nil {
4249
return fmt.Errorf("copying agent definition: %w", err)
4350
}
4451

4552
for _, skillPath := range input.SkillDirs() {
53+
if skillPath == "" {
54+
continue
55+
}
4656
if err := sandbox.Upload(sandboxName, skillPath,
4757
fmt.Sprintf("%s/skills/", configDir)); err != nil {
4858
return fmt.Errorf("copying skill %q: %w", skillPath, err)
4959
}
5060
fmt.Fprintf(os.Stderr, "Skill %q: uploaded to sandbox\n", filepath.Base(skillPath))
5161
}
5262

53-
if len(input.PluginDirs()) > 0 {
54-
if err := bootstrapPlugins(sandboxName, configDir, input.PluginDirs()); err != nil {
63+
var pluginDirs []string
64+
for _, p := range input.PluginDirs() {
65+
if p != "" {
66+
pluginDirs = append(pluginDirs, p)
67+
}
68+
}
69+
if len(pluginDirs) > 0 {
70+
if err := bootstrapPlugins(sandboxName, configDir, pluginDirs); err != nil {
5571
return fmt.Errorf("bootstrapping plugins: %w", err)
5672
}
5773
}
@@ -72,7 +88,7 @@ func (ClaudeRuntime) Run(params RunParams, printer *ui.Printer, start time.Time,
7288
defer cancel()
7389

7490
if parseErr := progressParser(stdout, printer, start, metrics); parseErr != nil {
75-
fmt.Fprintf(os.Stderr, " progress parser: %v\n", SanitizeOutput(parseErr.Error()))
91+
fmt.Fprintf(os.Stderr, " progress parser: %v\n", sanitizeOutput(parseErr.Error()))
7692
cancel()
7793
io.Copy(io.Discard, stdout)
7894
}
@@ -90,14 +106,13 @@ func (ClaudeRuntime) Run(params RunParams, printer *ui.Printer, start time.Time,
90106
return exitCode, nil
91107
}
92108

93-
func (ClaudeRuntime) ClearIterationArtifacts(sandboxName string) error {
94-
clearCmd := fmt.Sprintf("rm -rf %s/output/* %s/*.jsonl",
95-
sandbox.SandboxWorkspace, sandbox.SandboxClaudeConfig)
109+
func (r ClaudeRuntime) ClearIterationArtifacts(sandboxName string) error {
110+
clearCmd := fmt.Sprintf("rm -rf %s/output/* %s/*.jsonl", r.WorkspaceDir(), r.ConfigDir())
96111
_, _, _, err := sandbox.Exec(sandboxName, clearCmd, 10*time.Second)
97112
return err
98113
}
99114

100-
func (ClaudeRuntime) ExtractTranscripts(sandboxName, agentLabel, outputDir string) error {
115+
func (r ClaudeRuntime) ExtractTranscripts(sandboxName, agentLabel, outputDir string) error {
101116
if err := os.MkdirAll(outputDir, 0o755); err != nil {
102117
return fmt.Errorf("creating output dir: %w", err)
103118
}
@@ -108,7 +123,7 @@ func (ClaudeRuntime) ExtractTranscripts(sandboxName, agentLabel, outputDir strin
108123
}
109124
defer root.Close()
110125

111-
configDir := sandbox.SandboxClaudeConfig
126+
configDir := r.ConfigDir()
112127
stdout, _, _, err := sandbox.Exec(sandboxName,
113128
fmt.Sprintf("find %s -name '*.jsonl' 2>/dev/null || true", configDir),
114129
10*time.Second,
@@ -149,11 +164,11 @@ func (ClaudeRuntime) ExtractTranscripts(sandboxName, agentLabel, outputDir strin
149164
return nil
150165
}
151166

152-
func (ClaudeRuntime) ExtractDebugLog(sandboxName, localPath, debug string) error {
167+
func (r ClaudeRuntime) ExtractDebugLog(sandboxName, localPath, debug string) error {
153168
if debug == "" {
154169
return nil
155170
}
156-
remotePath := sandbox.SandboxWorkspace + "/" + claudeDebugLog
171+
remotePath := r.WorkspaceDir() + "/" + claudeDebugLog
157172
return sandbox.DownloadFile(sandboxName, remotePath, localPath)
158173
}
159174

internal/runtime/claude_progress.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ func emitToolProgress(printer *ui.Printer, toolName, context string, start time.
203203
msg = fmt.Sprintf("%s (%s, %d tools)", toolName, elapsed, toolCount)
204204
}
205205

206-
msg = SanitizeOutput(msg)
206+
msg = sanitizeOutput(msg)
207207
if isCI {
208208
fmt.Fprintf(os.Stderr, "::notice::%s\n", msg)
209209
}

internal/runtime/claude_progress_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -398,9 +398,9 @@ func TestSanitizeOutput(t *testing.T) {
398398
}
399399
for _, tt := range tests {
400400
t.Run(tt.name, func(t *testing.T) {
401-
got := SanitizeOutput(tt.input)
401+
got := sanitizeOutput(tt.input)
402402
if got != tt.want {
403-
t.Errorf("SanitizeOutput(%q) = %q, want %q", tt.input, got, tt.want)
403+
t.Errorf("sanitizeOutput(%q) = %q, want %q", tt.input, got, tt.want)
404404
}
405405
})
406406
}

internal/runtime/claude_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,27 @@ import (
1414
"github.com/fullsend-ai/fullsend/internal/sandbox"
1515
)
1616

17+
type bootstrapInput struct {
18+
sandboxName string
19+
agentPath string
20+
}
21+
22+
func (b bootstrapInput) SandboxName() string { return b.sandboxName }
23+
func (b bootstrapInput) AgentPath() string { return b.agentPath }
24+
func (b bootstrapInput) SkillDirs() []string { return nil }
25+
func (b bootstrapInput) PluginDirs() []string { return nil }
26+
27+
func TestBootstrap_EmptyAgentPath(t *testing.T) {
28+
err := ClaudeRuntime{}.Bootstrap(bootstrapInput{sandboxName: "test"})
29+
require.Error(t, err)
30+
assert.Contains(t, err.Error(), "agent path is required")
31+
}
32+
1733
func TestDefaultRuntime(t *testing.T) {
1834
backend := Default()
1935
assert.Equal(t, "claude", backend.Name())
2036
assert.Equal(t, sandbox.SandboxClaudeConfig, backend.ConfigDir())
37+
assert.Equal(t, sandbox.SandboxWorkspace, backend.WorkspaceDir())
2138
assert.Contains(t, backend.EnvExports()[0], "CLAUDE_CONFIG_DIR")
2239
assert.NotNil(t, backend.Transcripts)
2340
}

internal/runtime/claude_transcript.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,10 @@ func truncateError(msg string) string {
132132
func emitTranscriptErrors(w io.Writer, summaries []TranscriptError) {
133133
for _, s := range summaries {
134134
// Sanitize the error message to prevent GHA command injection.
135-
msg := SanitizeOutput(s.ErrorMessage)
135+
msg := sanitizeOutput(s.ErrorMessage)
136136
if msg == "" {
137-
msg = fmt.Sprintf("agent terminated with error (subtype: %s)", SanitizeOutput(s.Subtype))
137+
msg = fmt.Sprintf("agent terminated with error (subtype: %s)", sanitizeOutput(s.Subtype))
138138
}
139-
fmt.Fprintf(w, "::error title=Agent Error (%s)::%s\n", SanitizeOutput(s.Source), msg)
139+
fmt.Fprintf(w, "::error title=Agent Error (%s)::%s\n", sanitizeOutput(s.Source), msg)
140140
}
141141
}

internal/runtime/runtime.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type TranscriptError struct {
3535
type Runtime interface {
3636
Name() string
3737
ConfigDir() string
38+
WorkspaceDir() string
3839
EnvExports() []string
3940
Bootstrap(input BootstrapInput) error
4041
Run(params RunParams, printer *ui.Printer, start time.Time, metrics *RunMetrics) (exitCode int, err error)

internal/runtime/sanitize.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import (
88
// ansiEscRe matches ANSI CSI sequences, OSC sequences, and charset designators.
99
var ansiEscRe = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[()][A-Z0-9]`)
1010

11-
// SanitizeOutput strips ANSI escape sequences, control characters, and GHA
11+
// sanitizeOutput strips ANSI escape sequences, control characters, and GHA
1212
// workflow command markers from untrusted sandbox output.
13-
func SanitizeOutput(s string) string {
13+
func sanitizeOutput(s string) string {
1414
s = ansiEscRe.ReplaceAllString(s, "")
1515
s = strings.ReplaceAll(s, "::", ": :")
1616
for _, enc := range []string{"%0A", "%0a", "%0D", "%0d"} {

0 commit comments

Comments
 (0)