Documentation
¶
Overview ¶
Package claude implements agent.Adapter against the Claude Code CLI. Per Phase 5 design decision 2, the concrete adapter lives in a sub- package of agent/ (mirroring container/docker + container/native from Phase 4); the agent package itself stays import-free of harness specifics.
Phase 5 design decision 16 — Capabilities().NativeSchema is true: claude's --json-schema flag handles spec §4.2 layers 1+3 (constrained output + internal retry) inside the CLI. The adapter passes the step's OutputSchema straight to --json-schema and reads structured_output from the result event. Layer 2 (free-form parsing) is NEVER reached for this adapter; future non-native-schema adapters route through conformance Bucket 15 with their own structuring-call implementation (Phase 5 design Appendix H).
Phase 5 design decision 7 — gate independence is enforced structurally:
- Every Launch call is a fresh `claude -p` invocation.
- The command line NEVER contains --continue / --resume / --session-id.
- --no-session-persistence is ALWAYS passed so the host's ~/.claude/projects/ session journal never records AWF runs.
- ValidateConfig rejects with-keys named session_id/continue/resume with *ErrSessionReuseAttempted.
Streaming: Launch parses claude's --output-format stream-json line-by- line via io.Pipe + bufio.Scanner from the streaming Backend.Exec chunks channel (slice 5.3 Group A refactor). Each line becomes one StreamMessage; an assistant message may split into multiple AgentEvents (one per content block). Launch returns IMMEDIATELY under the γ contract with events + outcome channels open; the parser goroutine writes events progressively, then sends AgentOutcome on outcomeCh after claude exits.
(Slice 5.3 r2: doc-comment was in doc.go in r1; collapsed here per rule 2.)
Index ¶
- Constants
- Variables
- func ApplyPerRunConfigEnv(env map[string]string, inv agent.AgentInvocation)
- func ExtractResult(msg StreamMessage, model string) (agent.AgentResult, error)
- func MessageToEvents(msg StreamMessage) []agent.AgentEvent
- type Adapter
- func (*Adapter) Capabilities() agent.Caps
- func (a *Adapter) Launch(ctx context.Context, handle container.Handle, inv agent.AgentInvocation) (<-chan agent.AgentEvent, <-chan agent.AgentOutcome, error)
- func (*Adapter) Ref() string
- func (*Adapter) RequiredEnv() []string
- func (a *Adapter) ValidateConfig(with ir.RawConfig) error
- func (a *Adapter) Version(ctx context.Context, handle container.Handle) (string, error)
- type ErrAgentRuntimeNotFound
- type ErrBareRequiresAPIKey
- type ErrSessionReuseAttempted
- type ErrStreamParse
- type ErrUnexpectedExit
- type Option
- type StreamMessage
- type UsageRec
Constants ¶
const AdapterRef = "anthropic/claude-code"
AdapterRef is the agent-runtime identifier this package's Adapter returns from Ref(). Constant so cli/agent_registry.go and unit tests can refer to it without typing the string literal.
Variables ¶
var DefaultEnvAllowlist = []string{
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
"CLAUDE_CODE_OAUTH_TOKEN",
}
DefaultEnvAllowlist is the canonical set of env-var names claude reads per its auth-precedence docs. Single source of truth for `awf run`'s --agent-env default, `awf resume`'s implicit allowlist, and the integ tests' skipIfNoAuthEnv helper. If Anthropic adds a new auth env var, this is the only place that changes.
var ErrAuthFailureSentinel = errors.New("agent/claude: result event has is_error:true")
ErrAuthFailureSentinel is the sentinel returned by ExtractResult when the result event has subtype:"success" but is_error:true (auth failure path — verified against real claude 2.1.153). Launch wraps this as *agent.ErrAgentLaunch so the engine maps to retryable_failure (or permanent_failure via the adapter contract if the message text is "Not logged in").
var ErrNoResultEvent = errors.New("agent/claude: StreamMessage is not a result event")
ErrNoResultEvent is the sentinel returned by ExtractResult when the StreamMessage isn't a result event. Internal — Launch checks for it.
Functions ¶
func ApplyPerRunConfigEnv ¶ added in v0.1.2
func ApplyPerRunConfigEnv(env map[string]string, inv agent.AgentInvocation)
ApplyPerRunConfigEnv sets the claude per-run config-isolation environment on env, which MUST be a fresh, caller-owned map (never the adapter's shared a.env). It always disables non-essential traffic (headless hygiene). When the engine has computed a per-run config dir (inv.SessionConfigDir), it points claude at it AND relocates claude's XDG state+cache there.
The XDG redirect is load-bearing for NATIVE concurrency. claude-code keeps a per-version single-instance lock at $XDG_STATE_HOME/claude/locks/<version>.lock — OUTSIDE CLAUDE_CONFIG_DIR — plus a cache under $XDG_CACHE_HOME/claude. On the native backend every concurrent run inherits the shared host $HOME (hence the shared $XDG_STATE_HOME), so with only a per-run CLAUDE_CONFIG_DIR two concurrent runs of the same claude version still contend on the SAME lock and the loser fails ("1 concurrent OK, 2+ fail"). Pointing XDG_STATE_HOME/XDG_CACHE_HOME at per-run subdirs of the (sandbox-writable) config dir isolates that lock and cache per run. The subdirs sit alongside — not under — CLAUDE_CONFIG_DIR/projects, so the session-transcript subtree capture is unaffected.
HOME and XDG_DATA_HOME are deliberately NOT set: claude's versioned binary lives under $XDG_DATA_HOME (~/.local/share/claude/versions/<v>), which must stay shared and resolvable — relocating it would break binary resolution.
KEEP IN SYNC: agent/claude.Launch and agent/claudesession.Launch both call this.
func ExtractResult ¶ added in v0.1.1
func ExtractResult(msg StreamMessage, model string) (agent.AgentResult, error)
ExtractResult builds an AgentResult from a result-typed StreamMessage. Returns ErrNoResultEvent if the message type is not "result"; returns a non-nil error carrying "structured_output" verbiage for the error_max_structured_output_retries subtype (Launch maps to *agent.ErrUnparseableOutput); returns nil error + populated AgentResult for the success subtype (and is_error: false).
Auth failures: real claude returns {"subtype":"success", "is_error":true, "result":"Not logged in"} WITHOUT a structured_output field. Pre-fix ExtractResult would have returned AgentResult{Output: nil} with nil error, silently masking the auth failure as a schema-violation retry. We check is_error FIRST inside the success case and return a wrapped error carrying the result text so Launch can produce *agent.ErrAgentLaunch.
model is the value captured from the system/init event's "model" field. It is stored in Metrics.Model for auditability. The result event itself does not carry the model — the caller must thread it from the init event.
func MessageToEvents ¶ added in v0.1.1
func MessageToEvents(msg StreamMessage) []agent.AgentEvent
MessageToEvents splits one StreamMessage into one or more AgentEvents. Per Phase 5 design decision 14: assistant messages split per content block (one event per text / thinking / tool_use / tool_result). System / rate_limit / user / result emit a single event each.
Types ¶
type Adapter ¶
type Adapter struct {
// contains filtered or unexported fields
}
Adapter is the agent.Adapter implementation for Claude Code. Constructed by cli/agent_registry.go at CLI start-time and registered in *agent.Registry.
Lifetime: one Adapter per CLI invocation. Multiple Launch calls per Adapter (one per AgentStep dispatch; one per gate attempt; one per retry). All state held in Adapter is read-only after construction — Phase 5 design decision 7 (no session state crosses Launch boundaries).
func New ¶
New constructs an Adapter. Returns error only if an Option fails (none do today; the error return is for forward-compatibility with options that might need to validate, e.g. WithBackend in Task 15).
func (*Adapter) Capabilities ¶
Capabilities returns Caps{NativeSchema: true, IsolatedConfigDir: true, SurfacesLiveness: None} — Claude Code's --json-schema flag handles spec §4.2 layers 1+3 natively, and even though this adapter passes --no-session-persistence (no session reuse) it still wants a per-run CLAUDE_CONFIG_DIR so concurrent native runs don't collide on shared ~/.claude config/registry/statsig. SurfacesLiveness None: this adapter emits one AgentEvent per COMPLETE stream-json message (it does not pass --include-partial-messages, so there are no token deltas) and goes silent during tool execution, so a quiet stretch is not proof the turn hung — there is no liveness signal an idle watchdog can trust.
func (*Adapter) Launch ¶
func (a *Adapter) Launch(ctx context.Context, handle container.Handle, inv agent.AgentInvocation) (<-chan agent.AgentEvent, <-chan agent.AgentOutcome, error)
Launch runs `claude -p ...` inside handle via the streaming Backend.Exec. Returns IMMEDIATELY under the γ contract: events channel and outcome channel are both OPEN; the parser goroutine writes each AgentEvent as it's parsed from claude's stream-json stdout, then sends AgentOutcome on outcomeCh when the result event arrives + chunks close. The caller drains events concurrently — that's how `[<kind>] …` lines render progressively in stderr (realtime UX requirement).
Phase 5 design decision 7: NO --continue / --resume / --session-id; ALWAYS --no-session-persistence.
func (*Adapter) RequiredEnv ¶
RequiredEnv implements agent.CredentialNamer. Returns the CREDENTIAL env var names claude-code authenticates with. All three are defined in DefaultEnvAllowlist. Per errors.go / ErrBareRequiresAPIKey, bare mode needs ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN; CLAUDE_CODE_OAUTH_TOKEN is the OAuth path. At least one being set is sufficient for auth to proceed.
func (*Adapter) ValidateConfig ¶
ValidateConfig enforces the Claude Code adapter's with-schema. Returns *agent.ErrInvalidConfig for type / unknown-key violations, or one of the adapter-specific typed errors (*ErrSessionReuseAttempted, *ErrBareRequiresAPIKey) for the policy violations Phase 5 calls out.
The errors.As-friendly typed errors let cli/run.go map each class to a clear operator-facing message.
func (*Adapter) Version ¶
Version runs `claude --version` inside the supplied handle and parses the leading semver token. Per Phase 5 design decision 5, version IS per-container — the binary's PATH is per-container. Called once per (uses, container) pair at run start; recorded in run.started.Runtimes; re-resolved on resume; drift hard-errors (cli/resume.go).
type ErrAgentRuntimeNotFound ¶
ErrAgentRuntimeNotFound is returned by Version when `claude --version` failed (binary missing on PATH inside the container's exec environment, or exited non-zero). The Ref + Container fields locate the failure in run-start diagnostics.
func (*ErrAgentRuntimeNotFound) Error ¶
func (e *ErrAgentRuntimeNotFound) Error() string
func (*ErrAgentRuntimeNotFound) Unwrap ¶
func (e *ErrAgentRuntimeNotFound) Unwrap() error
type ErrBareRequiresAPIKey ¶
type ErrBareRequiresAPIKey struct {
AvailableKeys []string // the keys currently registered (helpful for debugging)
}
ErrBareRequiresAPIKey is returned by ValidateConfig when `with.bare` is true (the AWF reproducibility default per Phase 5 design decision 9) but neither ANTHROPIC_API_KEY nor ANTHROPIC_AUTH_TOKEN was registered in the adapter's env allowlist via WithEnv. Decision 15: bare mode disables Keychain + credentials-file + CLAUDE_CODE_OAUTH_TOKEN, so the only auth paths that work are the two API-key env vars.
Rejecting at ValidateConfig (rather than at Launch's "Not logged in" failure) gives the workflow author an early, actionable error.
func (*ErrBareRequiresAPIKey) Error ¶
func (e *ErrBareRequiresAPIKey) Error() string
type ErrSessionReuseAttempted ¶
type ErrSessionReuseAttempted struct {
Key string // the offending with-key (one of session_id, continue, resume)
}
ErrSessionReuseAttempted is returned by ValidateConfig when a with-key names a flag that would re-use a prior claude session (session_id / continue / resume). Phase 5 design decision 7 makes gate independence engine-enforced rather than convention-enforced; this is the validation surface.
func (*ErrSessionReuseAttempted) Error ¶
func (e *ErrSessionReuseAttempted) Error() string
type ErrStreamParse ¶
ErrStreamParse is returned by Launch when a stream-json line failed to decode. Wrapped into *agent.ErrAgentLaunch by the caller (transport class — retryable). Carries the raw line bytes (truncated) for the operator's diagnostic.
func (*ErrStreamParse) Error ¶
func (e *ErrStreamParse) Error() string
func (*ErrStreamParse) Unwrap ¶
func (e *ErrStreamParse) Unwrap() error
type ErrUnexpectedExit ¶
ErrUnexpectedExit is returned by Launch when claude exited (chunks channel closed, result delivered) but no `result` event was observed in the stream — typically a non-zero exit before claude could emit its final structured result. Wrapped into *agent.ErrAgentLaunch by the caller.
func (*ErrUnexpectedExit) Error ¶
func (e *ErrUnexpectedExit) Error() string
type Option ¶
type Option func(*Adapter)
Option configures the Adapter at construction time. Functional-options pattern matches the rest of the codebase (clock.IDGen options, signal.NewBroker, etc.).
func WithBackend ¶
WithBackend supplies the container.Backend the adapter uses to run claude inside the handle (Version, Launch). Required for Version and Launch to function; tests that only need Ref/Capabilities/ValidateConfig may omit it. Production wiring at cli/agent_registry.go passes the CLI's constructed Backend.
(Slightly redundant with Backend.Exec receiving handle on every call, but the adapter needs the Backend reference to call Exec at all; the Backend's identity is configured once at construction.)
func WithEnv ¶
WithEnv supplies the env-var allowlist the Adapter will forward into each `claude -p` invocation's exec environment. Keys are env var names (e.g. ANTHROPIC_API_KEY); values are the secrets read from the host environment. The map is copied into agent.SecretEnv (which redacts in fmt verbs and is json:"-" so secrets cannot reach the state log).
Production wiring: cli/agent_registry.go reads each named env var from os.Environ and passes the present ones via WithEnv. Tests inject directly. Empty map is permitted (auth will fail at Launch time with claude's "Not logged in" if no working credentials).
type StreamMessage ¶ added in v0.1.1
type StreamMessage struct {
Type string `json:"type"` // "system" | "assistant" | "user" | "result" | "rate_limit_event"
Subtype string `json:"subtype,omitempty"` // system: "init" | "hook_started" | "hook_response"; result: "success" | "error_max_structured_output_retries"
// system/init fields
SessionID string `json:"session_id,omitempty"`
CWD string `json:"cwd,omitempty"`
Model string `json:"model,omitempty"`
Tools []string `json:"tools,omitempty"`
MCPServers json.RawMessage `json:"mcp_servers,omitempty"`
Version string `json:"claude_code_version,omitempty"`
APIKeySource string `json:"apiKeySource,omitempty"`
PermissionMode string `json:"permissionMode,omitempty"`
OutputStyle string `json:"output_style,omitempty"`
Agents json.RawMessage `json:"agents,omitempty"`
Skills json.RawMessage `json:"skills,omitempty"`
Plugins json.RawMessage `json:"plugins,omitempty"`
UUID string `json:"uuid,omitempty"`
// assistant/user fields
Message json.RawMessage `json:"message,omitempty"`
Error string `json:"error,omitempty"`
// rate_limit_event
RateLimitInfo *rateLimitInfo `json:"rate_limit_info,omitempty"`
// result fields
IsError bool `json:"is_error,omitempty"`
APIErrorStatus json.RawMessage `json:"api_error_status,omitempty"`
DurationMS int64 `json:"duration_ms,omitempty"`
DurationAPIMS int64 `json:"duration_api_ms,omitempty"`
TTFTMS int64 `json:"ttft_ms,omitempty"`
NumTurns int `json:"num_turns,omitempty"`
Result string `json:"result,omitempty"`
StopReason string `json:"stop_reason,omitempty"`
TotalCostUSD float64 `json:"total_cost_usd,omitempty"`
Usage *UsageRec `json:"usage,omitempty"`
StructuredOutput json.RawMessage `json:"structured_output,omitempty"`
TerminalReason string `json:"terminal_reason,omitempty"`
FastModeState string `json:"fast_mode_state,omitempty"`
}
StreamMessage mirrors the discriminated-union Phase 5 design Appendix A pins. Empirically verified against claude 2.1.153 stream-json output.
func ParseStreamLine ¶ added in v0.1.1
func ParseStreamLine(b []byte) (StreamMessage, error)
ParseStreamLine decodes one stream-json line. Wraps json.Unmarshal errors as *ErrStreamParse for the operator-facing path.
type UsageRec ¶ added in v0.1.1
type UsageRec struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
CacheReadInputTokens int `json:"cache_read_input_tokens"`
CacheCreation json.RawMessage `json:"cache_creation,omitempty"`
ServiceTier string `json:"service_tier,omitempty"`
}