diff --git a/internal/llm/contracts.go b/internal/llm/contracts.go index c9dd44e..eb03d57 100644 --- a/internal/llm/contracts.go +++ b/internal/llm/contracts.go @@ -35,9 +35,10 @@ type Selection struct { // SelectedAgent is one selected reviewer agent. type SelectedAgent struct { - AgentID string - Rationale string - Files []string + AgentID string + Rationale string + Files []string + AllowedFiles []string } // SelectionOptions contains context needed to validate selection output. @@ -84,9 +85,10 @@ type selectionWire struct { } type selectedAgentWire struct { - AgentID string `json:"agent_id"` - Rationale string `json:"rationale"` - Files []string `json:"files"` + AgentID string `json:"agent_id"` + Rationale string `json:"rationale"` + Files []string `json:"files"` + AllowedFiles []string `json:"allowed_files,omitempty"` } type threadActionWire struct { @@ -155,10 +157,16 @@ func DecodeSelection(data []byte, opts SelectionOptions) (Selection, error) { return Selection{}, fmt.Errorf("llm: selected file %q is not in changed files", file) } } + for _, file := range agent.AllowedFiles { + if strings.TrimSpace(file) == "" || !opts.ChangedFiles[file] { + return Selection{}, fmt.Errorf("llm: allowed file %q is not in changed files", file) + } + } selection.SelectedAgents = append(selection.SelectedAgents, SelectedAgent{ - AgentID: agent.AgentID, - Rationale: sanitize(agent.Rationale), - Files: append([]string(nil), agent.Files...), + AgentID: agent.AgentID, + Rationale: sanitize(agent.Rationale), + Files: append([]string(nil), agent.Files...), + AllowedFiles: append([]string(nil), agent.AllowedFiles...), }) } diff --git a/internal/llm/contracts_test.go b/internal/llm/contracts_test.go index 7823ae9..a3a0bca 100644 --- a/internal/llm/contracts_test.go +++ b/internal/llm/contracts_test.go @@ -16,7 +16,7 @@ func TestDecodeSelection(t *testing.T) { } got, err := DecodeSelection([]byte(`{ "schema_version": 1, - "selected_agents": [{"agent_id":"agent-1","rationale":"why ","files":["main.go"]}], + "selected_agents": [{"agent_id":"agent-1","rationale":"why ","files":["main.go"],"allowed_files":["main.go"]}], "thread_actions": [{"thread_id":"thread-1","decision":"summarize_and_resolve","summary":" summary","safe_to_resolve_rationale":"safe"}], "reasoning":"because " }`), opts) @@ -26,6 +26,9 @@ func TestDecodeSelection(t *testing.T) { if got.SelectedAgents[0].AgentID != "agent-1" || got.ThreadActions[0].Decision != review.ThreadDecisionSummarizeAndResolve { t.Fatalf("DecodeSelection = %#v", got) } + if len(got.SelectedAgents[0].AllowedFiles) != 1 || got.SelectedAgents[0].AllowedFiles[0] != "main.go" { + t.Fatalf("DecodeSelection allowed_files = %#v, want main.go", got.SelectedAgents[0].AllowedFiles) + } if strings.Contains(got.ThreadActions[0].Summary, "