Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions docs/src/content/docs/troubleshooting/common-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,68 @@ mcp-servers:
API_KEY: "${{ secrets.MCP_API_KEY }}"
```

### OpenCode/Crush MCP Tools Not Being Called

When integrating OpenCode-compatible engines (such as `crush`) in AWF workflows (including smoke tests), runs can complete while never calling MCP tools or file tools.

Use this `.crush.json` structure. Port `10004` is the default local AWF API proxy port (used with `--enable-api-proxy` for OpenCode/Crush-compatible routing), while `MCP_GATEWAY_PORT` is a placeholder for the MCP gateway port.

```json
{
"provider": {
"copilot-proxy": {
"name": "Copilot Proxy",
"type": "openai-compatible",
"baseURL": "http://host.docker.internal:10004",
"models": ["gpt-4.1", "claude-sonnet-4-6"]
}
},
"model": "copilot-proxy/claude-sonnet-4-6",
"mcp": {
"safeoutputs": {
"type": "http",
"url": "http://host.docker.internal:${MCP_GATEWAY_PORT}/mcp/safeoutputs",
"headers": { "Authorization": "${MCP_GATEWAY_API_KEY}" },
"disabled": false,
"timeout": 30000
}
},
"agent": {
"build": {
"permission": {
"bash": "allow",
"edit": "allow",
"read": "allow",
"glob": "allow",
"grep": "allow",
"write": "allow",
"external_directory": "allow"
}
}
}
}
```

`MCP_GATEWAY_PORT` and `MCP_GATEWAY_API_KEY` are placeholders that are expanded from workflow environment variables when AWF renders the config at runtime. When running outside workflow context (such as local development), replace them with concrete values before writing `.crush.json`.

Key gotchas:

- Crush/OpenCode do not auto-discover MCP servers. Add an explicit top-level `mcp` section.
- Use routed gateway URLs: `http://host.docker.internal:${MCP_GATEWAY_PORT}/mcp/<server-name>`.
- ⚠️ Use `agent.build.permission` (singular). Using `permissions` (plural) is silently ignored by OpenCode-compatible config loaders, which leaves tools unavailable even though the run continues.
- In non-interactive mode (such as when running `crush run` in CI or AWF workflows), `external_directory` defaults to `ask`, which becomes an implicit deny without terminal prompts. Set it to `allow` only when the agent must access paths outside its primary workspace, such as `/tmp` or mounted external directories.
- For direct Copilot-compatible endpoints (`api.githubcopilot.com`), do not append `/v1` to the base URL. For other OpenAI-compatible providers, use the provider's expected base path (for example `https://models.inference.ai.azure.com`) so the client can append `/chat/completions` correctly.
- If you route through the local proxy (`http://host.docker.internal:10004`), keep the proxy URL as-is.
- When running through AWF `--enable-api-proxy`, provide `COPILOT_GITHUB_TOKEN` in the same execute step `env:` so the proxy can authenticate.

```yaml wrap
- name: Execute
env:
COPILOT_GITHUB_TOKEN: ${{ steps.copilot-token.outputs.token }}
run: |
awf --enable-api-proxy <workflow-args> -- crush run "<prompt>"
```

### Playwright Network Access Denied

Add domains to `network.allowed`:
Expand Down
4 changes: 3 additions & 1 deletion pkg/workflow/crush_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,9 @@ func (e *CrushEngine) GetExecutionSteps(workflowData *WorkflowData, logFile stri
// to prevent CI hanging on permission prompts.
func (e *CrushEngine) generateCrushConfigStep(_ *WorkflowData) GitHubActionStep {
// Build the config JSON with all permissions set to allow
configJSON := `{"agent":{"build":{"permissions":{"bash":"allow","edit":"allow","read":"allow","glob":"allow","grep":"allow","write":"allow","webfetch":"allow","websearch":"allow"}}}}`
// OpenCode/Crush uses "permission" (singular) — "permissions" (plural) is silently ignored.
// "external_directory" must be "allow" in non-interactive CI mode (defaults to "ask" → implicit deny).
configJSON := `{"agent":{"build":{"permission":{"bash":"allow","edit":"allow","read":"allow","glob":"allow","grep":"allow","write":"allow","webfetch":"allow","websearch":"allow","external_directory":"allow"}}}}`

// Shell command to write or merge the config with restrictive permissions
command := fmt.Sprintf(`umask 077
Expand Down
4 changes: 3 additions & 1 deletion pkg/workflow/crush_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,9 @@ func TestCrushEngineExecution(t *testing.T) {

assert.Contains(t, configContent, "Write Crush Config", "First step should be Write Crush Config")
assert.Contains(t, configContent, ".crush.json", "Config step should reference .crush.json")
assert.Contains(t, configContent, "permissions", "Config step should set permissions")
assert.Contains(t, configContent, `"permission"`, "Config step should use 'permission' (singular, not 'permissions')")
assert.Contains(t, configContent, `"external_directory":"allow"`, "Config step should allow external_directory for non-interactive CI")
assert.NotContains(t, configContent, `"permissions"`, "Config step must NOT use 'permissions' (plural) — silently ignored by OpenCode)")
assert.Contains(t, execContent, "Execute Crush CLI", "Second step should be Execute Crush CLI")
})
}
Expand Down
Loading