Skip to content

Releases: pacepace/aidc

v1.8.0

Choose a tag to compare

@pacepace pacepace released this 22 Sep 03:53
1cac85b

Reaching a database on another network

A proxied session can now reach a TCP service the host can reach — a database over ZeroTier, a VPN, the LAN — and nothing else. Name it in your own ~/.config/aidc/config.yaml (egress_tcp:), at create (--egress-tcp host:port), or on a running session (aidc egress <session> add host:port). Inside the session you connect exactly as you would outside; TLS stays end to end, and every connection is logged to the session's audit dir. The session still has no route out.

Security

  • A repo's own .aidc/config.yaml can no longer widen the sandbox. From a workspace or repo config only safe or tightening settings apply; anything that would open a path out is listed by aidc create and ignored unless you pass --trust-repo-config.
  • A session can no longer plant a hook that runs on your machine. ~/.claude/settings.json was bind-mounted read-write into every session; it is now copied in when the session is created. Existing sessions keep the old mount through aidc upgrade; aidc kill + aidc create closes it.
  • New package versions wait 14 days. The MCP server's dependencies are upgraded past ten advisories, and aidc never adopts a version published in the last two weeks — in its own builds and, as a system default for uv, pip and npm, inside sessions. Claude Code is exempt and stays current. One flag overrides per command; see the README.
  • Subdomains of known-malware domains are blocked too. Squid matched listed domains exactly, so www.<listed> went through.
  • A false setting in config.yaml (tld_taints, claude_resume, share_*) was silently ignored on machines with yq installed. Fixed.

