release

SwarmAI 1.0.23 Release Notes

What changed in SwarmAI 1.0.23 — Docker-backed tools.

9 min read

Released: 2026-05-10
Theme: Agents stop being bound to a fixed toolkit. Docker images become a first-class transport for callable capabilities, and the framework can build, validate, register, and select among them on the fly.


Headline

This release closes the loop on dynamic capability acquisition — the property where an agent identifies a gap, the framework generates a containerised capability that fills it, smoke-tests it, registers it with the live agent, and the agent uses it before the conversation ends. It is the one architectural axis where SwarmAI now does something Claude Code and the Agent SDK structurally cannot.

The mechanism: a Docker image carries a swarmai.manifest LABEL declaring its callable capabilities; the framework reads the manifest, materialises one BaseTool per capability, runs a smoke validator to reject hallucinated declarations, hot-registers the survivors with a running Agent, and proceeds. End-to-end demo lands in --dynamic-audit .


What's new

Container-as-Tool transport

  • ContainerBackedBaseTool — generic adapter that exposes any ContainerSkillSpec as a first-class BaseTool. The agent invokes it like any other tool; under the covers each call is a fresh docker run with the spec's entrypoint, env-var mapping, network mode, memory cap, and timeout.
  • KaliToolbox — hand-coded reference library of 18 Kali tools across six categories (network discovery, DNS/OSINT, web fingerprinting, SMB/AD enumeration, TLS/SSH inspection, hashes/exploits). One shared Kali Dockerfile cached by content hash; per-tool entrypoint override means 18 distinct callable tools share a single image build.
  • PentestTools — focused alpine-based tool factories (nmap_recon, whatweb, smbmap, ssh_audit, tls_check, dns_recon) for environments where the full Kali base is overkill.

Self-describing images

  • CapabilityManifest — typed schema (record-based) for declaring callable capabilities inside an image: name, description, triggerWhen, avoidWhen, category, tags, parameters, entrypoint, timeout, network, memory.
  • ManifestParser — Jackson-backed JSON parser; supports both raw JSON and base64-encoded transports (the latter for LABEL swarmai.manifest=). Unknown fields ignored for forward compatibility; required-field validation with actionable error messages.
  • ManifestImageReader — extracts a manifest from a built image. Tries LABEL transport via docker inspect first (no container start needed); falls back to /etc/swarmai/manifest.json via a one-shot docker run cat for images that forgot the LABEL.
  • ManifestToolFactory — generic manifest → List factory. Per-tool output parsers; bad capabilities are skipped rather than failing the whole manifest.
  • CodingAgentSkillGenerator prompt update — Codex (and any LLM-based skill generator) now receives explicit instructions for emitting capability manifests alongside generated Dockerfiles, including the LABEL swarmai.manifest= convention and required schema.

