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:
viettranx committed 2026-04-16 14:17:47 +07:00
1 parent f90f11a9f9
commit 95bdb23a36
10 files changed
+431 -1

No files matched your search

+1
View File
@@ -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
+21 -1
View File
@@ -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 ./...
+62
View File
@@ -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.
+193
View File
@@ -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.
+45
View File
@@ -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.
+23
View File
@@ -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"]
}
}
+23
View File
@@ -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"]
}
}
+19
View File
@@ -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"]
}
}