Wiki · Research
goose — local state machine, MCP extensions, ACP providers, and session isolation
Shelf
Research

goose — local state machine, MCP extensions, ACP providers, and session isolation

This extraction answers WI-023’s bounded product question: how Goose keeps a local agent loop re-entrant while its provider, extensions, MCP calls, ACP-backed agents, recipes, and sessions may be owned by different processes. It follows the shared spine and the product track template. It is an implementation extraction, not a security certification, provider comparison, or claim about hosted/current Goose behaviour.

Snapshot and read boundary

Measured — The source is the public aaif-goose/goose repository at commit 3810898a7447ec3299be72e223d3570a7aabf0ab, read 2026-08-15. git describe reports v2.0-rc-04-27-0-1051-g3810898a7; this is a pinned source snapshot, not a current-release claim. The repository’s LICENSE is Apache-2.0.

Measured — The bounded checkout measurement was:

git -C C:\Temp\rungs-follow-on-20260815\goose ls-files
  2,368 tracked files
    514 Rust files
    104 test-named/test-directory files
    332 Markdown files

The counts describe tracked paths at this commit. They do not measure compiled dependencies, desktop bundle size, network services, provider quality, or the permissions of an installed MCP server.

Documented — The read set is the goose-agent state machine; Goose’s agent, extension, MCP-client, tool approval/execution, configuration, ACP provider, session manager, recipe, and targeted state-machine tests. The selected executable evidence is the calculator MCP test double and the tool_lifecycle, reconstruction_isolation_lifecycle, recipe_scheduling_lifecycle, and provider_lifecycle test modules. The checkout was inspected at the pinned commit; no Goose binary, provider, MCP server, or ACP child process was run locally.

One local request through an MCP result

Implemented — goose_agent::machine::StateMachine::run loads a session_id, calls StateMachine::step, applies returned effects through EffectHandler, and reloads the session for the next step. It stops when no operation applies or when an operation yields to the client. Goose’s concrete state_machine::run adds usage accounting and emits the final assistant text. The persisted session, rather than an in-memory pipeline object, is the input to every iteration (machine.rs, state-machine/session.rs).

Implemented — InferenceRunner supplies provider-facing messages, extension prompt parts, MCP tools, and model configuration. It calls the selected Provider; a direct provider returns a stream of assistant text/tool requests, which become conversation effects. Provider errors become an explicit error message effect, while cancellation adds error responses for unanswered tool requests (ops_llm.rs).

Implemented — ToolExecutionOperation::inference_tools calls ExtensionManager::get_prefixed_tools_excluding(&session.id, ...). A model tool request is classified as executable, declined, or a parse error by pending_tool_requests; only known tools are dispatched. ExtensionManager::dispatch_tool_call resolves the prefixed tool to one extension, checks its configured availability, subscribes to notifications/action-required messages, and creates a ToolCallContext carrying session_id, working_dir, and the optional tool-call request id (ops_toolcalling.rs, extension_manager.rs, tool_execution.rs).

Implemented — McpClient::call_tool builds an MCP CallToolRequest and sends it with the session id, working-directory header, tool-call request id, and cancellation token. The result is returned as CallToolResult; notification and action-required streams remain available while the future completes (mcp_client.rs).

Implemented — The first-party calculator test extension exposes add, multiply, subtract, divide, add_values, add_with_audience, and request_value. Its call_tool records the ToolCallContext, validates JSON arguments, applies a checked arithmetic operation, and returns a successful CallToolResult containing result: <total>. Cancellation and a two-second barrier/timeout are explicit test paths (calculator_extension.rs).

Implemented — The result is converted into a user-role tool-response message and persisted by SessionManager::apply_effects; the next state-machine iteration sends that tool result back to the provider. The tool_lifecycle::basic_tool_calling test and the calculator’s result: 1 assertions exercise this pairing. reconstruction_isolation_lifecycle::tool_turn_reconstructs_after_every_applied_step rebuilds the pipeline after every applied step and asserts one tool call, one tool response, and the final assistant message (tool_lifecycle.rs, reconstruction_isolation_lifecycle.rs).

Ownership: direct provider, ACP provider, or MCP extension

