Documentation
¶
Overview ¶
Package action implements parsing, validation, storage, and capability enforcement for Aileron action files.
Per ADR-0001 and ADR-0003, an action is a single file in ~/.aileron/actions/ with TOML frontmatter delimited by `+++` and a Markdown body. The frontmatter is the contract Aileron executes; the body doubles as the LLM-facing function description when the action is surfaced to the agent's tool catalog.
Index ¶
- Variables
- func DefaultDir() string
- func DefaultStatePath() string
- func EnforceCapability(m *Manifest, connectorFQN, capability string) error
- func Validate(m *Manifest, file string) error
- type ApprovalPolicy
- type Boundary
- type Error
- type ExecuteStep
- type Executor
- type FailureClass
- type FileStateStore
- type Input
- type LoadResult
- type LoadedAction
- type Manifest
- type Match
- type Requires
- type RequiresConnector
- type Result
- type SandboxExecutor
- type State
- type StateStore
- type Store
- type StubExecutor
Constants ¶
This section is empty.
Variables ¶
var ErrNotFound = &Error{ Class: ClassNotFound, Boundary: BoundaryAction, Message: "action not found", }
ErrNotFound is the sentinel matched by errors.Is for "no installed action with the given name".
Functions ¶
func DefaultDir ¶
func DefaultDir() string
DefaultDir returns the user-level actions directory `~/.aileron/actions`. Falls back to `./.aileron/actions` if the user's home directory is not resolvable, matching the existing `internal/launch` behavior.
func DefaultStatePath ¶
func DefaultStatePath() string
DefaultStatePath returns the canonical overlay-file path `~/.aileron/action-state.json`. Falls back to `./.aileron/action-state.json` when the user's home directory is not resolvable — matching the fallback rule of DefaultDir for the actions directory itself.
func EnforceCapability ¶
EnforceCapability checks whether the given action manifest permits the supplied connector operation under its declared capability subset, per ADR-0003.
The action boundary is the second of the two defense-in-depth checks (the connector manifest is the first). A call that requires a capability outside the action's declared subset is denied here before it ever reaches the connector. Returns nil on permitted calls; on denial returns a structured *Error of class capability_denied with enough context for the caller to surface a precise message to the agent and audit log.
`connectorFQN` is the connector's fully-qualified URI (e.g. "github://aileron/slack"); it must match one of the manifest's [[requires.connectors]] entries exactly. `capability` is the operation the caller is attempting (e.g. "chat:write").
Types ¶
type ApprovalPolicy ¶
type ApprovalPolicy struct {
// Required gates execution on a user decision when true. Default
// false (i.e. absent block ⇒ no approval needed).
Required bool `toml:"required"`
}
ApprovalPolicy describes the approval gating for an action. v1 carries only `Required`; future fields (prompt templates, default-deny timeout overrides, allow-list of identities that can approve) attach here without breaking the existing manifest shape.
type Boundary ¶
type Boundary string
Boundary identifies which layer produced an error, per ADR-0010.
Action loading and capability enforcement live at the action boundary.
type Error ¶
type Error struct {
// Class is the canonical failure classification.
Class FailureClass
// Message is a human-readable description; safe to surface to the user.
// Does not contain credentials.
Message string
// Retriable signals whether the failure is safe to retry. Per ADR-0010,
// validation failures and capability denials are terminal.
Retriable bool
// Boundary is the layer that produced the error.
Boundary Boundary
// File is the path of the action file that produced the error, when
// applicable.
File string
// Line is the line within File where the error was detected, when
// applicable. Zero means "unknown / not applicable".
Line int
// Details is class-specific structured context (e.g. denied capability,
// requested op). Keys are stable strings.
Details map[string]any
}
Error is a structured action-layer error per ADR-0010. Loading and enforcement errors carry enough context (file, line, boundary) for callers to surface a precise message to the user without re-parsing or re-formatting.
type ExecuteStep ¶
type ExecuteStep struct {
// ID is a label for the step. Subsequent steps may reference its outputs
// via `${id.field}` interpolation in `Inputs`.
ID string `toml:"id"`
// Connector is the FQN of the connector whose operation this step calls.
// Must appear in `Requires.Connectors` and must match one of those FQNs
// exactly.
Connector string `toml:"connector"`
// Op is the connector operation invoked at this step.
Op string `toml:"op"`
// Idempotent overrides the connector's declared idempotency for this
// call (per ADR-0010). v1 recognizes the field but the runtime uses
// the connector's manifest-level declaration as the authoritative
// retry signal; per-call overrides are flagged for post-MVP retry tuning.
Idempotent *bool `toml:"idempotent"`
// Inputs is the keyed argument map passed to the connector op. Values
// may be string interpolations referencing call-time arguments
// (`${args.X}`) or the outputs of prior steps (`${step_id.field}`).
Inputs map[string]any `toml:"inputs"`
}
ExecuteStep is one step in the action's execution chain. The runtime invokes steps in declared order; on the first step failure the action terminates and returns the failing step's structured error (per ADR-0010). Successful prior steps are not auto-rolled-back.
type Executor ¶
type Executor interface {
Execute(ctx context.Context, name string, args map[string]any) (Result, error)
}
Executor runs an installed action with the given call-time arguments and returns a Result whose Content becomes the tool-result message the LLM observes.
Per ADR-0010, action-side failures are returned as Results carrying a non-nil failure.Failure (mapped to ADR-0010's structured envelope) rather than as Go errors — the LLM sees the failure as a tool result and can decide how to proceed (retry, fall back, ask the user). A returned `error` is reserved for gateway-fatal conditions that should terminate the conversation turn (e.g. action not found, executor misconfigured).
type FailureClass ¶
type FailureClass string
FailureClass is the closed-set classification of failures per ADR-0010. Adding a new class requires an ADR amendment.
const ( // ClassParseError is a structured parse failure for an action manifest. // Not part of the ADR-0010 closed taxonomy directly; surfaces during // load before any execution path runs. ClassParseError FailureClass = "parse_error" // ClassValidationError is a structured validation failure for an action // manifest (missing required field, invalid FQN, etc.). ClassValidationError FailureClass = "validation_error" // ClassCapabilityDenied is the canonical class from ADR-0010 for a // capability-subset refusal at the action boundary. ClassCapabilityDenied FailureClass = "capability_denied" // ClassNotFound names an action lookup that did not match any installed // action by name. ClassNotFound FailureClass = "not_found" )
type FileStateStore ¶
type FileStateStore struct {
// contains filtered or unexported fields
}
FileStateStore persists per-action preferences as a single JSON file. Format:
{
"<action-name>": { "enabled": false },
...
}
A missing or empty file is treated as "no overrides" — every action returns the default state. Writes go via a temp-file + rename so a crashed write can never leave a partial file on disk.
func NewFileStateStore ¶
func NewFileStateStore(path string) (*FileStateStore, error)
NewFileStateStore opens or initializes the overlay file at path. A non-existent file is not an error — the store starts empty and the file is created on the first Set. Returns an error only when the file exists but cannot be read or parsed.
func (*FileStateStore) All ¶
func (s *FileStateStore) All() map[string]State
All implements [StateStore.All].
func (*FileStateStore) Get ¶
func (s *FileStateStore) Get(name string) State
Get implements [StateStore.Get].
type Input ¶
type Input struct {
// Name is the argument's identifier. Referenced in
// `[[execute]]` step `inputs` blocks as `${args.<name>}`.
Name string `toml:"name"`
// Type is the JSON Schema primitive: "string", "integer",
// "number", or "boolean". Object/array types are post-MVP.
Type string `toml:"type"`
// Required defaults to true when nil. Set to a non-nil pointer
// (`required = false` in TOML) to mark the argument optional.
// Pointer rather than bool so absence is distinguishable from
// an explicit false.
Required *bool `toml:"required"`
// Description becomes the field-level prose the LLM sees in the
// `parameters.properties[name].description` slot. Required.
Description string `toml:"description"`
}
Input is one declared call-time argument. The set of inputs maps directly to the JSON Schema `parameters` object the LLM sees when Aileron augments the agent's tool catalog with this action.
func (Input) IsRequired ¶
IsRequired reports whether the input is required, applying the default-true rule when the manifest left `required` unset.
type LoadResult ¶
type LoadResult struct {
// Errors holds one *Error per failed action file. Files that loaded
// successfully produce no entry.
Errors []*Error
}
LoadResult aggregates per-file failures from a Load call. A LoadResult with a non-empty Errors slice still represents a successful Load — the store contains the actions that did parse and validate. Callers decide whether to log, surface, or fail closed on the errors.
func (*LoadResult) HasErrors ¶
func (r *LoadResult) HasErrors() bool
HasErrors reports whether any per-file failures were recorded.
type LoadedAction ¶
LoadedAction pairs a parsed manifest with the absolute path of its source file on disk.
type Manifest ¶
type Manifest struct {
// Name is the bare local handle for the action (e.g. "ship-update").
// Per ADR-0003, the user owns the file post-install and chooses the name;
// FQN does not apply to the action's own identity.
Name string `toml:"name"`
// Version is a strict semantic version (e.g. "1.0.0").
Version string `toml:"version"`
// Source is a fully-qualified URI recording where the action template was
// copied from (e.g. "hub://aileron/ship-update@1.0.0"). Provenance only —
// not consulted at runtime.
Source string `toml:"source"`
// Requires holds dependency declarations.
Requires Requires `toml:"requires"`
// Match describes how the runtime matches agent intent to this action
// ([ADR-0008]). The shape evolves as intent matching matures; v1 only
// requires the `intent` phrase to be present.
//
// [ADR-0008]: https://docs.withaileron.ai/adr/0008-intent-matching
Match Match `toml:"match"`
// Inputs declares the call-time arguments the action accepts. Per
// [ADR-0003], inputs become the JSON Schema `parameters` object the
// LLM sees when Aileron exposes the action as a tool. Empty when the
// action takes no arguments.
//
// [ADR-0003]: https://docs.withaileron.ai/adr/0003-action-model
Inputs []Input `toml:"inputs"`
// Execute is the ordered list of connector operations the action runs.
// v1 actions are linear chains with first-failure-terminates semantics
// (per ADR-0010).
Execute []ExecuteStep `toml:"execute"`
// Approval declares whether invocations of this action require explicit
// user approval before the runtime executes the underlying connector
// ops. When `Approval.Required` is true, `POST /v1/actions/{name}/run`
// holds its HTTP response open while it queues an approval request to
// the orchestrator; the response unblocks only when the user approves
// (success path: action runs, normal result returned) or denies (the
// runtime returns a `binding_required`-style failure with class
// `approval_denied`). Absent or `Required: false` means the action
// runs immediately as before. Per the action-approval design (#418),
// the user surface is Aileron's webapp; the agent learns to surface
// the approval URL to the user via templated tool descriptions in
// `aileron-mcp`.
Approval *ApprovalPolicy `toml:"approval"`
// Body is the Markdown content following the closing `+++` delimiter.
// The first paragraph (or a designated section) is surfaced to the LLM
// as the function `description` when the action is exposed as a tool.
Body string `toml:"-"`
}
Manifest is the parsed contents of a single action file: TOML frontmatter fields plus the Markdown body.
func Parse ¶
Parse splits the +++-delimited TOML frontmatter from the Markdown body and returns the populated Manifest. `file` is the path to the source file (used for structured error reporting); `data` is the file contents.
The contract:
- The file MUST begin with a `+++` line (after optional leading blank lines or BOMs are tolerated).
- The frontmatter block MUST close with a line containing only `+++`.
- Everything between is parsed as TOML.
- Everything after is captured verbatim as the Markdown body.
Any deviation surfaces as a ClassParseError carrying the offending file and (when known) line. Validation of required fields is performed by Validate.
func (Manifest) ApprovalRequired ¶
ApprovalRequired reports whether the action's manifest declares `[approval] required = true`. Returns false when `Approval` is nil or `Required` is unset, matching the default-no-approval behavior of unannotated actions.
type Match ¶
type Match struct {
// Intent is the canonical natural-language phrase the runtime matches
// against agent requests when surfacing this action.
Intent string `toml:"intent"`
}
Match holds intent-matching metadata for the action. Shape stays minimal in v1; richer fields are added as ADR-0008 matures.
type Requires ¶
type Requires struct {
// Connectors is the list of `[[requires.connectors]]` entries.
Connectors []RequiresConnector `toml:"connectors"`
}
Requires is the `[requires]` table holding connector dependencies.
type RequiresConnector ¶
type RequiresConnector struct {
// Name is the connector's fully-qualified URI per ADR-0002
// (e.g. "github://aileron/slack", "hub://aileron/slack").
Name string `toml:"name"`
// Version is the exact pinned semantic version.
Version string `toml:"version"`
// Hash is the content hash of the connector binary plus its manifest
// (e.g. "sha256:abc123..."). Verified at install time.
Hash string `toml:"hash"`
// Capabilities is the action's declared subset of operations on the
// connector (e.g. ["chat:write", "channels:read"]). Empty means the
// action declares no capabilities and may not invoke the connector.
Capabilities []string `toml:"capabilities"`
}
RequiresConnector pins a single connector dependency: its fully-qualified URI name, exact version, content hash, and the subset of declared capabilities the action will exercise. The capability subset is enforced at the action boundary at runtime — calls outside the subset are denied even when the connector's manifest permits the operation.
type Result ¶
type Result struct {
// Content is the success payload. JSON the LLM can parse is the
// preferred shape, but plain prose is acceptable for actions
// whose output is naturally a sentence.
Content string
// Failure marks an action-side error per ADR-0010. Mutually
// exclusive with a populated Content; the gateway's intercept
// layer renders the failure into the provider-shaped tool-result
// the LLM sees.
Failure *failure.Failure
}
Result is the synthesized tool-result content surfaced to the LLM. Success and failure are mutually exclusive: when Failure is non-nil the result is an error and Content is unused; when Failure is nil the action succeeded and Content carries the payload (a JSON document or plain prose).
On the wire, Failure is rendered into the agent-visible tool-result shape via failure.ToOpenAIToolMessage / failure.ToAnthropicToolResult, so the LLM sees a stable structured envelope regardless of provider.
type SandboxExecutor ¶
type SandboxExecutor struct {
Actions *Store
Store *cstore.Store
Runtime sandbox.Runtime
// Bindings resolves the credentials connectors need at call time
// per ADR-0005 + ADR-0006. The store is keyed by (connector FQN,
// capability kind); the executor consults it on each step that
// references a connector with a declared `[capabilities.credential]`
// and threads the matching `credential.Resolver` into the sandbox
// Call. Nil disables the credential path — connectors that emit a
// `credential` field on their http_request envelope receive
// `binding_required` from the sandbox boundary.
Bindings binding.Store
// contains filtered or unexported fields
}
SandboxExecutor is the production Executor for issue #359. It composes the action Store (action lookup), the cstore cstore.Store (connector binary lookup by content hash), and the sandbox sandbox.Runtime (per-call WASM instantiation) into the full execution chain ratified by ADR-0003 and ADR-0005:
- Resolve the named action.
- For each `[[execute]]` step, locate the connector entry the step references in the action's `[[requires.connectors]]` block.
- Apply the action-boundary capability check (defense-in-depth per ADR-0003 / issue #359 acceptance #7).
- Read the connector's manifest.toml + binary from the content-addressed store at the entry's hash.
- Compile (cached by hash) and invoke a fresh sandbox.
- Aggregate per-step outputs into the Result.
First-failure-terminates per ADR-0010: the first step error returns a Result whose Failure is populated; later steps are not invoked. Successful prior steps are *not* rolled back (ADR-0010's "no auto-compensation" rule).
func NewSandboxExecutor ¶
func NewSandboxExecutor(actions *Store, store *cstore.Store, runtime sandbox.Runtime, bindings binding.Store) *SandboxExecutor
NewSandboxExecutor builds a SandboxExecutor with the supplied dependencies. Callers must Close it (which closes cached sandbox.Connectors) when finished; the runtime itself is not owned by the executor and must be closed separately. Bindings may be nil — the credential-mediation path is then disabled and any connector that emits a `credential` field will receive `binding_required` from the sandbox boundary.
func (*SandboxExecutor) Close ¶
func (e *SandboxExecutor) Close(ctx context.Context) error
Close releases any cached compiled connectors.
func (*SandboxExecutor) Execute ¶
func (e *SandboxExecutor) Execute(ctx context.Context, name string, args map[string]any) (res Result, err error)
Execute implements Executor.
Emits an `aileron.action.execute` span around the full call and a nested `aileron.connector.call` span around each step's connector invocation. Span attributes use the OTel-namespaced shape shared with the audit log (PR #452) so spans and audit events carry identical keys — single source of truth.
type State ¶
type State struct {
// Enabled controls whether the action is exposed to the LLM as a
// callable tool. False hides the action from MCP's tools/list and
// causes the daemon to refuse direct calls to RunAction.
//
// Stored via a pointer so the on-disk overlay can omit the field
// entirely and let the default (`Enabled() == true`) apply. Callers
// should use [State.IsEnabled] rather than reading the pointer
// directly.
Enabled *bool `json:"enabled,omitempty"`
}
State holds per-action user preferences that live outside the manifest because they are user preferences rather than properties of the action itself. The overlay survives re-install (which may overwrite the manifest) and hand-edits to the manifest.
New fields added here should default to the value a freshly-installed action would receive — readers must never need to mutate the overlay just to get sensible behavior for an action with no overlay entry.
type StateStore ¶
type StateStore interface {
// Get returns the overlay entry for the action. A miss returns the
// zero value (which IsEnabled-reports true), so callers may read
// unconditionally without a separate "exists" check.
Get(name string) State
// Set persists the overlay entry for the action, replacing any
// prior value. Concurrent Sets are serialized; the final on-disk
// view reflects the last write to land.
Set(name string, s State) error
// All returns a snapshot of every recorded overlay entry, keyed by
// action name. Actions with no overlay entry are absent — callers
// must still treat absence as "default state".
All() map[string]State
}
StateStore reads and writes per-action user preferences. Implementations must be safe for concurrent use by daemon goroutines (HTTP handlers, reloads, background workers).
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is a thread-safe in-memory index of installed actions, keyed by `Manifest.Name`.
The store is the runtime's view of `~/.aileron/actions/` after a successful `Load`. Subsequent reloads replace the index atomically.
func (*Store) Get ¶
func (s *Store) Get(name string) (LoadedAction, error)
Get returns the loaded action for the given name. A miss returns ErrNotFound.
func (*Store) List ¶
func (s *Store) List() []LoadedAction
List returns all loaded actions, sorted by name.
func (*Store) Load ¶
func (s *Store) Load() (*LoadResult, error)
Load walks the store's directory and parses every `*.md` file it finds as an action manifest. Per-file errors are aggregated into a LoadResult so callers can surface every failure in a single pass rather than only the first.
A non-existent directory is not an error — it simply yields an empty store. The action surface is optional; users with no installed actions should not see startup failures.
type StubExecutor ¶
type StubExecutor struct{}
StubExecutor returns a placeholder JSON result describing what would have been executed. Retained for tests that don't need real connector execution; production wiring uses SandboxExecutor.