Expose Maple Agent Mode as an ACP harness - #714
Closed
AnthonyRonning wants to merge 4 commits into
Closed
Conversation
Deploying maple with
|
| Latest commit: |
69d246f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://c7bea48d.maple-ca8.pages.dev |
| Branch Preview URL: | https://codex-maple-buzz-acp-maple.maple-ca8.pages.dev |
Contributor
Author
|
Closing this exploratory draft because the implementation has been superseded by the merged Maple agent service refactor and follow-up ACP feature-flag work. Keeping the remote branch for historical reference. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Status
This is an exploratory draft and an architectural decision request.
The original question was: can Buzz control the actual Maple Agent—not a separately configured Goose process—without forking Buzz or Goose?
The answer is yes. This branch has completed three manual end-to-end Buzz GUI tests, including a substantive local-file task. The larger question for review is whether Maple should maintain this optional adapter and, if so, which parts should become durable internal abstractions.
This PR does not propose ACP as Maple's primary Agent architecture. Directly embedded Goose remains Maple's primary runtime.
Summary
This PR:
maple acpmode to the packaged Maple executable for stdio-based harnesses such as Buzz;Most of the implementation complexity is not basic ACP JSON-RPC. It is runtime ownership, lifecycle, account isolation, cancellation, event delivery, and bounded handling of Buzz's signing credentials.
The durable design and threat-model notes are also checked in as
docs/agent-mode-acp.md.What this proves
flowchart LR B["Buzz Desktop"] --> H["Buzz ACP harness"] H -->|"spawns packaged Maple executable<br/>ACP v1 over stdio"| C["maple acp connector"] C -->|"owner-only Unix socket"| S["Maple Desktop ACP service"] S --> R["Maple runtime facade"] R --> G["embedded Goose AgentManager"] G --> P["MapleProvider"] P --> A["authenticated MapleApiSession"] A --> O["OpenSecret inference"] G --> T["Maple developer, web, and skills clients"] T -->|"ephemeral BUZZ_* environment"| CLI["Buzz CLI"] CLI -->|"signed durable reply"| B G -->|"Maple timeline events"| R R -->|"ACP session/update"| HThe important distinction is that Buzz is not controlling a second Goose installation. It is controlling the same Maple Agent runtime the desktop UI uses.
That preserves:
Request flow
sequenceDiagram participant U as Owner in Buzz participant R as Buzz relay participant H as Buzz ACP harness participant C as maple acp connector participant D as Maple Desktop participant M as Maple Agent / embedded Goose participant B as Buzz CLI U->>R: Mention Maple in a channel R->>H: Owner-authorized channel event H->>C: Spawn with Buzz context and credentials C->>D: Private bridge hello over Unix socket H->>D: initialize, session/new, session/prompt D->>M: Create a real Maple task and start its run M-->>D: Thought, message, and terminal events D-->>H: ACP session/update and prompt result M->>B: Run Buzz CLI with scoped credentials B->>R: Publish signed durable reply R-->>U: Show reply in the Buzz threadACP streaming and durable Buzz publication are different paths. Buzz's prompt instructs the agent to publish useful results with the Buzz CLI. The ACP stream carries progress and completion; the CLI creates the signed channel reply.
Why we could not simply enable Goose ACP
Goose already has substantial ACP support. The problem is ownership and injection, not absence.
About the proposed
goose-acpcrateA natural embedding model would let a Goose Development Kit (GDK)-based agent add a
goose-acpcrate, instantiate a server, and plug in its existing loop. That is directionally the API Maple wants, but it is not the API exposed by Maple's pinned Goose commit.At
c3111c71cd682ed1d115741677f0ca9946c51499:goose-acp;goose-acp-macrospackage is a proc-macro crate for Goose custom-method dispatch and schema generation, not an ACP server or agent-loop abstraction;goose::acpmodule inside the maingoosecrate; andAcpProvidergoes in the opposite direction: it lets Goose consume another ACP agent as a model provider, rather than exposing an existing Goose/Maple loop as an ACP agent.If
goose-acpmeant the upstreamagent-client-protocolwire crate, then the suggestion is correct at the protocol layer—and this prototype already uses that crate directly. What is missing is the next layer down: a Goose ACP backend trait or constructor that can accept a host-owned loop.The generic parameters on
serve<R, W>apply to the byte streams, not the agent backend. The function and its handler require a concreteArc<GooseAcpAgent>; there is noAcpBackend,AgentLoop, or equivalent trait for Maple to implement.At Maple's pinned Goose commit, Goose exposes:
GooseAcpAgent;GooseAcpAgentOptions;serve(...)over arbitrary byte streams;goose acp; andgoose serve.However,
GooseAcpAgent::newconstructs fresh instances of:SessionManager;PermissionManager;AgentManager;The server factory takes the same standalone path. Goose's
serveaccepts an already-createdGooseAcpAgent; it does not adapt an arbitrary existing Goose runtime.Its public options do not let a host inject Maple's already-running managers, authenticated provider object, tool clients, task lifecycle, permission broker, or UI event projection.
Therefore:
goose acpwould create a standalone Goose runtime using Goose configuration and credentials;goose serveafter Maple initializes would still create a second Goose brain, store, permission boundary, and tool surface;MapleApiSession, Tauri handle, account state, and tool clients;Goose ACP could therefore be reused only by accepting “Goose controlled by Buzz,” not “Maple controlled by Buzz.”
The current implementation uses the standard
agent-client-protocolcrate and adapts a deliberately narrow surface into Maple-owned operations.Goose improvements that would make this substantially easier
The most useful upstream change would be separating Goose's ACP protocol mapping from ownership of the Goose runtime.
Prioritized seams:
AgentManager,SessionManager, andPermissionManagerhandles plus host-owned provider/model services.goose-acpcrate. A small library with a genericAcpBackendtrait, with Goose's current runtime as its default implementation, would make the “plug in your loop” model real for GDK embedders.The small connector and local IPC boundary would still be necessary. Buzz launches a subprocess containing its environment, while the authenticated Maple runtime lives in the already-running desktop process.
Implementation model
Connector and local transport
The packaged executable now has two modes:
maple acpforwards stdin/stdout to the running desktop service.The connector computes the same per-executable Unix-socket path as the desktop app. This prevents one development build from accidentally connecting to another installed or worktree build.
The endpoint is:
0600;0700runtime directory on Linux; andThe service is disabled by default and must currently be started manually. If its listener fails, Start drains the stale task and can bind a fresh listener rather than leaving the page permanently wedged.
ACP surface
Nomeans "not implemented by this prototype," not "fundamentally impossible." Effort is relative to this branch:Goose already implements many of these semantics, but at Maple's pinned revision its history replayer, response builders, tool converters, permission mapping, usage mapping, and handlers are private or
pub(crate)and operate on concreteGooseAcpAgentstate. Maple can port that behavior, or Goose could extract it, but Maple cannot currently plug its host-owned runtime into those handlers.initializesession/newcwdand creates a real Maple task. Optional additional workspace directories and generic ACP-provided MCP servers are graded separately below.session/prompttextsession/updatetext and thoughtGooseAcpAgent.session/cancelsession/listcwd. The adapter needs ACP field mapping and pagination plus a policy decision: may a same-user ACP client enumerate all Desktop tasks, or only ACP-created tasks? Restricting by origin would first require durable provenance because preview tasks are ordinary Maple user tasks.session/resumecwd, allowed roots, and active-run state, preserve its persisted model, and restore Maple provider/tool/permission state plus the current connection's transient environment. A service-global session lease is needed so two ACP clients cannot overwrite one task's credentials or tool context. This is a Maple lifecycle seam, not an agent-loop redesign.session/loadsession/closesession/deletesession/forkSessionManageralready has copy primitives and its ACP implementation shows copy, truncation, and activation. Maple needs an account-scoped wrapper, root/policy validation, fresh transient context, activation, and rollback for partial failures. ACP fork is still unstable, so enabling it also accepts an unstable wire commitment; no Goose fork is otherwise required.MapleProvider; provider switching should remain out of scope. The tested Buzz build tolerates this surface being absent and uses Maple's default.allow_oncefor forwarded requests, so delegation would materially weaken the current local-approval boundary. Goose's mapping exists, but it is not an injectable broker.ToolCallandToolCallUpdatecards. This is mostly projection work. Buzz also treats the initialtool_callnotification as activity, so projecting tool-call start would reset its idle watchdog during a long otherwise-silent tool, improving liveness as well as UI visibility.AcpEventProjectorwould avoid maintaining this nuanced mapping twice.MapleProvideralready carry images. Maple's non-UI send facade is text-only and ACP forcesvision_capable: false; support needs structured prompt input, MIME/base64/size limits, native model-capability resolution, and replay/UI policy. This is mainly a Maple facade change.file://text read, which Maple should not copy literally.Agentadd/remove path persists state, while lower-level publicExtensionManagermutations still lack a validated, authorized, per-session overlay with rollback and guaranteed cleanup. The ideal upstream seam is that overlay plus host authorization._goose/unstable/session/steer, not an ACP v1 method. Goose's underlyingAgent::steerqueue is public; Maple needs an active-agent/run facade,expectedRunIdvalidation, correlation updates, and prompt-end/cancel race tests. Buzz's cancel-and-merge fallback means this is not required for the tested flow, and adopting it would couple Maple to an unstable extension.This is Maple behavioral parity for the tested task path, not Goose ACP feature parity.
Two ACP v1 baseline caveats are worth making explicit: agents must accept
ResourceLinkprompt blocks and stdio MCP definitions. The prototype handles neither generically. The tested Buzz custom-harness path uses text prompts and does not depend on arbitrary MCP definitions; Maple also contains one exact Buzz compatibility adaptation. Image, audio, and embedded-resource capabilities are correctly advertised as unavailable.None of the
Norows blocks the tested Buzz harness. That Buzz build does not call list/load/resume/fork/close, tolerates absent model/config options by using the agent default, and falls back from native steering to cancel-and-merge. Initialtool_callnotifications would improve Buzz Desktop visibility and reset its idle watchdog during long tools. Standard usage would improve observability, while durable turn metrics currently rely on a Goose-private notification. Its signed channel reply remains a separate tool/CLI path.Runtime integration
The implementation adds narrow non-UI seams to Maple Agent:
The existing Tauri UI commands retain their current public return types. Agent runtime Stop/Restart drains ACP first so a live ACP connection cannot retain references to a replaced runtime or silently lose its Buzz environment.
Desktop configuration
A new Preview settings page on macOS/Linux provides:
acpargument;Logout, Agent-data/history clearing, Agent-runtime stop/restart, update restart, and app exit stop ACP before stopping or clearing Agent state.
The UI fences configuration and authenticated mutations to the current account. Status polling and emergency Stop stay local and do not refresh credentials. A failed configuration load keeps policy mutations locked rather than replacing the saved policy with permissive defaults.
Permission model
Two persisted policies are currently exposed:
read_onlyis labeled Require local approvals and maps to Maple/Goosesmart_approve.allow_allmaps to Maple/Gooseautofor unattended operation.The
read_onlyidentifier is historical shorthand, not a filesystem sandbox. Read operations can proceed, and a write-capable action can still occur after the user approves it in Maple Desktop. This mode is unsuitable for unattended Buzz operation.allow_alllets the connected client ask Maple to run commands, modify files, and perform external actions without another local prompt.Permission changes require Stop → change and save → Start. This closes an admission race where a new session could otherwise pin the old permissive policy while a restrictive save reported success.
Native configuration contains optional allowed project roots, but the preview UI does not expose them. Empty roots means any absolute cwd is accepted within Maple's OS permissions. Even a configured root only gates session admission; it does not confine every later file or shell path. Neither mode is an OS sandbox.
Trust and credential boundaries
owner-onlymodeThe credential path is intentionally narrow:
BUZZ_*environment values._maple/bridge/hellonotification across the owner-only socket before forwarding normal ACP.BUZZ_*variables plusPATH, rejecting null bytes and values over 16 KiB.This must not be described as “the agent never sees the Buzz key.” The trusted Maple Agent's shell commands can use it; that is how durable signed replies work. A command can inspect or transmit it.
allow_allshould be used only with trusted clients, prompts, projects, and toolchains.Credential-bearing Flatpak shell sessions fail closed because safe host-child lifetime handling has not been established there.
Buzz-specific behavior
Although the external wire format is standard ACP v1, the first adapter is deliberately Buzz-specialized:
BUZZ_*environment allowlist;buzz-dev-mcpcompatibility handling;Buzz ACPtask title;For
buzz-dev-mcp, Maple recognizes a narrowly shaped absolute stdio definition—matching server name and executable basename, no arguments, and an existing executable file—but adapts its environment into Maple's existing developer shell instead of launching a second general-purpose shell server.If this feature is maintained, Buzz constants and environment/MCP behavior should move into an explicit compatibility module instead of gradually becoming generic Agent-runtime behavior.
Manual end-to-end validation
Tested source state:
c3111c71cd682ed1d115741677f0ca9946c51499;3a4bf513df0e0c258587bfcbed9463d63723b56b;1; andThree Buzz GUI tests completed:
A deterministic channel mention produced exactly:
Buzz asked Maple to read the branch's actual
README.mdfrom disk and explain the project.Maple used its real local-file tooling and returned a substantive explanation covering:
VITE_OPEN_SECRET_API_URL;It also correctly observed that the README's privacy discussion primarily covers signing/update integrity rather than Maple's broader confidential-compute model.
After the final rebase and workspace-specific packaged-app rebuild, a second deterministic mention produced exactly:
Buzz showed the signed reply in the triggering thread. Maple finished with one connected client, one session, and zero active runs.
The substantive run took roughly two to three minutes and ended with zero active runs. The timing has not been profiled and should not be attributed to ACP. The response remained visible in the Buzz thread.
This is manual local integration evidence, not an automated compatibility, conformance, performance, load, billing, or security test.
Setup issues encountered
Exploration log
127.0.0.1, while the local relay tenant was registered underlocalhost. Because the host participates in relay community lookup, this produced an HTTP 404. Aligning the local relay/community URL fixed it without source changes.enabledflag does not auto-start it on the next launch.Tradeoffs and limitations
Benefits
Costs and current gaps
Buzz ACPtasks.allowedProjectRootsandmaxConnectionsexist in native config but are not exposed in the UI.Alternatives considered
Run
goose acpRejected for Maple parity. This would expose an independently configured Goose process, not Maple's authenticated desktop runtime.
Turn on
goose serveafter Maple initializesNot currently a switch on the existing runtime. Goose still creates its own managers, store, tools, permissions, and global configuration relationship. It would also introduce an HTTP/authentication surface while Buzz's custom harness contract is stdio-based.
Construct
GooseAcpAgentwith a Maple provider factoryCloser, but still produces a second AgentManager, SessionManager, PermissionManager, extension surface, and task lifecycle. The provider factory is necessary but insufficient and is not consistently used by all restoration/recreation paths.
Point Goose ACP at Maple's data directory
Rejected. Sharing persistence does not make independently cached runtimes one runtime and risks conflicting ownership.
Fork Goose
Would allow injection points, but creates exactly the maintenance burden this experiment was trying to avoid.
Fork Buzz
Unnecessary. Buzz already supports arbitrary custom ACP commands.
Maple-owned thin ACP adapter
Chosen because it is the only current option that makes Maple—not standalone Goose—the authoritative runtime.
Maintenance recommendation
Do not make ACP Maple's primary Agent abstraction.
The direct embedded-Goose path should remain:
ACP should remain an edge adapter:
If external harness interoperability is strategically useful, I recommend:
I would maintain this as an experimental adapter if Buzz and other external harnesses are strategically useful. I would not build Maple Agent around ACP or pursue complete Goose ACP parity speculatively.
Related prior work:
goose serve/ACP.Automated validation
The final rebased branch was validated with:
taodependency warnings were emitted.nix develop -c cargo test --manifest-path frontend/src-tauri/Cargo.toml --all-targets --lockedaarch64-darwinchecks, apps, and development shells.cd frontend nix develop .. -c src-tauri/scripts/run-with-desktop-onnxruntime.sh \ bun tauri build --debug --config ../.local/tauri-workspace.json.app,.dmg, and unsigned updater.app.tar.gzwith the managed workspace's unique Tauri identifier and backend URL. The command then exited non-zero at updater signing because this disposable workspace has no release private key..appwas ad-hoc signed and passedcodesign --verify --deep --strictbefore GUI testing.MAPLE-PR-READYround trip completed.The existing patched
taodependency emits known warnings during native builds; no new Maple warnings were accepted.Required before merge consideration
origin/masterat draft creation time.enabledshould auto-start after relaunch.Questions for reviewers
allow_allremain available, and how prominently should its Buzz credential implications be presented?