Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e552012
Add native Claude Code delegation
benthecarman Sep 19, 2026
9450dbc
Gate session agents on integration settings
benthecarman Sep 19, 2026
5b259ee
Document Claude setup and session selection
benthecarman Sep 19, 2026
4cc8256
Extract the host seam behind the desktop UI
benthecarman Sep 19, 2026
39547fe
Add the host wire protocol and remote client
benthecarman Sep 19, 2026
6e2da81
Add Noise transport, pairing, and maple-gpui serve
benthecarman Sep 19, 2026
66e51b7
Connect the desktop app to saved hosts
benthecarman Sep 19, 2026
6db9fd5
Let the desktop app serve paired devices
benthecarman Sep 19, 2026
b5650ff
Pick hosts and projects through one flow
benthecarman Sep 19, 2026
0c88ffb
Start on the host the last new task ran on
benthecarman Sep 20, 2026
8c6c064
Fix the first bugs found using remote hosts
benthecarman Sep 20, 2026
60e432b
Harden the remote protocol
benthecarman Sep 20, 2026
cbbbdc6
Carry request params as struct variants
benthecarman Sep 20, 2026
7603268
Split the network module by role
benthecarman Sep 20, 2026
1d32408
Resume Claude only once it confirmed the session
benthecarman Sep 20, 2026
b4172d8
Tidy the host seam
benthecarman Sep 20, 2026
52cbff1
Move the local host's helpers under host/local
benthecarman Sep 20, 2026
21bdec5
Scope paired devices and pairing codes per account
benthecarman Sep 20, 2026
87d18a6
Fix the desktop app's host and client roles
benthecarman Sep 20, 2026
3b5306c
Gather the remote roles under app/src/remote
benthecarman Sep 20, 2026
4daa510
Route task calls by host and settle host state
benthecarman Sep 20, 2026
e2cadca
Choose projects through the picker dialog alone
benthecarman Sep 20, 2026
9cf8c2b
Move host and picker code out of chat/mod.rs
benthecarman Sep 20, 2026
e3948f2
Drop the wire shims kept for compatibility
benthecarman Sep 20, 2026
b9c2b0b
Stream image uploads ahead of run.send
benthecarman Sep 20, 2026
a165f76
Describe remote development as built
benthecarman Sep 20, 2026
efa87e0
Show the pairing code large, mono, with a copy button
benthecarman Sep 20, 2026
067f33b
Name the open task's host in the header
benthecarman Sep 21, 2026
4f12b89
Spawn the Claude fixture itself as the slow descendant
benthecarman Sep 21, 2026
63bcff5
Create a blank task again on a newly picked host
benthecarman Sep 21, 2026
13772e5
Carry the build hash in both hellos
benthecarman Sep 21, 2026
c425602
Record each host's version on connect
benthecarman Sep 21, 2026
847e6bb
Show each host's version in Settings > Hosts
benthecarman Sep 21, 2026
4ee05f2
Document the hello build and host version line
benthecarman Sep 21, 2026
9ef3abc
Create the task on the first send, not New Task
benthecarman Sep 21, 2026
7a77176
Drop the blank-task workaround for host switches
benthecarman Sep 21, 2026
c80c871
Treat an empty task as the draft it is
benthecarman Sep 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .agents/skills/develop-maple-agent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,17 @@ cleanup. Do not run raw `cargo clean` against the shared cache.
For composer integrations and external providers, read
`apps/maple-agent/docs/external-agents.md`. Keep provider metadata in the
runtime catalog and pass typed selection kinds through the UI bridge so
user-controlled MCP names cannot shadow provider IDs. Preserve inherited
defaults for tasks without overrides and CUA's existing backend metadata.
user-controlled MCP names cannot shadow provider IDs. External providers must
be enabled in Settings before appearing in the composer, and each task must
explicitly select them. A saved task choice cannot bypass Settings. Preserve
CUA's existing backend metadata.
Exercise warm and cold session tool catalogs and ACP exclusion when changing
run-boundary admission.
run-boundary admission. Claude Code uses a native Rust transport adapted from
the pinned Goose provider in `external_agents/claude.rs`. Preserve the source
attribution when changing that adapted code. Keep process ownership in Maple's
contained host. The `claude_native_*` tests re-execute the
Rust test binary as a CLI fixture and need no Claude account or inference
request. Keep fixture launch and environment setup shared with the Codex tests.

