feat(gateway): command hooks for deterministic chat commands - #199
Conversation
[command_hooks] maps chat slash commands to shell commands whose stdout is relayed verbatim before backend dispatch - no agent turn, no tokens. A mapped command owns all its forms: message args are appended to the command line as trailing argv parameters, so command-shaped input never becomes prompt content. Hooks run through /bin/sh with a 15s timeout and a 64 KiB stdout cap; failures, empty output, and timeouts reply with a short deterministic error instead of falling back to the agent. A typing indicator (same refresh loop as agent runs) stays on for the whole hook execution. Hooks receive message context as env - PUSH_THREAD, PUSH_BACKEND, PUSH_ROW_ID, PUSH_SESSION_ID (read-only lookup; never creates rows). /help lists configured hooks under Custom commands. Hooks run in the thread queue like any message: no preemption of a running turn, replies stay in order; /stop remains the immediate exception.
d437d2d to
bed5732
Compare
Greptile SummaryThis PR adds deterministic gateway command hooks that execute configured shell commands without invoking an agent.
Confidence Score: 5/5The PR appears safe to merge; the latest change correctly rejects normalized command-name collisions and no outstanding findings remain. The latest validation checks for an existing normalized key before insertion, eliminating nondeterministic command selection. The current code also fully addresses every earlier hook-routing, resource-bounding, process-cleanup, metadata-scoping, and configuration-validation finding.
|
| Filename | Overview |
|---|---|
| src/config.rs | Adds command-hook configuration, normalization, validation, collision rejection, and focused tests. |
| src/gateway/worker.rs | Implements slash-command routing and bounded, timeout-controlled hook execution with process-group cleanup. |
| src/gateway/tests.rs | Covers successful hooks, argument handling, multiline routing, fallback behavior, output caps, and backend-scoped session metadata. |
| src/store.rs | Adds a read-only, backend-scoped session lookup for hook context. |
| docs/reference/cli.md | Documents command-hook configuration, execution semantics, limits, queueing, and security implications. |
Sequence Diagram
sequenceDiagram
participant User
participant Gateway
participant Queue as Thread Queue
participant Hook as /bin/sh Hook
participant Backend
User->>Gateway: Slash-command message
Gateway->>Queue: Enqueue message
Queue->>Gateway: Process in order
alt Exact built-in command
Gateway-->>User: Built-in response
else Configured command hook
Gateway->>Hook: "Execute with argv and PUSH_* environment"
Hook-->>Gateway: Bounded stdout / status
Gateway-->>User: Output or deterministic error
else Unknown slash command
Gateway->>Backend: Dispatch unchanged message
Backend-->>Gateway: Agent response
Gateway-->>User: Agent response
end
Reviews (3): Last reviewed commit: "fix(gateway): address hook runner review..." | Re-trigger Greptile
| // command line as trailing positional parameters (argv-safe, no | ||
| // re-parsing). Command-shaped input stays deterministic by | ||
| // default — it never becomes prompt content. | ||
| let hook = ctx.cfg.command_hooks.get(word)?; |
There was a problem hiding this comment.
Hook names are loaded without validation or normalization, but dispatch lowercases the incoming command before performing an exact map lookup. An entry such as Report = "..." is therefore shown by /help, while /Report misses the hook and falls through to the backend despite the documented case-insensitive behavior. Built-in and whitespace-containing keys can likewise be accepted but remain unreachable, creating misleading configuration and help output. Normalize or reject unusable names during configuration validation.
Knowledge Base Used: Configuration and environment
- Cap hook stdout/stderr incrementally at 64 KiB (previous cap sliced the buffered String, could panic on a multibyte boundary, and buffered the full output before truncating). Cap-exceeded is a deterministic error reply. - Run hooks in their own process group and signal the whole group on timeout and cap paths so backgrounded descendants do not outlive the hook. - Scope peek_session_id to the dispatching backend: after a route change, the stored session belongs to the previous backend and must not leak into hook env. - Split the command word on general whitespace so /report\nagents routes like /report agents. - Built-ins require exact matching again: /clear typo reaches the backend unchanged, as before hooks. - Validate command_hooks names at load (lowercase-normalized, ASCII word characters, no built-in shadowing); unreachable or ambiguous names now fail config load instead of silently never matching.
dc01b3a to
9e12c64
Compare
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
Problem
Every chat message today, including slash commands, costs a full backend turn. Gateway built-ins (
/clear,/help,/stop) short-circuit, but anything user-defined has to route through the LLM even when the desired behavior is fully deterministic; for example a command that runs a script and relays its output.Change
Adds a
[command_hooks]config table mapping chat slash commands to shell commands, handled in the existing gateway command path before backend dispatch:/report agents→report.sh agents, argv-safe, never re-parsed). Command-shaped input stays deterministic by default; it never becomes prompt content./bin/sh(POSIX) with a 15s timeout and a 64 KiB stdout cap; a typing indicator (channel permitting, same refresh loop as agent runs) stays on for the whole hook execution.PUSH_THREAD,PUSH_BACKEND,PUSH_ROW_ID,PUSH_SESSION_ID(set only when the thread already has a backend session, via a read-only lookup that never creates rows). Hooks run with the gateway's environment; the docs carry an explicit warning.Queueing semantics
Hooks run in the thread's existing job queue like any other message: if the backend is mid-reply, a hook command waits for that turn to finish and runs afterward; replies stay in order and never interleave, but the hook does not preempt a running turn.
/stopis the exception: it acts on the in-flight request immediately, as before. (If preemption for hook commands is ever wanted, that's a separate, larger feature.)Verification
cargo fmt --all --check,cargo clippy --locked --all-targets -- -D warnings,cargo build --locked,cargo test --lockedall pass (448 tests).mkdocs build --strict).Risk
Low: default config (
command_hooksempty) is behavior-identical to today. Thecommand()signature gainsasync(call site is already async context). Docs updated on the canonical page (docs/reference/cli.md). The hardened hook runner in #197 (process groups) is a better execution engine; this PR ships the simpler runner deliberately and the two can be unified when #197 lands.Review hardening (post automated review)
Addressed all findings from the automated review pass:
PUSH_SESSION_IDis backend-scoped; after a route change the previous backend's session cannot leak into hook env/report\nagentsroutes like/report agents)/clear typoreaches the backend unchanged)