# SwarmAI 1.0.23 — Container-as-Tool + Dynamic Capability Acquisition

**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 <CIDR>`.

---

## 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=<base64>`). 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<BaseTool>` 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=<base64>` 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<BaseTool>)`** — 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 `<missing>` (their build history isn't on this host). Rationale documented in `UpstreamImageAnalyzer`.

### Workflow CLI modes (in `codex-skill-creation`)

- **`--kali-audit <CIDR>`** — 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 <CIDR>`** — 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

| 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 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:

```java
// 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.
