// Package agents defines the Agent interface and supporting types. // Each agent (Auggie, Claude Code, Codex, etc.) implements this interface // in its own file, consolidating identity, discovery, models, protocol, // execution, and runtime configuration in one place. package agents import ( "context" "errors" "time" "github.com/kandev/kandev/internal/agent/mcpconfig" "github.com/kandev/kandev/internal/agent/usage" "github.com/kandev/kandev/internal/agentruntime" "github.com/kandev/kandev/pkg/agent" ) // ErrNotSupported is returned when an agent does not support an operation. var ErrNotSupported = errors.New("not supported by this agent") // Agent is the core interface for all coding agents. // // Models are no longer part of this interface — they come from the host utility // capability cache (which probes each ACP agent at boot to learn its models and // modes via session/new). See internal/agent/hostutility. type Agent interface { // --- Identity --- ID() string Name() string DisplayName() string Description() string Enabled() bool DisplayOrder() int // lower = higher priority in listings // --- Assets --- Logo(variant LogoVariant) []byte // nil if unavailable // --- Discovery --- IsInstalled(ctx context.Context) (*DiscoveryResult, error) // --- Execution --- BuildCommand(opts CommandOptions) Command // --- Permissions --- PermissionSettings() map[string]PermissionSetting // --- Runtime --- Runtime() *RuntimeConfig // --- Billing --- // BillingType returns how this agent is billed: api_key or subscription. // Computed at call time from credential files, not stored in the DB. BillingType() usage.BillingType // --- Remote Auth --- // RemoteAuth returns the auth methods this agent supports in remote environments. // Returns nil if the agent has no remote auth configuration. RemoteAuth() *RemoteAuth // --- Installation --- // InstallScript returns shell commands to pre-install the agent CLI in remote environments. // Returns empty string if no installation is needed. InstallScript() string } // VirtualAgent marks an agent family that is visible to settings and profile // configuration but cannot launch an inference process itself. type VirtualAgent interface { Agent IsVirtual() bool } // IsVirtualAgent reports whether an agent is a non-launchable virtual family. // Keeping this as an optional capability lets existing concrete agents remain // unchanged while callers can fail closed at launch and discovery boundaries. func IsVirtualAgent(agent Agent) bool { virtual, ok := agent.(VirtualAgent) return ok && virtual.IsVirtual() } // InferenceAgent is an optional capability marker for agents that support // one-shot LLM inference via the host utility manager. The actual model list // is populated dynamically from the ACP probe — agents no longer declare a // static model list. type InferenceAgent interface { // InferenceConfig returns the configuration for one-shot inference. InferenceConfig() *InferenceConfig } // ManagedNPMRuntimeAgent is an optional capability for built-in agents whose // ACP runtime is resolved through npm. Package names and ACP arguments are // defined by the agent implementation rather than caller-provided input. type ManagedNPMRuntimeAgent interface { ManagedNPMRuntime() ManagedNPMRuntimeSpec } // PassthroughAgent is an optional capability for agents that support CLI passthrough mode. type PassthroughAgent interface { PassthroughConfig() PassthroughConfig BuildPassthroughCommand(opts PassthroughOptions) Command } // NativeBinaryAgent is an optional capability for agents whose npm package also // ships a standalone CLI binary that behaves identically to `npx -y `. // When that binary is present in the execution environment, the lifecycle // prefers it (BuildCommand receives CommandOptions.PreferNativeBinary=true) to // skip the per-launch npm registry round-trip — which is slow behind a private // registry. Containerized runtimes ship a controlled image and keep npx. type NativeBinaryAgent interface { // NativeBinaryName is the executable to look for on PATH (e.g. "copilot"). // An empty string disables native-binary preference. NativeBinaryName() string } // LoginCommand describes an interactive CLI command for authenticating with // an agent. The kandev backend runs this under a PTY so the UI can render a // terminal and route keystrokes to the underlying process. type LoginCommand struct { // Cmd is the command + args to spawn, e.g. []string{"claude", "auth", "login"}. Cmd []string // Description renders above the terminal as a one-line hint, e.g. // "Authenticate with your Anthropic account." Description string } // LoginAgent is an optional capability for agents that need an interactive // login flow (browser OAuth callback, token prompt, etc.). Implement it to // surface a "Login" button in the UI that opens a PTY-backed terminal running // LoginCommand(). type LoginAgent interface { LoginCommand() *LoginCommand } // IsPassthroughOnly returns true if the agent only supports passthrough mode // (a raw CLI under a PTY, no ACP protocol mode). It governs execution mode — // notably seeding the agent's default profile as CLI passthrough — and is true // for every TUI agent regardless of MCP configuration. // // For "should interactive MCP tools be registered?", use // SupportsInteractiveMCPTools instead. The two questions used to share this one // predicate, which is why a custom TUI agent could never get ask_user_question. func IsPassthroughOnly(a Agent) bool { _, ok := a.(*TUIAgent) return ok } // SupportsInteractiveMCPTools reports whether interactive MCP tools (notably // ask_user_question) should be registered for the agent's session. // // A TUI agent with no MCP injection strategy is a plain terminal tool that // never speaks MCP: registering a tool it cannot call just means a question // that hangs forever. A TUI agent *with* a strategy wraps a real MCP-capable // CLI that kandev points at the per-session server, so it should get the same // tools as the built-in claude-acp agent running in passthrough mode. Every // non-TUI agent supports them. func SupportsInteractiveMCPTools(a Agent) bool { tui, ok := a.(*TUIAgent) if !ok { return true } return tui.MCPStrategy() != nil } // LogoVariant selects light or dark logo. type LogoVariant int const ( LogoLight LogoVariant = iota LogoDark ) // DiscoveryResult is the result of checking if an agent is installed. type DiscoveryResult struct { Available bool MatchedPath string SupportsMCP bool MCPConfigPaths []string InstallationPaths []string Capabilities DiscoveryCapabilities } // DiscoveryCapabilities describes what the agent supports. type DiscoveryCapabilities struct { SupportsSessionResume bool SupportsShell bool SupportsWorkspaceOnly bool } // CommandOptions are passed to BuildCommand. type CommandOptions struct { Model string SessionID string // for --resume flag ResumeAtMessageUUID string // for --resume-session-at flag (truncate conversation) AutoApprove bool PermissionPolicy string // "autonomous", "supervised", "plan" PermissionValues map[string]bool // e.g. {"allow_indexing": true} AgentType string // for --agent flag (e.g. "task" for subagent) // CLIFlagTokens are user-configured CLI flag argv tokens derived from // AgentProfile.CLIFlags (only Enabled entries, shell-tokenised). Appended // verbatim by lifecycle.CommandBuilder after the agent builds its command. CLIFlagTokens []string // CommandPrefixTokens are user-configured launcher tokens derived from // AgentProfile.CommandPrefix (shell-tokenised). Prepended to the built // command so the agent subprocess runs wrapped in a sandbox launcher // (e.g. ["greywall", "--"] → "greywall -- npx -y …"). CommandPrefixTokens []string // Runtime is the execution backend hosting the agent subprocess. // Agents whose binary lives in a different place inside a container // than on the host (currently only MockAgent) consult // Runtime.IsContainerized() to pick a host absolute path vs. a bare // name resolved via the container's PATH. Runtime agentruntime.Runtime // PreferNativeBinary is set by the lifecycle when a NativeBinaryAgent's // standalone CLI was found in the execution environment. Such agents emit // the native binary (e.g. "copilot --acp") instead of "npx -y ". PreferNativeBinary bool // ManagedRuntimeVersion is an internal exact version override for trusted // managed npm ACP runtimes. Empty uses the built-in exact version pin. ManagedRuntimeVersion string } // PassthroughOptions are passed to BuildPassthroughCommand. type PassthroughOptions struct { Model string SessionID string // ACP session ID; resumes a specific session via --resume Prompt string // initial prompt for new sessions Resume bool // generic "continue last session" (e.g. -c, --resume latest) PermissionValues map[string]bool // e.g. {"auto_approve": true} WorkDir string // MCPArgs are extra argv tokens produced by the agent's MCP passthrough // strategy (e.g. claude's "--mcp-config ", codex's repeated "-c // mcp_servers.…" overrides). Appended to the built command. MCPArgs []string // CLIFlagTokens are user-configured CLI flag argv tokens derived from // AgentProfile.CLIFlags (only Enabled entries, shell-tokenised). Unlike ACP // commands, which lifecycle.CommandBuilder appends centrally, passthrough // agents opt in by appending these tokens in BuildPassthroughCommand. CLIFlagTokens []string } // RuntimeConfig holds Docker / standalone runtime settings. type RuntimeConfig struct { Image string Tag string Cmd Command Entrypoint Command WorkingDir string Env map[string]string RequiredEnv []string Mounts []MountTemplate ResourceLimits ResourceLimits SessionConfig SessionConfig Protocol agent.Protocol ModelFlag Param // e.g. NewParam("--model", "{model}") WorkspaceFlag string // e.g. "--workspace-root" AssumeMcpSse bool // Override: assume agent supports SSE MCP servers even if not advertised AssumeMcpHttp bool // Override: assume agent supports HTTP MCP servers even if not advertised ProjectSkillDir string // CWD-relative path for project-level skills (e.g. ".claude/skills") UserSkillDir string // home-relative path for user-level skills (e.g. ".claude/skills") // ProjectMCPStrategy materializes resolved MCP servers into a project-local // config file before a protocol-mode agent subprocess starts. Use this for // agents whose ACP adapter does not wire session/new mcpServers through to // the underlying CLI. ProjectMCPStrategy mcpconfig.PassthroughMCPStrategy // RequiresProcessKill is true for agents whose subprocess should skip the // graceful stdin-close wait because it is known not to exit on EOF (e.g. // OpenCode's ACP runtime, which keeps its HTTP server and MCP child tree // alive). Agentctl still reaps the process group for every agent after // graceful shutdown or timeout; this flag only makes that reap immediate. RequiresProcessKill bool // StripEnv lists environment variables to remove from the agent's child // process environment entirely (not just set to empty). Some programs // distinguish unset from empty string and change behavior based on // presence rather than value. The process manager strips these from the // final child env after adapter merge; the inference executor strips // them from the one-shot probe/inference subprocess env. StripEnv []string } // MountTemplate defines a mount with template variables. type MountTemplate struct { Source string `json:"source"` Target string `json:"target"` ReadOnly bool `json:"read_only"` } // ResourceLimits defines resource constraints. type ResourceLimits struct { MemoryMB int64 `json:"memory_mb"` CPUCores float64 `json:"cpu_cores"` Timeout time.Duration `json:"timeout"` } // SessionConfig defines session resumption behaviour. type SessionConfig struct { NativeSessionResume bool HistoryContextInjection bool // Opt-in: inject conversation history on session resume for agents without native resume // NewSessionOnWorkspaceRebind forces session/new when an idle execution's // working directory changes. Some providers accept session/load with a new // cwd but keep tool execution pinned to the session's original directory. NewSessionOnWorkspaceRebind bool ResumeFlag Param CanRecover *bool SessionDirTemplate string SessionDirTarget string ForkSessionCmd Command ContinueSessionCmd Command } // SupportsRecovery returns whether the agent supports session recovery. // Returns true by default if CanRecover is not explicitly set. func (c SessionConfig) SupportsRecovery() bool { if c.CanRecover == nil { return true } return *c.CanRecover } // PermissionApplyMethodCLIFlag is the PermissionSetting.ApplyMethod sentinel // for permission flags that map to a CLI argument on the agent subprocess. // A typo in any one caller (e.g. "cli-flag") silently breaks the seed and // filter chain across agents, so the literal lives in exactly one place. const PermissionApplyMethodCLIFlag = "cli_flag" // PermissionApplyMethodAgentctlAutoApprove maps the profile auto_approve column // to AGENTCTL_AUTO_APPROVE_PERMISSIONS at launch (all ACP agents). const PermissionApplyMethodAgentctlAutoApprove = "agentctl_auto_approve" // PermissionKeyAutoApprove is the PermissionSettings map key wired to the // profile "Auto approve" toggle (see PermissionValues in buildAgentCommand). // Centralised for the same reason as PermissionApplyMethodCLIFlag: a typo in // any one agent silently disables its auto-approve flag. const PermissionKeyAutoApprove = "auto_approve" // PermissionKeyCursorForce is the Cursor-specific CLI --force toggle (unsandboxed // run-everything). Separate from PermissionKeyAutoApprove (agentctl ACP approval). const PermissionKeyCursorForce = "cursor_force" // PermissionKeyDangerouslySkipPermissions is wired to the profile's // DangerouslySkipPermissions column (a dedicated bool, not the cli_flags list). // Agents whose CLI accepts --dangerously-skip-permissions (or equivalent) // declare a PermissionSetting under this key with ApplyMethod=cli_flag so the // passthrough Settings() pass emits the flag from the profile bool. The // catalog seeder excludes this key to avoid duplicating the toggle into the // curated cli_flags list (which would also double-emit the flag at launch). const PermissionKeyDangerouslySkipPermissions = "dangerously_skip_permissions" // PermissionSetting defines metadata for a permission setting option. type PermissionSetting struct { Supported bool `json:"supported"` Default bool `json:"default"` Label string `json:"label"` Description string `json:"description"` ApplyMethod string `json:"apply_method,omitempty"` CLIFlag string `json:"cli_flag,omitempty"` CLIFlagValue string `json:"cli_flag_value,omitempty"` } // PassthroughConfig defines configuration for CLI passthrough mode. type PassthroughConfig struct { Supported bool Label string Description string PassthroughCmd Command ModelFlag Param PromptFlag Param PromptPattern string IdleTimeout time.Duration BufferMaxBytes int64 StatusDetector string CheckInterval time.Duration StabilityWindow time.Duration ResumeFlag Param // generic "continue last session" (e.g. NewParam("-c"), NewParam("--resume", "latest")) SessionResumeFlag Param // resume a specific session by ID (e.g. NewParam("--resume")) // MCPStrategy materializes resolved MCP servers into this CLI's passthrough // shape (config file + flag, repeated -c overrides, project file, or env var) // without touching the user's global config. Nil means no MCP injection. MCPStrategy mcpconfig.PassthroughMCPStrategy WaitForTerminal bool // AutoInjectPrompt is retained in discovered passthrough metadata for // compatibility. Initial prompt delivery is selected by PromptFlag: when it // is empty, the lifecycle manager writes the task description to PTY stdin. // The value no longer gates that delivery. AutoInjectPrompt bool // SubmitSequence is appended after the prompt text when auto-injecting // and when routing chat-compose messages to the PTY. "\r" for most TUIs. // Empty inherits DefaultPassthroughSubmitSequence at PTY write sites. SubmitSequence string // DisableBracketedPaste sends prompt bytes verbatim (plus SubmitSequence). // Claude Code enables bracketed-paste *mode* (?2004h) in its Ink TUI; injecting // ESC[200~…ESC[201~ delimiters breaks input (nothing appears in the prompt). DisableBracketedPaste bool // SubmitDelay is the wait inserted before each non-first chunk when writing the // prompt+submit sequence to PTY stdin. Ink-based TUIs (Claude Code) detect a // "paste burst" when many stdin bytes arrive in one read and absorb the // trailing \r into the pasted content instead of dispatching it as Enter. // Splitting the prompt body from the submit byte with a small delay forces the // submit to arrive as a discrete keystroke. 0 disables (other TUIs handle one // atomic write fine). SubmitDelay time.Duration } // DefaultBufferMaxBytes is the default maximum buffer size for passthrough mode (2 MB). const DefaultBufferMaxBytes int64 = 2 * 1024 * 1024 // DefaultResourceLimits is the standard resource limit set shared by most agents. var DefaultResourceLimits = ResourceLimits{ MemoryMB: 4096, CPUCores: 2.0, Timeout: time.Hour, } // DefaultProjectSkillDir is the fallback CWD-relative skill directory used when // an agent type does not declare a ProjectSkillDir in its RuntimeConfig. const DefaultProjectSkillDir = ".agents/skills" // ProjectSkillDirFromRuntime returns the CWD-relative skill directory for the // agent. Falls back to DefaultProjectSkillDir if the agent's RuntimeConfig does // not set one. func ProjectSkillDirFromRuntime(a Agent) string { if rt := a.Runtime(); rt != nil && rt.ProjectSkillDir != "" { return rt.ProjectSkillDir } return DefaultProjectSkillDir } // UserSkillDirFromRuntime returns the home-relative user skill directory for the // agent. An empty string means the agent does not expose a user-home skill dir. func UserSkillDirFromRuntime(a Agent) string { if rt := a.Runtime(); rt != nil { return rt.UserSkillDir } return "" } // InferenceConfig describes how an agent executes one-shot prompts via ACP. type InferenceConfig struct { // Supported indicates the agent can do one-shot inference. Supported bool // Command is the ACP command for one-shot inference. // e.g., ["npx", "-y", "@agentclientprotocol/claude-agent-acp"] Command Command // ModelFlag is the flag template for specifying the model (e.g., ["--model", "{model}"]). ModelFlag Param } // InferenceModel describes a model available for inference. type InferenceModel struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` IsDefault bool `json:"is_default"` } // RemoteAuth describes all auth methods an agent supports for remote environments. type RemoteAuth struct { Methods []RemoteAuthMethod `json:"methods"` } // RemoteAuthMethod describes one way an agent can authenticate in a remote environment. type RemoteAuthMethod struct { // Type is "env" (set env var via secret) or "files" (copy local files to remote). Type string `json:"type"` // EnvVar is the environment variable name (for type="env"). EnvVar string `json:"env_var,omitempty"` // SetupHint is a UI hint for the user (for type="env"). SetupHint string `json:"setup_hint,omitempty"` // SourceFiles maps OS name to relative paths from home dir (for type="files"). // Keys: "darwin", "linux", "windows". SourceFiles map[string][]string `json:"source_files,omitempty"` // TargetRelDir is the target directory relative to the remote user home (for type="files"). TargetRelDir string `json:"target_rel_dir,omitempty"` // Label is a UI label for the file copy option (for type="files"). Label string `json:"label,omitempty"` // SetupScript is an optional shell script that runs on the remote after the // env var is resolved. Used to bootstrap credential files from env vars. // Only meaningful for type="env". Can reference the env var by name. SetupScript string `json:"setup_script,omitempty"` } // PortableConfigAgent is an optional capability for agents that have a small, // explicitly allowlisted set of user configuration files that can be copied // into an isolated executor. It is intentionally separate from RemoteAuth. type PortableConfigAgent interface { PortableConfig() *PortableConfig } // PortableConfig declares safe configuration bundles for an agent. type PortableConfig struct { Bundles []PortableConfigBundle `json:"bundles"` } // PortableConfigBundle is one stable, user-selectable configuration unit. type PortableConfigBundle struct { ID string `json:"id"` Label string `json:"label"` Files []PortableConfigFile `json:"files"` } // PortableConfigFile maps an OS-specific host path relative to the host home // directory to a relative path below the executor user's home directory. type PortableConfigFile struct { SourcePaths map[string]string `json:"source_paths"` TargetPath string `json:"target_path"` } // Command is a domain value type representing a CLI command with arguments. // Serialize to []string only at system boundaries (process exec, Docker API, JSON DTOs). type Command struct { args []string } // NewCommand creates a Command from the given arguments. func NewCommand(args ...string) Command { return Command{args: append([]string{}, args...)} } // Args returns the raw string slice for serialization at system boundaries. func (c Command) Args() []string { return c.args } // IsEmpty reports whether the command has no arguments. func (c Command) IsEmpty() bool { return len(c.args) == 0 } // With returns a CmdBuilder seeded with this command's arguments, // allowing fluent extension without mutating the original. func (c Command) With() *CmdBuilder { return &CmdBuilder{args: append([]string{}, c.args...)} } // Param is a command fragment — one or more pre-split CLI arguments // (flags, flag+value pairs, templates with placeholders). // Composed into a Command via CmdBuilder methods. type Param struct { args []string } // NewParam creates a Param from the given arguments. func NewParam(args ...string) Param { return Param{args: append([]string{}, args...)} } // Args returns the raw string slice. func (p Param) Args() []string { return p.args } // IsEmpty reports whether the param has no arguments. func (p Param) IsEmpty() bool { return len(p.args) == 0 }