Validation + safety

  • ManifestSmokeValidator — runs every declared capability against safe synthetic inputs (127.0.0.1, example.com, linux, …) and reports pass/fail per tool. Catches hallucinated declarations: a tool that names a binary that isn't actually in the image gets dropped from the toolkit before the agent ever sees it.
  • ImageApprovalGate — pluggable human-in-the-loop checkpoint consulted before every new image build or pull. Five implementations: AutoApprovalGate (default, preserves backward compatibility), DenyGate, CallbackApprovalGate, ConsoleApprovalGate (interactive y/r/n on stdin/stdout, denies on EOF for CI safety), and RememberingApprovalGate (persists APPROVE_AND_REMEMBER decisions to ~/.swarmai/approved-images.txt so re-runs don't re-prompt).
  • ContainerSkillRuntime LABEL cap raised from 1 KB to 64 KB per label to accommodate base64-encoded capability manifests without truncation. Total per-image label size still well under Docker's documented ~256 KB limit.

Live agent extension

  • Agent.registerTool(BaseTool) / registerTools(List) — hot-add tools to a running agent. Backed by CopyOnWriteArrayList so concurrent registration during execution is safe. Skips duplicates by function name. Pairs with ManifestToolFactory for the dynamic-loop endpoint.
  • BaseTool.isDynamic() — new default method returning false. Override to true for tools created in code rather than as Spring beans (e.g. container-backed tools, MCP adapters, generated skills). The Agent's LLM-binding path uses this signal to route dynamic tools through FunctionToolCallback with explicit inputSchema(...) instead of toolNames(...), fixing "No ToolCallback found for tool name: …" errors when invoking dynamically-registered tools.

Toolkit selection

  • ToolRelevanceFilter — TF-IDF-weighted token overlap between a task description and each tool's blurb (description + triggerWhen + avoidWhen + category + tags). topK(task, tools, k) returns the most relevant subset for the agent's prompt; rank(...) returns the full scored list. Smoothed IDF makes rare, distinctive tokens dominate; common ones contribute little — no manual stopword list required.

Image catalogue + smart cleanup

  • ImageCatalog — discovers all framework-built images on the host via docker images --filter reference=swarmai-skill-*. Survives JVM restarts (unlike runtime.trackedImages() which only knows current-process builds). Enriches each entry with image ID, size, creation date, and parsed manifest.
  • ImagePruneAnalyzer — manifest-aware deduplication. Builds the inverse capability index (capability_name → [provider, …]), scores each provider on recency + size + manifest schema version, and decides per-capability winners. Images that don't win any capability are flagged redundant; manifest-less images bucket into "orphans" for explicit caller decision. Custom scorer pluggable for future signals (smoke pass-rate, last-used).
  • UpstreamImageAnalyzer — broader scan for upstream images (anything outside swarmai-skill-) that no longer have any references. Three independent guards before flagging: not in swarmai-skill-, not the FROM-base of any kept image (verified via RootFS.Layers prefix matching, the reliable Docker signal), and not the ancestor of any running or stopped container. Layer-prefix matching reliably protects images like alpine:3.19 whose layers are baked into keepers.
  • docker history-based base detection rejected in favour of RootFS.Layers prefix matching after IT proved that history reports base layers as (their build history isn't on this host). Rationale documented in UpstreamImageAnalyzer.

Workflow CLI modes (in codex-skill-creation)

  • --kali-audit — single Agent + 18 Kali tools, reactive tool selection driven by the LLM. The agent decides which tool to invoke next based on observed findings; no hardcoded phase ordering.
  • --dynamic-audit — full dynamic capability loop demonstration. Builds a small self-describing image (alpine + nmap + curl with embedded manifest), reads the manifest back, smoke-validates, relevance-filters, hot-registers tools with an Agent, runs the audit. End-to-end exercise of every new primitive.
  • --smart-prune [--apply] [--include-orphans] [--include-upstream] — manifest-aware image cleanup. Default is dry-run; --apply executes. --include-orphans opt-in for manifest-less framework images; --include-upstream opt-in for upstream images that have no FROM-base or container references.

Bug fixes (Codex review of PR #N)

  • P1: ContainerSkillRuntime.execute() now lazily builds Dockerfile-only specs instead of passing an empty image string to docker run. Affects KaliToolbox, PentestTools, and any ManifestToolFactory wired with .dockerfile(...).
  • P2: GeneratedSkill.executeContainer now preserves the original entrypoint when re-pinning a Dockerfile-spec to the built tag. Previously, any CONTAINER skill that overrode the image's default ENTRYPOINT silently ran the wrong binary.
  • P2: ContainerSkillRuntime.pullImage() now records pulled images in imagesWeBuilt so pruneImagesBuiltByMe() can reclaim disk for things this runtime brought down. Previously, pulled images were lost from the cleanup contract.

Test coverage delta

CategoryTests added
Capability manifest + parser12
Approval gate behaviours (AutoApprovalGate, DenyGate, CallbackApprovalGate, ConsoleApprovalGate, RememberingApprovalGate)12
Tool relevance filter9
Agent hot-tool registration (incl. concurrent)7
Image prune analyzer (single, newer-wins, smaller-wins, unique-cap, three-way overlap, orphans, byte-math, empty, custom scorer)9
Upstream image analyzer (unit + IT)7
Dynamic capability loop end-to-end (LABEL transport, factory, smoke, approval gate firing, deny blocking, hallucinated-tool rejection) + 3 Codex regression tests10

Total: ~66 new tests. Full suite: 1682+ green, 1 skipped (pre-existing), including 47+ tests that exercise real Docker daemon paths.


Breaking changes

None. This release is purely additive:

  • BaseTool.isDynamic() is a default method returning false, so existing implementations compile unchanged.
  • The Agent's tool-binding path adds the isDynamic() check alongside the existing instanceof McpToolAdapter || instanceof GeneratedSkill check; static Spring-bean tools continue to be passed by name.
  • ContainerSkillRuntime exposes new setters (setApprovalGate, setSource) and accessors; existing call sites with no approval gate get the auto-approving default.
  • The 1 KB → 64 KB LABEL cap is a relaxation, not a tightening.
  • Default ImageCatalog filter is reference=swarmai-skill-*; no ambient broadening of cleanup scope.

Migration

Existing applications need no changes to upgrade. To opt in to the new primitives:

// Read a manifest from a built image and register its tools with an agent
var manifest = new ManifestImageReader().read(imageTag).orElseThrow();
var factory = ManifestToolFactory.builder().runtime(runtime).image(imageTag).build();
List<BaseTool> tools = factory.toTools(manifest);
agent.registerTools(tools);

// Add human-in-the-loop image approval to a runtime
runtime.setApprovalGate(new ApprovalGates.RememberingApprovalGate(
        new ApprovalGates.ConsoleApprovalGate()));

// Dedupe + clean up manifest-bearing images
var catalog = new ImageCatalog().discover();
var analysis = ImagePruneAnalyzer.builder().build().analyze(catalog);
analyzer.execute(analysis, /* dryRun */ false);

Known limitations + roadmap

What's implemented but not yet wired end-to-end:

  • Codex-driven manifest generation in the dynamic loop. The prompt template now teaches Codex to emit manifests, but --dynamic-audit uses a hand-built reference image rather than asking Codex for a fresh one mid-task. Wiring the closed loop (gap → Codex → manifest-bearing Dockerfile → smoke validate → register → use) is the next concrete piece of work.
  • Smoke-test pass-rate signal in scorers. ManifestSmokeValidator returns per-tool pass/fail data; ImagePruneAnalyzer.Builder.scorer(...) accepts custom scoring; the persistence + history aggregation layer that connects them isn't built yet. Plug-point exists for callers who maintain their own reliability data.
  • Vulnerability scanning hook in ImageApprovalGate. Today the human approves with the Dockerfile contents and FROM image visible; integrating trivy image or docker scout output into the gate's prompt is a clean follow-up.
  • Live registration into a running Spring AI session still requires that the Agent was constructed without a fixed toolNames list. Agents using only Spring-bean tools can't currently grow at runtime — by design, since hot-registering Spring beans is its own can of worms.

Acknowledgements

Three of the bug fixes shipped this release came from Codex's automated review of PR #3042e186ca. Two P2s (entrypoint preservation, pull tracking) and one P1 (lazy Dockerfile build) — all real, all caught before merge. Worth keeping the bot in the loop.