Documentation
¶
Overview ¶
Package sessiongate holds the session gates the agent harness installs into every session it runs: structural checks Claude Code calls as hooks around a tool call and at the end of a turn. The wait gate rejects a Bash command that would wait on nothing (a shell poll loop, pgrep -f, a foreground sleep) and names the replacement; the ready gate runs tools/check-merge.sh in the worktree before a command marks a pull request ready and denies it on any consistency regression; the commit gate pushes the work branch after a git commit and opens its draft pull request, on the repository it pushed to, if none exists; the reply gate reads the turn's reply back from the event log and refuses one that answers unvetted operator terms without a research check per term, commits the agent's future behaviour with no enforcing artifact in the turn, or waits on a background result while no background task runs, so the turn continues from the typed reason; the endpoint gate refuses a command that would stop serving an operator-facing endpoint of the endpoint registry without the operator's retirement record. The question gate judges every question put to the operator, the ones a reply ends on and the ones an AskUserQuestion call asks before they are shown: a question names its operator class (meaning, trust boundary, class membership) or is the agent's to resolve, and no question or offered alternative may name what a recorded ruling rules out. A reply that reports a number to an operator who asked for a measurement names the quantity the operator asked for. The search gate denies the third grep or find call in a row of a turn and names csf search, which asks the knowledge index. Every decision is written to the run's event log under the run's trace. Given a directory with no run record, the gate answers in operator mode, for a session the harness does not run, such as the orchestrator's: the endpoint gate alone, unlogged.
A gate is a library. The binary Claude Code calls reads the hook's input, grants the process capability and writes the returned output.
Index ¶
- Constants
- Variables
- func IsSearchCall(tool session.ToolUse) bool
- func MissingResearchChecks(unvetted []terms.Term, checks []session.ResearchCheck) []terms.Term
- func ParseResearchChecks(reply string) ([]session.ResearchCheck, []error)
- func PublishBranch(ctx context.Context, launcher proc.ILauncher, client *github.GitHubClient, ...) (url string, opened bool, err error)
- func RunsGitCommit(command string) (bool, error)
- func RunsOwnedTicketItem(command string) (bool, error)
- func SearchCall(tool string, input ToolInput) (described string, ok bool)
- func SearchChain(earlier []session.ToolUse, tool string, input ToolInput) []string
- type Artifact
- type CitationCheck
- type Commitment
- type EndpointFinding
- type GitHubFinding
- type HookInput
- type HookOutput
- type HookSpecificOutput
- type Measurement
- type Question
- type QuestionClass
- type QuestionCollector
- type QuestionCounts
- type QuestionKey
- type ReplyFinding
- type Rule
- type RulingCollector
- type SessionGate
- type SessionGateOption
- func WithCitationCheck(check CitationCheck) SessionGateOption
- func WithEndpointRegistry(registry endpoint.Registry) SessionGateOption
- func WithGitHub(client *github.GitHubClient) SessionGateOption
- func WithLauncher(launcher proc.ILauncher) SessionGateOption
- func WithRunDirectory(directory string) SessionGateOption
- type ShellFinding
- type ToolInput
Constants ¶
const ( GateWait = "wait" GateReady = "ready" GateCommit = "commit" GateReply = "reply" // GateQuestion is the PreToolUse gate on AskUserQuestion; the questions // a reply ends on are the reply gate's. GateQuestion = "question" // GateEndpoint is the PreToolUse gate on commands that stop serving a // registered endpoint. GateEndpoint = "endpoint" // GateSearch is the PreToolUse gate on a chain of grep and find calls. GateSearch = "search" DecisionAllow = "allow" DecisionDeny = "deny" DecisionSkip = "skip" DecisionMalformed = "malformed" DecisionOpened = "opened" DecisionExists = "exists" DecisionFailed = "failed" // DecisionLimit records a reply the reply gate would refuse but passes, // the turn having been refused its limit of times already. DecisionLimit = "limit" // KeyQuestions is the verdicts on the questions a reply or an // AskUserQuestion call put to the operator, as the decision records them. KeyQuestions = "questions" )
The gates and their decisions, as the event log records them.
const ( ArtifactCommit = "commit" ArtifactGateOrHook = "gate_or_hook_change" ArtifactTicketItem = "ticket_item_with_owner" )
The enforcing artifacts a turn can produce, as the decision names them.
const ( VerdictBlocked = "blocked" VerdictPassed = "passed" VerdictLimit = "limit" )
VerdictBlocked, VerdictPassed and VerdictLimit are a question's verdicts: refused, passed, or refused but let through because the turn reached its refusal limit.
const ( // EnforcementEnforced and EnforcementUnenforced are the series' two // values of enforcement. EnforcementEnforced = "enforced" EnforcementUnenforced = "unenforced" )
The rulings series: the operator rulings in force, split by whether a gate enforces them. Their sum is every ruling; the unenforced count is driven to zero by gating rulings, never by recording fewer.
const MeasurementFence = "measurement"
MeasurementFence tags the fenced block holding a reply's measurement as one JSON object.
Variables ¶
var ( // ErrInvalidOption reports a nil option or an option value the gate // cannot use. ErrInvalidOption = errors.New("session gate: invalid option") // ErrNoLauncher reports a gate built without the process capability. ErrNoLauncher = errors.New("session gate: a process launcher is required") // ErrNoRunDirectory reports a gate built without its run directory. ErrNoRunDirectory = errors.New("session gate: a run directory is required") // ErrMalformedHookInput reports hook input that is not the JSON object // Claude Code sends. ErrMalformedHookInput = errors.New("session gate: malformed hook input") // ErrUnknownEvent reports a hook event the harness installs no gate on. ErrUnknownEvent = errors.New("session gate: no gate for this hook event") // ErrPush reports a work branch git could not push. ErrPush = errors.New("session gate: could not push the work branch") // ErrPullRequest reports a draft pull request the gate could not find or // open. ErrPullRequest = errors.New("session gate: could not open the draft pull request") // ErrNoGitHub reports a gate the binary granted no GitHub protocol client: // no token was found. ErrNoGitHub = errors.New("no GitHub token: set GITHUB_TOKEN or GH_TOKEN, or log gh in with its token in hosts.yml") )
var ErrUnparsedCommand = errors.New("session gate: command is not valid shell")
ErrUnparsedCommand reports a command the shell parser could not read.
Functions ¶
func IsSearchCall ¶ added in v0.3.0
IsSearchCall reports a tool call the search gate counts: a Grep or Glob call, or a Bash command that runs grep, egrep, fgrep, rg, ag, ack, find or fd anywhere in it, including in a pipeline, a list or sh -c. A command that runs csf search asks the index and is not one.
func MissingResearchChecks ¶ added in v0.3.0
MissingResearchChecks returns the unvetted terms no complete check covers, in order. A check covers a term when the two share a stem.
func ParseResearchChecks ¶ added in v0.3.0
func ParseResearchChecks(reply string) ([]session.ResearchCheck, []error)
ParseResearchChecks reads every research check a reply carries: each fenced block tagged research-check holding one JSON object or an array of them. A block that is neither is returned among the errors and skipped.
func PublishBranch ¶ added in v0.3.0
func PublishBranch(ctx context.Context, launcher proc.ILauncher, client *github.GitHubClient, state *session.RunState) (url string, opened bool, err error)
PublishBranch pushes a run's work branch and returns its open pull request on the repository it was pushed to, opening a draft one when it has none; opened reports which. It records the push on state. It is the harness's one way a session's work becomes a pull request: the commit gate calls it after a commit, and so does anything else that commits in a session's worktree. GitHub is reached through the protocol client, never a gh child.
func RunsGitCommit ¶
RunsGitCommit reports whether command runs git commit anywhere in it, including inside sh -c. Whether the commit succeeded is for git to say.
func RunsOwnedTicketItem ¶ added in v0.3.0
RunsOwnedTicketItem reports whether command creates or edits a GitHub issue with an assignee anywhere in it, including inside sh -c.
func SearchCall ¶ added in v0.3.0
SearchCall describes a tool call IsSearchCall reports, as the search gate's rejection quotes it; ok is false for every other call.
func SearchChain ¶ added in v0.3.0
SearchChain is the chain of search calls the call being judged would end: the consecutive searches that end earlier, the turn's calls before it, then the call itself, each described as SearchCall describes it. Any other call breaks a chain. A call that is not a search ends none, and the chain is nil.
Types ¶
type Artifact ¶ added in v0.3.0
Artifact is one enforcing artifact a turn produced: its kind and what it was.
func EnforcingArtifacts ¶ added in v0.3.0
EnforcingArtifacts returns the enforcing artifacts among the tool calls a turn made: a commit, a change to a gate or hook, or a ticket item with an owner.
type CitationCheck ¶ added in v0.3.0
CitationCheck is the proof check on a pull request's body: an improvement it claims must cite a recorded evaluation suite result (#416).
type Commitment ¶ added in v0.3.0
Commitment is one first-person future commitment found in a reply.
func FindCommitments ¶ added in v0.3.0
func FindCommitments(reply string) []Commitment
FindCommitments returns every first-person future commitment in reply, one per sentence, with the sentence it stands in and the first marker found there. Fenced and inline code are not prose and never match.
func FindWaits ¶ added in v0.3.0
func FindWaits(reply string) []Commitment
FindWaits returns every sentence of reply that says its work continues when a background result arrives, with the first marker found there. Fenced and inline code are not prose and never match.
type EndpointFinding ¶ added in v0.3.0
type EndpointFinding struct {
// Snippet is the offending command's source text.
Snippet string
// Endpoints are the registered endpoints it would stop serving; empty for
// a retirement, which stops nothing itself.
Endpoints []endpoint.Endpoint
// Retire marks csf endpoint retire, which records the operator's
// acknowledgement and so is the operator's to run.
Retire bool
}
EndpointFinding is one command the endpoint gate refuses.
func FindEndpointStops ¶ added in v0.3.0
func FindEndpointStops(command string, registry endpoint.Registry) ([]EndpointFinding, error)
FindEndpointStops parses command as bash and returns every command in it, or in a literal script handed to sh -c, that stops serving an active endpoint of the registry: csf stop or csf view -stop without a csf serve in the same command, kill of the registered host process, pkill or killall naming csf, and docker stop, rm or kill of an endpoint's container. csf endpoint retire is returned too: it is the operator's.
func (EndpointFinding) Message ¶ added in v0.3.0
func (finding EndpointFinding) Message() string
Message is the refusal the agent reads: the command, every endpoint it would stop with its users, and the two ways forward.
type GitHubFinding ¶ added in v0.3.0
type GitHubFinding struct {
Snippet string
// Tool is the replacing tool's name; empty when no one tool replaces
// the command, as for gh api.
Tool string
}
GitHubFinding is one gh command a session may not run, and the tool that replaces it.
func FindGitHubCommands ¶ added in v0.3.0
func FindGitHubCommands(command string) ([]GitHubFinding, error)
FindGitHubCommands parses command as bash and returns every gh pr, gh issue and gh api command in it, including inside sh -c, in source order.
func (GitHubFinding) Message ¶ added in v0.3.0
func (finding GitHubFinding) Message() string
Message is the refusal the agent reads.
type HookInput ¶
type HookInput struct {
SessionID string `json:"session_id"`
HookEventName string `json:"hook_event_name"`
ToolName string `json:"tool_name"`
ToolUseID string `json:"tool_use_id"`
ToolInput ToolInput `json:"tool_input"`
}
HookInput is the part of a command hook's standard input the gates read.
type HookOutput ¶
type HookOutput struct {
Decision string `json:"decision,omitempty"`
Reason string `json:"reason,omitempty"`
HookSpecificOutput *HookSpecificOutput `json:"hookSpecificOutput,omitempty"`
}
HookOutput is a command hook's standard output. A tool gate answers in HookSpecificOutput; the Stop gate answers with Decision and Reason, which is how a Stop hook makes the turn continue.
type HookSpecificOutput ¶
type HookSpecificOutput struct {
HookEventName string `json:"hookEventName"`
PermissionDecision string `json:"permissionDecision,omitempty"`
PermissionDecisionReason string `json:"permissionDecisionReason,omitempty"`
AdditionalContext string `json:"additionalContext,omitempty"`
}
HookSpecificOutput is the event-specific decision Claude Code reads.
type Measurement ¶ added in v0.3.0
type Measurement struct {
// Quantity is the quantity the operator named, in the operator's words.
Quantity string `json:"quantity"`
// Measured is what the reply measured.
Measured string `json:"measured"`
// Same is whether Measured is Quantity.
Same *bool `json:"same"`
// Difference is how Measured differs from Quantity when they are not the
// same.
Difference string `json:"difference"`
}
Measurement is the typed record a reply carries for a number it reports to an operator who asked for a measurement.
type Question ¶ added in v0.3.0
Question is one question an agent put to the operator: its text, the header the asking tool gave it, and the alternatives it offered.
func AskedQuestions ¶ added in v0.3.0
func AskedQuestions(input json.RawMessage) []Question
AskedQuestions reads the questions of one AskUserQuestion call's input; an input of another shape asks nothing.
func ReplyQuestions ¶ added in v0.3.0
ReplyQuestions reads the questions a reply ends on: when the reply's last prose ends in a question mark, every question sentence of its last paragraph, each offering the list items of that paragraph and of the paragraph before it. A reply that ends otherwise asks the operator nothing. Code is not prose.
type QuestionClass ¶ added in v0.3.0
type QuestionClass string
QuestionClass is the class a question to the operator falls in: one of the three only the operator can answer, or one of the sub-classes the agent owes a resolution for.
const ( ClassMeaning QuestionClass = "meaning" ClassTrustBoundary QuestionClass = "trust boundary" ClassClassMembership QuestionClass = "class membership" )
The operator classes: meaning, trust boundaries and class membership are the operator's to decide (#145).
const ( ClassMagnitude QuestionClass = "magnitude" ClassFact QuestionClass = "fact" ClassAuthorized QuestionClass = "authorized" ClassDefault QuestionClass = "default" ClassRelitigation QuestionClass = "re-litigation" ClassUnnamed QuestionClass = "unnamed" )
The agent's sub-classes: a question in one is refused with the resolution the agent owes instead. ClassRelitigation is a question or offered alternative a recorded ruling already answers; ClassUnnamed is a question that names no operator class.
func ClassifyQuestion ¶ added in v0.3.0
func ClassifyQuestion(question Question) QuestionClass
ClassifyQuestion places one question: the first agent sub-class whose lexicon matches it, else the operator class it names, else unnamed.
type QuestionCollector ¶ added in v0.3.0
type QuestionCollector struct {
// contains filtered or unexported fields
}
QuestionCollector exports the question gate's measurement, read afresh from the state directory at each scrape.
func NewQuestionCollector ¶ added in v0.3.0
func NewQuestionCollector(state string) *QuestionCollector
NewQuestionCollector builds the collector over the runs under state.
func (*QuestionCollector) Collect ¶ added in v0.3.0
func (collector *QuestionCollector) Collect(output chan<- prometheus.Metric)
Collect implements prometheus.Collector.
func (*QuestionCollector) Describe ¶ added in v0.3.0
func (collector *QuestionCollector) Describe(output chan<- *prometheus.Desc)
Describe implements prometheus.Collector.
type QuestionCounts ¶ added in v0.3.0
type QuestionCounts struct {
Questions map[QuestionKey]int
Overrides int
}
QuestionCounts is the question gate's measurement over every run under a state directory.
func CountQuestions ¶ added in v0.3.0
func CountQuestions(stateDirectory string) (QuestionCounts, error)
CountQuestions reads every run's event log under stateDirectory. A run whose log cannot be read contributes nothing.
type QuestionKey ¶ added in v0.3.0
QuestionKey is one series of the question count.
type ReplyFinding ¶ added in v0.3.0
ReplyFinding is one reason the reply gate refuses a reply.
func JudgeMeasurement ¶ added in v0.3.0
func JudgeMeasurement(operator string, reply string) []ReplyFinding
JudgeMeasurement applies the measured-quantity rule to one reply: operator is the turn's message when the operator wrote it, else empty. No finding means the reply passes.
func JudgeQuestions ¶ added in v0.3.0
func JudgeQuestions(questions []Question, rulings []session.Ruling) ([]session.QuestionVerdict, []ReplyFinding)
JudgeQuestions applies the question gate's rules to the questions of one reply or one AskUserQuestion call against the rulings in force: each question's verdict, in order, and the findings that refuse them.
func JudgeReply ¶ added in v0.3.0
func JudgeReply(reply string, unvetted []terms.Term, tools []session.ToolUse, background []session.BackgroundTask, strain affect.Strain) []ReplyFinding
JudgeReply applies the reply gate's rules to one reply: unvetted are the turn's unvetted terms, tools the tool calls the turn made, background the tasks the executor runs in the background as the turn ends and strain the operator's strain as the turn's message read. No finding means the reply passes.
type Rule ¶
type Rule string
Rule names a shell shape the wait gate rejects.
const ( // GateGitHub is the PreToolUse gate on Bash that refuses a gh command // reaching GitHub's issues, pull requests or API: a session reaches // GitHub through CSF's typed GitHub tools, which record every call. GateGitHub = "github" // RuleGitHubShell is a gh pr, gh issue or gh api command. RuleGitHubShell Rule = "github_shell" )
const ( // RuleQuestionClass: a question to the operator is in an operator class // and names it, or it is the agent's to resolve and is refused. RuleQuestionClass Rule = "question_class" // RuleRuling: a question, or an alternative it offers, names something a // recorded ruling rules out. RuleRuling Rule = "ruling" )
The question gate's rules: what a question to the operator may not be.
const ( // RuleResearchCheck: a reply to a message with unvetted terms carries a // complete research check for each, or it is refused. RuleResearchCheck Rule = "research_check" // RuleCommitment: a reply that commits the agent's future behaviour in // the first person ("I'll", "from now on", "going forward", "until then I // will", "every time") with no enforcing artifact produced in the same // turn is refused (#47, commitment without action). RuleCommitment Rule = "commitment" // RuleWaitWithoutChild: a reply that says the turn's work continues when // a background result arrives ("waiting on", "will follow", "once the run // finishes") while the session runs no background task is refused: no // completion will wake the session, so the wait is a commitment without // action (#47, #324). RuleWaitWithoutChild Rule = "wait_without_child" // RuleStrain: a reply to an operator message whose strain reads high is // at most affect.ReplyWordLimit words, the length past which the // operator's corrections rise (pkg/affect derives it). RuleStrain Rule = "strain" )
The reply gate's rules: what a reply to the operator may not do.
const ( // RulePollLoop is a while, until or for loop whose body or condition // sleeps: a hand-rolled poll. RulePollLoop Rule = "poll_loop" // RuleSelfMatchingPgrep is pgrep -f (or --full): its pattern is matched // against whole command lines, including the shell that runs it. RuleSelfMatchingPgrep Rule = "pgrep_full" // RuleForegroundSleep is a sleep the turn waits for. RuleForegroundSleep Rule = "foreground_sleep" )
The rules of the wait gate. Each is a way an agent waits that cannot end on its own: nothing joins a poll loop, a pgrep -f pattern matches the loop's own command line, and a foreground sleep holds the turn while waiting on nothing.
const RuleMeasuredQuantity Rule = "measured_quantity"
RuleMeasuredQuantity: a reply that reports a number to an operator who asked for a measurement names the quantity the operator asked for, what it measured, and whether the two are the same; a term it introduces for what it measured carries a research check.
const RuleSearchChain Rule = "search_chain"
RuleSearchChain is a turn's third grep or find call in a row: the agent is finding code by hand where the knowledge index answers in one call.
type RulingCollector ¶ added in v0.3.0
type RulingCollector struct {
// contains filtered or unexported fields
}
RulingCollector exports the rulings in force, read afresh from the state directory's ruling records at each scrape.
func NewRulingCollector ¶ added in v0.3.0
func NewRulingCollector(state string) *RulingCollector
NewRulingCollector builds the collector over the ruling records under state.
func (*RulingCollector) Collect ¶ added in v0.3.0
func (collector *RulingCollector) Collect(output chan<- prometheus.Metric)
Collect implements prometheus.Collector.
func (*RulingCollector) Describe ¶ added in v0.3.0
func (collector *RulingCollector) Describe(output chan<- *prometheus.Desc)
Describe implements prometheus.Collector.
type SessionGate ¶
type SessionGate struct {
// contains filtered or unexported fields
}
SessionGate answers the gated hooks of one run.
func NewSessionGate ¶
func NewSessionGate(options ...SessionGateOption) (*SessionGate, error)
NewSessionGate validates the whole option set before building the gate.
func (*SessionGate) Handle ¶
func (gate *SessionGate) Handle(ctx context.Context, event string, input []byte) (*HookOutput, error)
Handle answers one hook call: event is the hook event the harness installed the call on and input is the hook's standard input. A nil output means the call proceeds without a word from the gate. A gate that could not finish its work returns both the output telling the agent and the error.
type SessionGateOption ¶
type SessionGateOption func(gate *SessionGate) error
SessionGateOption configures a SessionGate.
func WithCitationCheck ¶ added in v0.3.0
func WithCitationCheck(check CitationCheck) SessionGateOption
WithCitationCheck grants the proof check the ready gate runs on the pull request's body. Without it no body is checked.
func WithEndpointRegistry ¶ added in v0.3.0
func WithEndpointRegistry(registry endpoint.Registry) SessionGateOption
WithEndpointRegistry grants the endpoint registry the endpoint gate protects. Without it the gate protects nothing.
func WithGitHub ¶ added in v0.3.0
func WithGitHub(client *github.GitHubClient) SessionGateOption
WithGitHub grants the GitHub protocol client the commit gate opens draft pull requests through. Without it the commit gate pushes and reports ErrNoGitHub.
func WithLauncher ¶
func WithLauncher(launcher proc.ILauncher) SessionGateOption
WithLauncher grants the process capability git and gh start through. Required.
func WithRunDirectory ¶
func WithRunDirectory(directory string) SessionGateOption
WithRunDirectory names the run the gate answers for: the directory the harness recorded it in. Required.
type ShellFinding ¶
type ShellFinding struct {
Rule Rule
// Snippet is the offending source text.
Snippet string
// Await is the csf await condition and operands that replace a poll
// loop, when the commands it runs name one.
Await string
}
ShellFinding is one rejected shape in a command.
func FindShellWaits ¶
func FindShellWaits(command string) ([]ShellFinding, error)
FindShellWaits parses command as bash and returns every shape the wait gate rejects, in source order. A literal script handed to sh -c or bash -c is parsed and checked the same way. Heredoc bodies and quoted text are data, not commands, and never match.
func (ShellFinding) Message ¶
func (finding ShellFinding) Message() string
Message is the rejection text the agent reads.
func (ShellFinding) Replacement ¶
func (finding ShellFinding) Replacement() string
Replacement is what to do instead, as the rejection message states it.
type ToolInput ¶ added in v0.3.0
type ToolInput struct {
Command string `json:"command"`
RunInBackground bool `json:"run_in_background"`
Pattern string `json:"pattern"`
Path string `json:"path"`
}
ToolInput is the part of a gated tool's input the gates read: Bash's command and whether it runs in the background, and the pattern and path of a Grep or Glob call.