SwarmAI 1.0.23 Release Notes
What changed in SwarmAI 1.0.23 — Docker-backed tools.
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 anyContainerSkillSpecas a first-classBaseTool. The agent invokes it like any other tool; under the covers each call is a freshdocker runwith 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 forLABEL 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 viadocker inspectfirst (no container start needed); falls back to/etc/swarmai/manifest.jsonvia a one-shotdocker run catfor images that forgot the LABEL.ManifestToolFactory— generic manifest →Listfactory. Per-tool output parsers; bad capabilities are skipped rather than failing the whole manifest.CodingAgentSkillGeneratorprompt update — Codex (and any LLM-based skill generator) now receives explicit instructions for emitting capability manifests alongside generated Dockerfiles, including theLABEL 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), andRememberingApprovalGate(persists APPROVE_AND_REMEMBER decisions to~/.swarmai/approved-images.txtso re-runs don't re-prompt).ContainerSkillRuntimeLABEL 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) CopyOnWriteArrayListso concurrent registration during execution is safe. Skips duplicates by function name. Pairs withManifestToolFactoryfor the dynamic-loop endpoint.BaseTool.isDynamic()— new default method returningfalse. Override totruefor 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 throughFunctionToolCallbackwith explicitinputSchema(...)instead oftoolNames(...), 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 viadocker images --filter reference=swarmai-skill-*. Survives JVM restarts (unlikeruntime.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 outsideswarmai-skill-) that no longer have any references. Three independent guards before flagging: not inswarmai-skill-, not the FROM-base of any kept image (verified viaRootFS.Layersprefix matching, the reliable Docker signal), and not the ancestor of any running or stopped container. Layer-prefix matching reliably protects images likealpine:3.19whose layers are baked into keepers.docker history-based base detection rejected in favour ofRootFS.Layersprefix matching after IT proved that history reports base layers as(their build history isn't on this host). Rationale documented inUpstreamImageAnalyzer.
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;--applyexecutes.--include-orphansopt-in for manifest-less framework images;--include-upstreamopt-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 todocker run. Affects KaliToolbox, PentestTools, and anyManifestToolFactorywired with.dockerfile(...). - P2:
GeneratedSkill.executeContainernow preserves the originalentrypointwhen 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 inimagesWeBuiltsopruneImagesBuiltByMe()can reclaim disk for things this runtime brought down. Previously, pulled images were lost from the cleanup contract.
Test coverage delta
| Category | Tests added |
|---|---|
| Capability manifest + parser | 12 |
| Approval gate behaviours (AutoApprovalGate, DenyGate, CallbackApprovalGate, ConsoleApprovalGate, RememberingApprovalGate) | 12 |
| Tool relevance filter | 9 |
| 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 tests | 10 |
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 adefaultmethod returningfalse, so existing implementations compile unchanged.- The Agent's tool-binding path adds the
isDynamic()check alongside the existinginstanceof McpToolAdapter || instanceof GeneratedSkillcheck; static Spring-bean tools continue to be passed by name. ContainerSkillRuntimeexposes 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
ImageCatalogfilter isreference=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-audituses 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.
ManifestSmokeValidatorreturns 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; integratingtrivy imageordocker scoutoutput 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
toolNameslist. 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.
