Skip to content

Commit 2191a0b

Browse files
feat(goose): MCP advisory integration + upstream hard-gate proposal
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.)
1 parent 0b0b039 commit 2191a0b

7 files changed

Lines changed: 431 additions & 8 deletions

File tree

plugins/goose/README.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# GoPlus AgentGuard — Goose integration (advisory, MCP-based)
2+
3+
Goose ([block/goose](https://github.com/block/goose)) is **not** integrated
4+
the same way as Cline, Hermes, or Continue. This README explains the
5+
limitation honestly and documents the install paths that ship today.
6+
7+
## TL;DR
8+
9+
```bash
10+
npm i -g @goplus/agentguard
11+
agentguard init --agent goose
12+
```
13+
14+
That writes `agentguard` as an MCP extension in `~/.config/goose/config.yaml`:
15+
16+
```yaml
17+
extensions:
18+
agentguard:
19+
type: stdio
20+
command: agentguard-mcp
21+
args: []
22+
timeout: 300
23+
enabled: true
24+
description: GoPlus AgentGuard MCP — security scanner + action evaluator
25+
```
26+
27+
Restart Goose. AgentGuard's scanner and action evaluator are now MCP-callable.
28+
29+
## Important: this is advisory, not a hard gate
30+
31+
Goose currently has **no out-of-process plugin API** for intercepting tool
32+
calls. The only true `Allow / Deny / RequireApproval` surface is the in-process
33+
Rust `ToolInspector` trait (`crates/goose/src/tool_inspection.rs`), which is
34+
compile-time — there is no dynamic loader for it.
35+
36+
What an MCP extension can do:
37+
38+
- **Provide tools the model may choose to call** (`scan_skill`, `evaluate_action`, etc.)
39+
- **Refuse to return data** when called
40+
41+
What an MCP extension **cannot** do:
42+
43+
- Sit on the execution path of every Goose tool call
44+
- Block a `developer__shell` call the model never routed through AgentGuard
45+
46+
In other words, if the agent decides to call `shell` with `rm -rf /` and
47+
never asks AgentGuard's MCP tools first, AgentGuard never gets a chance to
48+
veto it. The model is free to skip you. **Treat this as defense-in-depth,
49+
not a security boundary.**
50+
51+
For a real hard gate, see [UPSTREAM_PROPOSAL.md](./UPSTREAM_PROPOSAL.md) — a
52+
draft proposal to add a `SECURITY_INSPECTOR_ENDPOINT` webhook to Goose,
53+
mirroring its existing `SECURITY_PROMPT_CLASSIFIER_ENDPOINT`. That would
54+
turn AgentGuard into a genuine pre-execution inspector with `block` /
55+
`require_approval` returns.
56+
57+
## What the MCP integration is useful for
58+
59+
- **Pre-commit / pre-PR scanning** of skills the agent wants to install
60+
- **Action evaluation** when the agent explicitly asks "is this dangerous?"
61+
- **Audit logging** of evaluations the model did make through AgentGuard
62+
63+
These are real value-adds even without a hard gate, but be clear with users
64+
about the boundary.
65+
66+
## Manual install
67+
68+
If you'd rather not run `agentguard init --agent goose`, copy the snippet
69+
above into your existing `~/.config/goose/config.yaml` under `extensions:`
70+
(or create the file if it doesn't exist). On Windows the path is
71+
`%APPDATA%\Block\goose\config\config.yaml`.
72+
73+
The installer preserves any prior `extensions:` entries and is idempotent —
74+
re-running won't duplicate the block.
75+
76+
## Reference
77+
78+
- Goose extensions / MCP: https://block.github.io/goose/docs/getting-started/installation
79+
- ToolInspector trait (compile-time): `crates/goose/src/tool_inspection.rs`
80+
- Existing classifier endpoint (the model we're proposing to copy):
81+
`crates/goose/src/security/classification_client.rs` +
82+
`SECURITY_PROMPT_CLASSIFIER_ENDPOINT`

plugins/goose/UPSTREAM_PROPOSAL.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Goose upstream proposal — generic security webhook inspector
2+
3+
This document drafts a proposal to file against [block/goose](https://github.com/block/goose)
4+
that would give third-party security tools (AgentGuard, but also others) a
5+
real out-of-process hard gate on tool execution.
6+
7+
This is **not** a PR yet — it's the design that should accompany one.
8+
9+
## Problem
10+
11+
Goose currently has two ways to wire in security policy on tool calls:
12+
13+
1. **`ToolInspector` trait** (Rust, in-process, compile-time). The real
14+
`Allow / Deny / RequireApproval` decision surface, but there is no
15+
dynamic loader for it — third parties cannot ship one without forking
16+
and rebuilding Goose.
17+
2. **MCP extensions**. Model-callable. The agent can skip your tools, so
18+
this is advisory only — it is not a security boundary.
19+
20+
This means there is no supported way today for an external security service
21+
(static analyzer, runtime engine, anomaly detector) to gate Goose's tool
22+
execution without forking the binary.
23+
24+
## Prior art inside Goose
25+
26+
Goose already accepts an external HTTP endpoint for one specific
27+
classification job. From `crates/goose/src/security/classification_client.rs`
28+
and the config:
29+
30+
```
31+
SECURITY_PROMPT_CLASSIFIER_ENDPOINT
32+
SECURITY_PROMPT_ENABLED
33+
```
34+
35+
The classifier endpoint receives JSON, returns a classification, and Goose
36+
honors the result. This is the pattern we're asking to generalize.
37+
38+
## Proposal
39+
40+
Add a new `WebhookToolInspector` that registers automatically when a
41+
configured endpoint is set:
42+
43+
```yaml
44+
# ~/.config/goose/config.yaml
45+
SECURITY_INSPECTOR_ENABLED: true
46+
SECURITY_INSPECTOR_ENDPOINT: "http://127.0.0.1:7777/inspect"
47+
SECURITY_INSPECTOR_TIMEOUT_MS: 1500
48+
SECURITY_INSPECTOR_FAIL_OPEN: false # default: fail-closed
49+
SECURITY_INSPECTOR_AUTH_HEADER: "X-Token: …"
50+
```
51+
52+
At startup, `ToolInspectionManager::add_inspector` registers a built-in
53+
`WebhookToolInspector` if `SECURITY_INSPECTOR_ENABLED == true`. It calls the
54+
endpoint for every `ToolRequest`, with a JSON body like:
55+
56+
```json
57+
{
58+
"session_id": "sess_…",
59+
"tool_requests": [
60+
{ "tool": "developer__shell", "input": { "command": "rm -rf /" }, "call_id": "…" }
61+
],
62+
"messages": [ /* optional, truncated */ ],
63+
"goose_mode": "auto"
64+
}
65+
```
66+
67+
And expects back:
68+
69+
```json
70+
{
71+
"results": [
72+
{ "call_id": "…",
73+
"action": "Deny",
74+
"reason": "destructive command",
75+
"policy_version": "…" }
76+
]
77+
}
78+
```
79+
80+
`action` is one of `Allow`, `Deny`, `RequireApproval`. The webhook's response
81+
maps 1:1 to `InspectionAction` — no impedance mismatch with the existing
82+
trait.
83+
84+
## Why this is a small change
85+
86+
- It doesn't change the `ToolInspector` trait. The new inspector is just
87+
another implementer.
88+
- It mirrors a pattern Goose already accepts (the classifier endpoint), so
89+
it should pass the "is this in scope for this project?" smell test.
90+
- Fail-open vs fail-closed is configurable, with a security-safe default
91+
(fail-closed).
92+
- No new permissions to plumb through the trust system; existing
93+
`ToolInspectionManager` handles aggregation.
94+
95+
## What we'd ship in the PR
96+
97+
1. `crates/goose/src/security/webhook_inspector.rs` — `WebhookToolInspector` impl
98+
2. Config keys + parsing in `crates/goose/src/config/`
99+
3. Registration in `crates/goose/src/agents/` startup
100+
4. Tests (mocking the endpoint with `wiremock` or similar)
101+
5. A docs page in `docs/security/` covering trade-offs and recommended deploys
102+
103+
## Open questions to confirm with maintainers before opening the PR
104+
105+
- Naming: `SECURITY_INSPECTOR_*` vs `TOOL_INSPECTOR_*` vs scoped to a single
106+
vendor key. We'd prefer the generic name so other security vendors can use
107+
the same surface.
108+
- Should the webhook also see `PostToolUse` events for audit-only logging,
109+
or is pre-execution enough? (Our preference: pre-only, keep the surface
110+
minimal; audit lives elsewhere.)
111+
- Should responses be allowed to mutate the tool input (`overrideInput`),
112+
as Cline and Continue hooks do? Probably no for v1 — keeps the threat
113+
model simple.
114+
115+
---
116+
117+
**Status:** draft. We (GoPlus) intend to file an issue first to gauge
118+
maintainer interest before opening a PR. Anyone in this repo can take it on.

src/cli.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,18 @@ import {
4040
type CronBackend,
4141
type ThreatFeedCronRemovalResult,
4242
type OpenClawGatewayOptions,
43+
type CronAgentHost,
4344
} from './feed/cron.js';
4445

45-
const SUPPORTED_AGENT_INSTALLERS: AgentInstaller[] = ['claude-code', 'codex', 'openclaw', 'hermes', 'qclaw'];
46+
const SUPPORTED_AGENT_INSTALLERS: AgentInstaller[] = ['claude-code', 'codex', 'openclaw', 'hermes', 'qclaw', 'goose'];
4647
const AUTO_AGENT_DETECTION: Array<{ agent: AgentInstaller; dir: string }> = [
4748
{ agent: 'claude-code', dir: '.claude' },
4849
{ agent: 'openclaw', dir: '.openclaw' },
4950
{ agent: 'hermes', dir: '.hermes' },
5051
{ agent: 'qclaw', dir: '.qclaw' },
5152
{ agent: 'codex', dir: '.codex' },
53+
// 'goose' is intentionally NOT in auto-detection: config lives at
54+
// ~/.config/goose/ (outside cwd), and an MCP install is advisory-only.
5255
];
5356
const REQUIRED_INIT_COMMAND = 'agentguard init --agent auto';
5457

@@ -64,7 +67,7 @@ async function main() {
6467
.command('init')
6568
.description('Create ~/.agentguard/config.json and local runtime paths')
6669
.option('--level <level>', 'Protection level: strict | balanced | permissive')
67-
.option('--agent <agent>', 'Install hook/template for claude-code, codex, openclaw, hermes, or qclaw')
70+
.option('--agent <agent>', 'Install hook/template for claude-code, codex, openclaw, hermes, qclaw, or goose (MCP advisory)')
6871
.option('--cloud <url>', 'AgentGuard Cloud URL to store in local config')
6972
.option('--shell-hooks', 'For Hermes: install legacy shell hooks instead of the native plugin')
7073
.option('--force', 'Overwrite existing hook/template files')
@@ -106,7 +109,7 @@ async function main() {
106109
return;
107110
}
108111
if (!SUPPORTED_AGENT_INSTALLERS.includes(normalizedAgent as AgentInstaller)) {
109-
throw new Error('Invalid agent. Use auto, claude-code, codex, openclaw, hermes, or qclaw.');
112+
throw new Error('Invalid agent. Use auto, claude-code, codex, openclaw, hermes, qclaw, or goose.');
110113
}
111114
const agent = normalizedAgent as AgentInstaller;
112115
config.agentHost = agent;
@@ -1062,8 +1065,12 @@ function printCronRemovalSummary(results: ThreatFeedCronRemovalResult[]): void {
10621065
console.log('No AgentGuard subscribe cron job was found.');
10631066
}
10641067

1065-
function resolveCronAgentHost(config: AgentGuardConfig): AgentGuardAgentHost | undefined {
1066-
return config.agentHost ?? config.agentHosts?.[0];
1068+
function resolveCronAgentHost(config: AgentGuardConfig): CronAgentHost | undefined {
1069+
const host = config.agentHost ?? config.agentHosts?.[0];
1070+
if (host === 'claude-code' || host === 'codex' || host === 'openclaw' || host === 'hermes' || host === 'qclaw') {
1071+
return host;
1072+
}
1073+
return undefined;
10671074
}
10681075

10691076
function readStdinIfAvailable(): string {

src/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync }
22
import { dirname, join } from 'node:path';
33
import { homedir } from 'node:os';
44

5-
export type AgentGuardAgentHost = 'claude-code' | 'codex' | 'openclaw' | 'hermes' | 'qclaw';
5+
export type AgentGuardAgentHost = 'claude-code' | 'codex' | 'openclaw' | 'hermes' | 'qclaw' | 'goose';
66

77
export interface AgentGuardConfig {
88
version: 1;
@@ -226,7 +226,7 @@ function normalizeLevel(value: unknown): AgentGuardConfig['level'] | null {
226226
}
227227

228228
function normalizeAgentHost(value: unknown): AgentGuardAgentHost | undefined {
229-
return value === 'claude-code' || value === 'codex' || value === 'openclaw' || value === 'hermes' || value === 'qclaw'
229+
return value === 'claude-code' || value === 'codex' || value === 'openclaw' || value === 'hermes' || value === 'qclaw' || value === 'goose'
230230
? value
231231
: undefined;
232232
}

0 commit comments

Comments
 (0)