feat(goose): MCP advisory integration + upstream hard-gate proposal#112
feat(goose): MCP advisory integration + upstream hard-gate proposal#112theyavuzarslan wants to merge 2 commits into
Conversation
Goose has no out-of-process plugin API — the only true Allow/Deny/
RequireApproval surface is the in-process Rust ToolInspector trait
(crates/goose/src/tool_inspection.rs), which is compile-time. This PR
ships the two integrations that ARE possible today and clearly
distinguishes their security guarantees.
Surface 1 — MCP extension (advisory, ships today)
=================================================
`agentguard init --agent goose` writes/merges an `agentguard` entry under
`extensions:` in `~/.config/goose/config.yaml` (or `%APPDATA%/Block/goose/
config/config.yaml` on Windows, or `$GOOSE_CONFIG_DIR/config.yaml` if set):
extensions:
agentguard:
type: stdio
command: agentguard-mcp
args: []
timeout: 300
enabled: true
description: GoPlus AgentGuard MCP — security scanner + action evaluator
The model can now call AgentGuard's scanner/action-evaluator MCP tools.
This is **advisory only**: the agent can skip calling AgentGuard and go
straight to `developer__shell`, and AgentGuard never sees it.
plugins/goose/README.md states this in plain language so no user is
under the impression they have a hard gate.
Surface 2 — upstream hard-gate proposal (draft, no PR yet)
=========================================================
plugins/goose/UPSTREAM_PROPOSAL.md drafts a SECURITY_INSPECTOR_ENDPOINT
webhook ToolInspector for block/goose, mirroring their existing
SECURITY_PROMPT_CLASSIFIER_ENDPOINT pattern. That would let AgentGuard
(or any other vendor) become a real out-of-process pre-execution gate
with Allow/Deny/RequireApproval semantics. Includes JSON schema, fail
policy, and open questions to confirm with maintainers before opening
the PR.
Wiring
======
- 'goose' added to AgentInstaller, RuntimeAgentHost, AgentGuardAgentHost,
SUPPORTED_AGENT_INSTALLERS, normalizeAgentHost. Intentionally NOT in
AUTO_AGENT_DETECTION since Goose config lives at ~/.config/goose/
(outside cwd) and an advisory install shouldn't be auto-applied.
- resolveCronAgentHost narrowed to the cron-capable subset (matches the
pattern used when wiring other non-cron hosts).
- YAML merge: hand-rolled (no js-yaml dep). Handles three cases —
(a) no config.yaml at all → create with extensions: block,
(b) extensions: exists → insert agentguard: as a sibling child,
(c) agentguard: already present → no-op (idempotent).
Force mode strips the prior block and re-emits in canonical form.
Tests: 419/419 pass (was 415; +4 — fresh install, merge into existing
extensions preserving other entries and top-level keys, idempotency
under repeated runs, and a docs check that the README states the
"advisory, not a security boundary" framing.)
AgentGuard PR ReviewA few concrete issues remain in the Goose integration patch.
|
Three issues raised in review; all addressed.
1. HIGH — hand-rolled YAML mutation assumed extensions: was a simple
top-level block followed by indented children. Configs with comments
on entries, multiple top-level keys after extensions:, quoted keys,
or 4-space indentation could be corrupted or have unrelated content
moved into wrong indentation levels.
Fix: replaced the line-based merge with a real YAML parser
(eemeli/yaml, the de-facto comment-preserving Node YAML library;
added as a runtime dep). The merge now:
- parses with parseDocument() so comments and source positions
survive,
- mutates only the extensions.agentguard key via doc.setIn(),
- serializes via doc.toString() preserving siblings, top-level keys
before AND after extensions:, and comments,
- REFUSES to write the file when extensions: is the wrong shape
(sequence, scalar, null) or when the input is unparseable —
surfacing a clear error instead of silently clobbering user data.
2. MEDIUM — idempotency/--force depended on indentation-sensitive
regexes that missed tabs, quoted keys, and nested cases. Resolved
by the same fix: structured parsing via doc.has('extensions') and
extensions.get('agentguard') no longer cares about layout.
3. MEDIUM — agentHost adds 'goose' globally but resolveCronAgentHost
silently filters it out, leaving cron-related commands with
surprising behavior. Same fix shipped on the Continue branch
applies here:
- Split into resolveConfiguredAgentHost() returning the raw host
and resolveCronBackendHost() returning the cron-narrowed value.
- noteCronBackendFallbackIfNeeded() prints a one-line stderr note
on first call when the configured host has no agent-managed cron
backend, so users know they're getting system cron rather than
silent fall-back.
- Both cron entry points (subscribe install, disconnect cleanup)
now call the note + the narrowed helper.
Tests: 424/424 pass (was 419; +5 new — comments + inline comments +
trailing top-level keys survive merge; refusal on sequence-typed
extensions:; refusal on unparseable YAML with file untouched; quoted
keys + 4-space indent variant round-trips; --force replaces only the
agentguard entry).
End-to-end verified against a complex /tmp/goose-complex/.config/goose/
config.yaml containing comments, multiple existing extensions (slack,
github), and trailing top-level keys (GOOSE_PROVIDER,
SECURITY_PROMPT_ENABLED). After merge: all comments preserved, all
extensions kept, AgentGuard inserted as sibling, trailing top-level
keys intact.
Adds yaml@^2.6.1 to dependencies.
|
Thanks for the review. All three items addressed in `f66e6e4`:
Tests: 424/424 pass (was 419 before fixes; +5 new — comments + inline comments + trailing top-level keys survive merge; refusal on sequence-typed End-to-end verified against a
After merge: every comment, every existing extension, and every top-level key is preserved; Note on the new dep: |
Summary
Goose (block/goose) has no out-of-process plugin API today — the only real
Allow / Deny / RequireApprovalsurface is the in-process RustToolInspectortrait (crates/goose/src/tool_inspection.rs), which is compile-time. This PR ships the two integrations that are possible today and clearly distinguishes their security guarantees.Surface 1 — MCP extension (advisory, ships today)
`agentguard init --agent goose` writes/merges an `agentguard` entry under `extensions:` in `~/.config/goose/config.yaml` (or `%APPDATA%/Block/goose/config/config.yaml` on Windows, or `$GOOSE_CONFIG_DIR/config.yaml` if set):
```yaml
extensions:
agentguard:
type: stdio
command: agentguard-mcp
args: []
timeout: 300
enabled: true
description: GoPlus AgentGuard MCP — security scanner + action evaluator
```
The model can now call AgentGuard's scanner / action-evaluator MCP tools. This is advisory only: the agent can skip calling AgentGuard and go straight to `developer__shell`, and AgentGuard never sees it. `plugins/goose/README.md` states this in plain language so no user is under the impression they have a hard gate.
Surface 2 — upstream hard-gate proposal (draft, no PR yet)
`plugins/goose/UPSTREAM_PROPOSAL.md` drafts a `SECURITY_INSPECTOR_ENDPOINT` webhook `ToolInspector` for `block/goose`, mirroring their existing `SECURITY_PROMPT_CLASSIFIER_ENDPOINT` pattern. That would let AgentGuard (or any other vendor) become a real out-of-process pre-execution gate with `Allow` / `Deny` / `RequireApproval` semantics. Includes JSON schema, fail policy, and open questions to confirm with maintainers before opening that PR upstream.
What changed
Why no `GooseAdapter`?
There's no Goose-specific hook payload to parse — the MCP path uses the existing `agentguard-mcp` server and the engine's adapter layer is bypassed entirely. Adding an empty adapter would be ceremony without value.
Test plan
References:
🤖 Generated with Claude Code