Files
kandev/apps/backend/internal/agent/agents/helpers.go
T
2026-07-31 16:32:19 +01:00

169 lines
5.2 KiB
Go

package agents
import (
"strings"
)
// emptyPermSettings is a shared zero-value permission settings map used by ACP
// agents that have no CLI-driven permission flags. Permission stance is
// expressed via ACP session modes and per-tool-call permission prompts.
var emptyPermSettings = map[string]PermissionSetting{}
// agentctlAutoApproveSetting is the universal profile toggle for Kandev-side
// ACP permission auto-approval (not a subprocess CLI flag).
var agentctlAutoApproveSetting = PermissionSetting{
Supported: true,
Default: false,
Label: "Auto-approve all permissions",
Description: "Kandev allows every agent permission request without prompting you. Dangerous — use only in trusted workspaces.",
ApplyMethod: PermissionApplyMethodAgentctlAutoApprove,
}
// CatalogPermissionSettings returns the agent's curated permission catalogue
// plus the shared agentctl auto-approve entry advertised on every agent profile.
func CatalogPermissionSettings(ag Agent) map[string]PermissionSetting {
return mergeAgentctlAutoApprove(ag.PermissionSettings())
}
// StripEnvFor returns the runtime-declared environment keys that should be
// removed before spawning an agent subprocess. Inference-only paths derive
// this from RuntimeConfig instead of carrying a second declaration.
func StripEnvFor(ia InferenceAgent) []string {
if a, ok := ia.(Agent); ok {
if rt := a.Runtime(); rt != nil {
return rt.StripEnv
}
}
return nil
}
// RuntimeEnvFor returns a defensive copy of the agent runtime environment for
// one-shot probe and inference subprocesses.
func RuntimeEnvFor(ia InferenceAgent) map[string]string {
if a, ok := ia.(Agent); ok {
if rt := a.Runtime(); rt != nil && len(rt.Env) > 0 {
env := make(map[string]string, len(rt.Env))
for key, value := range rt.Env {
env[key] = value
}
return env
}
}
return nil
}
func mergeAgentctlAutoApprove(settings map[string]PermissionSetting) map[string]PermissionSetting {
merged := make(map[string]PermissionSetting, len(settings)+1)
for k, v := range settings {
merged[k] = v
}
merged[PermissionKeyAutoApprove] = agentctlAutoApproveSetting
return merged
}
// CmdBuilder constructs CLI command slices using a fluent API.
type CmdBuilder struct {
args []string
}
// Cmd starts building a command from a base command and arguments.
func Cmd(base ...string) *CmdBuilder {
return &CmdBuilder{args: append([]string{}, base...)}
}
// Model appends a model flag if model is non-empty.
// flag args support {model} placeholder, e.g. NewParam("--model", "{model}").
func (b *CmdBuilder) Model(flag Param, model string) *CmdBuilder {
if flag.IsEmpty() || model == "" {
return b
}
for _, arg := range flag.args {
b.args = append(b.args, strings.ReplaceAll(arg, "{model}", model))
}
return b
}
// Resume appends a resume flag with sessionID if applicable.
// Skipped when sessionID is empty, nativeResume is true, or flag is empty.
func (b *CmdBuilder) Resume(flag Param, sessionID string, nativeResume bool) *CmdBuilder {
if sessionID == "" || nativeResume || flag.IsEmpty() {
return b
}
b.args = append(b.args, flag.args...)
b.args = append(b.args, sessionID)
return b
}
// ResumeAt appends a --resume-session-at flag with the message UUID.
// Skipped when uuid is empty or flag is empty.
func (b *CmdBuilder) ResumeAt(flag Param, uuid string) *CmdBuilder {
if uuid == "" || flag.IsEmpty() {
return b
}
b.args = append(b.args, flag.args...)
b.args = append(b.args, uuid)
return b
}
// Permissions appends per-tool permission flags when not auto-approving.
// Each tool gets: flag tool:ask-user (e.g. --permission launch-process:ask-user).
func (b *CmdBuilder) Permissions(flag string, tools []string, opts CommandOptions) *CmdBuilder {
if opts.AutoApprove || flag == "" || len(tools) == 0 {
return b
}
for _, tool := range tools {
b.args = append(b.args, flag, tool+":ask-user")
}
return b
}
// Settings appends CLI flags for enabled permission settings.
func (b *CmdBuilder) Settings(settings map[string]PermissionSetting, values map[string]bool) *CmdBuilder {
if settings == nil || values == nil {
return b
}
for settingName, setting := range settings {
if !setting.Supported || setting.ApplyMethod != PermissionApplyMethodCLIFlag || setting.CLIFlag == "" {
continue
}
value, exists := values[settingName]
if !exists || !value {
continue
}
if setting.CLIFlagValue != "" {
b.args = append(b.args, setting.CLIFlag, setting.CLIFlagValue)
} else {
b.args = append(b.args, strings.Fields(setting.CLIFlag)...)
}
}
return b
}
// Prompt appends a prompt flag if prompt is non-empty.
// flag args support {prompt} placeholder, e.g. NewParam("--prompt", "{prompt}").
// If flag is empty, the prompt is appended as a positional argument.
func (b *CmdBuilder) Prompt(flag Param, prompt string) *CmdBuilder {
if prompt == "" {
return b
}
if flag.IsEmpty() {
b.args = append(b.args, prompt)
return b
}
for _, arg := range flag.args {
b.args = append(b.args, strings.ReplaceAll(arg, "{prompt}", prompt))
}
return b
}
// Flag appends arbitrary flag parts to the command.
func (b *CmdBuilder) Flag(parts ...string) *CmdBuilder {
b.args = append(b.args, parts...)
return b
}
// Build returns the final Command value.
func (b *CmdBuilder) Build() Command {
return Command{args: b.args}
}