## Security and publication

Expand Down
56 changes: 54 additions & 2 deletions apps/maple-agent/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/maple-agent/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["crates/maple-agent", "crates/maple-billing", "app"]
members = ["crates/maple-agent", "crates/maple-billing", "crates/maple-remote", "app"]

[workspace.package]
edition = "2024"
Expand Down
150 changes: 132 additions & 18 deletions apps/maple-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ crates/maple-agent/ Maple's transport-neutral agent runtime, extracted from
the Maple provider over the Maple Rust SDK, developer
tools, permission policy, account-scoped session
storage, and the ACP server.
crates/maple-remote/ The wire between a client and a host: framing,
JSON-RPC on the control channel, binary streams,
the host server, and the remote HostBackend.
crates/maple-billing/ HTTP client for the Maple billing API.
docs/ Theme spec measured from the Tauri app.
scripts/ One maintainer helper: screenshot.py takes a desktop
Expand All @@ -27,13 +30,21 @@ scripts/ One maintainer helper: screenshot.py takes a desktop

### Backend / frontend boundary

`app/src/backend.rs` owns the runtime: it is the only file that drives
`maple_agent`'s services, holding a private Tokio runtime and exposing an
async facade (`AgentBackend`) plus one event stream. UI modules import data
types from `maple_agent` (timeline items, session summaries) but talk to the
running agent through that facade only. This mirrors Maple's own edge-adapter
pattern, so a future process split replaces the facade without touching UI
code.
The UI talks to two facades and never to the runtime directly.

`app/src/backend.rs` (`AgentBackend`) holds the account: the private Tokio
runtime, sign-in and OAuth, billing, audio, and the in-process agent
service. Everything a client drives on a host (tasks, projects, runs,
permissions, integrations, session defaults) goes through the
`maple_agent::host::HostBackend` trait. `AgentBackend::local_host` hands out
the in-process implementation, `LocalHostBackend`, which wraps
`AgentRuntimeHandle` and owns the host-side pieces the UI must not reach
around it for: the git branch watch, directory suggestions, and the SQLite
readers for context usage, tool summaries, and the usage page. Hosts push
`HostEvent`s (runtime events plus branch reports) through one fan-out hub.
A remote host implements the same trait over the wire, so the UI never
branches on where a host runs. See
[`docs/remote-development.md`](docs/remote-development.md) for the plan.

