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
.aagentformat. Alexandria has a single unified package format —.atool— and thekindfield in the manifest distinguishes tools, skills, and agents. Agents ship as.atoolfiles with"kind": "agent".Manifests use schema v2. Schema v1 packages (with
llm-runtime,llm-backend,bundlekinds, or top-levelmodel/model_hintfields) are rejected by the installer with a clear migration error — runalex-sdk migrateto 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:
| Field | Type | Required | Notes |
|---|---|---|---|
schema_version | string | yes | Always "2". |
name | string | yes | "vendor/name" or just "name". |
version | string | yes | SemVer. |
kind | string | yes | One of tool, skill, agent. |
description | string | yes | |
config | object | yes | Shape depends on kind — see below. |
author | string | no | |
license | string | no | |
requires_alexandria | string | no | Minimum Alexandria version. |
dependencies | array | no | [{ "name": "...", "version": "..." }] — version is required. |
files | array | no | Files included; see §2.1. |
permissions | object | no | provides_tools, needs_tools, suggested_role. |
signature | object | no | Ed25519 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::Tool → ToolConfig):
"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::Skill → SkillConfig):
"config": {
"kind": "skill",
"system_prompt": "...",
"allowed_tools": ["..."],
"llm": "claude-opus-4-7", // Option<String>: null = orchestrator default
"tags": ["research", "writing"]
}
Agent (Kind::Agent → AgentConfig):
"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"
}
}
}
llmfield rules:nullmeans no preference — the orchestrator picks. During flatten, the root agent'sllmalways wins (includingnull), 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 beRefComponent(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:
- Reads
mytool-src/atool.json. - Reads each
files[].archive_path, computes SHA-256, writes it back into the manifest entry. - Emits a gzipped tar archive with
atool.jsonfirst, then each declared file.
3.4 Verify (optional, recommended)
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 markedexecutableget0o755. - For tool-kind packages only, writes a sentinel copy of
atool.jsonto<tools_dir>/<short_name>/atool.jsonsoalexandria tool start/stop/statuscan recognise the tool as Alexandria-managed.<short_name>is the segment after the/invendor/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_promptinAgentConfigis a string — if your prompt is large, keep it in a file inside the archive (e.g.prompts/system.md), declare it underfiles[], and have your loader read it from the install path. The built-inAgentConfigcurrently 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_winskeeps only the root's prompt;error_on_conflictaborts the install if any child differs.allowed_tools:union(default) takes the set-union and deduplicates;root_winskeeps only the root's list.llmandhistory_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 inallowed_toolsdirectly.
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
| What | Where |
|---|---|
| Pack / verify / install | crates/alex-package/src/lib.rs |
| Manifest types | crates/alex-package/src/manifest.rs |
| Flatten / runtime layer | crates/alex-package/src/runtime.rs |
| Signing / verification | crates/alex-package/src/sign.rs |
alexandria install | crates/alex-cli/src/commands/install.rs |
alexandria tool … | crates/alex-cli/src/commands/tool.rs |
| Round-trip test | crates/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.