Skip to main content

Module bundle_import

Module bundle_import 

Source
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§

AccountRequirement
One accounts/requirements.json entry — mirrors the shape bundle_export.rs writes (research report §5.3).
BundleImportFile
One file read from an import source (zip or raw list), path relative to the bundle root. Mirrors bundle_export::BundleExportFile.
ImportedContextFile
One instructions/context/* file, in the [{path, content}] shape db_bundles.context_files stores.
ImportedProjectInstruction
One components.projectInstructions entry, read back out of the bundle.
ParsedBundleImport
Parsed, validated bundle content ready for the RPC handler to resolve accounts against and write to the Store. mcp_servers still 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.
ParsedMcpServer
An MCP server parsed out of a components.mcpServers reference — Phase 3 spec §3.0. source_path is the stable, always-unique selection key (the manifest path reference, distinct from whatever "name" field happens to appear inside config’s arbitrary JSON, which has no uniqueness guarantee and isn’t even required to be present).
ParsedSkill
A skill parsed out of a skills/<slug>/SKILL.md file — the inverse of agent_config::render_skill_md.
WarningBudget
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 unbounded Vec<String> in memory before any cap ever runs. unbounded() preserves today’s existing bundle.import route’s real, already-shipped behavior exactly — this is a capability the parser gains, not a behavior change forced onto Phase 2’s existing caller.
WarningSink 🔒
Accumulates warnings against a WarningBudget. Exposes .push(String) so every existing warnings.push(format!(...)) call site in this module keeps working unchanged after its binding’s type changes from Vec<String> to this — only construction (WarningSink::new) and extraction (.into_vec()) differ.

Enums§

ImportInputMode
Which of the three bundle.import.preview/.commit input fields produced a given payload — mixed into the content-digest hash domain itself (Phase 3 spec §3.0.5, round 7) so a file_path preview can never be satisfied by a zip_base64 commit 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 — name is re-submitted verbatim as bundle_name when a user doesn’t edit the preview’s suggested value, so the canonical, bounded value must be what both preview and commit converge 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), requirement id/provider/env (round 11), context-file display_path (round 13), and bundle description (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.projectInstructions entries 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_array bound how many components.skills entries 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, unbounded instructions string could otherwise force.
MAX_INSTRUCTION_PROVIDER_VARIANTS 🔒
Maximum number of distinct keys accepted in a v0.2 components.instructions object ("default" plus per-provider variants) — ABF v0.2 §2.2. A real bundle needs at most one entry per known harness (nine, as of providers.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 than MAX_DISPLAY_FIELD_CHARS since 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_budget pushes when components.memory is present — ABF v0.2 §2.3. A shared constant (rather than a literal duplicated at both the push site and bundle_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 in bundle.rs keeps working because it references the same constant, not a copy of the string. Warning for an ABF carrying components.projectInstructions.
SCHEMA_V0_3
The $schema an ABF v0.3 manifest names.

Functions§

abf_format_version
The ABF format version a manifest declares through its $schema, or None when it names none this importer knows. The version field is the bundle’s own content version, not this.
bound_bundle_name
Bounds a bundle name to MAX_BUNDLE_NAME_CHARS via plain truncation (no ellipsis, unlike truncate_display) — this IS the canonical value, not a display abbreviation of a longer “real” one. Shared by parse_bundle_import’s own manifest-name bounding and the Phase 3 bundle.import.commit RPC’s bundle_name override (round 3), so a client-supplied override can’t bypass the same bound parsed.name is already held to.
bounded_instructions_preview
Bounds instructions_preview for the RPC response — returns (preview, truncated, total_chars). The full, untruncated instructions value 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 files RPC 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_slugs is whatever the caller already fetched from skill.catalog.list’s underlying skill_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) — hashes parse_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_base64 input — 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 representation parse_bundle_import builds internally before doing anything else. Extracted so the Phase 3 files-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 (plus MAX_ENTRY_COUNT) to the raw files RPC intake path — the path unzip_bundle_import never covers (reagent P1, PR #2379 round 5). Unlike zip entries, a raw file’s content is 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 explicit WarningBudget (Phase 3 spec §3.1, round 11).
is_requirements_json 🔒
True if path is (case-insensitively) the one file ever read out of the accounts/ directory. Used both to allow it into by_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 into instructions/AGENTS.md on 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 legacy armory.json.
mcp_server_display
Bounded {name, command} projection of an MCP server’s full config (Phase 3 spec §3.1, round 7) — the full config is never returned in preview/commit responses, only this small, defensively-extracted projection. Falls back to null for 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.json or armory.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 — matches bundle_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 explicit WarningBudget enforced at every warning-push site inside this function (Phase 3 spec §3.1, round 11) — the new bundle.import.preview/.commit RPC handlers call this directly with a tight budget; parse_bundle_import passes WarningBudget::unbounded, preserving today’s existing bundle.import route’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. label is 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>. Returns None for 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 documented server.json scope 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 preview and commit always 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 .abf zip 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 explicit WarningBudget (Phase 3 spec §3.1, round 11).