ExAthena.ToolContext (ExAthena v0.18.0)

Copy Markdown View Source

Context handed to every tool execution.

Carries the working directory the loop should treat as root, the current permission mode, the session id (if any), and a free-form assigns map for custom tools to stash arbitrary data the consumer needs (project id, conversation id, ticket id — whatever the host cares about).

Tools that don't care about context can ignore it, but cwd and phase are load-bearing for any tool that touches the filesystem or checks permissions.

Summary

Functions

Whether this context confines tools to allowed_roots.

Build a context. :cwd is required; everything else defaults.

Resolve a user-supplied path against ctx.cwd.

Whether an absolute path lies within one of roots. Compares on path segments (not string prefixes) so /foo never matches /foobar. A path equal to a root counts as inside it.

Types

phase()

@type phase() :: :plan | :default | :accept_edits | :trusted | :bypass_permissions

t()

@type t() :: %ExAthena.ToolContext{
  allowed_roots: [Path.t()] | nil,
  assigns: map(),
  cwd: Path.t(),
  phase: phase(),
  session_id: String.t() | nil,
  tool_call_id: String.t() | nil
}

Functions

confined?(tool_context)

@spec confined?(t()) :: boolean()

Whether this context confines tools to allowed_roots.

new(opts)

@spec new(keyword()) :: t()

Build a context. :cwd is required; everything else defaults.

resolve_path(ctx, path)

@spec resolve_path(t(), String.t()) :: {:ok, Path.t()} | {:error, term()}

Resolve a user-supplied path against ctx.cwd.

Unconfined (allowed_roots: nil, the default) keeps the historical behaviour: relative paths are expanded against cwd, .. traversal is rejected, and absolute paths pass through untouched.

Confined (allowed_roots is a list) expands the path to an absolute one (handling ../~ lexically), resolves symlinks (see within_roots?/2), and requires the canonical result to fall inside one of the roots, returning {:error, {:path_outside_roots, abs}} otherwise — so neither /etc/passwd, ../../secret, nor an in-root symlink pointing outside can escape.

within_roots?(path, roots)

@spec within_roots?(Path.t(), [Path.t()]) :: boolean()

Whether an absolute path lies within one of roots. Compares on path segments (not string prefixes) so /foo never matches /foobar. A path equal to a root counts as inside it.

Both path and roots are canonicalized first — every existing symlink component is resolved to its real target — so a symlink placed inside a root but pointing outside does not pass, while symlinks that stay inside the roots (and roots that are themselves symlinks, e.g. macOS /tmp/private/tmp) keep working. Symlink loops count as outside.