Skip to content
Open
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
16 changes: 16 additions & 0 deletions internal/runtime/bootstrap.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package runtime

import "fmt"

// BootstrapInput is the portable contract every runtime needs to provision
// agent content into the sandbox. Implementations live outside this package
// (runner adapter, tests).
Expand All @@ -16,3 +18,17 @@ type BootstrapInput interface {
SkillDirs() []string
PluginDirs() []string
}

// validateAgentNameMatch returns an error when requestedName and
// definitionName are both non-empty and do not match. Both ClaudeRuntime
// and PiRuntime call this shared helper so the mismatch message is defined
// in one place.
func validateAgentNameMatch(requestedName, definitionName string) error {
if requestedName == "" || definitionName == "" {
return nil
}
if definitionName != requestedName {
return fmt.Errorf("agent name mismatch: requested %q but definition declares %q", requestedName, definitionName)
}
return nil
}
14 changes: 14 additions & 0 deletions internal/runtime/claude.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ func (r ClaudeRuntime) Bootstrap(input BootstrapInput) error {
return fmt.Errorf("agent path is required")
}

// Validate the frontmatter name: field against the requested agent name.
// Claude Code resolves --agent by frontmatter name, not filename, so a
// mismatch means the runtime silently falls back to the default agent,
// producing an unconstrained run (#6764).
if agentName := input.AgentName(); agentName != "" {
if data, readErr := os.ReadFile(agentPath); readErr == nil {
if def, parseErr := parsePiAgent(data); parseErr != nil {
fmt.Fprintf(os.Stderr, "Agent name validation: skipped for %s: %v\n", agentPath, parseErr)
} else if err := validateAgentNameMatch(agentName, def.Name); err != nil {
return err
}
}
}

sandboxName := input.SandboxName()
configDir := r.ConfigDir()

Expand Down
86 changes: 86 additions & 0 deletions internal/runtime/claude_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,92 @@ func TestAgentDestName(t *testing.T) {
}
}

func TestValidateAgentNameMatch(t *testing.T) {
tests := []struct {
name string
requestedName string
definitionName string
wantErr string // empty means no error expected
}{
{
name: "matching names pass",
requestedName: "code",
definitionName: "code",
wantErr: "",
},
{
name: "mismatched names fail",
requestedName: "coder",
definitionName: "code",
wantErr: `agent name mismatch: requested "coder" but definition declares "code"`,
},
{
name: "empty requested name skips validation",
requestedName: "",
definitionName: "code",
wantErr: "",
},
{
name: "empty definition name skips validation",
requestedName: "coder",
definitionName: "",
wantErr: "",
},
{
name: "both empty skips validation",
requestedName: "",
definitionName: "",
wantErr: "",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := validateAgentNameMatch(tc.requestedName, tc.definitionName)
if tc.wantErr == "" {
assert.NoError(t, err)
} else {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.wantErr)
}
})
}
}

func TestBootstrap_AgentNameMismatch(t *testing.T) {
stubDir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(stubDir, "openshell"), []byte("#!/bin/sh\nexit 0\n"), 0o755))
t.Setenv("PATH", stubDir)

agentFile := filepath.Join(t.TempDir(), "agent.md")
require.NoError(t, os.WriteFile(agentFile, []byte("---\nname: code\n---\n# Code agent"), 0o644))

err := ClaudeRuntime{}.Bootstrap(bootstrapInput{
sandboxName: "test-sandbox",
agentPath: agentFile,
agentName: "coder",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "agent name mismatch")
assert.Contains(t, err.Error(), `"coder"`)
assert.Contains(t, err.Error(), `"code"`)
}

func TestBootstrap_AgentNameMatch(t *testing.T) {
stubDir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(stubDir, "openshell"), []byte("#!/bin/sh\nexit 0\n"), 0o755))
t.Setenv("PATH", stubDir)

agentFile := filepath.Join(t.TempDir(), "agent.md")
require.NoError(t, os.WriteFile(agentFile, []byte("---\nname: code\n---\n# Code agent"), 0o644))

err := ClaudeRuntime{}.Bootstrap(bootstrapInput{
sandboxName: "test-sandbox",
agentPath: agentFile,
agentName: "code",
})
assert.NoError(t, err)
}

func TestBuildRunCommand_Basic(t *testing.T) {
cmd := testRunCommand("hello-world", "", "/sandbox/workspace/repo", nil, "")
assert.Contains(t, cmd, "cd /sandbox/workspace/repo")
Expand Down
6 changes: 6 additions & 0 deletions internal/runtime/pi_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ func (r PiRuntime) Bootstrap(input BootstrapInput) error {
if err != nil {
return err
}
// Fail fast when the harness-configured agent name does not match the
Comment thread
ralphbean marked this conversation as resolved.
// definition's frontmatter name: — the runtime would silently fall back
// to the default agent, producing an unconstrained run (#6764).
if err := validateAgentNameMatch(input.AgentName(), def.Name); err != nil {
return err
}
agentName := input.AgentName()
if agentName == "" {
agentName = def.Name
Expand Down
33 changes: 33 additions & 0 deletions internal/runtime/pi_bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,39 @@ func TestPiRuntimeBootstrap_NoSecurityNoHooks(t *testing.T) {
assert.True(t, os.IsNotExist(err), "no hook extension without security config")
}

func TestPiRuntimeBootstrap_AgentNameMismatch(t *testing.T) {
work := t.TempDir()
logPath := filepath.Join(work, "openshell.log")
store := filepath.Join(work, "store")
fakeOpenshellPi(t, logPath, store, "/dev/null")

agentFile := writeAgentFile(t, "---\nname: code\n---\n# Code agent")
err := PiRuntime{}.Bootstrap(bootstrapInput{
sandboxName: "sb",
agentPath: agentFile,
agentName: "coder",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "agent name mismatch")
assert.Contains(t, err.Error(), `"coder"`)
assert.Contains(t, err.Error(), `"code"`)
}

func TestPiRuntimeBootstrap_AgentNameMatch(t *testing.T) {
work := t.TempDir()
logPath := filepath.Join(work, "openshell.log")
store := filepath.Join(work, "store")
fakeOpenshellPi(t, logPath, store, "/dev/null")

agentFile := writeAgentFile(t, "---\nname: triage\n---\n# Triage agent")
err := PiRuntime{}.Bootstrap(bootstrapInput{
sandboxName: "sb",
agentPath: agentFile,
agentName: "triage",
})
assert.NoError(t, err)
}

func TestPiRuntimeBootstrap_PreflightFailure(t *testing.T) {
binDir := t.TempDir()
script := "#!/bin/sh\nfor last; do :; done\ncase \"$last\" in \"pi --version\") echo 'sh: pi: not found' >&2; exit 127 ;; esac\nexit 0\n"
Expand Down
Loading