Documentation
¶
Overview ¶
Package harness defines the public execution seam for runner adapters.
Index ¶
- Constants
- Variables
- func AdvertisedName(selector string) string
- func ResolveBinary(name string) string
- func SelectorFor(advertised string) string
- func StripClarification(text string) string
- func TrustedFileOwner(info os.FileInfo) bool
- func ValidateReasoningEffort(name, effort string) error
- type Adapter
- type Clarification
- type Credential
- type CredentialKind
- type Emit
- type EnvironmentConfig
- type EnvironmentVariable
- type Event
- type Info
- type Launch
- type PermissionAsker
- type PermissionRequest
- type PermissionVerdict
- type Result
Constants ¶
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.
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.
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 ¶
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.
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
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
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
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
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.
func TrustedFileOwner ¶ added in v0.2.25
TrustedFileOwner accepts files owned by the runner account or root.
func ValidateReasoningEffort ¶ added in v0.2.25
ValidateReasoningEffort validates the transport value. Model-specific support is checked against the installed Codex app-server catalog before a turn starts.
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 ¶
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
// Environment is what else the identity brings to the launch: further
// credentials the caller wants the turn to run with, exported into the
// child's environment under the names given (a GitHub token as GH_TOKEN,
// say). They travel, live and die exactly as Value does — per launch, in
// memory, never on disk — and the runner has already refused every name
// outside its allow-list, so an adapter sees only credential names.
Environment []EnvironmentVariable
}
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 ¶
Emit receives adapter events. Implementations must treat an error as a terminal failure and stop producing events for the launch.
type EnvironmentConfig ¶ added in v0.2.25
type EnvironmentConfig struct {
PathPrepend []string `json:"pathPrepend,omitempty"`
Variables map[string]string `json:"variables,omitempty"`
}
EnvironmentConfig is local operator policy, never part of a task request. Values are literal: no shell evaluation, tilde expansion, or env-file loading.
func LoadEnvironmentConfig ¶ added in v0.2.25
func LoadEnvironmentConfig(path string, optional bool) (EnvironmentConfig, error)
LoadEnvironmentConfig reads a bounded, explicit operator file. Only an absent default file is optional; a misspelled explicit path is an error.
func (EnvironmentConfig) Apply ¶ added in v0.2.25
func (e EnvironmentConfig) Apply(base []string) []string
Apply preserves the adapter's sanitized environment, overrides explicitly configured variables, and prepends tool paths without changing process env.
func (EnvironmentConfig) Validate ¶ added in v0.2.25
func (e EnvironmentConfig) Validate() error
func (EnvironmentConfig) ValidateForWorktreeRoot ¶ added in v0.2.25
func (e EnvironmentConfig) ValidateForWorktreeRoot(worktreeRoot string) error
ValidateForWorktreeRoot also excludes tools controlled by managed tasks.
type EnvironmentVariable ¶ added in v0.2.16
EnvironmentVariable is one name=value an identity brings to a 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"`
SandboxMode string `json:"sandboxMode,omitempty"`
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"`
ReasoningEffort string `json:"reasoningEffort,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.
Source Files
¶
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. |