From 95bdb23a3688390d144ea0e81349b5941bc668d5 Mon Sep 17 00:00:00 2001 From: viettranx Date: Wed, 15 Apr 2026 21:20:57 +0700 Subject: [PATCH] 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 --- CLAUDE.md | 1 + Makefile | 22 ++- docs/17-changelog.md | 62 ++++++ docs/agent-hooks.md | 193 +++++++++++++++++++ examples/hooks/README.md | 45 +++++ examples/hooks/audit-tool-usage.json | 23 +++ examples/hooks/auto-lint-after-write.json | 23 +++ examples/hooks/block-rm-rf.json | 19 ++ examples/hooks/notify-discord-on-stop.json | 22 +++ examples/hooks/session-context-injector.json | 22 +++ 10 files changed, 431 insertions(+), 1 deletion(-) create mode 100644 docs/agent-hooks.md create mode 100644 examples/hooks/README.md create mode 100644 examples/hooks/audit-tool-usage.json create mode 100644 examples/hooks/auto-lint-after-write.json create mode 100644 examples/hooks/block-rm-rf.json create mode 100644 examples/hooks/notify-discord-on-stop.json create mode 100644 examples/hooks/session-context-injector.json diff --git a/CLAUDE.md b/CLAUDE.md index 45df479b..14a77b76 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/Makefile b/Makefile index a497b804..679c1dca 100644 --- a/Makefile +++ b/Makefile @@ -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 ./... diff --git a/docs/17-changelog.md b/docs/17-changelog.md index e23f1d51..93faf3ab 100644 --- a/docs/17-changelog.md +++ b/docs/17-changelog.md @@ -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..`; 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. diff --git a/docs/agent-hooks.md b/docs/agent-hooks.md new file mode 100644 index 00000000..9514d286 --- /dev/null +++ b/docs/agent-hooks.md @@ -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": ""} + } +} +``` + +- 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..` (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. diff --git a/examples/hooks/README.md b/examples/hooks/README.md new file mode 100644 index 00000000..2c5321ea --- /dev/null +++ b/examples/hooks/README.md @@ -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": } +``` + +## 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. diff --git a/examples/hooks/audit-tool-usage.json b/examples/hooks/audit-tool-usage.json new file mode 100644 index 00000000..b3254bb9 --- /dev/null +++ b/examples/hooks/audit-tool-usage.json @@ -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"] + } +} diff --git a/examples/hooks/auto-lint-after-write.json b/examples/hooks/auto-lint-after-write.json new file mode 100644 index 00000000..f7cb9b98 --- /dev/null +++ b/examples/hooks/auto-lint-after-write.json @@ -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"] + } +} diff --git a/examples/hooks/block-rm-rf.json b/examples/hooks/block-rm-rf.json new file mode 100644 index 00000000..50b2ed08 --- /dev/null +++ b/examples/hooks/block-rm-rf.json @@ -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"] + } +} diff --git a/examples/hooks/notify-discord-on-stop.json b/examples/hooks/notify-discord-on-stop.json new file mode 100644 index 00000000..2e9beb4e --- /dev/null +++ b/examples/hooks/notify-discord-on-stop.json @@ -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"] + } +} diff --git a/examples/hooks/session-context-injector.json b/examples/hooks/session-context-injector.json new file mode 100644 index 00000000..8656c82b --- /dev/null +++ b/examples/hooks/session-context-injector.json @@ -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"] + } +}