The runtime was originally copied from Research’s Tauri source (now
`apps/maple-research/frontend/src-tauri/src`) (`agent.rs`,
Expand Down Expand Up @@ -74,10 +85,10 @@ Cargo manifests and lockfile; Research has an independent dependency graph.
keeps its row after the turn ends, and Maple tells the task when it
finishes, with a bounded result in the running turn or a new turn Maple
starts automatically. The task can use `load` to retrieve any truncated output.
- External agents: a task can hand work to the Codex CLI installed on
- External agents: a task can hand work to Codex or Claude Code installed on
this computer with the `agent_start`, `agent_send`, `agent_status`,
`agent_cancel`, and `list_agent_providers` tools, once Codex is enabled
under Settings > Integrations. Codex runs in the project with its own
`agent_cancel`, and `list_agent_providers` tools, once the provider is enabled
under Settings > Integrations. Each agent runs in the project with its own
account, context, and sandbox settings; whatever it asks approval for
comes to you through Maple's permission card, and Allow all grants it. Its progress streams
into the tool call's row and its row above the composer has a Stop
Expand Down Expand Up @@ -121,6 +132,28 @@ Cargo manifests and lockfile; Research has an independent dependency graph.
Nothing is downloaded or installed by the app.
- Window size and maximized state persist between launches.

### Hosts

Tasks can run on another machine. Settings > Hosts pairs this device with a
host running `maple-gpui serve` (address plus the one-time code the host
printed) and lists the paired hosts with their connection state. Each row
also shows the version and build the host announced ("last seen" while it
is offline) and says when the host is behind this app, a different build
of the same version, newer than this app, or older than a release the
update check found. Saved hosts connect at launch and reconnect with
backoff. Their tasks join the
sidebar, badged with the host name once more than one host is known, and
the project switcher filters by host. With a task open the header names
the host it runs on; on the new-task screen a chip there names the host
new tasks run on and switches it. Both appear once more than one host is
known. The task itself is created there when its first message is sent.
The host the last new task ran on is the target again at the next launch
once it connects. Choosing a project opens one picker for every
host: a search box over the host's recent projects and folders and a row
that opens a typed path. Host-scoped settings (defaults, system prompt,
integrations, usage) get a host selector when more than one host is
connected. Offline hosts stay listed without their tasks until they return.

### Integrations preview

On macOS and Linux, Settings > Integrations can set up computer use inside
Expand Down Expand Up @@ -158,16 +191,27 @@ account configuration that may roam between devices.
The embedded design, migration rules, privacy boundary, and preview limits are
documented in [`docs/embedded-cua.md`](docs/embedded-cua.md).

#### Claude Code

Settings > Integrations lists Claude Code (`claude`) alongside Codex, with the
same per-task selection, streamed activity, permission cards, and Stop control.
Install the Claude Code CLI on the app's PATH and sign in using
`claude auth login`. Maple uses a Rust transport adapted from Goose's Claude
Code provider. The CLI is the only external runtime dependency. The integration
is off by default. Enable it in Settings to show it in the composer, then
select it for the tasks that should use it. See
[external agents](docs/external-agents.md#how-claude-code-is-driven).

#### Codex

Settings > Integrations also lists the Codex CLI when `codex` is on the PATH
(the login shell's PATH on macOS). The card shows the installed version and
whether Codex is signed in; Maple never runs Codex's sign-in itself. The
toggle is off by default. The composer lists Codex alongside CUA and custom
MCP servers, with an independent choice for each task. Tasks without an
explicit Codex choice inherit the Settings default on every run, including
older tasks; composer overrides survive relaunches. Enabling it gives runs
the external-agent tools and installs the `handoff`, `committee`, and `advisor` skills into the
toggle is off by default. Enabling it makes Codex available in the composer
alongside CUA and custom MCP servers. Each task must select Codex explicitly;
that choice survives relaunches but only applies while Settings enables Codex.
Selecting it gives that task the external-agent tools. Enabling it in Settings
installs the `handoff`, `committee`, and `advisor` skills into the
account's Goose skills directory; disabling removes only the files Maple
wrote. Codex needs version 0.143 or newer. See
[`docs/external-agents.md`](docs/external-agents.md).
Expand Down Expand Up @@ -346,6 +390,7 @@ maple-gpui Open the desktop app.
maple-gpui acp Serve the Agent Client Protocol on stdio.
maple-gpui proxy [FLAGS] Serve an OpenAI-compatible HTTP endpoint.
maple-gpui login Sign in with email and password from a terminal.
maple-gpui serve [FLAGS] Publish this machine's runtime to paired clients.
maple-gpui --version Print the version.
```

Expand Down Expand Up @@ -380,6 +425,68 @@ Without `--cors`, the proxy rejects requests that carry browser-only headers
(`Origin`, `Sec-Fetch-Site`) so a web page cannot spend a saved key through
loopback. With `--cors`, a default key is refused for the same reason.

### `maple-gpui serve`

```
maple-gpui serve Listen for paired clients.
maple-gpui serve pair Publish a one-time pairing code.
maple-gpui serve devices list Paired devices.
maple-gpui serve devices revoke DEV Forget a device by key or name.

--listen ADDR:PORT bind address (default 0.0.0.0:7130, env MAPLE_SERVE_LISTEN)
--name NAME host name clients show (default: hostname, env MAPLE_SERVE_NAME)
```

Runs this machine as a host for the desktop app on another machine, over
a LAN or a Tailscale network. It reuses the sign-in saved by `login` or
the desktop app and hosts its own runtime. Without a saved sign-in it
exits with a message; when the Maple server cannot be reached at start
(a unit that comes up before the network) it serves anyway, requests fail
until the sign-in goes through, and the sign-in is retried in the
background with growing pauses. The desktop app can serve the
same way: Settings > Hosts > "Allow remote connections" (off by default)
listens on the same port, publishes pairing codes, and lists paired
devices; the command and the window share the host key, the device list,
and the lock, so only one of them serves at a time. A device is admitted by a
one-time code: run `serve pair` on the host, enter the code in the app
within five minutes, and both sides pin each other's key; later connections
need no code. Devices and codes belong to the account that is hosting:
`serve pair` and `serve devices` act on the saved sign-in and refuse to run
without one, and a device paired while one account was signed in is not
admitted after another account signs in. Traffic is Noise-encrypted inside a plain WebSocket, so
pairing is the only gate and the listener binds every interface by default.
Repeated wrong codes lock the source address out. Revoking a device ends
its live connections within seconds. One `serve` per data root; a lock
file refuses a second. Each connect is logged with the client's name,
key, version, and build. See [`docs/remote-development.md`](docs/remote-development.md).

`serve` is written to run under systemd: it stops cleanly on SIGTERM as
well as Ctrl-C, reports `READY=1` once the port is bound and `STOPPING=1`
on the way out when `NOTIFY_SOCKET` is set, and logs to stderr for the
journal. A user unit:

```ini
[Unit]
Description=Maple host
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
NotifyAccess=main
ExecStart=%h/.local/bin/maple-gpui serve --listen 100.64.0.7:7130
Restart=on-failure
RestartSec=5
TimeoutStopSec=15

[Install]
WantedBy=default.target
```

Run `maple-gpui login` once as that user first, then
`systemctl --user enable --now maple-serve`; `loginctl enable-linger` keeps
it up after logout.

## Build features

The default build has every mode. Cargo features turn modes off, so a
Expand All @@ -391,6 +498,7 @@ window and its display libraries:
| `desktop` | The gpui window. Without it the binary is headless. |
| `acp` | `maple-gpui acp` and `maple_agent::acp`. |
| `proxy` | `maple-gpui proxy`. |
| `serve` | `maple-gpui serve` and the `maple-remote` host side. |

```sh
cargo build --release -p maple-gpui --no-default-features --features acp
Expand Down Expand Up @@ -430,17 +538,23 @@ The roots follow the platform, the same way the Tauri app's

| Path | Content |
| --- | --- |
| `<config>/settings.json` | App settings. |
| `<config>/agent/accounts/<scope>/config.json` | Per-account agent configuration (default root, model, custom MCP servers, project trust). May roam between machines. |
| `<config>/settings.json` | Client-side app settings, plus per-host task and project state under `hosts`. |
| `<config>/agent/accounts/<scope>/config.json` | Per-account agent configuration (default root, model, custom MCP servers, project trust, session defaults: permission mode, web access, harness instructions). May roam between machines. |
| `<config>/agent/accounts/<scope>/goose/config/` | Goose permission file for the account. |
| `<config>/agent/accounts/<scope>/goose/config/skills/` | Skills the account's tasks can load, including the delegation skills Maple installs while Codex is enabled. |
| `<config>/agent/accounts/<scope>/goose/config/skills/` | Skills the account's tasks can load, including the delegation skills Maple installs while any external agent is enabled. |
| `<config>/agent/goose-runtime/` | Goose process configuration. |
| `<local data>/auth.json` | Sign-in credentials (mode 0600). Device-local; never in a roaming profile. |
| `<local data>/agent/accounts/<scope>/integrations.json` | Per-account defaults and validated launch details for integrations detected on this device. |
| `<local data>/agent/accounts/<scope>/goose/data/sessions/sessions.db` | Goose session history and usage ledger (SQLite, WAL). |
| `<local data>/agent/accounts/<scope>/tool_summaries.db` | Model-written one-line summaries of tool calls (SQLite, WAL). |
| `<local data>/agent/accounts/<scope>/attachments/` | Image attachments. |
| `<local data>/agent/acp/accounts/<scope>/config.json` | ACP configuration. |
| `<local data>/remote/host_key.json` | This machine's static Noise key as a host (mode 0600). |
| `<local data>/remote/device_key.json` | This machine's static Noise key as a client device (mode 0600). |
| `<local data>/agent/accounts/<scope>/hosts.json` | Hosts this account paired with: key, name, addresses, last seen version and build. |
| `<local data>/remote/accounts/<scope>/devices.json` | Devices paired into this account on this host. Another account's host never admits them. |
| `<local data>/remote/accounts/<scope>/pending_pairing.json` | The pairing code `serve pair` published for this account, until used or expired (mode 0600). |
| `<local data>/remote/serve.lock`, `serve.json` | The running host's lock and its listen address. |
| `<local data>/logs/maple-gpui.log` | Log file. Panics are logged here too. |

`<scope>` is the SHA-256 of the account's user id. Small JSON files are
Expand Down
Loading