harness

package
v0.2.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package harness defines the public execution seam for runner adapters.

Index

Constants

View Source
const (
	// ClarificationOpen and ClarificationClose delimit the question, and only
	// the text between them reaches the human.
	ClarificationOpen  = "<<<RAILGRID_CLARIFICATION>>>"
	ClarificationClose = "<<<END_RAILGRID_CLARIFICATION>>>"
)

The clarification block: how a harness says it has stopped to ask a human something. An adapter teaches its model the convention and parses the block back out; a CONSUMER needs the same two strings to take them out of the text it shows a person.

They live here rather than in one adapter because they have two audiences. Keeping them private meant the question was extracted correctly into the runner's receipt while the raw markers still reached the transcript, which is what a person then read.

View Source
const (
	// SelectorClaude is how a person selects headless Claude Code.
	SelectorClaude = "claude"
	// SelectorCodex is how a person selects the Codex app-server.
	SelectorCodex = "codex"
)

There are deliberately TWO names for a harness, and conflating them has already produced two separate bugs, so the mapping lives here and nowhere else.

  • The SELECTOR name is what a human writes to choose one: `--harness claude` on the agent, `spec.harness.enabled: [claude]` on an edge. It names the thing you install, and it is short because people type it.
  • The ADVERTISED name is what the harness calls itself in the runner's capabilities response: `claude-code`. It is the product's own name, and it is what `StartRequest.RequiredHarness` is matched against, because that field exists to pin what actually answered.

They differ only for Claude Code, which is exactly why it goes wrong: code that assumes they are equal works for Codex and silently fails for Claude — once as "no Claude runner is ever ready", once as a dispatch refused for a harness mismatch that did not exist.

View Source
const MaxPermissionInputBytes = 4 << 10

MaxPermissionInputBytes bounds the rendering of a tool input that travels to a human. Tool inputs routinely carry whole files; an approval card is not where a file belongs, and an unbounded field is a way to push arbitrary bytes through every layer between here and a portal.

Variables

View Source
var Selectors = []string{SelectorClaude, SelectorCodex} //nolint:gochecknoglobals

Selectors are every harness a railgrid agent can supervise, in the order a port allocator should consider them.

View Source
var WellKnownBinDirs = []string{
	"~/.local/bin",
	"~/bin",
	"/usr/local/bin",
	"/opt/homebrew/bin",
	"/opt/local/bin",
	"/usr/bin",
	"/bin",
	"/snap/bin",
	"~/.npm-global/bin",
	"~/.bun/bin",
	"~/.volta/bin",
	"~/.asdf/shims",
	"~/.nvm/current/bin",
	"~/node_modules/.bin",
}

WellKnownBinDirs are the directories a coding agent is commonly installed into, searched in this order when PATH does not already resolve it.

A managed runner is started by the Edge agent under a service account whose PATH is whatever the init system gave it — usually /usr/bin:/bin, never the interactive shell's. An operator who installed Claude Code or Codex the normal way (a per-user install under ~/.local/bin, Homebrew on macOS, a global npm prefix) then sees only `exec: "claude": executable file not found in $PATH`, with nothing on the machine actually missing. Looking in these places costs one stat each and turns that into a working runner.

Entries beginning with "~/" are expanded against the RUNNER's home, which is the account the harness process itself runs as.

Functions

func AdvertisedName added in v0.2.8

func AdvertisedName(selector string) string

AdvertisedName returns the name the harness selected by selector reports in a capabilities response, which is what RequiredHarness must be set to.

An unknown selector comes back unchanged rather than empty: the caller's own "unknown harness" message is clearer than a blank field, and inventing a name would only move the failure to the runner.

func ResolveBinary added in v0.2.7

func ResolveBinary(name string) string

ResolveBinary returns the path the harness should execute for name.

It prefers what the environment already resolves, so an operator who set an explicit path or a working PATH keeps exactly what they configured. Only when that fails does it search WellKnownBinDirs. A name that is already a path is returned untouched, and a name found nowhere is returned untouched too: the caller's own "not found in $PATH" error is the clearest thing a reader can act on, and inventing a path would only move the failure.

func SelectorFor added in v0.2.8

func SelectorFor(advertised string) string

SelectorFor is the inverse: the selector a person would write for a harness that advertised this name. It is how a reader turns a runner's capabilities answer back into the field they have to edit.

func StripClarification added in v0.2.8

func StripClarification(text string) string

StripClarification removes a clarification block from text meant for a person, leaving whatever the model wrote around it.

It is deliberately forgiving in a way the PARSER is not. The parser decides whether the harness is really waiting on an answer, so it refuses anything malformed rather than parking work on a question nobody asked. This only decides what a person reads, where a leftover marker is the worse outcome — so an unterminated opener takes the rest of the text with it, which is the question the model was in the middle of writing.

Types

type Adapter

type Adapter interface {
	Probe(context.Context) (Info, error)
	Run(context.Context, Launch, Emit) (Result, error)
}

Adapter probes its executable and runs one isolated agent turn.

type Clarification

type Clarification struct {
	ID   string `json:"id"`
	Text string `json:"text"`
}

Clarification is a bounded product question that can be answered by a caller before resuming the existing harness session.

type Credential added in v0.2.8

type Credential struct {
	Kind  CredentialKind
	Value string
}

Credential is the caller's harness identity for ONE launch.

