Skip to main content

Creating .atool Packages (Tools and Agents)

Practical guide to authoring Alexandria packages. Grounded in the code in crates/alex-package/ and crates/alex-cli/src/commands/install.rs.

There is no separate .aagent format. Alexandria has a single unified package format — .atool — and the kind field in the manifest distinguishes tools, skills, and agents. Agents ship as .atool files with "kind": "agent".

Manifests use schema v2. Schema v1 packages (with llm-runtime, llm-backend, bundle kinds, or top-level model / model_hint fields) are rejected by the installer with a clear migration error — run alex-sdk migrate to upgrade.


1. On-disk format

An .atool file is a gzipped tar archive with atool.json at the root.

mytool-0.1.0.atool # gzipped tar
├── atool.json # required manifest, JSON
├── bin/mytool # files referenced by manifest.files[]
├── share/schema.proto # optional supporting files
└── ...

Source of truth: crates/alex-package/src/lib.rs (pack, verify, install).

Naming convention (informal — the installer does not enforce it):

{name}-{version}.atool
{name}_{version}_{arch}.atool # if the binary is arch-specific

2. The atool.json manifest

Defined in crates/alex-package/src/manifest.rs. Minimum required keys:

FieldTypeRequiredNotes
schema_versionstringyesAlways "2".
namestringyes"vendor/name" or just "name".
versionstringyesSemVer.
kindstringyesOne of tool, skill, agent.
descriptionstringyes
configobjectyesShape depends on kind — see below.
authorstringno
licensestringno
requires_alexandriastringnoMinimum Alexandria version.
dependenciesarrayno[{ "name": "...", "version": "..." }]version is required.
filesarraynoFiles included; see §2.1.
permissionsobjectnoprovides_tools, needs_tools, suggested_role.
signatureobjectnoEd25519 signing block; absent = unsigned.

2.1 files[] entries

{
"archive_path": "bin/mytool", // path inside the tarball
"install_path": "tools/mytool", // where to extract, relative to data_dir
"executable": true, // applies 0o755 on Unix
"sha256": "" // pack() fills this in automatically
}

pack() computes and overwrites the SHA-256 for every declared file at pack time. verify() re-checks them on the install path.

2.2 config shape per kind

The config object is { "kind": "<same-as-top-level>", ...fields }. The nested kind is the serde tag from Config (alias PackageConfig).

Tool (Kind::ToolToolConfig):

"config": {
"kind": "tool",
"binary": "bin/mytool", // required, relative to archive root
"default_port": 7800, // Option<u16>: null = unset, 0 = error
"transport": "http", // "http" | "sse"
"args": ["--flag"],

// Optional k8s tier — only meaningful when k8s_image is non-empty.
"k8s_image": "registry.example.com/mytool:1.2.3",
"k8s_capabilities": ["fs.read", "net"],
"k8s_port": 9000, // default 9000
"k8s_transport": "grpc", // "grpc" | "http" | "sse"; default "grpc"
"k8s_resources": {
"requests": { "cpu": "100m", "memory": "128Mi" },
"limits": { "cpu": "500m", "memory": "512Mi" }
},
"k8s_min_warm": 1, // 0 = scale-to-zero (planned)
"k8s_idle_timeout_seconds": 300
}

Skill (Kind::SkillSkillConfig):

"config": {
"kind": "skill",
"system_prompt": "...",
"allowed_tools": ["..."],
"llm": "claude-opus-4-7", // Option<String>: null = orchestrator default
"tags": ["research", "writing"]
}

Agent (Kind::AgentAgentConfig):

"config": {
"kind": "agent",
"system_prompt": "You are ...",
"allowed_tools": ["pdf-parser", "web-search"],
"llm": "claude-opus-4-7", // Option<String>: null = orchestrator default
"history_limit": 50,

// Composition — sub-agents, sub-skills, and ref'd tools.
"components": [
{
"name": "summariser",
"id": "acme/summariser@0.1.0",
"kind": "skill",
"config": { "kind": "skill", "system_prompt": "...", "llm": null }
},
{ "ref": "acme/web-search@1.0.0" }
],
"install": {
"flatten": {
"system_prompt": "concat", // "concat" | "root_wins" | "error_on_conflict"
"allowed_tools": "union" // "union" | "root_wins"
}
}
}

llm field rules: null means no preference — the orchestrator picks. During flatten, the root agent's llm always wins (including null), so sub-component preferences never override the package author's choice.

Components: inline components with kind: "tool" are rejected at validate time. Tools must always be RefComponent (the { "ref": "..." } form). Only sub-agents and sub-skills can be inline.


3. Authoring a tool package

3.1 Directory layout

Lay out a source directory matching the archive_paths you will declare:

mytool-src/
├── atool.json
└── bin/
└── mytool # the daemon binary

3.2 atool.json

{
"schema_version": "2",
"name": "acme/mytool",
"version": "0.1.0",
"kind": "tool",
"description": "Example MCP tool daemon",
"author": "ACME",
"license": "Apache-2.0",
"config": {
"kind": "tool",
"binary": "bin/mytool",
"default_port": 7800,
"transport": "http",
"args": []
},
"files": [
{
"archive_path": "bin/mytool",
"install_path": "tools/mytool/bin/mytool",
"executable": true
}
],
"permissions": {
"provides_tools": ["mytool"],
"suggested_role": "worker"
}
}

3.3 Pack the archive

There is no alexandria pack CLI subcommand today. The packing API lives in alex_package::pack. To produce a .atool from a layout dir, drive it from a small Rust binary or a test, e.g.:

use std::path::Path;