Fixed

  • The proxy no longer drops every connection for ~20 seconds when the malware list refreshes (#34). Squid asks a small lookup helper that follows the list on disk, so it never reloads.
  • aidc create --port works on a machine that never ran aidc proxy.
  • Your Claude Code status line shows inside sessions.

Upgrading

aidc update, then aidc rebuild, restart aidc mcp, and recreate sessions with aidc kill + aidc createaidc upgrade keeps a session's proxy and its old settings mount, so it does not pick up the proxy or settings changes. Sessions reaching a database need the new proxy stack.

Full details: CHANGELOG.md [1.8.0].

v1.7.0

Choose a tag to compare

@pacepace pacepace released this 17 Sep 19:24
b445325

Replies from a shared session used to arrive late, twice, or not at all. This release
decides what a session is doing from Claude's transcript instead of watching its screen,
makes the send queue never drop a prompt, and fixes every delivery path that lost a reply.

It was tested end to end against a real orchestrator, with two agents driving both sides
through twelve scenarios. That test found five bugs; all of them are fixed here.

Replies arrive, once.

  • Webhooks survive an aidc-mcp restart and pick up where they left off, so a reply
    written while the MCP was down is delivered rather than skipped.
  • The first reply after Claude restarts inside a session is no longer lost.
  • A reply left in a damaged transcript (a torn write, a compaction) is delivered before
    the watcher follows Claude onto a new file.
  • session_resend can reach a session's earlier transcripts.

Interrupts reach the orchestrator. Press Esc after Claude has started writing and the
partial answer is delivered, marked interrupted. Press Esc before it writes anything and
that is recognised too, from the prompt Claude puts back in the input box, within seconds.

A queued prompt is never lost. No deadline; held while Claude restarts; failed pastes
retried; saved across an MCP restart. If its session is removed, the conversation that
sent it is told. If it has waited ten minutes, it is told that too. aidc upgrade keeps a
session's queue instead of looking like a kill.

What you type in a session is visible to the orchestrator, and with
metallm.send_speaker it is recorded as yours instead of answered. Off by default.

Failures say what kind they are. Every failed tool result carries an error_code, so
a caller can tell "retry" from "tell the person" without reading the sentence.

Dev containers cannot reach the MCP control plane through their proxy, for sessions
whose traffic goes through it.

session_create over MCP works, and only when you allow it. It never worked in a
released image. It is now offered only with mcp.session_create: true, which mounts your
home into the MCP container, and it refuses a repo or workspace outside that directory.

aidc kill removes a session's networks even when a port forward is attached, instead
of reporting success and leaving one behind.

Upgrading: run aidc rebuild and aidc mcp restart. The container-side fixes do not
take effect until that image is replaced. Existing sessions keep working; they pick up the
new dev image on aidc upgrade <session>.

v1.6.0

Choose a tag to compare

@pacepace pacepace released this 12 Sep 03:16
2e94383

Your scratchpad now follows a session across the container boundary

share_memory has always carried a session's conversation between your host and the dev container. Its working files never followed.

Claude Code keeps them in /tmp/claude-<uid>/<encoded-repo>/<session-id>/scratchpad/ and tasks/. Inside a session that path lived on the container's writable layer, so aidc upgrade and aidc kill destroyed it. Pop a session out to your host and resume it and you kept the transcript but lost everything the session had been building.

aidc create now bind-mounts your per-repo scratchpad at the identical path inside the container. Pop a session out, resume it on the host, and it finds its own files; pop it back in and they are still there.

Nothing to configure — it is on by default, like share_memory. Turn it off with share_scratchpad: false.

Bounded on purpose

Only the current repo's subdirectory is bridged, never the whole /tmp/claude-<uid> root, which holds every other project's scratchpad. That is the same line aidc already draws in refusing to share all of ~/.claude with the sandbox.

It is a live read-write host path, chosen over a one-way copy because a copy cannot carry work back in — which is half the point. The exposure is one directory, and it is the same trade the repo mount already makes.

It cannot cost you a session

Both directory levels sit under /tmp, which is world-writable. A path already held by another user, or a planted symlink, is refused rather than followed, and every failure costs only the bridge — never the session you were trying to create.

Known limits

  • macOS: the bridge is currently skipped. It requires your host uid to match the container's vscode (1000); on Docker Desktop, which remaps bind-mount ownership, that check is more conservative than it needs to be. Recorded as a known gap.
  • Existing sessions do not gain the mount. aidc upgrade reuses the compose file rendered when the session was created. Run aidc kill + aidc create to pick it up.
  • The bridged directories live on your host and are not removed by aidc kill — that is what makes popping back in work. Nothing sweeps them, so they accumulate until a reboot clears /tmp.

Full detail in the changelog.

v1.5.1

Choose a tag to compare

@pacepace pacepace released this 11 Sep 21:07
0981ecc

Changed

The docs and the MCP tool text describe "the orchestrator", not a specific product. The README sections on callback delivery now document the contract itself for any MCP-capable orchestrator: the endpoint shape, the bearer, and both payload shapes (session_invoke_async's and the session watcher's, including prompt_origin). Examples use neutral names, and the tool descriptions and error messages an orchestrating model reads say "the orchestrator".

The metallm.callback_url config key is unchanged and documented as keeping its historical name, so no configuration needs to change.

Upgrading

aidc update && aidc rebuild
aidc mcp stop && aidc mcp start     # the reworded tool text lives in the MCP image

Running sessions need nothing; aidc upgrade <name> when convenient.

Full changelog: v1.5.0...v1.5.1

v1.5.0

Choose a tag to compare

@pacepace pacepace released this 11 Sep 16:47
ada2005

Changed

The dev container owns its Claude login. Each session keeps Claude's config directory on its own volume (CLAUDE_CONFIG_DIR=/home/vscode/.claude, the layout Anthropic's reference devcontainer uses) and you /login inside it once:

aidc attach my-feature
# window 0: /login → open the URL, paste the code back

That login refreshes itself, survives aidc restart and aidc upgrade, works with Remote Control, and can be a different account from the host's. Run /login again inside the session to switch accounts, for example when one hits its session limit.

Why: the host's .credentials.json and ~/.claude.json used to be bind-mounted into the container. Claude Code replaces both files by rename, so the mounted credentials went stale on the host's first refresh (anthropics/claude-code#18443) and could not be written from inside, and the read-only ~/.claude.json could never record a new account. That is why in-container logins expired after a few hours and /login with another account never took.

Onboarding state (theme, output style, this project's trust decision) is seeded once from the host's ~/.claude.json, without the account, API keys, MCP server definitions or prompt history, so the first launch goes straight to the login prompt. Per-project memory and conversation history, settings.json and plugins are bridged exactly as before. The long-lived token path (aidc claude-token) is unchanged and still skips the login, at the cost of Remote Control.

Fixed

  • aidc upgrade now actually moves a session onto the new version's image. It recreated the dev container from the create-time compose file, which pins the image at the tag current back then, so across a version bump the container came back on the old image while the command reported the new one.

Removed

  • aidc auth-bridge, aidc reauth, the macOS Keychain extraction, and the share_auth config key. With nothing bridged there is nothing to keep fresh. A watcher left running by an earlier aidc on macOS is retired by the next aidc create or aidc kill.

Upgrading

aidc update && aidc rebuild
aidc mcp stop && aidc mcp start

Then, per running session, either aidc upgrade <name> (its old host login mounts are removed; the next launch runs Claude's onboarding and asks for /login) or aidc kill <name> + aidc create <name> ... for the seeded start.

Full changelog: v1.4.1...v1.5.0

v1.4.1

Choose a tag to compare

@pacepace pacepace released this 11 Sep 05:43
645b4ed

Fixed

A prompt typed straight into a watched session no longer reaches the orchestrator as an unexplained reply.

If you aidc attach into a session that MetaLLM is driving over session_send and type into the Claude window yourself, the reply used to arrive at the orchestrator with no trace of your prompt. It read the reply as an answer to whatever it had last sent and could not tell where the new instructions came from.

In the JSONL transcript a pasted prompt and a typed one are identical, so the MCP now keeps a record of every prompt it injects itself (<session>.sent-prompts.json beside the watermarks, bounded to 200 entries / 24 h). At delivery, any prompt on the turn that is not in that record is prepended to the content under a note saying the user typed it at the terminal, so the reply reads in context.

  • Slash commands render as /name args.
  • Hook feedback, task notifications, auto-continues, interrupt markers, and any wrapped line (local-command output, ! bash-mode input and output) are never attributed to the person.
  • The callback payload gains prompt_origin (terminal / orchestrator / ""). It is additive; an existing MetaLLM keeps working without changes.
  • Exactly-once delivery is unchanged: the ledger still keys on the bare reply.

Upgrading

aidc update && aidc rebuild
aidc mcp stop && aidc mcp start     # the watcher fix lives in the MCP image

Then aidc upgrade <session> per running session, or aidc kill + aidc create.

Full changelog: v1.4.0...v1.4.1

v1.4.0

Choose a tag to compare

@pacepace pacepace released this 10 Sep 17:53
afb86df

Added

aidc update updates the CLI itself, the way brew upgrade or apt upgrade would. It detects how this copy was installed and runs the matching step:

  • release installer under ~/.local/share/aidc/: re-runs the latest release's install.sh, which installs alongside the old version and flips the symlink
  • Homebrew: brew upgrade on the installed formula (aidc or aidc@X.Y)
  • git checkout: git pull --ff-only

aidc update --check reports the installed and latest versions and changes nothing. The CLI and the images are separate, so follow an update with aidc rebuild, then aidc upgrade <session> per running session.

This exists because the failure is quiet: a stale installed copy rebuilds images from its own old tree, and nothing tells you the checkout you just pulled is not the aidc on your PATH.

The Homebrew tap updates itself on every release, with no token to rotate. The workflow now pushes the tap over SSH with a write deploy key that never expires and can reach nothing else. release/tap-deploy-key.sh creates or rotates it and prints only the fingerprint. Dry-run dispatches now prove push authorization, not just clone access.

Upgrading

Once on 1.4.0, future upgrades are one command:

aidc update && aidc rebuild

To get onto 1.4.0 from an earlier release, run the installer one-liner from the README (or brew upgrade aidc, or git pull), then aidc rebuild. Then aidc upgrade <session> per session, or aidc kill + aidc create to start from the new bake.

Full changelog: v1.3.1...v1.4.0

v1.3.1

Choose a tag to compare

@pacepace pacepace released this 10 Sep 05:33
1a06b04

Fixed

aidc rebuild now actually refreshes Claude Code. The dev-base image installs Claude Code with Anthropic's native installer, but that step was an ordinary cached Docker layer and nothing above it changes between releases. Every rebuild silently reused the version fetched the first time the layer was built, so an image rebuilt today could still carry a release from a month ago. The install step now takes a CLAUDE_CODE_REFRESH build arg used purely to invalidate that layer, and aidc rebuild passes a fresh value each run. Everything above stays cached; only the installer re-runs.

Note that a session's home directory is a named volume seeded from the image only on first create, so aidc upgrade keeps the session's existing Claude Code. Existing sessions pick up new releases through Claude Code's in-session auto-update; a fresh aidc create starts from the newly baked version.

Upgrading

git pull && aidc rebuild        # or: brew upgrade aidc

Then aidc upgrade <session> per session, or aidc kill + aidc create to start from the new bake.

Full changelog: v1.3.0...v1.3.1

v1.3.0

Choose a tag to compare

@pacepace pacepace released this 05 Sep 23:50
d111920

A security release. Upgrade if you rely on aidc's egress filtering — until now
it could be bypassed trivially, and silently.

The proxy was advisory

aidc routed the dev container's HTTP through Squid by setting HTTP_PROXY. That
is a request, not a constraint. From a real session:

$ env -u HTTP_PROXY -u HTTPS_PROXY -u http_proxy -u https_proxy \
    curl -s -o /dev/null -w '%{http_code}' https://example.com/
200
$ grep -c example.com /var/log/squid/access.log
0

Four env -u flags and the sandbox was open. Worse than the bypass itself is the
second line: zero entries in the access log. The blocklist never applied, and
because the policy sidecar tails that log, taint detection was blind to it. Squid
only ever saw the traffic that asked to be seen.

Anything ignoring those variables had the same free pass — a library that doesn't
read them, a raw socket, a statically linked binary. No agent malice required.

NET-10 claimed processes inside the dev container "cannot reconfigure or bypass"
the proxy. That was not true, and had not been true in any released version.

Egress is now enforced by topology

The session bridge is a Docker internal network. Docker installs no
masquerade rule for it, so there is no route off that bridge at all. Squid is
dual-homed onto a separate egress network and is the only way out.

This is deliberately not a firewall rule. The dev container runs --privileged
(it needs it for DinD), so anything inside it can flush any rule it can see —
enforcement has to live where the agent cannot reach. Attacked as root, with the
proxy variables stripped, an explicit default route added via Squid, ip_forward
enabled, and its own MASQUERADE installed:

attempt result
direct HTTPS, proxy env stripped blocked
raw TCP to 1.1.1.1:80 Network is unreachable
root + own default route + MASQUERADE still blocked
proxied HTTP 200
blocklisted .cn via Squid 403

Only squid, refresher (fetches threat feeds) and policy (POSTs the taint
webhook) join the egress network. dev and audit never do.

The test that should have caught it

make smoke had a step called "proxy enforcement" that passed on every release.
It only ever tested the proxied path — curl -x http://aidc-proxy:3128 — which
succeeds just as happily on a sandbox enforcing nothing, because Squid answers
when you ask it to. Nothing checked that going around Squid failed.

The suite now asserts that stripping the proxy variables fails, that raw TCP to a
public IP fails, and that Squid is still reachable, so it can no longer go green
on this class of bug.

Attaching networks still works

aidc create --network and aidc network <session> add are unchanged. A session
still reaches an attached project's postgres, NATS or redis by container name, on
any port.

Because most compose bridges are NATed — and an internal bridge has no default
route of its own — attaching one does restore general internet egress as a side
effect. aidc cannot prevent that; it does not own that network. Detaching closes
it again, and the smoke suite now asserts exactly that scoping. Attach the
narrowest network that does the job.

Escape hatch

aidc create myproj --egress direct     # or `egress: direct` in .aidc/config.yaml

Restores the pre-1.3.0 NATed bridge for sessions needing reachability an attached
network cannot provide — ZeroTier/Tailscale hosts, direct DNS. aidc create states
plainly that enforcement is off for that session.

Compatibility

Declared --port forwards and aidc proxy are unchanged from your side, but are
now dual-homed aidc/forwarder sidecars — an internal network cannot publish
ports, so ports: on the dev service would have silently done nothing. DinD image
pulls route through Squid (the inner daemon already inherits the proxy env).

Existing sessions keep their current behaviour until recreated. aidc kill +
aidc create picks up enforcement; aidc upgrade does not, since it reuses the
compose file rendered at create time.

Existing sessions are not converted

aidc upgrade reuses the compose file rendered at create time, so upgrading a
session created before 1.3.0 leaves it on its old NATed bridge — upgraded, but
still bypassable. aidc status <name> now reports which posture a session is in,
and aidc upgrade warns before preserving an unenforced one.

To convert: aidc kill <name> && aidc create <name> ...

v1.2.0

Choose a tag to compare

@pacepace pacepace released this 05 Sep 22:08
ace4ef5

A feature release: an aidc session can now reach a stack that is already running
elsewhere on the host.

Sessions can reach another stack's services

Until now a session's dev container sat alone on aidc-<session>-net, and
aidc proxy only went host → container. There was no way for a session to talk
to a database, queue, or cache belonging to another compose project — which is
exactly what you need when the thing you are debugging is that stack.

A session's dev container can now attach to named Docker bridge networks and
resolve that project's containers by name.

Declared — survives restart, upgrade, and recreate:

aidc create metallm --network metallm_default
# or in .aidc/config.yaml
networks:
  - metallm_default

Repeatable, and the CLI merges with config rather than overriding it (unlike
--dns) — every named network is something the session needs, so more sources
means more networks. Networks are validated immediately after config load,
before any image build, so a typo costs you a message rather than a full
dev-base rebuild.

Adhoc — attaches a session that is already running, no recreate:

aidc network metallm add metallm_default
aidc network metallm ls          # marks each attachment declared or adhoc
aidc network metallm rm metallm_default

Then, from inside the session: psql -h metallm-postgres-1 -U metallm.

aidc restart aidc upgrade aidc kill + create
declared survives survives survives
adhoc survives lost lost

Adhoc attachments survive a restart, unlike adhoc port forwards which
aidc restart sweeps. The difference is real rather than an inconsistency: a
port forward is a separate sidecar container pointed at a container that is
going away, while a network endpoint is part of the dev container's own config
and comes back with it. aidc upgrade now names the adhoc attachments it is
about to drop, with the commands to re-add them.

aidc status <name> gained an attached networks section, listed even when
empty.

The gateway pin

Every attachment pins gw_priority so aidc-<session>-net keeps the default
route. This is the load-bearing part of the change, and it is not what the
obvious implementation would have done. Measured on Docker 29.1.3:

default route ends up on
docker network connect ext dev ext — steals it immediately
Compose priority: 100 on the aidc net ext — steals it anyway
--gw-priority / Compose gw_priority the aidc net — correct

Compose's priority only orders the connect sequence; it does not choose the
gateway. Without the pin, attaching a network silently reroutes all of the
session's egress — squid-proxied traffic included — out through someone else's
bridge, with nothing in the audit trail to show for it. A unit test and a smoke
assertion both pin this, the latter by reading the container's own
/proc/net/route.

gw_priority needs Docker Compose 2.34+, so aidc create probes for it and
fails with that reason rather than rendering a file whose pin would be silently
dropped. Sessions with no attachment render a byte-identical compose file to
before
, so they neither change behaviour nor acquire a new Compose version
floor.

This widens the sandbox, on purpose

Everything on an attached network is reachable from the session on every
port
. That traffic does not pass through squid, so the blocklist does not
apply and taint detection cannot see it. The attachment is bidirectional.

Only the dev service is ever attached — squid, refresher, policy, and audit
stay isolated on the session's own network. host, none, and Docker's default
bridge are refused outright. The CLI help, the config template, aidc create's
output, and aidc status all say this at the point of use, and
docs/done/design-07-safety-model.md now sizes the residual risk honestly
instead of implying the proxy covers everything.

Attach the narrowest network that does the job, and treat anything on it as
being inside the blast radius.