PathLoop ownerState carried by GooseFailure/control boundary
Direct model providerGoose’s StateMachine and InferenceRunnerPersisted Goose conversation, tool metadata, usage, recipe, and session configProvider stream/error is translated into conversation effects; Goose runs tool approval and MCP dispatch.
ACP-backed providerThe external ACP agent owns its internal prompt/tool loop; Goose’s AcpProvider adapts it to ProviderOne ACP session id, mode/config options, handoff marker, pending confirmations, and streamed updatesACP child process/protocol owns agent execution; Goose maps text/tool/permission updates and can resume only when the agent advertises session/load.
MCP-backed extensionGoose owns the model loop; the MCP server owns the capability implementationExtension client/server connection, tool list cache, ToolCallContext, and MCP request/response idsMCP transport/server owns extension effects; Goose owns tool availability, confirmation, cancellation, and message persistence.

Implemented — AcpProvider::connect starts an ACP client loop on a dedicated current-thread Tokio runtime, initializes the protocol, and eagerly sends session/new. AcpProvider reports manages_own_context() == true, routes permission through ActionRequired, and keeps the ACP session id separate from the Goose session id. resume uses ACP session/load when available and closes the replaced session; otherwise it returns an explicit unsupported error (provider.rs).

Implemented — ACP stream sends a prompt to the external agent, buffers text/thought/tool updates, maps ACP tool calls into Goose tool-request messages, forwards permission requests to a pending confirmation map, and emits provider usage on completion. Cancelled/refused prompts roll back the one-time handoff-context marker so a retry can resend context; a completed prompt commits it. This is an ownership boundary, not a second Goose MCP loop.

Implemented — An ACP child is spawned with configured command/args, piped stdin/stdout/stderr, kill_on_drop(true), configured environment additions/removals, and a PATH adjustment. The source does not establish a sandbox, privilege drop, or network isolation for that child; those remain external operating-system or provider policy (provider.rs).

Implemented — MCP extensions are represented by ExtensionConfig variants for stdio, streamable HTTP, built-in, and platform extensions. ExtensionManager caches prefixed tool schemas, invalidates the cache on notifications/tools/list_changed, and dispatches by the resolved extension owner. A platform extension runs in Goose’s process; stdio/HTTP extensions cross a process/network boundary (extension.rs, extension_manager.rs).

Permission, secrets, filesystem, environment, and sandbox boundaries

Implemented — ToolApprovalOperation reconstructs pending requests from persisted messages, inspects them with ToolInspectionManager, and marks each request with goose.executable. It emits a user-only ActionRequired message for requests needing approval. handle_approval_tool_requests waits on ToolConfirmationRouter; allow-once/always-allow dispatches the call, while deny paths append an explicit DECLINED_RESPONSE. Always-allow/deny decisions update the per-tool permission manager (ops_tool_approval.rs, tool_execution.rs).

Implemented — PermissionManager persists user and smart-approve levels in permission.yaml, with always_allow, ask_before, and never_allow lists. MCP tool annotations with read_only_hint == false seed smart-approve ask_before; this is a local policy store, not proof that a remote server is safe (permission.rs).

Implemented — Config secrets use keyring storage when available and a file fallback under the Goose config directory. Config::get_secrets reads environment variables for the primary key or the configured secret storage for the group; file-based tests cover restricted permissions and cache invalidation (base.rs). Extension configs resolve secret-backed values at client creation and keep that resolved snapshot in memory rather than serializing it.

Implemented — Stdio extension Envs rejects a denylist of path, loader, interpreter, profile, temporary-directory, and Windows process-location variables (including PATH, LD_PRELOAD, PYTHONPATH, NODE_OPTIONS, TEMP, and USERPROFILE) before spawning. Allowed configured variables are passed to the child; the source does not claim a complete environment sandbox (extension.rs).

Implemented — ToolCallContext.working_dir is copied into MCP request context. Goose’s ACP filesystem tools resolve relative paths against the session working directory and include the working directory in terminal requests; ACP shell requests also include an AGENT_SESSION_ID environment value (fs.rs).

Strongest counter-evidence — The inspected default path does not create a general sandbox for MCP stdio processes or ACP children. Permission confirmation constrains when Goose dispatches a tool, but once an allowed process runs, its filesystem, network, inherited privileges, and server logic remain outside Goose’s guarantee. The Container type in the selected agent path only holds a Docker container id; it is not evidence that every tool runs inside Docker.

Recipe and session identity