fn main() -> anyhow::Result<()> {
alex_package::pack(
Path::new("./mytool-src"),
Path::new("./mytool-0.1.0.atool"),
)?;
Ok(())
}

pack() does three things:

  1. Reads mytool-src/atool.json.
  2. Reads each files[].archive_path, computes SHA-256, writes it back into the manifest entry.
  3. Emits a gzipped tar archive with atool.json first, then each declared file.
let manifest = alex_package::verify(Path::new("./mytool-0.1.0.atool"))?;
println!("{} {}", manifest.name, manifest.version);

verify() extracts to a tempdir, re-parses atool.json, and re-hashes every file with a non-empty sha256. Mismatch ⇒ bail!.

3.5 Install

alexandria install ./mytool-0.1.0.atool

What this does (crates/alex-cli/src/commands/install.rs):

  • Calls alex_package::verify() on the archive.
  • Calls alex_package::install(), which extracts every declared file to <data_dir>/<install_path>. Files marked executable get 0o755.
  • For tool-kind packages only, writes a sentinel copy of atool.json to <tools_dir>/<short_name>/atool.json so alexandria tool start/stop/status can recognise the tool as Alexandria-managed. <short_name> is the segment after the / in vendor/name.

You can also install from a registry:

alexandria install acme/mytool@0.1.0 --registry https://registry.alexandria.dev

Resolution order for the registry URL: --registry flag → REGISTRY_URL env → config.registries[].url (first enabled) → default https://registry.alexandria.dev.

3.6 Run

alexandria tool start mytool # spawns config.binary, writes pid file
alexandria tool status
alexandria tool stop mytool

tool start looks up <tools_dir>/mytool/atool.json, resolves the binary at <tools_dir>/mytool/<config.binary>, spawns it with config.args, and waits up to ~4 s for <run_dir>/mytool.sock to appear.


4. Authoring an agent package

Same archive shape. The only differences are kind, config, and that there is no binary to run (agents are config, not daemons).

myagent-src/
├── atool.json
└── prompts/
└── system.md # optional, if you want the prompt as a file

atool.json:

{
"schema_version": "2",
"name": "acme/research-agent",
"version": "0.1.0",
"kind": "agent",
"description": "Research assistant agent",
"config": {
"kind": "agent",
"system_prompt": "You are a senior research assistant. ...",
"allowed_tools": ["web-search", "pdf-parser"],
"llm": "claude-opus-4-7",
"history_limit": 100
},
"permissions": {
"needs_tools": ["web-search", "pdf-parser"],
"suggested_role": "coordinator"
}
}

Pack and install identically to a tool. The installer does not write a sentinel for agent-kind packages — only tools get the tools_dir/<name>/atool.json sentinel because only tools have a daemon lifecycle.

Tools listed in permissions.needs_tools require admin approval before the agent can actually use them; this is surfaced in the install summary.

The system_prompt in AgentConfig is a string — if your prompt is large, keep it in a file inside the archive (e.g. prompts/system.md), declare it under files[], and have your loader read it from the install path. The built-in AgentConfig currently stores the prompt inline.


5. Composing agents (replaces v1 bundles)

Schema v2 removes the bundle kind. Multi-component packages are expressed instead as an agent with components — the agent is the root, and sub-agents, sub-skills, and ref'd tools are its children.

{
"schema_version": "2",
"name": "acme/data-pack",
"version": "1.0.0",
"kind": "agent",
"description": "Research agent that delegates to a summariser sub-skill and uses a ref'd PDF tool",
"dependencies": [
{ "name": "acme/pdf-parser", "version": ">=0.1.0" }
],
"config": {
"kind": "agent",
"system_prompt": "You research and summarise.",
"allowed_tools": ["pdf-parser"],
"llm": "claude-opus-4-7",
"history_limit": 50,
"components": [
{
"name": "summariser",
"id": "acme/summariser@0.1.0",
"kind": "skill",
"config": {
"kind": "skill",
"system_prompt": "Summarise in three bullets.",
"llm": null,
"tags": ["summarisation"]
}
},
{ "ref": "acme/pdf-parser@0.1.0" }
],
"install": {
"flatten": {
"system_prompt": "concat",
"allowed_tools": "union"
}
}
}
}

Flatten install (--mode=flatten)

When installed with --mode=flatten, the installer collapses the component tree into a single composed agent:

  • system_prompt: concat (default) joins each component's prompt under a ## <component_name> header, with the root's block last; root_wins keeps only the root's prompt; error_on_conflict aborts the install if any child differs.
  • allowed_tools: union (default) takes the set-union and deduplicates; root_wins keeps only the root's list.
  • llm and history_limit: always root-wins. Sub-component preferences are discarded.
  • Delegate tools: every inline sub-agent generates a synthetic delegate.<name> tool the root can dispatch to. Ref components do not generate delegate tools — they're external and the root must list them in allowed_tools directly.

The flatten layer also detects dependency conflicts across components: if two sub-trees pin incompatible versions of the same dependency, the install aborts with a clear error.


6. Reference

WhatWhere
Pack / verify / installcrates/alex-package/src/lib.rs
Manifest typescrates/alex-package/src/manifest.rs
Flatten / runtime layercrates/alex-package/src/runtime.rs
Signing / verificationcrates/alex-package/src/sign.rs
alexandria installcrates/alex-cli/src/commands/install.rs
alexandria tool …crates/alex-cli/src/commands/tool.rs
Round-trip testcrates/alex-package/src/lib.rs pack_verify_install_roundtrip

If you change the manifest schema, update both the Rust types and any tools that emit atool.json. There is no separate schema file — the Rust types are the schema.