From 341aa26ed0e46cf677dfb48ccf3be22e11d674f0 Mon Sep 17 00:00:00 2001 From: fullsend-code <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:50:02 +0000 Subject: [PATCH 1/5] fix(#6764): fail when agent definition name mismatches requested name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code resolves --agent by the frontmatter name: field, not the filename. When the harness config names an agent "coder" but the definition declares name: "code", Claude silently falls back to the default agent — producing an unconstrained run with none of the intended agent behavior. Add validateAgentName() to ClaudeRuntime.Bootstrap that reads the agent definition, parses the frontmatter name: field, and returns an error when it does not match the requested agent name. Add the same check inline in PiRuntime.Bootstrap (which already parses the definition). When the definition has no frontmatter or no name: field, validation is skipped — the runtime uses its own resolution chain. The error message includes both names and advises updating the harness config or definition frontmatter. Closes #6764 --- internal/runtime/claude.go | 39 ++++++++++ internal/runtime/claude_test.go | 100 ++++++++++++++++++++++++++ internal/runtime/pi_bootstrap.go | 11 +++ internal/runtime/pi_bootstrap_test.go | 33 +++++++++ 4 files changed, 183 insertions(+) diff --git a/internal/runtime/claude.go b/internal/runtime/claude.go index 6c43a691a1..5d75727078 100644 --- a/internal/runtime/claude.go +++ b/internal/runtime/claude.go @@ -47,6 +47,10 @@ func (r ClaudeRuntime) Bootstrap(input BootstrapInput) error { return fmt.Errorf("agent path is required") } + if err := validateAgentName(input.AgentName(), agentPath); err != nil { + return err + } + sandboxName := input.SandboxName() configDir := r.ConfigDir() @@ -515,6 +519,41 @@ func buildPluginConfigs(plugins []string, pluginsBase, mktBase, marketplace, ver return result, nil } +// validateAgentName reads the agent definition at agentPath, extracts the +// frontmatter name: field, and returns an error when it does not match +// requestedName. Claude Code resolves --agent by the frontmatter name, not +// the filename, so a mismatch means the runtime will silently fall back to +// the default agent — producing an unconstrained run (#6764). +// +// When the definition has no frontmatter or no name: field, validation is +// skipped: the runtime uses its own resolution chain (filename, positional +// argument). +func validateAgentName(requestedName, agentPath string) error { + if requestedName == "" { + return nil + } + data, err := os.ReadFile(agentPath) + if err != nil { + // Let the caller's own ReadFile produce the canonical error. + return nil + } + def, err := parsePiAgent(data) + if err != nil || def.Name == "" { + // Unparseable or unnamed — skip validation; the runtime will + // fall back to its own resolution chain. + return nil + } + if def.Name != requestedName { + return fmt.Errorf( + "agent name mismatch: requested %q but definition declares name: %q — "+ + "the runtime will receive --agent %q and silently fall back to the default agent; "+ + "update the agent name in the harness config or the definition frontmatter so they match", + requestedName, def.Name, requestedName, + ) + } + return nil +} + // agentDestName returns the sandbox filename for the agent definition. // When agentName is non-empty it produces {name}.md; otherwise it falls // back to the source file's basename. diff --git a/internal/runtime/claude_test.go b/internal/runtime/claude_test.go index 2ab1f87346..d120cfdf40 100644 --- a/internal/runtime/claude_test.go +++ b/internal/runtime/claude_test.go @@ -105,6 +105,106 @@ func TestAgentDestName(t *testing.T) { } } +func TestValidateAgentName(t *testing.T) { + tests := []struct { + name string + requestedName string + fileContent string + wantErr string // empty means no error expected + }{ + { + name: "matching names pass", + requestedName: "code", + fileContent: "---\nname: code\n---\n# Agent", + wantErr: "", + }, + { + name: "mismatched names fail", + requestedName: "coder", + fileContent: "---\nname: code\n---\n# Agent", + wantErr: `agent name mismatch: requested "coder" but definition declares name: "code"`, + }, + { + name: "empty requested name skips validation", + requestedName: "", + fileContent: "---\nname: code\n---\n# Agent", + wantErr: "", + }, + { + name: "no frontmatter skips validation", + requestedName: "coder", + fileContent: "# Agent without frontmatter", + wantErr: "", + }, + { + name: "empty frontmatter name skips validation", + requestedName: "coder", + fileContent: "---\ndescription: some agent\n---\n# Agent", + wantErr: "", + }, + { + name: "nonexistent file skips validation", + requestedName: "coder", + fileContent: "", // will use a path that doesn't exist + wantErr: "", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var agentPath string + if tc.fileContent != "" { + f := filepath.Join(t.TempDir(), "agent.md") + require.NoError(t, os.WriteFile(f, []byte(tc.fileContent), 0o644)) + agentPath = f + } else { + agentPath = filepath.Join(t.TempDir(), "nonexistent.md") + } + err := validateAgentName(tc.requestedName, agentPath) + 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") diff --git a/internal/runtime/pi_bootstrap.go b/internal/runtime/pi_bootstrap.go index 017767397d..292d66b90b 100644 --- a/internal/runtime/pi_bootstrap.go +++ b/internal/runtime/pi_bootstrap.go @@ -92,6 +92,17 @@ func (r PiRuntime) Bootstrap(input BootstrapInput) error { if err != nil { return err } + // Fail fast when the harness-configured agent name does not match the + // definition's frontmatter name: — the runtime would silently fall back + // to the default agent, producing an unconstrained run (#6764). + if reqName := input.AgentName(); reqName != "" && def.Name != "" && reqName != def.Name { + return fmt.Errorf( + "agent name mismatch: requested %q but definition declares name: %q — "+ + "the runtime will receive --agent %q and silently fall back to the default agent; "+ + "update the agent name in the harness config or the definition frontmatter so they match", + reqName, def.Name, reqName, + ) + } agentName := input.AgentName() if agentName == "" { agentName = def.Name diff --git a/internal/runtime/pi_bootstrap_test.go b/internal/runtime/pi_bootstrap_test.go index b7192bd5b7..31ab34a950 100644 --- a/internal/runtime/pi_bootstrap_test.go +++ b/internal/runtime/pi_bootstrap_test.go @@ -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" From 5a87c33dbdea936c63b075c1419a663839224e87 Mon Sep 17 00:00:00 2001 From: fullsend-fix <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:18:47 +0000 Subject: [PATCH 2/5] refactor(runtime): extract shared checkAgentName helper to reduce duplication The agent-name-mismatch validation logic and error message were duplicated between validateAgentName (claude.go) and inline code in PiRuntime.Bootstrap (pi_bootstrap.go). Extract a shared checkAgentName helper that both call sites delegate to. Addresses review feedback on #6766 --- internal/runtime/claude.go | 34 +++++++++++++++++++++----------- internal/runtime/pi_bootstrap.go | 9 ++------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/internal/runtime/claude.go b/internal/runtime/claude.go index 5d75727078..2055270165 100644 --- a/internal/runtime/claude.go +++ b/internal/runtime/claude.go @@ -519,6 +519,24 @@ func buildPluginConfigs(plugins []string, pluginsBase, mktBase, marketplace, ver return result, nil } +// checkAgentName 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 checkAgentName(requestedName, definitionName string) error { + if requestedName == "" || definitionName == "" { + return nil + } + if definitionName != requestedName { + return fmt.Errorf( + "agent name mismatch: requested %q but definition declares name: %q — "+ + "the runtime will receive --agent %q and silently fall back to the default agent; "+ + "update the agent name in the harness config or the definition frontmatter so they match", + requestedName, definitionName, requestedName, + ) + } + return nil +} + // validateAgentName reads the agent definition at agentPath, extracts the // frontmatter name: field, and returns an error when it does not match // requestedName. Claude Code resolves --agent by the frontmatter name, not @@ -538,20 +556,12 @@ func validateAgentName(requestedName, agentPath string) error { return nil } def, err := parsePiAgent(data) - if err != nil || def.Name == "" { - // Unparseable or unnamed — skip validation; the runtime will - // fall back to its own resolution chain. + if err != nil { + // Unparseable — skip validation; the runtime will fall back to + // its own resolution chain. return nil } - if def.Name != requestedName { - return fmt.Errorf( - "agent name mismatch: requested %q but definition declares name: %q — "+ - "the runtime will receive --agent %q and silently fall back to the default agent; "+ - "update the agent name in the harness config or the definition frontmatter so they match", - requestedName, def.Name, requestedName, - ) - } - return nil + return checkAgentName(requestedName, def.Name) } // agentDestName returns the sandbox filename for the agent definition. diff --git a/internal/runtime/pi_bootstrap.go b/internal/runtime/pi_bootstrap.go index 292d66b90b..df5be8a968 100644 --- a/internal/runtime/pi_bootstrap.go +++ b/internal/runtime/pi_bootstrap.go @@ -95,13 +95,8 @@ func (r PiRuntime) Bootstrap(input BootstrapInput) error { // Fail fast when the harness-configured agent name does not match the // definition's frontmatter name: — the runtime would silently fall back // to the default agent, producing an unconstrained run (#6764). - if reqName := input.AgentName(); reqName != "" && def.Name != "" && reqName != def.Name { - return fmt.Errorf( - "agent name mismatch: requested %q but definition declares name: %q — "+ - "the runtime will receive --agent %q and silently fall back to the default agent; "+ - "update the agent name in the harness config or the definition frontmatter so they match", - reqName, def.Name, reqName, - ) + if err := checkAgentName(input.AgentName(), def.Name); err != nil { + return err } agentName := input.AgentName() if agentName == "" { From 1886fbc104daa9b12b51c5daa510f6738473329d Mon Sep 17 00:00:00 2001 From: fullsend-fix <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:47:03 +0000 Subject: [PATCH 3/5] fix(runtime): address review feedback on PR #6766 - Rename checkAgentName to validateAgentNameMatch to follow the established validate* naming convention in the package - Shorten mismatch error message to terse ASCII diagnostic matching the existing fmt.Errorf style in the package - Log a warning to stderr when parsePiAgent fails in validateAgentName so operators have visibility into structural definition issues Addresses review feedback on #6766 --- internal/runtime/claude.go | 21 ++++++++------------- internal/runtime/claude_test.go | 2 +- internal/runtime/pi_bootstrap.go | 2 +- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/internal/runtime/claude.go b/internal/runtime/claude.go index 2055270165..9bd369922d 100644 --- a/internal/runtime/claude.go +++ b/internal/runtime/claude.go @@ -519,20 +519,16 @@ func buildPluginConfigs(plugins []string, pluginsBase, mktBase, marketplace, ver return result, nil } -// checkAgentName 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 checkAgentName(requestedName, definitionName string) error { +// 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 name: %q — "+ - "the runtime will receive --agent %q and silently fall back to the default agent; "+ - "update the agent name in the harness config or the definition frontmatter so they match", - requestedName, definitionName, requestedName, - ) + return fmt.Errorf("agent name mismatch: requested %q but definition declares %q", requestedName, definitionName) } return nil } @@ -557,11 +553,10 @@ func validateAgentName(requestedName, agentPath string) error { } def, err := parsePiAgent(data) if err != nil { - // Unparseable — skip validation; the runtime will fall back to - // its own resolution chain. + fmt.Fprintf(os.Stderr, "warning: skipping agent name validation for %s: %v\n", agentPath, err) return nil } - return checkAgentName(requestedName, def.Name) + return validateAgentNameMatch(requestedName, def.Name) } // agentDestName returns the sandbox filename for the agent definition. diff --git a/internal/runtime/claude_test.go b/internal/runtime/claude_test.go index d120cfdf40..ada16ad005 100644 --- a/internal/runtime/claude_test.go +++ b/internal/runtime/claude_test.go @@ -122,7 +122,7 @@ func TestValidateAgentName(t *testing.T) { name: "mismatched names fail", requestedName: "coder", fileContent: "---\nname: code\n---\n# Agent", - wantErr: `agent name mismatch: requested "coder" but definition declares name: "code"`, + wantErr: `agent name mismatch: requested "coder" but definition declares "code"`, }, { name: "empty requested name skips validation", diff --git a/internal/runtime/pi_bootstrap.go b/internal/runtime/pi_bootstrap.go index df5be8a968..233da1f46c 100644 --- a/internal/runtime/pi_bootstrap.go +++ b/internal/runtime/pi_bootstrap.go @@ -95,7 +95,7 @@ func (r PiRuntime) Bootstrap(input BootstrapInput) error { // Fail fast when the harness-configured agent name does not match the // definition's frontmatter name: — the runtime would silently fall back // to the default agent, producing an unconstrained run (#6764). - if err := checkAgentName(input.AgentName(), def.Name); err != nil { + if err := validateAgentNameMatch(input.AgentName(), def.Name); err != nil { return err } agentName := input.AgentName() From d1684cf3fbea572237d1615ca71e82bab56bc368 Mon Sep 17 00:00:00 2001 From: fullsend-fix <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 21:13:35 +0000 Subject: [PATCH 4/5] fix(runtime): address review feedback on PR #6766 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move validateAgentNameMatch and validateAgentName from claude.go to bootstrap.go alongside BootstrapInput — both are cross-runtime bootstrap helpers, and the codebase convention places shared helpers in their own files (cf. sanitize.go). Align the stderr message with the package's label-style prefix convention: "Agent name validation: skipped for ..." replaces the precedent-less "warning: skipping ..." prefix. Addresses review feedback on #6766 --- internal/runtime/bootstrap.go | 45 +++++++++++++++++++++++++++++++++++ internal/runtime/claude.go | 40 ------------------------------- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/internal/runtime/bootstrap.go b/internal/runtime/bootstrap.go index bb59d290b2..9d3f9fbbe8 100644 --- a/internal/runtime/bootstrap.go +++ b/internal/runtime/bootstrap.go @@ -1,5 +1,10 @@ package runtime +import ( + "fmt" + "os" +) + // BootstrapInput is the portable contract every runtime needs to provision // agent content into the sandbox. Implementations live outside this package // (runner adapter, tests). @@ -16,3 +21,43 @@ 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 +} + +// validateAgentName reads the agent definition at agentPath, extracts the +// frontmatter name: field, and returns an error when it does not match +// requestedName. Claude Code resolves --agent by the frontmatter name, not +// the filename, so a mismatch means the runtime will silently fall back to +// the default agent — producing an unconstrained run (#6764). +// +// When the definition has no frontmatter or no name: field, validation is +// skipped: the runtime uses its own resolution chain (filename, positional +// argument). +func validateAgentName(requestedName, agentPath string) error { + if requestedName == "" { + return nil + } + data, err := os.ReadFile(agentPath) + if err != nil { + // Let the caller's own ReadFile produce the canonical error. + return nil + } + def, err := parsePiAgent(data) + if err != nil { + fmt.Fprintf(os.Stderr, "Agent name validation: skipped for %s: %v\n", agentPath, err) + return nil + } + return validateAgentNameMatch(requestedName, def.Name) +} diff --git a/internal/runtime/claude.go b/internal/runtime/claude.go index 9bd369922d..8d187a2fb3 100644 --- a/internal/runtime/claude.go +++ b/internal/runtime/claude.go @@ -519,46 +519,6 @@ func buildPluginConfigs(plugins []string, pluginsBase, mktBase, marketplace, ver return result, nil } -// 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 -} - -// validateAgentName reads the agent definition at agentPath, extracts the -// frontmatter name: field, and returns an error when it does not match -// requestedName. Claude Code resolves --agent by the frontmatter name, not -// the filename, so a mismatch means the runtime will silently fall back to -// the default agent — producing an unconstrained run (#6764). -// -// When the definition has no frontmatter or no name: field, validation is -// skipped: the runtime uses its own resolution chain (filename, positional -// argument). -func validateAgentName(requestedName, agentPath string) error { - if requestedName == "" { - return nil - } - data, err := os.ReadFile(agentPath) - if err != nil { - // Let the caller's own ReadFile produce the canonical error. - return nil - } - def, err := parsePiAgent(data) - if err != nil { - fmt.Fprintf(os.Stderr, "warning: skipping agent name validation for %s: %v\n", agentPath, err) - return nil - } - return validateAgentNameMatch(requestedName, def.Name) -} - // agentDestName returns the sandbox filename for the agent definition. // When agentName is non-empty it produces {name}.md; otherwise it falls // back to the source file's basename. From 738662790cba609e77aa06ba326250d24d01e89e Mon Sep 17 00:00:00 2001 From: fullsend-fix <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:22:36 +0000 Subject: [PATCH 5/5] refactor(runtime): remove validateAgentName wrapper to eliminate code duplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the validateAgentName file-reading wrapper from bootstrap.go and inline its logic in ClaudeRuntime.Bootstrap. This leaves a single shared helper (validateAgentNameMatch) that both runtimes call directly with pre-extracted names, eliminating the duplicated read→parse→validate pattern between the wrapper and PiRuntime.Bootstrap. Addresses #6766 --- internal/runtime/bootstrap.go | 31 +--------------- internal/runtime/claude.go | 14 ++++++- internal/runtime/claude_test.go | 66 +++++++++++++-------------------- 3 files changed, 39 insertions(+), 72 deletions(-) diff --git a/internal/runtime/bootstrap.go b/internal/runtime/bootstrap.go index 9d3f9fbbe8..942141f6eb 100644 --- a/internal/runtime/bootstrap.go +++ b/internal/runtime/bootstrap.go @@ -1,9 +1,6 @@ package runtime -import ( - "fmt" - "os" -) +import "fmt" // BootstrapInput is the portable contract every runtime needs to provision // agent content into the sandbox. Implementations live outside this package @@ -35,29 +32,3 @@ func validateAgentNameMatch(requestedName, definitionName string) error { } return nil } - -// validateAgentName reads the agent definition at agentPath, extracts the -// frontmatter name: field, and returns an error when it does not match -// requestedName. Claude Code resolves --agent by the frontmatter name, not -// the filename, so a mismatch means the runtime will silently fall back to -// the default agent — producing an unconstrained run (#6764). -// -// When the definition has no frontmatter or no name: field, validation is -// skipped: the runtime uses its own resolution chain (filename, positional -// argument). -func validateAgentName(requestedName, agentPath string) error { - if requestedName == "" { - return nil - } - data, err := os.ReadFile(agentPath) - if err != nil { - // Let the caller's own ReadFile produce the canonical error. - return nil - } - def, err := parsePiAgent(data) - if err != nil { - fmt.Fprintf(os.Stderr, "Agent name validation: skipped for %s: %v\n", agentPath, err) - return nil - } - return validateAgentNameMatch(requestedName, def.Name) -} diff --git a/internal/runtime/claude.go b/internal/runtime/claude.go index 8d187a2fb3..e23173ea82 100644 --- a/internal/runtime/claude.go +++ b/internal/runtime/claude.go @@ -47,8 +47,18 @@ func (r ClaudeRuntime) Bootstrap(input BootstrapInput) error { return fmt.Errorf("agent path is required") } - if err := validateAgentName(input.AgentName(), agentPath); err != nil { - return err + // 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() diff --git a/internal/runtime/claude_test.go b/internal/runtime/claude_test.go index ada16ad005..42062bc8bf 100644 --- a/internal/runtime/claude_test.go +++ b/internal/runtime/claude_test.go @@ -105,61 +105,47 @@ func TestAgentDestName(t *testing.T) { } } -func TestValidateAgentName(t *testing.T) { +func TestValidateAgentNameMatch(t *testing.T) { tests := []struct { - name string - requestedName string - fileContent string - wantErr string // empty means no error expected + name string + requestedName string + definitionName string + wantErr string // empty means no error expected }{ { - name: "matching names pass", - requestedName: "code", - fileContent: "---\nname: code\n---\n# Agent", - wantErr: "", + name: "matching names pass", + requestedName: "code", + definitionName: "code", + wantErr: "", }, { - name: "mismatched names fail", - requestedName: "coder", - fileContent: "---\nname: code\n---\n# Agent", - wantErr: `agent name mismatch: requested "coder" but definition declares "code"`, + 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: "", - fileContent: "---\nname: code\n---\n# Agent", - wantErr: "", + name: "empty requested name skips validation", + requestedName: "", + definitionName: "code", + wantErr: "", }, { - name: "no frontmatter skips validation", - requestedName: "coder", - fileContent: "# Agent without frontmatter", - wantErr: "", + name: "empty definition name skips validation", + requestedName: "coder", + definitionName: "", + wantErr: "", }, { - name: "empty frontmatter name skips validation", - requestedName: "coder", - fileContent: "---\ndescription: some agent\n---\n# Agent", - wantErr: "", - }, - { - name: "nonexistent file skips validation", - requestedName: "coder", - fileContent: "", // will use a path that doesn't exist - wantErr: "", + name: "both empty skips validation", + requestedName: "", + definitionName: "", + wantErr: "", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var agentPath string - if tc.fileContent != "" { - f := filepath.Join(t.TempDir(), "agent.md") - require.NoError(t, os.WriteFile(f, []byte(tc.fileContent), 0o644)) - agentPath = f - } else { - agentPath = filepath.Join(t.TempDir(), "nonexistent.md") - } - err := validateAgentName(tc.requestedName, agentPath) + err := validateAgentNameMatch(tc.requestedName, tc.definitionName) if tc.wantErr == "" { assert.NoError(t, err) } else {