Expand description
Armory Bundle Format (ABF) importer — Phase 2 of
docs/specs/SPEC_ABF_V0_1_SINGLE_FILE_AND_IMPORTER_2026_08_01.md.
Inverse of bundle_export.rs: takes a bundle’s files (either a raw
[{path, content}] list or an unpacked .abf zip archive) and
validates + parses them into data ready for the bundle.import RPC
handler to write to the Store.
Pure functions only — no I/O, no Store access, no account resolution
(account resolution needs db_accounts, which only the RPC handler has
access to; see the spec’s §4.5). Mirrors bundle_export.rs’s shape and
quality bar deliberately: the same “warn, don’t silently drop” and
“reject the whole thing on structural failure, but never partially
import on a per-entry problem” philosophy, since the two modules are
meant to round-trip against each other.
Structs§
- Account
Requirement - One
accounts/requirements.jsonentry — mirrors the shapebundle_export.rswrites (research report §5.3). - Bundle
Import File - One file read from an import source (zip or raw list), path relative
to the bundle root. Mirrors
bundle_export::BundleExportFile. - Imported
Context File - One
instructions/context/*file, in the[{path, content}]shapedb_bundles.context_filesstores. - Imported
Project Instruction - One
components.projectInstructionsentry, read back out of the bundle. - Parsed
Bundle Import - Parsed, validated bundle content ready for the RPC handler to resolve
accounts against and write to the Store.
mcp_serversstill contains whatever${VAR}-style placeholders the exporter’s redaction left in place — account resolution (§4.5, RPC-handler-side) substitutes real account bindings where exactly one match is found and leaves the rest untouched. - Parsed
McpServer - An MCP server parsed out of a
components.mcpServersreference — Phase 3 spec §3.0.source_pathis the stable, always-unique selection key (the manifest path reference, distinct from whatever"name"field happens to appear insideconfig’s arbitrary JSON, which has no uniqueness guarantee and isn’t even required to be present). - Parsed
Skill - A skill parsed out of a
skills/<slug>/SKILL.mdfile — the inverse ofagent_config::render_skill_md. - Warning
Budget - Bounds how many warnings a parse call accumulates, and how long each
individual warning string may be — Phase 3 spec
(
SPEC_ABF_IMPORT_UI_PHASE3_2026_08_02.md) §3.1, round 11: the cap must live at the point warnings are actually PRODUCED, not only at a later RPC-response-serialization boundary, or a hostile archive can still force the parser itself to build an unboundedVec<String>in memory before any cap ever runs.unbounded()preserves today’s existingbundle.importroute’s real, already-shipped behavior exactly — this is a capability the parser gains, not a behavior change forced onto Phase 2’s existing caller. - Warning
Sink 🔒 - Accumulates warnings against a
WarningBudget. Exposes.push(String)so every existingwarnings.push(format!(...))call site in this module keeps working unchanged after its binding’s type changes fromVec<String>to this — only construction (WarningSink::new) and extraction (.into_vec()) differ.
Enums§
- Import
Input Mode - Which of the three
bundle.import.preview/.commitinput fields produced a given payload — mixed into the content-digest hash domain itself (Phase 3 spec §3.0.5, round 7) so afile_pathpreview can never be satisfied by azip_base64commit of the identical underlying bytes (both canonicalize to the same raw zip bytes and would otherwise hash identically), closing the gap a bare byte-digest comparison left open.
Constants§
- LEGACY_
MANIFEST_ FILE - The manifest of ABF v0.1 and v0.2 archives, read forever.
- MANIFEST_
FILE - The manifest an ABF v0.3 archive carries (SPEC_AGENT_BUNDLE_FORMAT_V0_3_2026_10_05.md §2.1).
- MAX_
ACCOUNT_ 🔒REQUIREMENTS - Maximum number of account requirements accepted from a single bundle’s
accounts/requirements.json. A real bundle needs a handful at most — one per distinct external account it depends on. Exists to bound the RPC handler’s downstream per-provider account-lookup work (codex P1, PR #2379 round 4): an untrusted archive’s requirements array, unbounded, could otherwise drive a synchronous store query for every row. - MAX_
BUNDLE_ NAME_ CHARS - Maximum character length of a bundle’s manifest
name, enforced at parse time (Phase 3 spec §3.1, round 13) rather than at a later display boundary —nameis re-submitted verbatim asbundle_namewhen a user doesn’t edit the preview’s suggested value, so the canonical, bounded value must be what bothpreviewandcommitconverge on from the moment parsing completes. Generous for any real bundle name. - MAX_
DISPLAY_ FIELD_ CHARS - Fixed-character display cap shared by every “meant to be short” bounded
field this spec defines: skill
description/slug(rounds 10/12), requirementid/provider/env(round 11), context-filedisplay_path(round 13), and bundledescription(round 12 self-audit). - MAX_
ENTRY_ 🔒COUNT - Maximum number of entries an archive may contain. A legitimate bundle (instructions, a handful of context files, a handful of skills, one manifest) never comes close to this; it exists purely to bound the CPU cost of iterating + path-sanitizing + hashing every entry, independent of the per-entry/aggregate byte caps below (reagent P2, PR #2379 round 2) — a crafted archive of many tiny/valid entries passes both size caps while still forcing unbounded iteration work.
- MAX_
ENTRY_ 🔒UNCOMPRESSED_ BYTES - Per-entry decompressed-size cap. Generous for any legitimate instructions/SKILL.md/context file (10 MB of text is already an absurdly large single bundle component) while bounding how much a single malicious entry can force into memory.
- MAX_
IMPORTED_ PROJECT_ INSTRUCTIONS - Cap on how many
components.projectInstructionsentries are read back. - MAX_
IMPORTED_ SKILLS - Maximum number of skills actually imported (i.e. written to the Store)
from a single bundle.
MAX_ENTRY_COUNT/capped_component_arraybound how manycomponents.skillsentries are even LOOKED AT, but codex P1 (PR #2379 round 7) points out that’s still far too high a ceiling for the RPC handler’s write side: importing up to that many skills means up to that many separate synchronous Store transactions, each creating a permanent, globally-visible skill row — a compact malicious archive (well within the size caps) could otherwise monopolize the handler and pollute the installation’s skill catalog. A real bundle needs a handful to a few dozen skills at most. - MAX_
INSTRUCTIONS_ PREVIEW_ CHARS - Cap on
instructions_preview(Phase 3 spec §3.1, round 5) — generous for a glance-and-decide preview; bounds worst-case JSON-escaped response size to roughly 300 KB rather than the ~300 MB a maximally-adversarial, unboundedinstructionsstring could otherwise force. - MAX_
INSTRUCTION_ 🔒PROVIDER_ VARIANTS - Maximum number of distinct keys accepted in a v0.2
components.instructionsobject ("default"plus per-provider variants) — ABF v0.2 §2.2. A real bundle needs at most one entry per known harness (nine, as ofproviders.rs) plus"default"; generous headroom for future providers without leaving this unbounded, mirroring this module’s other component-count caps (e.g.MAX_ACCOUNT_REQUIREMENTS). - MAX_
MCP_ DISPLAY_ FIELD_ CHARS - Fixed-character display cap for an MCP server’s projected
name/command(Phase 3 spec §3.1, round 7) — smaller thanMAX_DISPLAY_FIELD_CHARSsince these are meant to be short identifiers/executable names, not free-form text. - MAX_
TOTAL_ 🔒UNCOMPRESSED_ BYTES - Aggregate decompressed-size cap across the whole archive. Bounds a
zip-bomb-style archive built from many medium-sized entries that would
each individually pass
MAX_ENTRY_UNCOMPRESSED_BYTES. - MEMORY_
COMPONENT_ 🔒IGNORED_ WARNING - PROJECT_
INSTRUCTIONS_ 🔒NOT_ APPLIED_ WARNING - The exact warning
parse_bundle_import_with_budgetpushes whencomponents.memoryis present — ABF v0.2 §2.3. A shared constant (rather than a literal duplicated at both the push site andbundle_import_for_agent_impl’s filter site, reagent P1 PR #2527) so the two can never drift apart: if this message ever changes, the filter inbundle.rskeeps working because it references the same constant, not a copy of the string. Warning for an ABF carryingcomponents.projectInstructions. - SCHEMA_
V0_ 3 - The
$schemaan ABF v0.3 manifest names.
Functions§
- abf_
format_ version - The ABF format version a manifest declares through its
$schema, orNonewhen it names none this importer knows. Theversionfield is the bundle’s own content version, not this. - bound_
bundle_ name - Bounds a bundle name to
MAX_BUNDLE_NAME_CHARSvia plain truncation (no ellipsis, unliketruncate_display) — this IS the canonical value, not a display abbreviation of a longer “real” one. Shared byparse_bundle_import’s own manifest-namebounding and the Phase 3bundle.import.commitRPC’sbundle_nameoverride (round 3), so a client-supplied override can’t bypass the same boundparsed.nameis already held to. - bounded_
instructions_ preview - Bounds
instructions_previewfor the RPC response — returns(preview, truncated, total_chars). The full, untruncatedinstructionsvalue is unaffected; this only bounds what’s echoed back for display (Phase 3 spec §3.1, rounds 5 and 8). - capped_
component_ 🔒array - Bounds a manifest component array’s length BEFORE any per-entry
processing happens, so one cap protects every warning a per-entry
loop can produce (non-string entries, unsafe paths, not-found
lookups, malformed content, …) at once — codex P1, PR #2379 round
7: round 6 bounded only the “duplicate reference” warning
specifically; a manifest filled with non-string junk values hit an
entirely different (still unbounded) warning path for the same
amplification effect. Reuses
MAX_ENTRY_COUNT— a manifest component array legitimately needs the same “more entries than any real bundle uses” ceiling a zip archive’s entry count does. - check_
entry_ 🔒size - Single choke point for the per-entry/aggregate size caps, shared by
BOTH intake paths (zip decompression and the raw
filesRPC list) — reagent P1 / codex P1, PR #2379 round 5. Two separate gaps motivated unifying this instead of patching each path independently: - classify_
skill_ collision "none"/"name_conflict"/"duplicate_in_bundle"— Phase 3 spec §3.1’s two-pass skill collision classification.global_slugsis whatever the caller already fetched fromskill.catalog.list’s underlyingskill_list_global()(Store access lives in the RPC handler, not here — this stays a pure function so it’s testable with a seeded fake global-skill list, per the spec’s own §6 testing notes). Pass 1 (global catalog) takes priority over pass 2 (intra-bundle duplicate) when both apply, matching §3.1’s stated precedence.- content_
digest_ files - SHA-256 content digest for
files-mode input (§3.0.5, round 6) — hashesparse_bundle_import’s own effective, order-resolved representation (dedup_files_by_path’s normalize-then-first-wins reduction, sorted by normalized key), not the raw input array. This makes the digest order-independent for genuinely equivalent inputs while remaining sensitive to any reordering that would actually change which entry the parser’s first-wins rule keeps. - content_
digest_ raw_ bytes - SHA-256 content digest for
file_path/zip_base64input — both canonicalize to the same thing (raw zip bytes), differentiated only by the mode tag mixed into the hash domain (§3.0.5, round 7). - dedup_
files_ 🔒by_ path - Normalize every input file’s path and reduce to a first-wins,
deduped
(path -> content)map — the exact effective representationparse_bundle_importbuilds internally before doing anything else. Extracted so the Phase 3files-mode content digest (§3.0.5, round 6) can compute over the IDENTICAL order-resolved representation the parser itself uses, rather than a naive raw-input sort that could disagree with which entry the parser’s own first-wins rule actually keeps. - duplicate_
in_ bundle_ slugs - Every parsed skill slug that appears more than once across the WHOLE
bundle — the
"duplicate_in_bundle"collision pass (Phase 3 spec §3.1, codex P1 round 2), computed independently of any external global-catalog state so it’s pure and directly testable. - enforce_
raw_ files_ caps - Applies
check_entry_size’s caps (plusMAX_ENTRY_COUNT) to the rawfilesRPC intake path — the pathunzip_bundle_importnever covers (reagent P1, PR #2379 round 5). Unlike zip entries, a raw file’scontentis already fully materialized (no separate declared-vs-actual distinction, no incremental decompression to bound), so this only needs the shared per-entry/aggregate check, no backstop-read machinery. - enforce_
raw_ files_ caps_ with_ budget - Same as
enforce_raw_files_caps, but with an explicitWarningBudget(Phase 3 spec §3.1, round 11). - is_
requirements_ 🔒json - True if
pathis (case-insensitively) the one file ever read out of the accounts/ directory. Used both to allow it intoby_path(the accounts/ allowlist above) and to REJECT it from every OTHER component category’s generic lookup (codex P1, PR #2379 round 6) — components.instructions/mcpServers must never be able to pull its raw JSON content in as if it were ordinary bundle content (e.g. straight intoinstructions/AGENTS.mdon a later export, or into an mcpServer entry), since only the dedicated accounts/requirements.json parser is meant to ever read it. - manifest_
file_ name - Which manifest file a set of archive paths carries:
bundle.json, else the legacyarmory.json. - mcp_
server_ display - Bounded
{name, command}projection of an MCP server’s fullconfig(Phase 3 spec §3.1, round 7) — the fullconfigis never returned in preview/commit responses, only this small, defensively-extracted projection. Falls back tonullfor either field when absent or not a string, since MCP JSON has no required shape (§3.0). - parse_
bundle_ import - Parse and validate a bundle’s files into
ParsedBundleImport. Structural failures (no manifest,bundle.jsonorarmory.json, or a malformed one) reject the whole import —Err— since there is nothing safe to partially write. Per-entry problems (a missing referenced file, an unsafe path, a malformed SKILL.md) degrade to a warning and that entry is skipped — matchesbundle_export.rs’s own philosophy, and lets a lossy import still produce a usable bundle rather than an all-or-nothing failure on e.g. one corrupt skill among five good ones. - parse_
bundle_ import_ with_ budget - Same as
parse_bundle_import, but with an explicitWarningBudgetenforced at every warning-push site inside this function (Phase 3 spec §3.1, round 11) — the newbundle.import.preview/.commitRPC handlers call this directly with a tight budget;parse_bundle_importpassesWarningBudget::unbounded, preserving today’s existingbundle.importroute’s behavior exactly. - parse_
instruction_ 🔒component_ paths - Parse one
components.instructions-shaped path array (either the v0.1 flat array itself, or one key’s array within the v0.2 keyed-object shape) into its joined instructions text.labelis used only for warning messages ("instructions"for the flat/default case,"instructions.<provider>"for a variant) so every caller’s warnings are traceable to the specific key that produced them. - parse_
skill_ 🔒md - Parse a SKILL.md file (
render_skill_md’s exact output shape, and nothing more general — this is not a YAML parser, just the inverse of the one writer this codebase has). Expects exactly:---\nname: "<json-string>"\ndescription: "<json-string>"\n---\n\n<body>. ReturnsNonefor anything that doesn’t match — a real Agent Skills SKILL.md from elsewhere in the ecosystem may use additional frontmatter fields or different quoting; broadening this to a general YAML parser is a follow-up, not silently guessed at here (mirrors the exporter’s own documentedserver.jsonscope decision). - truncate_
display - Fixed-character truncation shared by every bounded-display projection
this module (and its RPC callers, Phase 3 spec §3.1/§3.2) define —
stated once so
previewandcommitalways apply IDENTICAL bounds to the equivalent field, rather than independent per-endpoint copies drifting apart (the exact class of gap codex found and re-found across rounds 9 and 12). - unzip_
bundle_ import - Unpack a
.abfzip archive into a flat file list, applying the same path-safety check as everywhere else in this module to every entry name before it’s trusted (§4.3.4) — a zip’s own internal paths are exactly as untrusted as any other part of the archive’s content. Directory entries are skipped; anything that fails the safety check is dropped with a warning rather than surfaced as a file. - unzip_
bundle_ import_ with_ budget - Same as
unzip_bundle_import, but with an explicitWarningBudget(Phase 3 spec §3.1, round 11).