mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-11 03:13:24 +00:00
docs(hooks): user guide + example configs + changelog + Make targets
- docs/agent-hooks.md: handler reference, lifecycle events, security model - examples/hooks/: 5 runnable JSON configs (audit, lint, block-rm-rf, Discord notify, context injector) - docs/17-changelog.md: Wave 0 entry - Makefile: hooks-specific test targets - CLAUDE.md: cross-reference
This commit is contained in:
1 parent
f90f11a9f9
commit
95bdb23a36
10 files changed
+431
-1
No files matched your search
@@ -245,6 +245,7 @@ Go conventions to follow:
|
||||
- **DB query reuse:** Before adding a new DB query for key entities (teams, agents, sessions, users), check if the same data is already fetched earlier in the current flow/pipeline. Prefer passing resolved data through context, event payloads, or function params rather than re-querying. Duplicate queries waste DB resources and add latency
|
||||
- **Solution design:** When designing a fix or feature, identify the root cause first — don't just patch symptoms. Think through production scenarios (high concurrency, multi-tenant isolation, failure cascades, long-running sessions) to ensure the solution holds up. Prefer explicit configuration over runtime heuristics. Prefer the simplest solution that addresses the root cause directly
|
||||
- **Tenant-scope guards on admin writes:** `RoleAdmin` is not a tenant check. Writes to **global** tables (no `tenant_id` column — e.g. `builtin_tools`, disk config, package mgmt) must gate with `http.requireMasterScope` / WS `requireMasterScope(requireOwner(...))`. Writes to **tenant-scoped** tables must gate with `http.requireTenantAdmin` + SQL `WHERE tenant_id = $N`. Shared predicate: `store.IsMasterScope(ctx)`. See `CONTRIBUTING.md` → "Tenant-scope guards" for the full decision table and anti-patterns.
|
||||
- **Skip load / stress / benchmark tests.** Do NOT write throughput benchmarks, p95/p99 latency assertions, or `runtime.ReadMemStats`-based memory-leak tests for regular feature work. They flake on shared CI runners, waste runner time, and rarely catch real bugs. Only add load tests when explicitly requested for a specific investigation. For normal "prove it works" coverage, use unit + integration + chaos tests.
|
||||
|
||||
## Mobile UI/UX Rules
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ VERSION ?= $(shell git describe --tags --abbrev=0 --match "v[0-9]*" 2>/dev/null
|
||||
LDFLAGS = -s -w -X github.com/nextlevelbuilder/goclaw/cmd.Version=$(VERSION)
|
||||
BINARY = goclaw
|
||||
|
||||
.PHONY: build build-full build-tui run clean version up down logs reset test vet check-web dev migrate setup ci desktop-dev desktop-build desktop-dmg
|
||||
.PHONY: build build-full build-tui run clean version up down logs reset test vet check-web dev migrate setup ci desktop-dev desktop-build desktop-dmg test-hooks test-hooks-unit test-hooks-e2e test-hooks-chaos test-hooks-rbac test-hooks-tracing
|
||||
|
||||
# Build backend only (API-only, no embedded web UI)
|
||||
build:
|
||||
@@ -94,6 +94,26 @@ test-scenarios:
|
||||
# Critical tests (P0 + P1) - run before merge
|
||||
test-critical: test-invariants test-contracts
|
||||
|
||||
# ── Agent Hooks targets (phase 4) ──
|
||||
# Requires TEST_DATABASE_URL pointing at a pgvector:pg18 container on :5433
|
||||
test-hooks-unit:
|
||||
go test -race ./internal/hooks/... ./internal/gateway/methods/
|
||||
|
||||
test-hooks-e2e:
|
||||
go test -race -timeout=180s -tags integration -run "TestHooksE2E" ./tests/integration/
|
||||
|
||||
test-hooks-chaos:
|
||||
go test -race -timeout=180s -tags integration -run "TestHooksChaos" ./tests/integration/
|
||||
|
||||
test-hooks-rbac:
|
||||
go test -race -timeout=90s -tags integration -run "TestHooksRBAC" ./tests/integration/
|
||||
|
||||
test-hooks-tracing:
|
||||
go test -race -timeout=90s -tags integration -run "TestHooksTracing" ./tests/integration/
|
||||
|
||||
# Full hook test suite (unit + integration)
|
||||
test-hooks: test-hooks-unit test-hooks-e2e test-hooks-chaos test-hooks-rbac test-hooks-tracing
|
||||
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
|
||||
@@ -6,6 +6,68 @@ All notable changes to GoClaw Gateway are documented here. Format follows [Keep
|
||||
|
||||
## [Unreleased] — 2026-04-15
|
||||
|
||||
#### Agent Hooks System — Phase 3: Prompt Handler + Web UI (2026-04-15)
|
||||
|
||||
Third handler type (`prompt`) with full cost safeguards, WS RPC surface, complete Web UI, 3-language i18n, and production wiring. Closes Issue #875 feature parity with Claude-Code-style hooks.
|
||||
|
||||
### Added
|
||||
- **`internal/hooks/handlers/prompt.go`**: LLM hook handler with structured tool-call output (`decide` tool), anti-injection system preamble, fenced user-input delimiter, in-memory decision cache (60s TTL keyed by `sha256(hook_id||version||tool_name||tool_input)`), per-turn invocation cap (default 5), provider resolver abstraction
|
||||
- **`internal/hooks/handlers/prompt_resolver.go`**: `RegistryResolver` maps model aliases (haiku/sonnet/opus, gpt-*, gemini-*, qwen-*) to tenant providers; falls back to system configs (`hooks.prompt.provider`, `background.provider`) then first registered
|
||||
- **`internal/hooks/budget/` package**: `Store` + `Dialect` interface for atomic per-tenant monthly token budget deduction; `ShouldWarn` helper at 20% threshold
|
||||
- **Budget store implementations**: `pg.PGHookBudget` (single UPDATE + RETURNING with UPSERT seed on month rollover) and `sqlitestore.SqliteHookBudget` (tx-wrapped equivalent)
|
||||
- **WS RPC methods** (`internal/gateway/methods/hooks.go`): `hooks.list` (viewer+), `hooks.create`/`update`/`delete`/`toggle` (admin+, master-scope for global), `hooks.test` (operator+, dryRun no audit write), `hooks.history` (viewer+, stub pending phase 4 pagination)
|
||||
- **Protocol constants**: `MethodHooksList/Create/Update/Delete/Toggle/Test/History` in `pkg/protocol/methods.go`
|
||||
- **Web UI `/hooks` page**: single route with `useParams().id` — list view when no id, detail view when present (CLAUDE.md "route params as source of truth"). Tabs: Overview | Config | Test | History
|
||||
- **Web UI components**: `hook-list-row`, `hook-form-dialog` (3 sub-forms per handler type; command radio disabled on Standard with tooltip), `hook-test-panel` (fires `dryRun=true`, decision badge + duration + stdout/stderr/status), `hook-diff-viewer` (side-by-side JSON diff via highlight.js), `hook-history-table`, `hook-overview-tab`
|
||||
- **Zod + TanStack Query**: `schemas/hooks.schema.ts` (prompt requires matcher|if_expr + prompt_template), `hooks/use-hooks.ts` (useHooksList/Hook/CreateHook/UpdateHook/DeleteHook/ToggleHook/TestHook/HookHistory)
|
||||
- **i18n backend**: 6 new keys (`hook.invalid_matcher`, `hook.command_disabled_standard`, `hook.prompt_requires_matcher`, `hook.circuit_breaker_tripped`, `hook.budget_exceeded`, `hook.per_turn_cap_reached`) in en/vi/zh catalogs
|
||||
- **i18n UI**: `ui/web/src/i18n/locales/{en,vi,zh}/hooks.json` with identical key ordering (per CLAUDE.md memory rule)
|
||||
- **Production wiring**: `cmd/gateway_managed.go` registers `HandlerPrompt` in dispatcher handlers map; `cmd/gateway.go` registers `HookMethods` in router
|
||||
- **Sidebar nav**: "Hooks" entry with `Webhook` icon in Capabilities group
|
||||
|
||||
### Security / cost safeguards
|
||||
- **Structured output enforcement**: evaluator MUST call the `decide` tool; free-text responses fail-closed to block
|
||||
- **Sanitized input**: only `tool_input` reaches the evaluator inside a fenced `USER INPUT` block; raw user message never included
|
||||
- **Injection detection flag**: evaluator can signal `injection_detected=true` via structured output
|
||||
- **Version-busted cache**: hook config edits bump `version` → cache key changes → forces re-evaluation
|
||||
- **Atomic budget deduct**: no select-then-update race (L2 mitigation)
|
||||
|
||||
### Testing
|
||||
- **Unit**: `prompt_test.go` (14 tests — cache hit/miss, version bust, per-turn cap, fail-closed paths, schema assertions), `prompt_injection_test.go` (4 tests — hostile inputs, unicode homoglyphs, nested JSON, fenced delimiter integrity), `budget_test.go` (9 tests — atomic deduct, exceed-returns-err, month rollover, zero-cost, nil tenant, warn threshold)
|
||||
- **WS RPC unit**: `gateway/methods/hooks_test.go` (param parsing + HookTestResult struct round-trip)
|
||||
- **UI**: 56/56 vitest passing including 20 new hook-specific tests (schema validation, list rendering, test panel, diff viewer, i18n contracts)
|
||||
|
||||
#### Agent Hooks System — Phase 4: Hardening + Docs (2026-04-15)
|
||||
|
||||
Integration / chaos / RBAC / tracing coverage + user documentation + copy-paste example hooks library. Merge gate.
|
||||
|
||||
### Added
|
||||
- **Integration tests** (`tests/integration/hooks_e2e_test.go`): 7 events × HTTP/command handler coverage, tenant isolation (cross-tenant events don't hit other tenants' hooks), context-update injection path, edition gate at dispatch time
|
||||
- **Chaos tests** (`hooks_chaos_test.go`): provider down (fail-closed block on blocking event), per-hook timeout respected, circuit breaker auto-disables after 3 consecutive blocks and persists `enabled=false`, `ErrLoopDepthExceeded` when depth > `MaxLoopDepth` (M5), `dedup_key` unique index suppresses double audit rows on retry (H6), 5xx retry-once-then-error
|
||||
- **RBAC tests** (`hooks_rbac_test.go`): store tenant isolation (List/Get/Update/Delete all respect tenant_id), global-scope visible to all tenants, ResolveForEvent unions tenant + global hooks, `HasMinRole` matrix over the hooks.* method surface
|
||||
- **Tracing tests** (`hooks_tracing_test.go`): `EmitHookSpan` writes row with canonical name `hook.<handler_type>.<event>`; dispatcher + traced-handler wrapper emits spans end-to-end
|
||||
- **User docs** `docs/agent-hooks.md`: concepts, security model, handler reference, matcher guide, safeguard table, Web UI walkthrough, observability (audit table + tracing spans + slog keys), troubleshooting, migration notes, known limitations
|
||||
- **Example library** `examples/hooks/`: `block-rm-rf.json` (command, Lite), `auto-lint-after-write.json` (http async), `audit-tool-usage.json` (tenant-wide audit), `session-context-injector.json` (context bootstrap), `notify-discord-on-stop.json` (webhook notification), `README.md` with safety guidance
|
||||
|
||||
### Fixed
|
||||
- **`PGHookStore.GetByID` + `SqliteHookStore.GetByID`** now enforce tenant scope: non-master callers only see own + global rows. Previously returned any row by UUID which leaked cross-tenant.
|
||||
- **Existing pipeline integration tests** now call `security.SetAllowLoopbackForTest(true)` via shared helper; no longer flake on SSRF block for httptest endpoints.
|
||||
|
||||
### Testing
|
||||
- All hook integration tests pass under `TEST_DATABASE_URL` PG container (7 new test files, 18 new top-level tests)
|
||||
- `go build` + `go build -tags sqliteonly` + `go vet` clean
|
||||
- `pnpm test` 56/56 pass
|
||||
- Race detector clean for `internal/hooks/...` + `internal/gateway/methods/`
|
||||
|
||||
### Deferred (post-MVP)
|
||||
- Cluster-wide prompt decision cache via Redis
|
||||
- Skill-frontmatter hooks
|
||||
- `agent` handler type (inter-agent delegation via prompt instead)
|
||||
- Desktop Wails UI parity for hooks page
|
||||
- Load/throughput benchmarks (explicitly dropped from Phase 4 scope)
|
||||
|
||||
---
|
||||
|
||||
#### Agent Hooks System — Phase 1: Foundation (2026-04-15)
|
||||
|
||||
Lifecycle hook infrastructure: event dispatcher (sync/async paths), audit logging, database schema (PostgreSQL + SQLite), store interface, config validation, edition gating. Handlers + pipeline integration deferred to Phase 2.
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
# Agent Hooks
|
||||
|
||||
Lifecycle hooks let you intercept, observe, or inject behavior at defined points in the agent loop. Use cases: block unsafe tool calls, auto-lint after writes, inject session context, notify on stop, audit tool usage.
|
||||
|
||||
## Concepts
|
||||
|
||||
### Events
|
||||
Seven lifecycle events fire during an agent session:
|
||||
|
||||
| Event | Blocking | When it fires |
|
||||
|---|---|---|
|
||||
| `session_start` | no | A new session is established. |
|
||||
| `user_prompt_submit` | **yes** | Before the user's message enters the pipeline. |
|
||||
| `pre_tool_use` | **yes** | Before any tool call executes. |
|
||||
| `post_tool_use` | no | After a tool call completes. |
|
||||
| `stop` | no | The agent session terminates normally. |
|
||||
| `subagent_start` | **yes** | A sub-agent is spawned. |
|
||||
| `subagent_stop` | no | A sub-agent finishes. |
|
||||
|
||||
Blocking events wait for the sync chain to return an allow/block decision. Non-blocking events fire hooks asynchronously for observation only.
|
||||
|
||||
### Handler types
|
||||
|
||||
| Handler | Editions | Notes |
|
||||
|---|---|---|
|
||||
| `command` | Lite only | Local shell command; exit 2 → block, exit 0 → allow. |
|
||||
| `http` | Lite + Standard | POST to endpoint; JSON body → decision. SSRF-protected. |
|
||||
| `prompt` | Lite + Standard | LLM-based evaluation with structured tool-call output. Prompt-injection-resistant, budget-bounded. Requires `matcher` or `if_expr`. |
|
||||
|
||||
### Scopes
|
||||
- **global** — applies to all tenants. Master scope required to create.
|
||||
- **tenant** — applies to one tenant (any agent).
|
||||
- **agent** — applies to a specific agent within a tenant.
|
||||
|
||||
Hooks resolve in priority order, highest first. A single `block` decision short-circuits the chain.
|
||||
|
||||
## Security model
|
||||
|
||||
- **Edition gating**: `command` handler blocked on Standard at both config-time AND dispatch-time (defense in depth).
|
||||
- **Tenant isolation**: all reads/writes scope by `tenant_id` unless caller is in master scope. Global hooks use a sentinel tenant id.
|
||||
- **SSRF protection**: HTTP handler validates URLs before request, pins resolved IP, blocks loopback/link-local/private ranges.
|
||||
- **PII redaction**: audit rows truncate error text to 256 chars; full error is encrypted (AES-256-GCM) in `error_detail`.
|
||||
- **Fail-closed**: any unhandled error in a blocking event yields `block`. Timeouts respect `OnTimeout` (default `block` for blocking events).
|
||||
- **Circuit breaker**: N consecutive blocks/timeouts in a rolling window auto-disables the hook and persists `enabled=false`.
|
||||
- **Loop detection**: sub-agent hook chains bounded at depth 3 (`MaxLoopDepth`).
|
||||
|
||||
## Handler reference
|
||||
|
||||
### command
|
||||
|
||||
```json
|
||||
{
|
||||
"handler_type": "command",
|
||||
"config": {
|
||||
"command": "bash /path/to/script.sh",
|
||||
"allowed_env_vars": ["MY_VAR"],
|
||||
"cwd": "/workspace"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Stdin: JSON-encoded event payload.
|
||||
- Stdout (exit 0): optional `{"continue": false}` → block; anything else → allow.
|
||||
- Exit 2: block.
|
||||
- Other non-zero exits: error (fail-closed for blocking events).
|
||||
- Env allowlist: only listed keys are passed through to prevent secret leakage.
|
||||
|
||||
### http
|
||||
|
||||
```json
|
||||
{
|
||||
"handler_type": "http",
|
||||
"config": {
|
||||
"url": "https://example.com/webhook",
|
||||
"headers": {"Authorization": "<AES-encrypted>"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Method: POST, body = event JSON.
|
||||
- Authorization header values are stored AES-256-GCM encrypted; decrypted at dispatch.
|
||||
- 1 MiB response cap.
|
||||
- Retries once on 5xx with 1s backoff; 4xx fail-closed (no retry).
|
||||
- Response body:
|
||||
```json
|
||||
{ "decision": "allow" | "block", "additionalContext": "...", "updatedInput": {}, "continue": true }
|
||||
```
|
||||
- Non-JSON 2xx → allow.
|
||||
|
||||
### prompt
|
||||
|
||||
```json
|
||||
{
|
||||
"handler_type": "prompt",
|
||||
"matcher": "^(exec|shell|write_file)$",
|
||||
"config": {
|
||||
"prompt_template": "Evaluate safety of this tool call.",
|
||||
"model": "haiku",
|
||||
"max_invocations_per_turn": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Required:
|
||||
- `prompt_template` — system-level instruction the evaluator receives.
|
||||
- `matcher` or `if_expr` — runaway-cost guard; prevents firing the LLM on every event.
|
||||
|
||||
Safeguards:
|
||||
- **Structured output**: evaluator MUST call a `decide(decision, reason, injection_detected, updated_input)` tool. Free-text responses fail-closed.
|
||||
- **Sanitized input**: only `tool_input` reaches the evaluator (sandwiched in a fenced `USER INPUT` block with anti-injection preamble); the raw user message is never included.
|
||||
- **Decision cache**: 60s TTL keyed by `sha256(hook_id || version || tool_name || tool_input)`. Hook edits bump version → busts cache.
|
||||
- **Per-turn cap**: default 5 invocations per user turn; configurable.
|
||||
- **Per-tenant monthly budget**: atomic UPDATE deduct; warn at 20% remaining, block at 0.
|
||||
|
||||
## Matchers
|
||||
|
||||
- **matcher** — POSIX-ish regex applied to `tool_name`. Example: `^(exec|shell|write_file)$`.
|
||||
- **if_expr** — [cel-go](https://github.com/google/cel-go) expression evaluated against `{tool_name, tool_input, depth}`. Example: `tool_name == "exec" && size(tool_input.cmd) > 80`.
|
||||
|
||||
Both are optional for `command` / `http`. At least one is required for `prompt`.
|
||||
|
||||
## Safeguards summary
|
||||
|
||||
| Safeguard | Default | Overridable per hook |
|
||||
|---|---|---|
|
||||
| Per-hook timeout | 5s | yes (`timeout_ms`, max 10s) |
|
||||
| Chain budget | 10s | no (dispatcher constant) |
|
||||
| Circuit threshold | 5 blocks in 1 minute | no (dispatcher constant) |
|
||||
| Prompt per-turn cap | 5 | yes (`max_invocations_per_turn`) |
|
||||
| Prompt decision cache TTL | 60s | no |
|
||||
| Tenant monthly token budget | 1,000,000 | seeded per tenant |
|
||||
|
||||
## Web UI walkthrough
|
||||
|
||||
Navigate to **Hooks** in the sidebar. The list view shows all hooks visible under the current role + tenant scope.
|
||||
|
||||
1. **Create** → pick event, handler type (command disabled on Standard), scope, matcher, then fill the handler-specific sub-form.
|
||||
2. **Test** panel → fires the hook with a sample event (`dryRun=true`, no audit row written). Shows decision badge, duration, stdout/stderr (command), status code (http), reason (prompt). If the response includes `updatedInput`, a side-by-side JSON diff is rendered.
|
||||
3. **History** tab → paginated executions from `hook_executions` (full implementation lands with operational pagination; current release shows the note state).
|
||||
4. **Overview** tab → summary card with event, type, scope, matcher.
|
||||
|
||||
## Observability
|
||||
|
||||
### Audit table (`hook_executions`)
|
||||
|
||||
| Column | Notes |
|
||||
|---|---|
|
||||
| `hook_id` | `ON DELETE SET NULL` → executions preserved after hook deletion. |
|
||||
| `dedup_key` | Unique index prevents double rows on retry (H6). |
|
||||
| `error` | Truncated to 256 chars; full detail in `error_detail` (encrypted). |
|
||||
| `metadata` | JSONB: `matcher_matched`, `cel_eval_result`, `stdout_len`, `http_status`, `prompt_model`, `prompt_tokens`, `trace_id`. |
|
||||
|
||||
### Tracing spans
|
||||
|
||||
Every hook execution emits a span named `hook.<handler_type>.<event>` (e.g. `hook.prompt.pre_tool_use`) with:
|
||||
|
||||
- `status` — `completed` or `error`.
|
||||
- `duration_ms`.
|
||||
- `metadata.decision` — `allow`/`block`/`error`/`timeout`.
|
||||
- `parent_span_id` — nested under the current pipeline span when one exists.
|
||||
|
||||
No-op when no tracing collector is attached to ctx (safe in tests and for tenants without tracing enabled).
|
||||
|
||||
### Metrics (slog keys)
|
||||
|
||||
- `security.hook.circuit_breaker` — tripped breaker.
|
||||
- `security.hook.audit_write_failed` — audit row write error.
|
||||
- `security.hook.resolve_error` — store resolve error (blocking event → fail-closed).
|
||||
- `security.hook.loop_depth_exceeded` — `MaxLoopDepth` violation.
|
||||
- `security.hook.prompt_parse_error` — evaluator returned malformed structured output.
|
||||
- `security.hook.budget_deduct_failed` / `budget_precheck_failed` — budget store error.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---|---|---|
|
||||
| HTTP hook always returns `error` | SSRF block on loopback (test). | Call `security.SetAllowLoopbackForTest(true)` in tests. |
|
||||
| Prompt hook blocks everything | Evaluator returning free-text (no tool call). | Review `prompt_template`; keep it short + imperative. |
|
||||
| Hook stopped firing | Circuit breaker tripped (5 blocks/min). | `UPDATE agent_hooks SET enabled=true WHERE id=...` after fixing upstream cause. |
|
||||
| UI `command` radio greyed out | Standard edition. | Use HTTP or prompt, or upgrade to Lite. |
|
||||
| Per-turn cap hit | `max_invocations_per_turn` too low. | Raise in hook config; review matcher tightness. |
|
||||
| Budget exceeded | Tenant spent monthly token budget. | Raise `tenant_hook_budget.budget_total` or wait for rollover. |
|
||||
|
||||
## Migration notes
|
||||
|
||||
No data migration required when upgrading from a pre-hooks release. Migration `000052_agent_hooks` creates `agent_hooks`, `hook_executions`, and `tenant_hook_budget`. SQLite users must also have schema version 20 or later.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Prompt decision cache is per-process (no cluster-wide Redis yet).
|
||||
- No skill-frontmatter hooks (planned post-MVP).
|
||||
- No `agent` handler type (inter-agent delegation via prompt instead).
|
||||
- Desktop Wails UI has list/detail parity but no dedicated mobile-optimized form.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Example Hooks Library
|
||||
|
||||
Copy-paste-ready hook configurations for common use cases. Each JSON is a valid `hooks.create` WS payload — send it via the Web UI → **Hooks → Create** (paste into the relevant fields) or POST directly to the RPC endpoint.
|
||||
|
||||
## Safety reminders
|
||||
|
||||
- **Review regex matchers** before enabling. A too-broad matcher on a prompt hook drains token budget. Prefer anchored patterns like `^(exec|shell)$`.
|
||||
- **`command` hooks require Lite edition.** On Standard the UI greys them out and dispatch fails closed.
|
||||
- **SSRF-safe HTTP hooks**: URLs are resolved + pinned before the request. Loopback/private ranges are blocked in production.
|
||||
- **Authorization headers** get AES-256-GCM encrypted at rest. Never commit plaintext secrets to these JSON files.
|
||||
- **Test in staging first.** Use the `Test` tab in the UI — it runs with `dryRun=true` and does NOT write to `hook_executions`.
|
||||
|
||||
## Catalog
|
||||
|
||||
| File | Event | Handler | Scope | Purpose |
|
||||
|---|---|---|---|---|
|
||||
| `block-rm-rf.json` | `pre_tool_use` | command | agent | Block dangerous `rm -rf /` via local shell script. Lite only. |
|
||||
| `auto-lint-after-write.json` | `post_tool_use` | http | agent | Fire-and-forget lint request after file writes. |
|
||||
| `audit-tool-usage.json` | `post_tool_use` | http | tenant | Stream every tool invocation to an external audit sink. |
|
||||
| `session-context-injector.json` | `session_start` | http | agent | Injects project metadata into the agent context at start. |
|
||||
| `notify-discord-on-stop.json` | `stop` | http | tenant | Discord webhook notification when a session ends. |
|
||||
|
||||
## Usage
|
||||
|
||||
### Via Web UI
|
||||
|
||||
1. Open `/hooks` → click **Create hook**.
|
||||
2. Copy fields from the example JSON into the form.
|
||||
3. For `http.config.headers`, paste your secret in the Authorization field — the server encrypts it before storing.
|
||||
4. Click **Save**, then **Test** with a sample event before enabling in production.
|
||||
|
||||
### Via WS RPC
|
||||
|
||||
```bash
|
||||
wscat -c ws://localhost:18790/ws
|
||||
# After connect:
|
||||
> {"id":"1","method":"hooks.create","params": <paste JSON here> }
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- `tenant_id` omitted → current tenant from WS session. Set `scope: "global"` to apply cross-tenant (master required).
|
||||
- `agent_id` required for `scope: "agent"`; otherwise leave null.
|
||||
- `priority: 10` is the recommended default. Higher priority hooks run first; first `block` wins the chain.
|
||||
- `on_timeout: "block"` for anything security-sensitive; `"allow"` for observation-only.
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"event": "post_tool_use",
|
||||
"handler_type": "http",
|
||||
"scope": "tenant",
|
||||
"matcher": "",
|
||||
"if_expr": "",
|
||||
"config": {
|
||||
"url": "https://audit.internal.example.com/hooks/tool-usage",
|
||||
"headers": {
|
||||
"Authorization": "Bearer REPLACE_ME_ENCRYPTED",
|
||||
"X-Goclaw-Audit": "tool-usage"
|
||||
}
|
||||
},
|
||||
"timeout_ms": 2000,
|
||||
"on_timeout": "allow",
|
||||
"priority": 1,
|
||||
"enabled": true,
|
||||
"source": "seed",
|
||||
"metadata": {
|
||||
"notes": "Streams every tool invocation to an external audit sink for compliance. Tenant-wide.",
|
||||
"tags": ["observability", "compliance", "audit"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"event": "post_tool_use",
|
||||
"handler_type": "http",
|
||||
"scope": "agent",
|
||||
"matcher": "^(write_file|edit_file)$",
|
||||
"if_expr": "",
|
||||
"config": {
|
||||
"url": "https://ci.internal.example.com/hooks/lint",
|
||||
"headers": {
|
||||
"Authorization": "Bearer REPLACE_ME_ENCRYPTED",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"timeout_ms": 3000,
|
||||
"on_timeout": "allow",
|
||||
"priority": 10,
|
||||
"enabled": true,
|
||||
"source": "api",
|
||||
"metadata": {
|
||||
"notes": "Fire-and-forget lint ping after file writes. Non-blocking event — won't stall the agent loop.",
|
||||
"tags": ["observability", "quality"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"event": "pre_tool_use",
|
||||
"handler_type": "command",
|
||||
"scope": "agent",
|
||||
"matcher": "^(exec|shell)$",
|
||||
"if_expr": "",
|
||||
"config": {
|
||||
"command": "grep -qE 'rm +-[rR][fF]? +/' && exit 2 || exit 0"
|
||||
},
|
||||
"timeout_ms": 2000,
|
||||
"on_timeout": "block",
|
||||
"priority": 100,
|
||||
"enabled": true,
|
||||
"source": "api",
|
||||
"metadata": {
|
||||
"notes": "Blocks `rm -rf /` style commands before exec. Lite edition only.",
|
||||
"tags": ["safety", "command"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"event": "stop",
|
||||
"handler_type": "http",
|
||||
"scope": "tenant",
|
||||
"matcher": "",
|
||||
"if_expr": "",
|
||||
"config": {
|
||||
"url": "https://discord.com/api/webhooks/REPLACE_ME",
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"timeout_ms": 3000,
|
||||
"on_timeout": "allow",
|
||||
"priority": 1,
|
||||
"enabled": false,
|
||||
"source": "seed",
|
||||
"metadata": {
|
||||
"notes": "Discord webhook fired on every session stop. Disabled by default — enable after replacing the webhook URL. Discord expects {\"content\":\"...\"} but the hook ships event JSON; consider putting an adapter in front.",
|
||||
"tags": ["notify", "discord", "session"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"event": "session_start",
|
||||
"handler_type": "http",
|
||||
"scope": "agent",
|
||||
"matcher": "",
|
||||
"if_expr": "",
|
||||
"config": {
|
||||
"url": "https://ctx.internal.example.com/hooks/session-bootstrap",
|
||||
"headers": {
|
||||
"Authorization": "Bearer REPLACE_ME_ENCRYPTED"
|
||||
}
|
||||
},
|
||||
"timeout_ms": 4000,
|
||||
"on_timeout": "allow",
|
||||
"priority": 50,
|
||||
"enabled": true,
|
||||
"source": "api",
|
||||
"metadata": {
|
||||
"notes": "On session_start, fetches project-specific metadata and injects via additionalContext. The remote endpoint should respond with {\"decision\":\"allow\",\"additionalContext\":\"...\"}.",
|
||||
"tags": ["context", "session", "bootstrap"]
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user