From 87d39849e330ad84f2b10f04b95e302798664284 Mon Sep 17 00:00:00 2001 From: Bruno Clermont Date: Fri, 12 Jun 2026 22:22:40 -0400 Subject: [PATCH] docs(hooks): add JavaScript handler type documentation - New doc: hooks-javascript.md with ES5.1 JS handler guide - Covers sandboxed runtime, event payloads, examples, safety limits - Update triple-sync: README.md, js/docs-app.js, index.html - Add reference to script handler in hooks-quality-gates.md - JavaScript hooks execute in-process via goja ES5.1 runtime Co-Authored-By: Claude Haiku 4.5 --- README.md | 1 + advanced/hooks-javascript.md | 345 ++++++++++++++++++++++++++++++++ advanced/hooks-quality-gates.md | 1 + index.html | 1 + js/docs-app.js | 1 + 5 files changed, 349 insertions(+) create mode 100644 advanced/hooks-javascript.md diff --git a/README.md b/README.md index 6de8d24..a2fdc2a 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ - [Browser Automation](advanced/browser-automation.md) - [Extended Thinking](advanced/extended-thinking.md) - [Hooks & Quality Gates](advanced/hooks-quality-gates.md) +- [JavaScript Hooks](advanced/hooks-javascript.md) - [Authentication & OAuth](advanced/authentication.md) - [API Keys & RBAC](advanced/api-keys-rbac.md) - [CLI Credentials](advanced/cli-credentials.md) diff --git a/advanced/hooks-javascript.md b/advanced/hooks-javascript.md new file mode 100644 index 0000000..2b74975 --- /dev/null +++ b/advanced/hooks-javascript.md @@ -0,0 +1,345 @@ +# JavaScript Hooks + +> Execute sandboxed ES5.1 JavaScript to intercept, audit, or transform agent events in real-time — with full control over blocking decisions and event mutations. + +## Overview + +The **script** handler type runs user-provided JavaScript in a sandboxed goja ES5.1 runtime. Unlike `http` (external service) or `prompt` (LLM-based) handlers, scripts execute immediately in-process with zero latency — ideal for lightweight filtering, logging, or input transformation. + +**Key traits:** +- **Sandboxed** — ES5.1 only, no file I/O, network, or external modules. `console.log()` output is captured and truncated to 4 KiB. +- **Fast** — executes in-process with program caching; repeated invocations reuse compiled bytecode. +- **Simple** — single function signature `handle(event)` that returns a decision object. +- **Available on** — Lite + Standard editions (no edition restrictions). + +--- + +## Handler Signature + +```javascript +function handle(event) { + return { + decision: "allow", // "allow" or "block" + reason: "Reason for decision", + updatedInput: {}, // optional: mutate tool input / message + additionalContext: "..." // optional: logging / audit info + }; +} +``` + +**Required:** +- `decision` — string: `"allow"` or `"block"` + +**Optional:** +- `reason` — explains the decision (logged, visible in audit) +- `updatedInput` — object: mutations to apply to the event (tool_input, message body, etc.) +- `additionalContext` — string: extra metadata for audit trail + +--- + +## Event Payload + +The `event` object structure depends on the hook's **event type**: + +### `pre_tool_use` +```javascript +{ + toolName: "exec", + toolInput: { cmd: "ls -la" }, + depth: 0 // 0 = main agent, 1+ = subagent nesting +} +``` + +### `user_prompt_submit` +```javascript +{ + message: "What is the weather?", + sessionKey: "sess-abc123" +} +``` + +### `post_tool_use` +```javascript +{ + toolName: "exec", + toolInput: { cmd: "ls" }, + toolOutput: "file1.txt\nfile2.txt", + exitCode: 0, + depth: 0 +} +``` + +### `session_start` +```javascript +{ + sessionKey: "sess-abc123", + agentKey: "assistant" +} +``` + +### Subagent events (`subagent_start`, `subagent_stop`) +```javascript +{ + parentAgentKey: "main", + subagentKey: "researcher", + depth: 1 +} +``` + +--- + +## Examples + +### Example 1: Block dangerous commands + +```javascript +function handle(event) { + const dangerous = /^(rm|dd|mkfs|dd)/i; + + if (dangerous.test(event.toolInput.cmd)) { + return { + decision: "block", + reason: "Dangerous command pattern blocked: " + event.toolInput.cmd + }; + } + + return { decision: "allow" }; +} +``` + +### Example 2: Redact PII from user input + +```javascript +function handle(event) { + if (event.toolName !== "web_fetch") { + return { decision: "allow" }; + } + + // Redact email addresses from URLs + const redacted = event.toolInput.url.replace( + /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/gi, + "[EMAIL]" + ); + + return { + decision: "allow", + updatedInput: { url: redacted }, + reason: "PII redacted from fetch URL" + }; +} +``` + +### Example 3: Audit and allow (non-blocking) + +```javascript +function handle(event) { + // For non-blocking events like post_tool_use, always allow + // and log metadata for compliance audits. + + return { + decision: "allow", + additionalContext: JSON.stringify({ + timestamp: new Date().toISOString(), + toolName: event.toolName, + inputSize: JSON.stringify(event.toolInput).length, + outputSize: (event.toolOutput || "").length + }) + }; +} +``` + +### Example 4: Rate limit by pattern + +```javascript +function handle(event) { + // Example: limit recursive API calls + if (event.depth > 3) { + return { + decision: "block", + reason: "Subagent nesting exceeds depth limit" + }; + } + + return { decision: "allow" }; +} +``` + +--- + +## Built-in Functions + +The sandboxed runtime includes ES5.1 globals: + +```javascript +// Math, Object, Array, String, Number, Boolean, Date, RegExp, JSON +JSON.stringify(obj) +JSON.parse(str) +Math.max(1, 2, 3) +new Date().toISOString() +"hello".toUpperCase() +``` + +**Not available:** +- File I/O (`fs`, `readFile`, etc.) +- Network (`fetch`, `http`, `ws`) +- Child processes (`exec`, `spawn`) +- Modules (`require`, `import`) +- `eval()` or `Function()` constructors +- `setTimeout` / `setInterval` (no async) + +--- + +## Safety & Limits + +| Limit | Value | Notes | +|---|---|---| +| Script source size | 32 KiB | Validated at create + execute time | +| Stdout capture | 4 KiB | Excess writes are dropped with `... truncated` marker | +| Global concurrency | 10 slots | Max parallel script executions across all tenants | +| Per-tenant concurrency | 3 slots | Prevents one tenant from starving the pool | +| Program cache size | 500 entries | LRU eviction; keyed by (hookID, version) | +| Execution timeout | 5 s (per hook) | Configurable via `timeout_ms` (max 10 s) | + +**Circuit breaker:** 5 consecutive errors (or timeouts) in a 1-minute rolling window auto-disables the hook (`enabled=false`). Fix the underlying issue and re-enable via `hooks.toggle`. + +--- + +## Matcher & Filtering + +Scripts run on **all matching events** by default. Use `matcher` or `if_expr` to skip unnecessary invocations: + +```json +{ + "handler_type": "script", + "event": "pre_tool_use", + "matcher": "^(exec|shell|python_exec)$", + "config": { + "source": "function handle(e) { ... }" + } +} +``` + +**matcher** — POSIX-ish regex against `toolName`: +```javascript +"^(exec|shell)$" // matches "exec" or "shell" +"^sql_" // matches "sql_query", "sql_execute", etc. +``` + +**if_expr** — [cel-go](https://github.com/google/cel-go) expression: +```javascript +tool_name == "exec" && size(tool_input.cmd) > 100 +tool_name.startsWith("db_") +depth > 0 // subagent only +``` + +Both are optional but **recommended** to avoid unnecessary script overhead. + +--- + +## Configuration + +### Via WebSocket + +```json +{ + "type": "req", + "id": "1", + "method": "hooks.create", + "params": { + "event": "pre_tool_use", + "handler_type": "script", + "scope": "tenant", + "name": "Block dangerous commands", + "matcher": "^(exec|shell)$", + "timeout_ms": 3000, + "config": { + "source": "function handle(e) { ... }" + } + } +} +``` + +### Via Web UI + +1. Navigate to **Hooks** in the sidebar. +2. Click **Create**. +3. Select **Event** (e.g., `pre_tool_use`). +4. Select **Handler Type** → **Script**. +5. Set **Scope** (global / tenant / agent). +6. (Optional) Set **Matcher** regex or **If expr** to filter events. +7. Paste your JavaScript into the **Script Source** field. +8. Set **Timeout** (default 5000 ms, max 10000 ms). +9. Set **On Timeout** (default `block` for blocking events; `allow` for non-blocking). +10. Click **Create**. + +--- + +## Testing + +Use the **Test panel** in the Web UI to dry-run your script: + +1. Open the hook detail page. +2. Click **Test**. +3. Select an **event type**. +4. Fill sample event data (e.g., `toolName="exec"`, `toolInput={cmd:"ls"}`). +5. Click **Run Test**. + +The panel shows: +- ✅ **Decision** badge (allow / block) +- ⏱️ **Duration** (ms) +- 📝 **Stdout** from `console.log()` calls +- 🔄 **Updated input** (if `updatedInput` was returned) +- ❌ **Errors** (if the script threw) + +--- + +## Troubleshooting + +| Symptom | Cause | Fix | +|---|---|---| +| Script hangs (timeout) | Infinite loop in JS; no network wait expected | Rewrite to exit deterministically. Max timeout is 10 s. | +| "return statement not allowed outside function" | Missing outer `function handle(event) {}` wrapper | Wrap code: `function handle(event) { ... }` | +| `updatedInput` not applied | Returned but source is `builtin` | Only `ui` source scripts mutate events (safety). Builtin scripts return read-only recommendations. | +| "SyntaxError: Unexpected token" | ES6+ syntax (arrow functions, const, etc.) | Use ES5.1 only: `function`, `var`, `for`, etc. | +| Hook disabled after errors | Circuit breaker tripped (5 errors in 60 s) | Fix the underlying JS bug, then re-enable via `hooks.toggle { enabled: true }` | +| Script blocked by CORS / auth | Scripts cannot make HTTP requests | Use `http` handler type or `prompt` handler type instead. | + +--- + +## Best Practices + +1. **Keep it simple** — short, focused logic. Move complex policy to `prompt` (LLM) or `http` (external service). + +2. **Use matchers** — avoid running on every event: + ```json + "matcher": "^(exec|shell|python)$" + ``` + +3. **Log for audit** — return `additionalContext`: + ```javascript + return { + decision: "allow", + additionalContext: "Checked tool=" + event.toolName + }; + ``` + +4. **Handle missing fields** — be defensive: + ```javascript + if (!event.toolInput || !event.toolInput.cmd) { + return { decision: "allow" }; + } + ``` + +5. **Test first** — use the Test panel before enabling on live sessions. + +6. **Monitor timeout** — if scripts frequently timeout, increase `timeout_ms` or simplify logic. + +--- + +## What's Next + +- [Agent Hooks](/advanced/hooks-quality-gates) — complete hooks overview (command, http, prompt handlers) +- [WebSocket Protocol](/websocket-protocol) — full `hooks.*` method reference +- [Exec Approval](/exec-approval) — human-in-the-loop approval for shell commands + + diff --git a/advanced/hooks-quality-gates.md b/advanced/hooks-quality-gates.md index 48460ef..691b28f 100644 --- a/advanced/hooks-quality-gates.md +++ b/advanced/hooks-quality-gates.md @@ -35,6 +35,7 @@ Seven lifecycle events fire during an agent session: | `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. Budget-bounded, requires `matcher` or `if_expr` | +| `script` | Lite + Standard | Sandboxed ES5.1 JavaScript; in-process execution with zero latency. See [JavaScript Hooks](/advanced/hooks-javascript) | ### Scopes diff --git a/index.html b/index.html index eb85762..fb78659 100644 --- a/index.html +++ b/index.html @@ -213,6 +213,7 @@ Browser Automation Extended Thinking Hooks & Quality Gates + JavaScript Hooks Authentication & OAuth API Keys & RBAC CLI Credentials diff --git a/js/docs-app.js b/js/docs-app.js index 38813e7..4ed83b2 100644 --- a/js/docs-app.js +++ b/js/docs-app.js @@ -187,6 +187,7 @@ const DOC_MAP = { 'browser-automation': docEntry('advanced', 'browser-automation', 'Browser Automation', 'Browser Automation', '浏览器自动化'), 'extended-thinking': docEntry('advanced', 'extended-thinking', 'Extended Thinking', 'Extended Thinking', '扩展思考'), 'hooks-quality-gates': docEntry('advanced', 'hooks-quality-gates', 'Hooks & Quality Gates', 'Hooks & Quality Gates', 'Hooks 与质量门控'), + 'hooks-javascript': docEntry('advanced', 'hooks-javascript', 'JavaScript Hooks', 'JavaScript Hooks', 'JavaScript Hooks'), 'authentication': docEntry('advanced', 'authentication', 'Authentication & OAuth', 'Authentication', '认证与 OAuth'), 'api-keys-rbac': docEntry('advanced', 'api-keys-rbac', 'API Keys & RBAC', 'API Keys & RBAC', 'API Keys 与 RBAC'), 'cli-credentials': docEntry('advanced', 'cli-credentials', 'CLI Credentials', 'CLI Credentials', 'CLI 凭证'),