Documentation
¶
Overview ¶
Raw shell dispatch: normalize a whole command, resolve one request/execution path scope, authorize each stage or the literal subject, then run direct pipelines or the literal form through the shared runner —.
Structured-tool dispatch: exact-name registry lookup, effective-cwd path-scope resolution, permission authorization, and tool invocation — including vcs_<action> launch-failure cwd handling.
Index ¶
- Constants
- Variables
- func DispatchPreparedTool(ctx context.Context, prepared PreparedTool, authorizer Authorizer) (tools.Result, error)
- func DispatchShell(ctx context.Context, input string, home string, processCWD string, ...) (tools.Result, error)
- func DispatchTool(ctx context.Context, name string, args map[string]any, home string, ...) (tools.Result, error)
- func InvokeDecided(ctx context.Context, prepared PreparedTool, decision permissions.Decision) (tools.Result, error)
- type Authorizer
- type BatchAuthRequest
- type BatchAuthorizer
- type DeniedError
- type IndependentAuthRequest
- type IndependentAuthorizer
- type MalformedCommandError
- type NoSuchToolError
- type PipelineRunner
- type PreparedTool
- func (p PreparedTool) DelegateDeclaredDelete() map[string]bool
- func (p PreparedTool) DelegateDeclaredWrite() map[string]bool
- func (p PreparedTool) DelegatePrepared() bool
- func (p PreparedTool) DelegateRefusal() error
- func (p PreparedTool) DelegateSchedulerTargets() []string
- func (p PreparedTool) ExternalRead() bool
- func (p PreparedTool) IndependentRequest() IndependentAuthRequest
- func (p PreparedTool) Name() string
- func (p PreparedTool) PreparedMove() (source, destination string)
- func (p PreparedTool) PreparedTarget() string
- func (p PreparedTool) Scope() string
- type PromptDisplay
- type Runner
- type UnsupportedConstructError
Constants ¶
const ( ActionCreateFile = "Create file" ActionEditFile = "Edit file" ActionDeleteFile = "Delete file" ActionCreateDirectory = "Create directory" // ActionMoveFile heads the move prompt. Its value is the bare verb "Move" (not "Move file") because a // move relocates a file or a directory; the "source -> destination" subject already names what moves. ActionMoveFile = "Move" ActionRunCommand = "Run command" ActionRunTool = "Run tool" )
Action header labels the prompt shows above the subject of the action. They are the human-readable framing the permission prompt renders instead of the raw subject-kind/subject matching internals: a file write shows "Create file"/"Edit file", a file removal "Delete file", a directory creation "Create directory", a rename "Move" (over "source -> destination"), any shell command "Run command", and any other structured tool the generic "Run tool" fallback — never the bare registered tool token. The dispatch producers (tool.go, shell.go) populate them on PromptDisplay.Action.
Variables ¶
var ErrPipelineUnsupported = runner.ErrPipelineUnsupported
ErrPipelineUnsupported is the sentinel a PipelineRunner's preflight returns when it cannot run the stages as one pipeline, sending the pipe group to the sequential Exec loop instead.
Functions ¶
func DispatchPreparedTool ¶
func DispatchPreparedTool(ctx context.Context, prepared PreparedTool, authorizer Authorizer) (tools.Result, error)
func DispatchShell ¶
func DispatchShell(ctx context.Context, input string, home string, processCWD string, authorizer Authorizer, runner Runner, guard ...securitystate.Policy) (tools.Result, error)
DispatchShell normalizes, authorizes, and runs one raw shell command from the agent. It performs no pattern-based shell-to-structured-tool rewriting: a raw command always goes through the shell path. input is the verbatim command; home anchors ~ expansion during path-scope resolution; processCWD is codemeld's working directory; authorizer and runner are the bound permission and command-runner seams.
func DispatchTool ¶
func DispatchTool(ctx context.Context, name string, args map[string]any, home string, processCWD string, registry *tools.Registry, authorizer Authorizer, guard ...securitystate.Policy) (tools.Result, error)
DispatchTool looks up a structured tool by exact name, authorizes it against its effective execution directory, and — only if authorized — invokes it. name is the exact registered tool name; args are the decoded tool arguments passed straight to the tool; home anchors ~ expansion during path-scope resolution; processCWD is codemeld's working directory and the default path scope for a tool with no distinct execution directory; registry is the tool registry; authorizer is the bound permission seam shared with shell dispatch.
The routes, in order: an unknown name returns *NoSuchToolError immediately, without prompting — this also covers a vcs_<action> name that was never registered because no VCS template is active. Otherwise the authorization path scope is resolved before prompting: a tool exposing EffectiveCWD anchors on its resolved execution directory, every other tool on processCWD. A VCS launch-failure cwd error (cwd escape or repo-root disappearance) carries the attempted path scope, so dispatch authorizes against that carried scope and still invokes the tool so the VCS action tool produces its launch-failure result and paired tool_exec_* events; an effective-cwd error with no concrete path scope returns before prompting. A denied authorization returns *DeniedError and the tool is never invoked; a granted authorization invokes the registered tool, which owns its own invocation (a vcs_<action> tool routes to the VCS action invoker — dispatch never special-cases shell equivalents or rewrites a raw shell command into a structured call).
func InvokeDecided ¶
func InvokeDecided(ctx context.Context, prepared PreparedTool, decision permissions.Decision) (tools.Result, error)
InvokeDecided invokes a prepared call whose authorization already completed — the path a group authorized on one prompt takes, so no member re-prompts and every member runs against the exact target its decision covered. A denial returns *DeniedError without invoking anything.
Types ¶
type Authorizer ¶
type Authorizer interface {
// Authorize drives one permission check for req to a Decision, prompting the user when no real
// grant and no turn-cache entry already resolves it. A denied check carries a non-empty
// Decision.DenyEnvelope; any other returned Decision (once/session/folder/global) authorizes the
// request. A non-nil error is an internal failure (cancellation, event-emit, prompt error).
//
// display is the human-readable prompt payload (action header, subject of the action, verbatim
// body) carried separately from req because matching keys on req.Subject (the registered tool
// name for a structured tool, the canonical command otherwise) while the prompt shows a plain
// action screen. Shell paths pass effective source text in display.Subject; req.Subject
// remains canonical for matching. The structured-tool path builds it from the decoded tool name
// and arguments so the prompt shows the file/content for a write rather than the JSON call.
//
// modeCtx is the mode-authorization context: the dispatch caller fills the
// request-derived fields (operation class, tool name, pre-normalization shell stage data,
// resolved target path) and the production binding overlays the run-scoped active mode and
// project root before driving permissions.Authorize, where Plan/Build posture is decided.
Authorize(ctx context.Context, req permissions.MatchRequest, display PromptDisplay, modeCtx permissions.ModeContext) (permissions.Decision, error)
}
Authorizer is the narrow dispatch-local seam over permissions.Authorize. The startup layer binds the full permissions.AuthorizeDeps bundle (grant table, turn-scoped cache, prompt func, persist func, event emitters) behind a concrete implementation so internal/dispatch need not import internal/config, internal/tui, or internal/events. It mirrors the seam structured-tool dispatch uses, keeping both dispatch paths free of those collaborators.
type BatchAuthRequest ¶
type BatchAuthRequest struct {
Req permissions.MatchRequest
Display PromptDisplay
ModeCtx permissions.ModeContext
}
BatchAuthRequest is one pipeline stage handed to BatchAuthorizer.AuthorizeBatch: the same (MatchRequest, PromptDisplay, ModeContext) triple a single Authorizer.Authorize call takes. The batched authorizer resolves each without prompting and, for the stages that remain, shows one command-level approval; each stage stays its own subject (no compound subject), so its grant, cache key, and events are identical to the per-stage path.
type BatchAuthorizer ¶
type BatchAuthorizer interface {
AuthorizeBatch(ctx context.Context, reqs []BatchAuthRequest) ([]permissions.Decision, error)
}
BatchAuthorizer is the dispatch-local seam over permissions.AuthorizeBatch: it resolves a set of pipeline stages without prompting and issues ONE command-level approval for the stages that still need a prompt, applying that single outcome to them all (the presentation complement to the per-stage Authorizer). It is a SEPARATE seam from Authorizer — not a new method on it — so an authorizer that does not batch prompts needs no batched-prompt stub: dispatch bridges it through perStageBatchAuthorizer. The production authorizer (internal/bootstrap) implements it, and dispatchDirect and dispatchStaged authorize every pipeline's stages through it. The returned decisions are index-aligned with reqs; any decision carrying a non-empty DenyEnvelope denies the whole pipeline.
type DeniedError ¶
DeniedError reports that authorization denied the shell command. Envelope is the synthetic deny-reason tool-error envelope (built by permissions.BuildDenyEnvelope), which the caller delivers to the agent as the tool result. A denied command is never executed; the error signals the refusal without producing a tools.Result that the tool_exec_* events would otherwise log as command output. The same shape serves a single denied literal subject and a denied pipeline stage.
func (*DeniedError) Error ¶
func (e *DeniedError) Error() string
type IndependentAuthRequest ¶
type IndependentAuthRequest struct {
Req permissions.MatchRequest
Display PromptDisplay
ModeCtx permissions.ModeContext
PreparedTarget string
}
IndependentAuthRequest is one independently authorizable call handed to IndependentAuthorizer.AuthorizeIndependent. It carries the same (MatchRequest, PromptDisplay, ModeContext) triple a single Authorize takes, plus the prepared target the group prompt lists for it.
type IndependentAuthorizer ¶
type IndependentAuthorizer interface {
AuthorizeIndependent(ctx context.Context, reqs []IndependentAuthRequest) ([]permissions.Decision, error)
}
IndependentAuthorizer authorizes a group of independently authorizable calls on one prompt and returns an index-aligned decision per call. It is a separate seam from BatchAuthorizer, whose direct-pipeline stages share one outcome and stop at the first deny: here every call keeps its own subject, decision, grant, and deny envelope, so a denied call does not block its siblings.
type MalformedCommandError ¶
type MalformedCommandError struct {
Err error
}
MalformedCommandError reports a shell command codemeld could not parse or resolve before authorization: a split or canonicalization failure, a model-supplied `cd` prefix naming a path that will not resolve, or a direct or staged stage whose program is not found. Like UnsupportedConstructError no prompt is shown and no grant can authorize it, so the caller delivers the wrapped reason to the agent as the tool result rather than failing the turn. Scope resolution without a `cd` hint, authorization, and execution failures are infrastructure faults and are never wrapped in it.
func (*MalformedCommandError) Error ¶
func (e *MalformedCommandError) Error() string
func (*MalformedCommandError) Unwrap ¶
func (e *MalformedCommandError) Unwrap() error
type NoSuchToolError ¶
type NoSuchToolError struct {
Name string
}
NoSuchToolError reports that no tool is registered under the requested name. Dispatch returns it to the agent without ever consulting authorization. A vcs_<action> name that is absent because no VCS template was active (no detection match) is indistinguishable from any other registry miss at dispatch time and surfaces through this same error.
func (*NoSuchToolError) Error ¶
func (e *NoSuchToolError) Error() string
type PipelineRunner ¶
type PipelineRunner interface {
PreflightPipeline(ctx context.Context, specs []runner.RunSpec) error
Pipeline(ctx context.Context, specs []runner.RunSpec) ([]runner.PipelineResult, error)
}
PipelineRunner is the dispatch-local mirror of runner.PipelineRunner: the optional capability a Runner may also implement to run a pipe group's stages concurrently over OS pipes. It carries no capture bound because the implementer owns it.
type PreparedTool ¶
type PreparedTool struct {
// contains filtered or unexported fields
}
func PrepareTool ¶
func (PreparedTool) DelegateDeclaredDelete ¶
func (p PreparedTool) DelegateDeclaredDelete() map[string]bool
func (PreparedTool) DelegateDeclaredWrite ¶
func (p PreparedTool) DelegateDeclaredWrite() map[string]bool
DelegateDeclaredWrite is the child's canonical write ceiling. Each caller gets its own copy, so an authorizer that observes the declaration cannot widen what the child is built from.
func (PreparedTool) DelegatePrepared ¶
func (p PreparedTool) DelegatePrepared() bool
func (PreparedTool) DelegateRefusal ¶
func (p PreparedTool) DelegateRefusal() error
DelegateRefusal is the delegate tool's own rejection of this call's arguments, if preparation made one. Such a call carries no candidate, so authorization judges it as an empty declaration: a caller that authorizes it first gets the ceiling's answer, and one that does not gets the refusal to hand back to the model.
func (PreparedTool) DelegateSchedulerTargets ¶
func (p PreparedTool) DelegateSchedulerTargets() []string
DelegateSchedulerTargets is the path set this call mutates for write-conflict ordering: the declared write and delete leaves and move endpoints plus the missing parents the child's authorized creates would make. Those parents order the call and grant the child nothing.
func (PreparedTool) ExternalRead ¶
func (p PreparedTool) ExternalRead() bool
func (PreparedTool) IndependentRequest ¶
func (p PreparedTool) IndependentRequest() IndependentAuthRequest
IndependentRequest is the authorization request DispatchPreparedTool would make for this prepared call, so a caller can authorize a group of prepared calls together and then dispatch each against the very target it authorized.
func (PreparedTool) Name ¶
func (p PreparedTool) Name() string
func (PreparedTool) PreparedMove ¶
func (p PreparedTool) PreparedMove() (source, destination string)
func (PreparedTool) PreparedTarget ¶
func (p PreparedTool) PreparedTarget() string
PreparedTarget is the exact target this call was prepared against — the resolved file for a target-scoped tool, otherwise the resolved path scope — so a group prompt can name what each call will actually touch.
func (PreparedTool) Scope ¶
func (p PreparedTool) Scope() string
type PromptDisplay ¶
type PromptDisplay struct {
Action string
Subject string
Body string
// OutOfRootFileWrite marks a mutating file prompt whose resolved write target lies outside the project
// root — a single-target edit/create/mkdir (via outOfRootFileTool) or one out-of-root endpoint of a move
// (set per authorized scope by dispatchPreparedMultiScope). It re-enables the "allow in this
// folder permanently" option for those tools only; the prompt UI keys on it to offer a folder grant
// anchored at the out-of-root folder. It is false (its zero value) for shell, literal, every other
// structured tool, any in-root edit/create/mkdir, and an in-root move endpoint — so no other out-of-root
// prompt is broadened. Set only by DispatchTool on the mutating-file dispatch paths; the shell producers
// leave it unset.
OutOfRootFileWrite bool
// OutOfRootReadScope marks a read-only navigation tool (read/glob/grep/list/tree) prompt whose resolved
// scope lies outside the project root. Like OutOfRootFileWrite it re-enables the folder grant
// option for that out-of-root scope, but for a read-only grant (subject = the read tool name), so a
// folder/session/global grant authorizes descendant reads for that tool without ever authorizing a
// write. It is false for shell, literal, every other tool, the edit/create tools, and any in-project
// read. Set only by DispatchTool on the read-tool dispatch path (via externalReadTool).
OutOfRootReadScope bool
// GrantStoredSubject is the capability subject (permissions.CapabilityWrite "write" or
// CapabilityRead "read*") a grant should store when this structured-tool prompt is approved with a
// grant-creating option: create/edit fold to "write" and the five read/search tools fold to
// "read*", so one approval covers every sibling member tool at the scope. It rides forward to the
// prompt and back into the stored grant subject while MatchRequest.Subject stays the concrete tool
// name, so request matching, cache keys, prompt-shown, and mode-auto records are unchanged. Empty for
// shell, literal, tools that map to no capability, and in-project reads (which never create a grant);
// an empty value leaves the stored subject the concrete request subject. Set only by DispatchTool.
GrantStoredSubject string
// Pipeline is display-only source text for a direct pipeline.
Pipeline string
// WholeCommand marks a literal-route prompt as a whole-command approval: Subject is the entire
// command run verbatim through the shell, not one stage of a direct pipeline. It lets the
// prompt frame a literal approval distinctly from a per-stage direct approval. True only on the
// literal route; false for direct stages and every structured tool. Display-only.
WholeCommand bool
// RequestedTarget is the lexical path a prepared structured tool was called with, carried
// separately because a generic tool's Subject is the registered tool name. It is copied from
// the validated arguments — never parsed out of the rendered JSON, never the resolved target —
// and is display-only.
RequestedTarget string
HostRequest string
DirectHTTPFallback bool
ExceptionalAuthority string
}
PromptDisplay is the human-readable permission-prompt payload a dispatch producer builds for one authorization, carried separately from the matching permissions.MatchRequest so the prompt can render a plain "are you sure?" approval screen without re-parsing the matching subject or the raw tool-call JSON. It supersedes the prior lone command-text string:
- Action is the header (one of the Action* constants above).
- For shell subjects, Subject is effective source text, not the canonical matching subject.
The matching subject and subject kind stay on MatchRequest for grants and the event log and are never shown through this payload.
type Runner ¶
type Runner interface {
// NewInvocationID mints a fresh UUIDv7 invocation id for one command invocation.
NewInvocationID() string
// Exec runs a single command (no shell interpretation, no chaining) and returns its result.
Exec(ctx context.Context, spec runner.RunSpec) (runner.RunResult, error)
}
Runner is the dispatch-local seam over the shared command runner (runner.Runner satisfies it). It is mirrored here so shell dispatch can be tested with a fake runner without depending on the concrete DirectExecRunner. Every RunSpec passed to Exec must carry an InvocationID minted by NewInvocationID, which the runner validates before launching.
type UnsupportedConstructError ¶
type UnsupportedConstructError struct {
Construct string
}
UnsupportedConstructError reports a shell command rejected by pipeline splitting because it uses a construct outside the allowlist: `$`, a backtick, or a bare newline. Bare simple compounds route onto the literal/compound path, so `&&`, `;`, and `||` are absent here. Such a command short-circuits with this error to the agent — no prompt is shown and no grant can authorize it. Construct names the refused construct for the agent-facing message.
func (*UnsupportedConstructError) Error ¶
func (e *UnsupportedConstructError) Error() string