It is dispatch data, not enrollment: the coordinator sends it with every start and resume, the runner keeps it in memory for the life of the attempt and never writes it to durable state, and an adapter puts it in front of its child for exactly one turn. That is what makes a runner a shared machine rather than a shared billable identity — nothing on the host authenticates to a model provider on its own.

It carries no json tags because it must never be serialized: every wire shape that could hold one (the runner's StartRequest, its durable journal) strips it before anything is written down.

func (Credential) Empty added in v0.2.8

func (c Credential) Empty() bool

Empty reports whether c carries nothing to inject.

type CredentialKind added in v0.2.8

type CredentialKind string

CredentialKind names which harness identity a launch carries, and therefore how the adapter hands it to its child. The kinds are harness-specific on purpose: an adapter that is given a credential it cannot use must refuse it rather than guess.

const (
	// CredentialClaudeOAuth is a `claude setup-token` value, injected as
	// CLAUDE_CODE_OAUTH_TOKEN.
	CredentialClaudeOAuth CredentialKind = "claude-oauth"
	// CredentialClaudeAPIKey is an Anthropic API key, injected as
	// ANTHROPIC_API_KEY.
	CredentialClaudeAPIKey CredentialKind = "claude-apikey"
	// CredentialCodexAuth is a Codex login session file, materialized as the
	// home's auth.json for the duration of one launch.
	CredentialCodexAuth CredentialKind = "codex-auth"
)

type Emit

type Emit func(Event) error

Emit receives adapter events. Implementations must treat an error as a terminal failure and stop producing events for the launch.

type Event

type Event struct {
	Type          string          `json:"type"`
	SessionID     string          `json:"sessionID,omitempty"`
	TurnID        string          `json:"turnID,omitempty"`
	Message       string          `json:"message,omitempty"`
	Data          json.RawMessage `json:"data,omitempty"`
	Clarification *Clarification  `json:"clarification,omitempty"`
}

Event is a bounded adapter event forwarded to the runner state engine.

type Info

type Info struct {
	Name    string   `json:"name"`
	Version string   `json:"version"`
	Ready   bool     `json:"ready"`
	Reasons []string `json:"reasons,omitempty"`
}

Info describes an adapter's executable and readiness state.

type Launch

type Launch struct {
	AttemptID    string `json:"attemptID"`
	Workdir      string `json:"workdir"`
	SessionID    string `json:"sessionID,omitempty"`
	Instructions string `json:"instructions"`
	Model        string `json:"model,omitempty"`
	// Credential is the identity this turn runs as. It is never serialized.
	Credential Credential `json:"-"`
	// Permissions, when set, is how this launch reaches a human for a tool call
	// the permission mode does not pre-approve. nil means there is nobody to
	// ask, and an adapter must then keep its harness from prompting at all —
	// which is what every caller that predates this field gets, unchanged.
	Permissions PermissionAsker `json:"-"`
}

Launch identifies one worker attempt and the thread turn it should execute.

SessionID, when set, is the harness session this turn continues. It may be a session an earlier ATTEMPT created — that is how a conversation spans turns — so an adapter must resume it rather than assume it owns it.

type PermissionAsker added in v0.2.8

type PermissionAsker interface {
	AskPermission(ctx context.Context, request PermissionRequest) (PermissionVerdict, error)
}

PermissionAsker is how an adapter reaches a human mid-turn. The runner implements it; an adapter calls it and BLOCKS, because the tool call it is answering is blocked too.

AskPermission returns only when there is a verdict or when ctx is done. A ctx-done return must be treated as a denial by the adapter rather than as an error that fails the turn: the turn is being cancelled, and a permission call left hanging would keep the harness child alive after everything else has been torn down.

type PermissionRequest added in v0.2.8

type PermissionRequest struct {
	// ID is stable for one session and one tool call, and a resume must echo
	// it. It is the same fencing a Clarification has, for the same reason: a
	// verdict that arrives for a request the harness is no longer waiting on
	// must be refused rather than applied to whatever is waiting now.
	ID string `json:"id"`
	// Tool is the harness tool name the call is for, e.g. "Bash" or "WebFetch".
	Tool string `json:"tool"`
	// Input is a bounded JSON rendering of the tool's input, exactly as the
	// harness proposed it. It is what the human is shown and what the verdict
	// authorizes — that call, those arguments.
	Input string `json:"input,omitempty"`
}

PermissionRequest is one harness permission prompt waiting on a human.

type PermissionVerdict added in v0.2.8

type PermissionVerdict struct {
	Allow   bool   `json:"allow"`
	Message string `json:"message,omitempty"`
}

PermissionVerdict is a human's answer to a PermissionRequest.

A denial is a real answer, not a failure: it goes back to the harness, which continues the turn and can explain itself or take another route. Message is what the harness is told, and it is the only place the human's words reach the model.

type Result

type Result struct {
	Phase         string         `json:"phase"`
	SessionID     string         `json:"sessionID,omitempty"`
	Blocker       string         `json:"blocker,omitempty"`
	Clarification *Clarification `json:"clarification,omitempty"`
}

Result is the terminal state of one adapter launch.

Directories

Path Synopsis
Package claude adapts headless Claude Code to the runner harness.
Package claude adapts headless Claude Code to the runner harness.
Package codex adapts the Codex app-server protocol to the runner harness.
Package codex adapts the Codex app-server protocol to the runner harness.
internal
proc
Package proc holds the process-group primitives every harness adapter needs.
Package proc holds the process-group primitives every harness adapter needs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL