Status: Draft v0.1 — spec only; implementation aspirational (v2).
Closes VISION.md §6. One copilot session coordinates work across multiple shims running on different machines: dev box + build server
- NAS + Pi cluster. The single-shim case (one session ⇄ one shim) is what v1 ships; this doc pins how the wire + platform expand to N shims so the v1 design doesn't paint us into a 1:1 corner.
The simplest mental model: today session_id is unique per copilot
chat session and routes to one shim via ShimConnectionManager. In
multi-machine v2, the same session_id can have N attached shims,
each identified by machine_id. Tool calls carry an optional
target_machine hint; absent the hint the platform picks via routing
policy.
Asks the shim for its current machine_id + capability snapshot, plus
whatever the platform-side registry knows about siblings. Issued by
the platform's LocalPCRouter on the first multi-shim turn so Claude
can list_machines().
{
"type": "MACHINE_LIST_REQUEST",
"id": "req-uuid",
"ts": 1234567890.0,
"payload": {}
}The shim responds with its own metadata; the platform synthesizes the union across all attached shims for the same session.
{
"type": "MACHINE_LIST_RESPONSE",
"id": "req-uuid",
"ts": 1234567890.1,
"payload": {
"machine_id": "this-shim-uuid",
"hostname": "alices-laptop",
"platform": "darwin",
"arch": "arm64",
"capabilities": ["shell", "files", "computer_use"],
"hardware_devices": []
}
}{
"type": "FILE_READ",
"id": "req-uuid",
"ts": 1234567890.0,
"version": "2.0",
"target_machine": "alices-laptop", // null = router decides
"payload": { "path": "/Users/alice/notes.md", ... }
}When target_machine is null and N > 1 shims are attached, the
platform-side LocalPCRouter falls back to a routing policy (see
below) and stamps the chosen machine_id on the wire envelope before
forwarding. The shim never has to know it was chosen — it just
executes.
Common ask: "Read /Users/alice/data.csv on laptop, write to
/srv/incoming/data.csv on the build server." The MVP shape: platform
streams the read response through itself and into the write request.
Adequate for tens-of-MB files; an optional v3 enhancement is a
direct shim-to-shim tunnel that bypasses the platform for bulk
transfers.
The wire op the platform issues is a synthesized pair — there's no
single FILE_COPY wire message. Same goes for executing the output of
machine A's command as input to machine B's: the platform glues two
single-machine ops.
autogpt_platform/backend/backend/copilot/tools/local_pc_router.py
(new module, v2).
class LocalPCRouter:
"""Picks a shim for a tool call when N > 1 are attached to a session.
Routing policy precedence:
1. explicit `target_machine` in the tool call → use that.
2. capability requirement (e.g. EXECUTE_COMMAND needing `shell`
on a host with the file in question already → bias toward
that machine).
3. user-configured default machine (per-session preference saved
to Redis copilot:localpc:default_machine:{user_id}).
4. round-robin across all attached shims for the session.
"""
async def pick(
self, session_id: str, op: WireOp, *, hint: str | None = None
) -> str:
...ShimConnectionManager grows a dict[session_id, dict[machine_id, ws]]
instead of dict[session_id, ws]. The new tool list_machines (an
MCP tool the platform exposes) returns the per-machine snapshots so
Claude can reason about which machine to target.
A new MCP tool exposed when N ≥ 2 shims are attached:
list_machines() -> list[MachineSnapshot] where MachineSnapshot is
the MACHINE_LIST_RESPONSE payload shape plus a last_active_ts.
The existing local_pc_* tools (screenshot, click, execute_command,
file_read, etc.) all gain an optional target_machine: str | null
parameter that maps onto the wire envelope's field. Unset = router
decides; set = override.
The platform's tool descriptions auto-include the routing semantics when N ≥ 2 ("If you don't pass target_machine, the router picks based on capability + recency.") so the LLM knows when targeting matters vs. when it's safe to omit.
- First connection (N = 1): indistinguishable from v1. Tool calls
go to the single shim;
target_machinealways omitted. - Second connection (N = 2): platform sends
MACHINE_LIST_REQUESTto both, caches the responses, and starts includingtarget_machineon outbound envelopes. The first shim has no idea the topology changed — its existing in-flight ops continue against its ownmachine_id. - Shim disconnect mid-session (N ⇒ N-1): pending ops against the
dead
machine_idraiseMACHINE_UNREACHABLE(new error code). The LLM sees the translation and either retries on a different machine or asks the user to bring the machine back. - Same machine re-attach (re-HELLO with same
machine_id): per the Q2 session-ownership rules locked in #36, this is a re-connect / takeover, not a new attachment.ShimConnectionManagerreplaces the old ws + keeps the samemachine_idslot in the session. - Different-machine reattach with conflicting
target_machine: the router refuses withMACHINE_NOT_ATTACHED. The platform never silently picks a "close enough" substitute machine.
Each shim authenticates independently against the same OAuth
application (the user runs autogpt-shim auth on each machine).
Sessions are bound to (user_id, session_id), not (user_id, machine_id, session_id) — so a machine the user has authed and
connected can join any of their existing sessions.
This means: a user with two laptops both running the shim, joining the same session_id, get N = 2 attached. The router treats them as peers. There's no "primary" machine and no implicit hierarchy.
Per-machine capability gating still works as before: HELLO declares
what the machine can do, and the router refuses ops on machines that
lack the requested capability with CAPABILITY_NOT_GRANTED.
MACHINE_NOT_ATTACHED—target_machinespecified but no shim with thatmachine_idis currently attached to this session.MACHINE_UNREACHABLE— shim was attached but disconnected before the op completed. Caller can retry on another machine via the same router, or wait and retry on this one.MACHINE_AMBIGUOUS— multiple shims match a partial machine identifier (e.g. user passedtarget_machine: "laptop"and two hostnames start with "laptop"). Router refuses; caller picks one unambiguously.
v2 (when first customer asks for it):
- N attached shims per session
target_machineon every wire envelopelist_machinesMCP tool- Round-robin + capability-based routing
- Cross-shim file transfers via platform-mediated read-then-write
v3 (only if v2 produces real demand):
- Direct shim-to-shim tunnel for large file transfers (avoids the platform bandwidth tax)
- Cross-machine pipeline ops (run command A on machine X, pipe stdout into command B on machine Y)
- Distributed work — split a Claude turn across multiple machines that each see only their own slice of the conversation
- Cluster-aware policies (CPU-bound work to the workstation, IO-bound to the NAS, etc.)
v3 is well past the holy-grail line. Probably never ships unless the product pivots.
- Routing policy weights beyond the precedence list above — implementation details that should be tunable per deployment.
- Multi-tenant ("can my organization share a shim with another user?") — explicit no in v1 per the threat model in SECURITY.md; v2 multi-machine doesn't relax it.
- Cross-machine permissions inheritance (a tool call originated on
machine A but writes a file on machine B — who owns the audit
trail?) — answered at v2 implementation time; the AUDIT_LOG.md
format already supports
details.target_machineif needed.
- VISION.md §6 — the original capability description.
- PROTOCOL.md — envelope, version negotiation (#35), session ownership (#36).
- SECURITY.md — single-tenant invariant.
- AUDIT_LOG.md — per-record
details.machine_idif v2 needs it.