action

package
v0.0.0-...-b7a385f Latest Latest
Warning

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

Go to latest
Published: May 12, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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

func EnforceCapability(m *Manifest, connectorFQN, capability string) error

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").

func Validate

func Validate(m *Manifest, file string) error

Validate checks a parsed manifest against the schema described in ADR-0001 and ADR-0003. Returns a *Error on the first failure. Successive fields are checked in declared order so authors see one specific message at a time rather than a flood.

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.

const (
	// BoundaryAction names the action layer (manifest parse failures and
	// capability-subset denials).
	BoundaryAction Boundary = "action"
	// BoundaryRuntime names the surrounding runtime layer.
	BoundaryRuntime Boundary = "runtime"
)

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.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

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].

func (*FileStateStore) Set

func (s *FileStateStore) Set(name string, st State) error

Set implements [StateStore.Set].

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

func (i Input) IsRequired() bool

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

type LoadedAction struct {
	Manifest *Manifest
	Path     string
}

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

func Parse(file string, data []byte) (*Manifest, error)

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

func (m Manifest) ApprovalRequired() bool

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:

  1. Resolve the named action.
  2. For each `[[execute]]` step, locate the connector entry the step references in the action's `[[requires.connectors]]` block.
  3. Apply the action-boundary capability check (defense-in-depth per ADR-0003 / issue #359 acceptance #7).
  4. Read the connector's manifest.toml + binary from the content-addressed store at the entry's hash.
  5. Compile (cached by hash) and invoke a fresh sandbox.
  6. 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.

func (State) IsEnabled

func (s State) IsEnabled() bool

IsEnabled reports whether the action is enabled, applying the default-true rule when the overlay omitted the field.

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 NewStore

func NewStore(dir string) *Store

NewStore constructs an empty Store rooted at dir. Use Load to populate it.

func (*Store) Dir

func (s *Store) Dir() string

Dir returns the directory the store is rooted at.

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.

func (StubExecutor) Execute

func (StubExecutor) Execute(_ context.Context, name string, args map[string]any) (Result, error)

Execute returns a Result whose Content is a small JSON object summarising the call. Always succeeds; never returns a Go error.

Jump to

Keyboard shortcuts

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