fix: address remote persona review findings #63

Closed
rodin wants to merge 2 commits from issue-60-remote-personas into main
3 changed files with 25 additions and 11 deletions
Showing only changes of commit 5fac8bc505 - Show all commits
+10 -8
View File
@@ -79,7 +79,6 @@ func main() {
aicoreAPIURL := flag.String("aicore-api-url", envOrDefault("AICORE_API_URL", ""), "SAP AI Core API URL (for provider=aicore)") aicoreAPIURL := flag.String("aicore-api-url", envOrDefault("AICORE_API_URL", ""), "SAP AI Core API URL (for provider=aicore)")
aicoreResourceGroup := flag.String("aicore-resource-group", envOrDefault("AICORE_RESOURCE_GROUP", "default"), "SAP AI Core resource group (for provider=aicore)") aicoreResourceGroup := flag.String("aicore-resource-group", envOrDefault("AICORE_RESOURCE_GROUP", "default"), "SAP AI Core resource group (for provider=aicore)")
flag.Parse()
flag.Parse() flag.Parse()
if *versionFlag { if *versionFlag {
@@ -119,7 +118,6 @@ func main() {
// Persona loading is deferred until after giteaClient is initialized, // Persona loading is deferred until after giteaClient is initialized,
// so we can try loading from the target repo first. // so we can try loading from the target repo first.
var persona *review.Persona var persona *review.Persona
var personaErr error
// Validate reviewer-name: only safe characters allowed in sentinel // Validate reviewer-name: only safe characters allowed in sentinel
if err := validateReviewerName(*reviewerName); err != nil { if err := validateReviewerName(*reviewerName); err != nil {
@@ -184,6 +182,8 @@ func main() {
remotePersonas, err := review.LoadRemotePersonas(ctx, fetcher, owner, repoName) remotePersonas, err := review.LoadRemotePersonas(ctx, fetcher, owner, repoName)
if err != nil { if err != nil {
slog.Warn("could not load remote personas", "repo", fmt.Sprintf("%s/%s", owner, repoName), "error", err) slog.Warn("could not load remote personas", "repo", fmt.Sprintf("%s/%s", owner, repoName), "error", err)
// Assign empty map so the lookup below doesn't panic
remotePersonas = map[string]*review.Persona{}
} }
Review

[MINOR] Remote persona metadata (e.g., DisplayName) is now untrusted input and is later incorporated into the review body. If the Markdown renderer on the Gitea instance is misconfigured or insufficiently sanitizes HTML, this could enable content/HTML injection. Consider sanitizing or escaping persona.DisplayName before embedding it in Markdown.

**[MINOR]** Remote persona metadata (e.g., DisplayName) is now untrusted input and is later incorporated into the review body. If the Markdown renderer on the Gitea instance is misconfigured or insufficiently sanitizes HTML, this could enable content/HTML injection. Consider sanitizing or escaping persona.DisplayName before embedding it in Markdown.
if p, ok := remotePersonas[*personaName]; ok { if p, ok := remotePersonas[*personaName]; ok {
@@ -191,9 +191,10 @@ func main() {
slog.Info("loaded persona from target repo", "persona", persona.Name, "display", persona.DisplayName) slog.Info("loaded persona from target repo", "persona", persona.Name, "display", persona.DisplayName)
} else { } else {
// Fall back to built-in persona // Fall back to built-in persona
persona, personaErr = review.LoadBuiltinPersona(*personaName) var err error
if personaErr != nil { persona, err = review.LoadBuiltinPersona(*personaName)
slog.Error("failed to load persona", "persona", *personaName, "error", personaErr) if err != nil {
slog.Error("failed to load persona", "persona", *personaName, "error", err)
Review

[MINOR] The err2 variable name in the personaFile branch is a workaround for shadowing from the outer err in the personaName branch, but the two branches are mutually exclusive (else if). A cleaner approach would be to use a fresh err variable inside the else-if block with its own scope: if err := validateWorkspacePath(...); err != nil { ... } followed by persona, err = review.LoadPersona(resolvedPath) where err is declared at block scope. The current err2 naming is functional but slightly non-idiomatic.

**[MINOR]** The `err2` variable name in the `personaFile` branch is a workaround for shadowing from the outer `err` in the `personaName` branch, but the two branches are mutually exclusive (else if). A cleaner approach would be to use a fresh `err` variable inside the else-if block with its own scope: `if err := validateWorkspacePath(...); err != nil { ... }` followed by `persona, err = review.LoadPersona(resolvedPath)` where `err` is declared at block scope. The current `err2` naming is functional but slightly non-idiomatic.
os.Exit(1) os.Exit(1)
} }
slog.Info("loaded built-in persona", "persona", persona.Name, "display", persona.DisplayName) slog.Info("loaded built-in persona", "persona", persona.Name, "display", persona.DisplayName)
@@ -204,9 +205,10 @@ func main() {
slog.Error("invalid persona-file path", "error", err) slog.Error("invalid persona-file path", "error", err)
os.Exit(1) os.Exit(1)
} }
persona, personaErr = review.LoadPersona(resolvedPath) var err2 error
if personaErr != nil { persona, err2 = review.LoadPersona(resolvedPath)
slog.Error("failed to load persona file", "file", *personaFile, "error", personaErr) if err2 != nil {
slog.Error("failed to load persona file", "file", *personaFile, "error", err2)
os.Exit(1) os.Exit(1)
} }
slog.Info("loaded persona from file", "file", *personaFile, "persona", persona.Name) slog.Info("loaded persona from file", "file", *personaFile, "persona", persona.Name)
+14 -2
View File
@@ -40,7 +40,9 @@ func LoadRemotePersonas(ctx context.Context, fetcher PersonaFetcher, owner, repo
return LoadRemotePersonasFromPath(ctx, fetcher, owner, repo, DefaultPersonasPath) return LoadRemotePersonasFromPath(ctx, fetcher, owner, repo, DefaultPersonasPath)
} }
// LoadRemotePersonasFromPath is like LoadRemotePersonas but allows specifying a custom path. // LoadRemotePersonasFromPath loads personas from a custom path in a remote repository.
// It behaves the same as LoadRemotePersonas but allows specifying a path other than
// the default .review-bot/personas directory.
func LoadRemotePersonasFromPath(ctx context.Context, fetcher PersonaFetcher, owner, repo, path string) (map[string]*Persona, error) { func LoadRemotePersonasFromPath(ctx context.Context, fetcher PersonaFetcher, owner, repo, path string) (map[string]*Persona, error) {
entries, err := fetcher.ListContents(ctx, owner, repo, path) entries, err := fetcher.ListContents(ctx, owner, repo, path)
if err != nil { if err != nil {
1
@@ -52,8 +54,17 @@ func LoadRemotePersonasFromPath(ctx context.Context, fetcher PersonaFetcher, own
return nil, fmt.Errorf("list remote personas: %w", err) return nil, fmt.Errorf("list remote personas: %w", err)
} }
// Cap the number of files to process to prevent resource exhaustion
// from repos with thousands of small files.
const maxPersonaFiles = 50
result := make(map[string]*Persona) result := make(map[string]*Persona)
processed := 0
for _, entry := range entries { for _, entry := range entries {
if processed >= maxPersonaFiles {
slog.Warn("persona file limit reached", "limit", maxPersonaFiles, "repo", fmt.Sprintf("%s/%s", owner, repo))
break
}
if ctx.Err() != nil { if ctx.Err() != nil {
return nil, ctx.Err() return nil, ctx.Err()
} }
1
@@ -85,6 +96,7 @@ func LoadRemotePersonasFromPath(ctx context.Context, fetcher PersonaFetcher, own
} }
result[persona.Name] = persona result[persona.Name] = persona
processed++
slog.Debug("loaded remote persona", "name", persona.Name, "file", entry.Path) slog.Debug("loaded remote persona", "name", persona.Name, "file", entry.Path)
} }
2
@@ -148,5 +160,5 @@ func isNotFoundError(err error) bool {
return false return false
} }
errStr := err.Error() errStr := err.Error()
return strings.Contains(errStr, "HTTP 404") || strings.Contains(errStr, "not found") return strings.Contains(errStr, "HTTP 404")
} }
+1 -1
View File
@@ -379,7 +379,7 @@ func TestIsNotFoundError(t *testing.T) {
}{ }{
{"nil error", nil, false}, {"nil error", nil, false},
{"HTTP 404", errors.New("HTTP 404: not found"), true}, {"HTTP 404", errors.New("HTTP 404: not found"), true},
{"not found text", errors.New("path not found"), true}, {"not found text", errors.New("path not found"), false},
{"server error", errors.New("server error"), false}, {"server error", errors.New("server error"), false},
{"HTTP 500", errors.New("HTTP 500: internal error"), false}, {"HTTP 500", errors.New("HTTP 500: internal error"), false},
} }