Refactor Maple Agent runtime for thin Desktop and ACP adapters - #732
Merged
Conversation
AnthonyRonning
force-pushed
the
codex-maple-agent-service-refactor-maple
branch
from
August 4, 2026 20:46
285d9f6 to
75a72f0
Compare
Deploying maple with
|
| Latest commit: |
75a72f0
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://70d45aab.maple-ca8.pages.dev |
| Branch Preview URL: | https://codex-maple-agent-service-re.maple-ca8.pages.dev |
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 a draft architecture and implementation review.
This branch supersedes the implementation and architecture proposed in #714, while leaving that PR open as the original exploration record. It preserves the successful proof that Buzz can control the real Maple Agent without forking Buzz or Goose, but restructures the work around a maintainable Maple-owned runtime service.
The intended merge boundary is:
Summary
This PR:
MapleAgentService;maple acpstdio connector backed by an owner-only local Unix socket;The important result is not merely that Maple speaks ACP. It is that there remains exactly one Maple-controlled Goose runtime.
Architecture
flowchart LR UI["Maple Agent UI"] --> FE["AgentRuntimeService"] FE --> Tauri["Thin Tauri commands and event projection"] Client["Buzz or another ACP client"] --> Connector["maple acp stdio connector"] Connector --> Socket["Owner-only local Unix socket"] Socket --> ACP["ACP transport and protocol adapter"] Tauri --> Core["MapleAgentService typed runtime facade"] ACP --> Core Host["AgentHostLifecycle"] -. "serializes stop, restart, clear, update, and exit" .-> ACP Host -. "composite lifecycle" .-> Core Core --> Goose["Existing embedded Goose runtime"] Goose --> Provider["MapleProvider and authenticated MapleApiSession"] Goose --> Tools["Maple developer, web, and Skills clients"]There is one account-scoped runtime. Desktop and ACP independently project onto it; neither adapter calls through the other.
The core service owns Maple semantics:
The adapters own surface-specific concerns:
agent-eventenvelopes, and UI projectionmaple acpconnectorThe core imports no Tauri or ACP protocol types. “Thin adapter” refers to this ownership boundary, not necessarily line count: a safe ACP edge still needs substantial transport, correlation, backpressure, cleanup, and IPC logic.
Why this refactor is useful beyond ACP
This is directionally the architecture Maple Agent should have even if ACP remains hidden.
Primary Agent Mode stays direct:
External harnesses remain optional:
The service is a Maple-owned superset. Desktop and ACP do not need identical command sets, event types, or capabilities. Each adapter exposes the subset appropriate for its caller without forcing Maple's domain model to conform to ACP.
This PR intentionally does not extract the core into a standalone crate or decompose every large Agent module. It remains composed inside the Tauri binary. A future headless host could justify further extraction, but this pass lets primary Maple Agent features continue driving the internal design.
Desktop compatibility
The Desktop boundary remains intentionally stable:
agent-eventenvelopes remain;Stop and restart now return a small lifecycle outcome:
This is intentional. If the core runtime successfully changes state but ACP cleanup fails, Desktop must apply the authoritative runtime status instead of remaining visually stale. The frontend unwraps this result, refreshes sessions, and then shows the cleanup warning.
Surface ownership and isolation
A late review pass found several places where a shared runtime could accidentally expose one surface's live state to another. This branch now makes ownership explicit:
controlled_externally; an orphaned pending request becomescancelled.The intended distinction is:
Focused regression tests cover status filtering, cancellation scope, timeline isolation, terminal cleanup, and persisted permission reconciliation.
Caller-owned ACP approvals
The surface that starts a run owns its unresolved interactive permission decisions:
sequenceDiagram participant Client as ACP caller participant Adapter as Maple ACP adapter participant Core as MapleAgentService participant Goose as Embedded Goose Client->>Adapter: session/prompt Adapter->>Core: send message with connection-scoped context Core->>Goose: run with Maple provider, tools, and policy Goose-->>Core: action requires permission Core->>Core: apply Maple automatic classifier alt Covered by Maple policy Core->>Goose: automatic decision else Unresolved ACP-owned request Core-->>Adapter: typed run-scoped permission request Adapter->>Client: request_permission Note over Adapter,Client: allow_once or reject_once Client-->>Adapter: caller decision Adapter->>Core: opaque exact-run response Core->>Goose: permission confirmation end Goose-->>Core: ordered events and terminal state Core-->>Adapter: bounded run stream Adapter-->>Client: session/update and prompt resultThis branch deliberately does not add a “wait for approval inside Maple Desktop” broker for ACP runs.
Behavior is fail-closed:
allow_onceandreject_once; andThe old exploratory
allow_allsetting is migrated to the caller-mediatedread_only/smart_approvepolicy.read_onlyis a retained serialized name, not a literal filesystem sandbox.Buzz's current behavior is to select
allow_onceautomatically. That is Buzz's product decision, not a second Maple approval mode.Maple does not yet have a separate non-overridable “dangerous deny” classifier. If one is introduced later, it should run as Maple policy before any ACP permission request, not become a competing interactive broker.
Composite host lifecycle
ACP and the core runtime have separate internal state, but process-level lifecycle operations must cover both:
sequenceDiagram participant UI as Desktop frontend participant Fence as Account operation fence participant Host as Native host lifecycle participant ACP as ACP listener participant Core as Maple Agent runtime UI->>Fence: blockAndDrain(user) UI->>Host: stop or restart Host->>ACP: attempt shutdown ACP-->>Host: success or cleanup warning Host->>Core: always stop or restart Core-->>Host: authoritative runtime status Host-->>UI: status plus optional acpShutdownError UI->>UI: apply status and refresh sessions alt ACP cleanup warning UI->>UI: surface warning Note over UI: security-sensitive credential cleanup remains blocked endThe host lifecycle now:
The frontend performs one native composite stop. The manual
agent_acp_stopcommand remains reserved for the settings page because that action also updates saved configuration.Why Goose ACP is not instantiated directly
The Goose team's suggested shape—add an ACP crate, instantiate a server, and plug in the existing GDK loop—is directionally exactly what Maple wants.
At Maple's pinned Goose revision,
c3111c71cd682ed1d115741677f0ca9946c51499, the public Rust API is not yet that shape.About the
goose-acpcrateAt this revision:
goose-acp;goose-acp-macrosexists, but it is a proc-macro crate for custom JSON-RPC dispatch and schema generation;goose::acpmodule; andserve<R, W>transport is concrete overArc<GooseAcpAgent>, not a host-supplied backend or loop trait.GooseAcpAgent::new(GooseAcpAgentOptions)accepts a provider factory, builtins, paths, platform/source roots, naming policy, and scheduler. It does not accept an existing:AgentManager;SessionManager;PermissionManager;Its constructor creates fresh session and permission managers, a fresh provider inventory, an
AgentConfigusingConfig::global(), and a freshAgentManager.Consequently:
goose acpwould expose independently configured Goose;goose serveafter Maple initializes would still create a second runtime;GooseAcpAgentwith a Maple provider factory would solve provider creation only;Maple's semantic boundary is not a bare Goose
Agent. It includes:MapleApiSessionandMapleProvider;Goose ACP also owns permission requests and confirmations directly, advertises persistent choices such as allow/reject always, can substitute ACP-client filesystem/terminal behavior, and accepts generic client MCP definitions. Bypassing Maple's wrapper would therefore change both its security policy and its product behavior.
Goose's mature history replay, response builders, tool converters, usage mapping, permission mapping, and handlers are useful, but at this revision they are private or
pub(crate)and tied to concreteGooseAcpAgentstate.Upstream seams that would help
The ideal upstream improvement is to separate ACP protocol projection from runtime ownership. Useful options include:
AgentEvent -> session/updateprojector.A
GooseAcpAgent::from_componentsconstructor plus pluggable prompt runner, permission broker, tool setup, and projector would also address most of the gap.Even with those seams, Maple would retain the small local connector: Buzz owns the spawned stdio process and its environment, while Maple authentication lives in the already-running Desktop process.
Until then, the narrow adapter over
agent-client-protocol1.0.1 is the smallest no-fork path that exposes Maple rather than a parallel Goose runtime.Current ACP surface
The detailed capability-by-capability analysis is in
docs/agent-mode-acp.md. The condensed state is:initializesession/newcwdsession/promptsession/cancelsession/listsession/closesession/resumesession/forksession/loadNomeans “not implemented by this preview,” not “fundamentally impossible.”Two ACP v1 baseline gaps are important:
ResourceLinkprompt blocks are not accepted; andThe preview should therefore be described as parity for the tested Buzz task path, not complete ACP v1 conformance or Goose ACP parity.
None of the missing rows blocks the previously tested Buzz flow. Buzz does not require list/load/resume/fork/close for that path, tolerates Maple's default model/config, and falls back from its unstable native steering extension.
Buzz compatibility and credentials
The wire surface is standard ACP v1, but the first consumer requires explicit compatibility behavior:
BUZZ_*allowlist plusPATH;buzz-dev-mcpdefinition;Buzz ACPtask title; andIf this integration is maintained, those constants and transformations should move behind a dedicated Buzz compatibility module.
External tool context is installed as an exclusive, revocable per-session lease. It is checked by account, session, and installation identity and never mutates Maple's process-global environment. Revocation is linearized with process launch so no already-copied context can start another command after the revoke barrier returns.
This improves containment, but it is not a sandbox:
Flatpak credential-bearing execution fails closed. Windows local IPC is unsupported.
Feature gate and configuration
The Agent connections surface is:
VITE_FORCE_FEATURE_FLAGS=agent_connectionsoverride;Activation is manual after every Maple launch. A saved
enabledvalue does not auto-start the listener.The page supports:
acpargument;Policy changes require Stop → Save → Start to avoid an admission race. Maple currently defaults to one ACP connection, matching the validated Buzz setup.
Historical Buzz end-to-end evidence
The original macOS exploration used:
c3111c71cd682ed1d115741677f0ca9946c51499;3a4bf513df0e0c258587bfcbed9463d63723b56b;1; andallow_allpolicy.Two Buzz GUI tasks completed:
MAPLE-GUI-OK.README.mdusing its local tools and posted a substantive explanation of the project.This proves that the connector, local IPC, Maple runtime, local tools, ACP stream, and signed Buzz publication path can work together.
It does not prove the final caller-mediated permission path end to end. The old
allow_allpath has been removed and migrated. Current caller ownership is covered by implementation and focused tests, but this PR should not claim a fresh caller-owned Buzz GUI result unless one is explicitly rerun.The README task took roughly two to three minutes. That has not been profiled and should not be attributed to ACP.
Validation
Automated and local validation on the rebased branch:
cargo check --testspassed.cargo test --all-targetspassed in the final pre-commit run: 264 passed, 0 failed, 1 model-backed OCR test ignored.cargo fmt --check, strictcargo clippy -- -D warnings, Prettier, andgit diff --checkpassed. Patchedtaoemits its existing dependency warnings.OpenSecreton127.0.0.1:31061, Billing on127.0.0.1:36201).Maple.appwas built, ad-hoc signed, verified under the exact workspace bundle ID, and confirmed to contain the local OpenSecret endpoint. The expected updater-signing step reported the intentionally absent private key only after producing the.app,.dmg, and updater archive.MAPLE-REVIEW-SMOKE-OKresponse.Current test coverage includes:
Still absent:
Tradeoffs
Benefits
Costs
Maintenance recommendation
Keep Maple Agent's primary path direct and continue building Maple features against
MapleAgentService.Keep ACP default-off at the edge. Add protocol capabilities only for demonstrated consumers rather than chasing speculative Goose parity.
Maintaining this bounded adapter is reasonable if external harness interoperability remains strategically useful. Maple should not:
The best long-term reduction in Maple-owned ACP code is an upstream Goose embedding seam for runtime injection, permission routing, transient context, and event projection.
If the adapter remains:
Suggested review order
frontend/src-tauri/src/agent.rs— Maple service, runtime handles, routing, cancellation, permissions, timelines.frontend/src-tauri/src/agent_tauri.rs— thin Desktop projection and preserved event contract.frontend/src-tauri/src/agent_host.rs— composite lifecycle behavior.frontend/src-tauri/src/agent/tool_context.rsand developer-tool changes — bounded transient context and process cleanup.frontend/src-tauri/src/agent_acp.rs— protocol, IPC, backpressure, caller permissions, and Buzz adaptation.docs/agent-mode-acp.md— detailed support matrix, trust model, and maintenance guidance.Questions for reviewers
MapleAgentServicethe right durable boundary for Maple-controlled Goose?