agent-orchestrator/backend/internal/adapters/agent/kiro/kiro.go

189 lines
6.8 KiB
Go

// Package kiro implements the Kiro (AWS) agent adapter: launching new headless
// sessions, resuming hook-tracked sessions, installing workspace-local hooks,
// and reading hook-derived session info.
//
// Kiro is AWS's agentic coding assistant. Its terminal CLI ships as the
// `kiro-cli` binary. AO launches Kiro with a workspace-local custom agent so
// both worker and orchestrator sessions can use Kiro's normal interactive
// approval flow. See https://kiro.dev/docs/cli/headless/ and
// https://kiro.dev/docs/cli/reference/cli-commands/.
//
// Launch delivers the initial prompt as a positional argument after `--` so a
// leading "-" is not parsed as a flag. Permission/approval modes map onto
// Kiro's tool-trust flags (`--trust-all-tools`, `--trust-tools=<categories>`).
// Restore uses `kiro-cli chat --resume-id <UUID>` with the native session id
// captured from a Kiro hook payload.
//
// AO-managed sessions derive native session identity and display metadata from
// Kiro's native hooks (see hooks.go) rather than transcript scans.
package kiro
import (
"context"
"strings"
"sync"
"github.com/aoagents/agent-orchestrator/backend/internal/adapters"
"github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/agentbase"
"github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/binaryutil"
"github.com/aoagents/agent-orchestrator/backend/internal/domain"
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
)
// Plugin is the Kiro agent adapter. It is safe for concurrent use; the binary
// path is resolved once and cached under binaryMu.
type Plugin struct {
agentbase.Base
binaryMu sync.Mutex
resolvedBinary string
}
// New returns a ready-to-register Kiro adapter.
func New() *Plugin {
return &Plugin{}
}
var _ adapters.Adapter = (*Plugin)(nil)
var _ ports.Agent = (*Plugin)(nil)
// Manifest returns the adapter's static self-description.
func (p *Plugin) Manifest() adapters.Manifest {
return adapters.Manifest{
ID: "kiro",
Name: "Kiro",
Description: "Run Kiro (AWS) worker sessions.",
Version: "0.0.1",
Capabilities: []adapters.Capability{
adapters.CapabilityAgent,
},
}
}
// GetLaunchCommand builds the argv to start a new Kiro session:
// `kiro-cli chat --agent ao [trust flags] [-- <prompt>]`.
//
// The prompt is passed as a positional argument after `--` so a leading "-" is
// not read as a flag. Kiro runs interactively for both workers and orchestrators;
// standing instructions come from the generated custom agent.
func (p *Plugin) GetLaunchCommand(ctx context.Context, cfg ports.LaunchConfig) (cmd []string, err error) {
binary, err := p.kiroBinary(ctx)
if err != nil {
return nil, err
}
cmd = []string{binary, "chat", "--agent", kiroAgentName}
appendApprovalFlags(&cmd, cfg.Permissions)
prompt := cfg.Prompt
if prompt != "" {
cmd = append(cmd, "--", prompt)
}
return cmd, nil
}
// GetPromptDeliveryStrategy reports how Kiro receives the initial task prompt.
// Orchestrator standing instructions are delivered through the generated
// custom-agent prompt, so no command or post-start prompt injection is needed
// there.
func (p *Plugin) GetPromptDeliveryStrategy(ctx context.Context, cfg ports.LaunchConfig) (ports.PromptDeliveryStrategy, error) {
if err := ctx.Err(); err != nil {
return "", err
}
if cfg.Prompt != "" {
return ports.PromptDeliveryInCommand, nil
}
if cfg.Kind == domain.KindOrchestrator {
return ports.PromptDeliveryCustomAgent, nil
}
return ports.PromptDeliveryInCommand, nil
}
// GetRestoreCommand rebuilds the argv that continues an existing Kiro session.
// ok is false when the hook-derived native session id has not landed yet, so
// callers can fall back to fresh launch behavior.
func (p *Plugin) GetRestoreCommand(ctx context.Context, cfg ports.RestoreConfig) (cmd []string, ok bool, err error) {
if err := ctx.Err(); err != nil {
return nil, false, err
}
agentSessionID := strings.TrimSpace(cfg.Session.Metadata[ports.MetadataKeyAgentSessionID])
if agentSessionID == "" {
return nil, false, nil
}
binary, err := p.kiroBinary(ctx)
if err != nil {
return nil, false, err
}
cmd = []string{binary, "chat", "--agent", kiroAgentName, "--resume-id", agentSessionID}
appendApprovalFlags(&cmd, cfg.Permissions)
return cmd, true, nil
}
// SessionInfo surfaces Kiro hook-derived metadata. Metadata is intentionally
// nil for Kiro: callers get the normalized fields directly.
func (p *Plugin) SessionInfo(ctx context.Context, session ports.SessionRef) (ports.SessionInfo, bool, error) {
if err := ctx.Err(); err != nil {
return ports.SessionInfo{}, false, err
}
info, ok := agentbase.StandardSessionInfo(session)
return info, ok, nil
}
var kiroBinarySpec = binaryutil.BinarySpec{
Label: "kiro",
Names: []string{"kiro-cli"},
WinNames: []string{"kiro-cli.cmd", "kiro-cli.exe", "kiro-cli"},
UnixPaths: []string{"/usr/local/bin/kiro-cli", "/opt/homebrew/bin/kiro-cli"},
UnixHomePaths: [][]string{{".kiro", "bin", "kiro-cli"}, {".local", "bin", "kiro-cli"}},
// The native Kiro installer location is probed before the npm shim, matching
// the pre-refactor order so a native install still wins when both are present.
WinPaths: []binaryutil.WinPath{
{Base: binaryutil.WinLocalAppData, Parts: []string{"Programs", "kiro", "kiro-cli.exe"}},
{Base: binaryutil.WinAppData, Parts: []string{"npm", "kiro-cli.cmd"}},
{Base: binaryutil.WinAppData, Parts: []string{"npm", "kiro-cli.exe"}},
{Base: binaryutil.WinHome, Parts: []string{".kiro", "bin", "kiro-cli.exe"}},
},
}
// ResolveKiroBinary returns the path to the kiro-cli binary on this machine,
// searching PATH then a handful of well-known install locations. It returns a
// wrapped ports.ErrAgentBinaryNotFound when kiro-cli is absent.
func ResolveKiroBinary(ctx context.Context) (string, error) {
return binaryutil.ResolveBinary(ctx, kiroBinarySpec)
}
func (p *Plugin) kiroBinary(ctx context.Context) (string, error) {
p.binaryMu.Lock()
defer p.binaryMu.Unlock()
if p.resolvedBinary != "" {
return p.resolvedBinary, nil
}
binary, err := ResolveKiroBinary(ctx)
if err != nil {
return "", err
}
p.resolvedBinary = binary
return binary, nil
}
// appendApprovalFlags maps AO's permission modes onto Kiro's tool-trust flags.
// Default emits no flag so Kiro uses its normal interactive approval flow.
// accept-edits grants the write-capable built-in tools; auto/bypass grant all
// tools.
func appendApprovalFlags(cmd *[]string, permissions ports.PermissionMode) {
switch ports.NormalizePermissionMode(permissions) {
case ports.PermissionModeDefault:
// No flag: defer to Kiro's normal interactive approval flow.
case ports.PermissionModeAcceptEdits:
*cmd = append(*cmd, "--trust-tools=fs_read,fs_write")
case ports.PermissionModeAuto:
*cmd = append(*cmd, "--trust-all-tools")
case ports.PermissionModeBypassPermissions:
*cmd = append(*cmd, "--trust-all-tools")
}
}