Implemented — Session persists id, working_dir, conversation, provider/model, usage, extension data, recipe, user recipe values, schedule id, mode, and optional parent_session_id. SessionManager::get_session(id, true) reloads the conversation from SQLite; apply_effects writes messages, replacements, recipe, extension data, and usage back to that session id (session_manager.rs, state-machine/session.rs).

Implemented — RecipeOperation::run_command resolves a recipe slash command, validates an optional structured-output schema, persists it through GooseEffect::SetRecipe, and appends a private agent-visible prompt. Recipe parameters/instructions/extensions are therefore part of the session’s repeatable input, while the final-output tool is generated from the persisted response schema (ops_recipe.rs).

Implemented — The recipe tests cover inherited parameters and instructions, extension advertisement, child turn limits, retry checks, scheduler configuration, and structured final output. recipe_retry_and_final_output_run_to_completion demonstrates a failed shell success check producing a bounded retry error and a separate recipe using FINAL_OUTPUT_TOOL_NAME to produce validated JSON (recipe_scheduling_lifecycle.rs).

Implemented — SessionManager::copy_session creates a fresh id, copies working directory, recipe, user values, extension/provider/model/mode metadata, and copies the conversation. It does not share the original session id; parent_session_id is available for explicit child trees. The reconstruction/isolation test creates a second session with a different working directory, asserts every calculator context carries that session id and directory, and shows totals do not cross between sessions (session_manager.rs, reconstruction_isolation_lifecycle.rs).

Strongest counter-evidence — Session identity isolates persisted messages, recipe metadata, working directory, and tool context; it does not automatically isolate a shared remote MCP server, provider account, OS process, or external side effect. ACP state can only be loaded when the child agent advertises ACP session/load; a Goose SQLite session may therefore outlive an unavailable provider-side continuation.

Continuity matrix

BoundaryWhat the pinned implementation preservesWhat it does not establish
IdentityGoose session id, ACP session id, tool-call request id, message ids, extension-prefixed tool namesThat a provider or MCP server uses the same identity outside the request metadata.
StateSQLite session row/conversation, recipe, extension data, usage, provider/model settings, tool metadataLive child-process memory, remote server state, or an unflushed in-flight stream.
External effectsMCP results/notifications, ACP updates, approval actions, provider usage, recipe success checksReversibility, transactional rollback, network policy, or filesystem snapshots.
Human authorityTool inspection, action-required messages, confirmation router, ACP permission mapping, chat-mode skipThat a user-approved tool is benign or that remote policy was reviewed.
EvidencePersisted messages, tool response content, usage, structured action-required records, recipe outputA tamper-evident transcript of every process/network effect or provider token stream.
RecoveryState-machine reconstruction, cancellation response pairing, ACP handoff rollback, session copy/fork, bounded recipe retriesGuaranteed recovery after a process dies, a remote session cannot load, or an external side effect partially completes.

Opinion — Goose’s durable unit is a re-entrant session plus its persisted conversation and recipe, while capability execution is an explicitly named MCP/ACP boundary. That makes session identity and user-visible approval useful control points, but it is not a transaction across the model, Goose database, child process, remote MCP server, and ACP agent.

Candidate pattern consequences (deferred to WI-028)

CandidateEvidenceProvisional disposition
session-isolationImplemented — every state-machine step reloads by session id; tool context includes session id and working directory; reconstruction tests assert separate totals and contexts.Candidate; reconcile with existing session wording.
confirmation-gateImplemented — tool inspection, goose.executable, action-required messages, and allow/deny persistence gate dispatch.Candidate; compare with structural/approval gates already in the catalogue.
protocol-escape-hatchImplemented — MCP and ACP preserve a Goose loop while delegating capability or loop ownership across a protocol boundary.Candidate; do not treat delegation as proof of protocol conformance.
agent-facing-interfaceImplemented — provider errors, tool results, approval requests, notifications, and recipe output are represented as messages/events visible to the model or user.Candidate; compare with existing interface findings.
handoffImplemented — ACP’s first-prompt context marker commits only after completion and rolls back on cancellation/refusal.Candidate; narrow to ownership-transfer recovery.
neighbourOpinion — ACP and MCP make adjacent agents/capabilities addressable without collapsing their identity into Goose’s session.Candidate only; needs WI-028 cross-subject comparison.

No catalogue, module, or CLI file changed. WI-025 owns normative MCP semantics and WI-028 owns cross-track reconciliation.