docs(grok): document Grok's reduced guardrail posture (honest, not blocking)

Grok agents run on opencode, not Claude Code, so they do NOT have full
guardrail parity — claiming otherwise would be false. Document it truthfully
and keep them usable rather than blocking them.

- Panel routing card: an amber caveat shown in Grok/Mix mode — command/
  secret-exfil guard + cost cap apply to Grok, but the prompt-injection guard
  does NOT (opencode cannot block a turn); Anthropic/Ollama/Self-Hosted run
  through Claude Code with the full guard set; prefer those for agents that
  ingest untrusted or cross-agent content; Grok is safe for trusted work.
- docs/self/architecture/llm-provider-security.md: the reference — the two
  runtimes, which provider uses which, the per-guardrail parity matrix, why
  the injection/stop gaps exist (opencode hooks are observe-only), and the
  routing recommendation (delivery roles handling untrusted content → a
  Claude-Code-runtime provider).

Panel tsc + eslint clean.
This commit is contained in:
Renn F
2026-06-18 12:58:10 +02:00
parent 2089b9e765
commit 61c8faa8da
2 changed files with 52 additions and 0 deletions
@@ -0,0 +1,38 @@
# LLM provider security posture
RoboCo routes each agent to one of several LLM providers (the **Routing** card in the control panel). This note states — truthfully — what protections an agent gets on each provider, so the choice is made knowingly. The short version: **Grok agents do not have full guardrail parity with the rest, but they are still usable.** Use them for trusted work; prefer a Claude-Code-runtime provider for agents that ingest untrusted or cross-agent content.
## Two runtimes, not five
An agent has two layers: the **model** (the brain) and the **runtime** (the agent program that drives it — reads files, calls tools, loops). RoboCo's guardrails are implemented as **Claude Code hooks**, so they only exist when the runtime is Claude Code.
| Provider (routing mode) | Runtime | Model |
|---|---|---|
| Anthropic | Claude Code | Claude (opus/sonnet) |
| Ollama (Cloud) | Claude Code (via `ANTHROPIC_BASE_URL` injection) | the Ollama model |
| Self-Hosted | Claude Code (via `ANTHROPIC_BASE_URL` injection) | your endpoint's model |
| **Grok (xAI)** | **opencode** | **grok-build-0.1** |
Anthropic, Ollama, and Self-Hosted all run on **Claude Code** and therefore keep the **full guard set**. Only **Grok** runs on a different runtime — **opencode** — because the `claude` binary rejects any non-Claude model id, so Grok cannot run inside Claude Code. opencode is to Grok what Claude Code is to Claude.
## Guardrail parity matrix
| Guardrail | Claude Code runtime (Anthropic / Ollama / Self-Hosted) | Grok (opencode) |
|---|---|---|
| MCP gateway + role tool-manifest | yes | yes (mounted by construction) |
| Command / secret-exfiltration guard (bash, credential files, internal-host calls, PAT exfil) | yes (`bash-guard-hook.sh`, PreToolUse) | **yes** — ported to opencode as the `secret-scrub.js` plugin (`tool.execute.before`) |
| Budget / runaway-cost kill-switch | yes (`post-tool-budget` hook against the SDK server) | **yes** — orchestrator-side cost watchdog (`ROBOCO_GROK_MAX_COST_USD`) reading the opencode store |
| Prompt-injection guard (rejects "ignore previous instructions", role-override, fake escalations in incoming A2A / task / notification content) | yes (`user-prompt-hook.sh`, UserPromptSubmit, denies the turn) | **no** — opencode's incoming-message hook (`message.updated`) is observe-only and cannot block a turn before the model reads it |
| Stop-guard (terminal-verb enforcement before a run ends) | yes (`stop-hook.sh`, Stop) | **no** — opencode's stop/idle hooks are observe-only |
## Why the two gaps exist (not a defer — a runtime limitation)
opencode's plugin API exposes only observe-only events for incoming messages and session-stop, and **no token/usage hook at all**. So the prompt-injection and stop-guard hooks — both of which must *block* an action — have no faithful opencode equivalent today. Closing them would require an upstream opencode feature (a blocking message/stop hook). The budget guard was movable to the orchestrator (it reads the opencode cost store), which is why Grok keeps budget parity but not injection/stop parity.
## What this means for routing
- **Grok is safe for trusted, self-contained work** — and for the interactive intake/secretary roles, whose input comes directly from the CEO (a small injection surface).
- **The real injection exposure is the delivery roles** (developer / qa / pm / documenter), which routinely ingest *other agents'* and external content as data. The prompt-injection guard is what stops a poisoned A2A message or task description from steering them off-task; on Grok that guard is absent.
- **Recommendation:** route delivery agents that handle untrusted or cross-agent content to a Claude-Code-runtime provider (Anthropic / Ollama / Self-Hosted). Route Grok where the content is trusted, or accept the reduced posture knowingly. The command/secret-exfiltration guard — the one that prevents actual credential leakage — *is* present on Grok, so the gap is about being socially-engineered off-task, not about secret exfiltration.
This is the honest claim: **not full parity, still usable.** The control panel's Routing card surfaces a short version of this when Grok or Mix mode is selected.
@@ -491,6 +491,20 @@ export function AIRoutingCard() {
saved those agents will fall back to Anthropic at spawn.
</p>
) : null}
{currentMode === "grok" || currentMode === "mix" ? (
<p className="text-xs text-amber-600 flex items-start gap-1">
<AlertTriangle className="h-3 w-3 shrink-0 mt-0.5" />
<span>
Grok agents run on the opencode runtime, not Claude Code: the
command / secret-exfiltration guard and the cost cap apply, but
the prompt-injection guard does not (opencode cannot block a
turn before the model reads it). Anthropic, Ollama and
Self-Hosted all run through Claude Code with the full guard set
prefer them for agents that ingest untrusted or cross-agent
content. Grok is safe for trusted work.
</span>
</p>
) : null}
</section>
{/* -------- Self-Hosted model picker (when self_hosted mode active) -------- */}