.atool Manifest Specification — v2
An .atool file is a gzip-compressed tar archive. The archive must contain a
file named atool.json at its root. All other files referenced in the manifest
must also be present in the archive.
Schema v2 is the current and only supported schema. A package declaring
"schema_version": "1" is rejected with a clear migration error — run
alex-sdk migrate to upgrade.
What changed from v1
| Area | v1 | v2 |
|---|---|---|
Valid kind values | tool, agent, skill, llm-runtime, llm-backend, bundle | tool, skill, agent only |
| Agent LLM hint | model: String | llm: Option<String> |
| Skill LLM hint | model_hint: String | llm: Option<String> |
| Agent composition | not modeled | components: [], install.flatten |
| Tool default port | default_port: u16 (0 = unset) | default_port: Option<u16> (absent = unset; 0 is a validation error) |
| Dependency version | optional | required — no #[serde(default)] |
| Signature fields | top-level | nested under a Signature block |
llm-runtime, llm-backend, and bundle were removed deliberately. LLM
endpoints are registered via alexandria llm add, not installed as packages.
Bundles are replaced by Agent composition via components.
Archive layout
{name}_{version}_{arch}.atool (*.tar.gz)
├── atool.json # manifest — always at root
├── bin/<binary> # tool kinds only
└── components/<name>/... # inline component files (agent kinds)
Top-level manifest fields
| Field | Type | Required | Description |
|---|---|---|---|
schema_version | string | yes | Must be "2". "1" → migration error. |
name | string | yes | Package identifier, e.g. "vendor/name" or "name". |
version | string | yes | Semantic version string, e.g. "1.2.0". |
kind | string | yes | One of tool, skill, agent. |
description | string | yes | Human-readable one-line summary. |
author | string | no | Author name or email. |
license | string | no | SPDX license identifier, e.g. "MIT". |
requires_alexandria | string | no | Minimum Alexandria version. |
dependencies | Dependency[] | no | Other .atool packages this one needs. |
config | Config | yes | Kind-specific configuration (discriminated union). |
files | FileEntry[] | no | Files included in the archive. |
permissions | Permissions | no | Permissions this package requests. |
signature | Signature | no | Ed25519 signing block. Absent = unsigned. |
Dependency
{ "name": "vendor/other-tool", "version": ">=1.0.0" }
| Field | Type | Description |
|---|---|---|
name | string | Package name. |
version | string | Required — version constraint, free-form, checked by the installer. |
Config (discriminated union)
The config object is tagged by an inner kind field that must match the
top-level kind.
kind: "tool"
{
"kind": "tool",
"binary": "bin/my-server",
"default_port": 7800,
"transport": "http",
"args": ["--verbose"],
"k8s_image": "registry.example.com/my-tool:1.2.0",
"k8s_capabilities": ["fs.read", "net.outbound"],
"k8s_port": 9000,
"k8s_transport": "grpc",
"k8s_resources": {
"requests": { "cpu": "100m", "memory": "128Mi" },
"limits": { "cpu": "500m", "memory": "512Mi" }
},
"k8s_min_warm": 0,
"k8s_idle_timeout_seconds": 300
}
| Field | Type | Default | Description |
|---|---|---|---|
binary | string | — | Relative path to the server binary inside the archive. |
default_port | u16? | null | MCP server port. null = unset. 0 is a validation error. |
transport | string | "http" | "http" or "sse". |
args | string[] | [] | Extra arguments forwarded to the binary. |
k8s_image | string | "" | OCI image URI for the managed k8s Deployment. Empty = not k8s-deployable. |
k8s_capabilities | string[] | [] | Capability tags advertised by the tool. |
k8s_port | u16 | 9000 | Container port. |
k8s_transport | string | "grpc" | "grpc", "http", or "sse". |
k8s_resources.requests | {cpu, memory} | {} | k8s CPU/memory requests. |
k8s_resources.limits | {cpu, memory} | {} | k8s CPU/memory limits. |
k8s_min_warm | u32 | 0 | Minimum replicas. 0 enables scale-to-zero (v1.1+). |
k8s_idle_timeout_seconds | u32 | 300 | Idle despawn budget. |
A tool is k8s-deployable iff k8s_image is non-empty.
kind: "skill"
{
"kind": "skill",
"system_prompt": "Summarise the following text in three bullets.",
"allowed_tools": ["fs"],
"llm": "claude-haiku-4-5",
"tags": ["summarisation", "text"]
}
| Field | Type | Default | Description |
|---|---|---|---|
system_prompt | string | — | Prompt template for the skill. |
allowed_tools | string[] | [] | Tools the skill may use. |
llm | string? | null | Preferred LLM id. null = orchestrator default. Replaces model_hint. |
tags | string[] | [] | Searchable tags. |
kind: "agent"
{
"kind": "agent",
"system_prompt": "You are a research assistant.",
"allowed_tools": ["web", "fs"],
"llm": "claude-opus-4-7",
"history_limit": 40,
"components": [ /* see below */ ],
"install": {
"flatten": {
"system_prompt": "concat",
"allowed_tools": "union"
}
}
}
| Field | Type | Default | Description |
|---|---|---|---|
system_prompt | string | "" | System prompt injected on every request. |
allowed_tools | string[] | [] | MCP tool names this agent may call. |
llm | string? | null | Preferred LLM id. null = orchestrator default. |
history_limit | u32 | 0 | Message history window (0 = server default). |
components | Component[]? | null | Sub-agents / sub-skills (composition tree). |
install | InstallBlock? | null | Install-time composition rules. |
Component (agents only)
Component is an untagged union — the installer disambiguates by shape.
InlineComponent
An embedded sub-agent or sub-skill defined directly in the parent manifest.
{
"name": "summariser",
"id": "acme/summariser@0.1.0",
"kind": "skill",
"config": {
"kind": "skill",
"system_prompt": "Summarise in three bullets.",
"allowed_tools": ["fs"],
"llm": null,
"tags": ["summarisation"]
}
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Local name for prompt headers and merging. |
id | string | yes | Fully-qualified id "ns/name@version" used for dedup/coalescing. |
kind | "agent" | "skill" | yes | Tools cannot be inline — use RefComponent instead. |
config | Config | yes | Same shape as the top-level config. |
components | Component[]? | no | Nested components (recursive). |
files | FileEntry[]? | no | Files this component contributes. |
permissions | Permissions? | no | Permissions this component contributes. |
dependencies | Dependency[]? | no | Component-local dependencies. |
Validation: an inline component with
kind: "tool"is rejected byAToolManifest::validate(). Tools must always beRefComponent.
RefComponent
A reference to an externally installed component.
{ "ref": "vendor/web-tool@1.0.0" }
| Field | Type | Description |
|---|---|---|
ref | string | "ns/name@version" — must be already installed (or fetchable). |
InstallBlock — flatten composition
When installed with --mode=flatten, the installer collapses an agent's
component tree into a single composed agent. The flatten rules govern how
fields merge.
{
"flatten": {
"system_prompt": "concat",
"allowed_tools": "union"
}
}
system_prompt merge modes
| Mode | Behaviour |
|---|---|
concat (default) | Concatenate all prompts with ## <component_name> headers. Root's block is last. |
root_wins | Use root's prompt only; discard children's. |
error_on_conflict | Abort install if any child has a different system prompt. |
allowed_tools merge modes
| Mode | Behaviour |
|---|---|
union (default) | Set-union across the tree, deduplicated. |
root_wins | Use root's allowed_tools only. |
Implicit rules
llm— alwaysroot_wins. A root withllm: nulloverrides children's preferences (the package author's deliberate flexibility is preserved).history_limit— alwaysroot_wins.- 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).
FileEntry
{
"archive_path": "bin/my-server",
"install_path": "tools/my-server",
"executable": true,
"sha256": "abc123..."
}
| Field | Type | Description |
|---|---|---|
archive_path | string | Path inside the archive (relative). |
install_path | string | Destination relative to the Alexandria data root. |
executable | bool | Set 0o755 on install (Unix). |
sha256 | string | SHA-256 hex digest — populated by alexandria pack, checked by verify. Empty = skipped. |
Permissions
{
"provides_tools": ["my-server"],
"needs_tools": ["fs", "web"],
"suggested_role": "worker"
}
| Field | Type | Description |
|---|---|---|
provides_tools | string[] | MCP tool names this package exposes (tool kinds). |
needs_tools | string[] | MCP tool names this package needs (agent/skill kinds — requires admin approval). |
suggested_role | string | worker (default), coordinator, or supervisor. |
Signature
Ed25519 signing block. Absent = unsigned package. Installs in --require-signed
mode reject unsigned packages.
{
"alg": "ed25519",
"key_fingerprint": "a1b2c3d4e5f6a7b8",
"value": "hex-encoded-signature-bytes",
"scope": "bundle"
}
| Field | Type | Description |
|---|---|---|
alg | string | Always "ed25519" (currently the only supported algorithm). |
key_fingerprint | string | First 8 bytes of SHA-256 of the public key, hex-encoded. |
value | string | Hex-encoded signature bytes over the canonical signing payload. |
scope | "bundle" | "per-component" | bundle signs the whole archive; per-component signs each component file individually. |
The signing payload is built deterministically from manifest digests and is independent of field ordering, so the same package always produces the same signature input.
Complete examples
Standalone MCP tool
{
"schema_version": "2",
"name": "acme/pdf-parser",
"version": "0.3.1",
"kind": "tool",
"description": "Extract text and metadata from PDF files",
"author": "Acme Corp",
"license": "MIT",
"requires_alexandria": "1.0.0",
"dependencies": [],
"config": {
"kind": "tool",
"binary": "bin/pdf-parser",
"default_port": 7800,
"transport": "http",
"args": []
},
"files": [
{
"archive_path": "bin/pdf-parser",
"install_path": "tools/pdf-parser",
"executable": true,
"sha256": ""
}
],
"permissions": {
"provides_tools": ["pdf-parser"],
"needs_tools": [],
"suggested_role": "worker"
}
}
k8s-deployable tool
{
"schema_version": "2",
"name": "acme/pdf-parser",
"version": "0.3.1",
"kind": "tool",
"description": "Extract text and metadata from PDF files",
"config": {
"kind": "tool",
"binary": "bin/pdf-parser",
"transport": "http",
"k8s_image": "registry.example.com/acme/pdf-parser:0.3.1",
"k8s_capabilities": ["fs.read"],
"k8s_port": 9000,
"k8s_transport": "grpc",
"k8s_resources": {
"requests": { "cpu": "100m", "memory": "128Mi" },
"limits": { "cpu": "500m", "memory": "512Mi" }
},
"k8s_min_warm": 0,
"k8s_idle_timeout_seconds": 300
},
"files": [],
"permissions": { "provides_tools": ["pdf-parser"] }
}
Standalone skill
{
"schema_version": "2",
"name": "acme/summarise",
"version": "0.1.0",
"kind": "skill",
"description": "Summarise a document in three bullet points",
"config": {
"kind": "skill",
"system_prompt": "Summarise the following text in exactly three concise bullet points.",
"allowed_tools": [],
"llm": null,
"tags": ["summarisation", "text"]
},
"files": [],
"permissions": {}
}
Composed agent with sub-skills and a ref'd tool
{
"schema_version": "2",
"name": "acme/researcher",
"version": "1.0.0",
"kind": "agent",
"description": "Research assistant with web access and a summariser sub-skill",
"dependencies": [
{ "name": "acme/web-tool", "version": ">=1.0.0" }
],
"config": {
"kind": "agent",
"system_prompt": "You are a research assistant. Delegate summarisation to the sub-skill.",
"allowed_tools": ["web"],
"llm": "claude-opus-4-7",
"history_limit": 40,
"components": [
{
"name": "summariser",
"id": "acme/summariser@0.1.0",
"kind": "skill",
"config": {
"kind": "skill",
"system_prompt": "Summarise in three bullets.",
"allowed_tools": [],
"llm": null,
"tags": ["summarisation"]
}
},
{ "ref": "acme/web-tool@1.0.0" }
],
"install": {
"flatten": {
"system_prompt": "concat",
"allowed_tools": "union"
}
}
},
"files": [],
"permissions": {
"provides_tools": [],
"needs_tools": ["web"],
"suggested_role": "coordinator"
}
}
Integrity verification
When a package is packed with alexandria pack, the sha256 field of each
FileEntry is computed automatically. alexandria install calls verify
before extracting, which re-hashes every file and rejects any archive where
the computed digest does not match the manifest value.
Files with an empty sha256 field are skipped during verification (useful for
generated or dynamic files).
When a signature block is present, verify also checks the Ed25519
signature against the signing payload and the embedded key fingerprint.
alexandria install --require-signed rejects unsigned packages outright.
Validation rules
AToolManifest::validate() enforces the following beyond what serde
catches:
schema_versionmust be"2"."1"→ clear migration error. Anything else → error.- Inline components with
kind: "tool"are rejected. Tools must always beRefComponent. ToolConfig.default_port == Some(0)is rejected. Usenullto mean "unset".Dependency.versionis required (no default).
Versioning policy
schema_versionis incremented only for breaking changes.- Fields marked optional (
#[serde(default)]) will not be removed without a version bump. - New optional fields may be added at any time without incrementing the version.