refactor: redesign gateway and runtime middleware chain#105
refactor: redesign gateway and runtime middleware chain#105
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors the gateway and container runtime execution flow to use a shared, ordered middleware-chain model, splitting “pre-agent” and “post-agent” responsibilities into explicit middleware phases.
Changes:
- Introduces a shared middleware contract (
Middleware, phases, results, tool decisions) and an orderedMiddlewareChainexecutor with timeouts. - Refactors
handleGatewayMessage()to run gateway turn preparation and post-processing through a dedicated middleware chain (session/media/memory/prompt/audit + afterAgent processing). - Updates the container runtime tool execution path to use before/after tool middleware hooks (including loop detection + security checks) and adds regression tests.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/middleware-chain.test.ts | Adds unit coverage for ordered state updates, halting, and tool argument modification/denial behavior. |
| tests/container-runtime-middleware.test.ts | Adds regression tests for container tool-deny patterns (bash exfiltration + loop guard). |
| src/middleware/types.ts | Defines the shared middleware types/phases and tool decision contract. |
| src/middleware/chain.ts | Implements ordered middleware execution with timeout handling and result application. |
| src/gateway/gateway-service.ts | Refactors gateway turn handling into beforeAgent/afterAgent middleware stages. |
| container/src/index.ts | Routes tool execution through container middleware hooks and uses rewritten args when applicable. |
| container/src/extensions.ts | Replaces runtime “extensions” with middleware + middleware chain, including security and loop-detection middleware. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
container/src/extensions.ts
Outdated
| modified: | ||
| decision.action === 'continue' && | ||
| JSON.stringify(args) !== JSON.stringify(ctx.toolArgs), |
There was a problem hiding this comment.
The emitted before_tool_call event sets modified only when decision.action === 'continue', so it will incorrectly report modified: false for the primary mutation path (decision.action === 'modify'). Update this flag to reflect actual argument rewrites (e.g., treat modify as modified, and/or compare original vs final args regardless of action).
| modified: | |
| decision.action === 'continue' && | |
| JSON.stringify(args) !== JSON.stringify(ctx.toolArgs), | |
| modified: JSON.stringify(args) !== JSON.stringify(ctx.toolArgs), |
| import { expect, test } from 'vitest'; | ||
|
|
||
| import { getRuntimeConfig } from '../src/config/runtime-config.js'; | ||
| import { MiddlewareChain } from '../src/middleware/chain.js'; |
There was a problem hiding this comment.
This test imports getRuntimeConfig() from src/config/runtime-config, which initializes runtime config on module load (creates/reads config file, starts a watcher unless HYBRIDCLAW_DISABLE_CONFIG_WATCHER is set). Unlike most gateway tests, it doesn’t call setupGatewayTest().setupHome(), so it can write to a developer’s real HOME and keep file watchers open. Consider using setupGatewayTest (or stubbing HOME / disabling the watcher) and/or mocking getRuntimeConfig() to keep the unit test hermetic.
src/gateway/gateway-service.ts
Outdated
| const activeGatewayRequest = registerActiveGatewayRequest({ | ||
| sessionId: req.sessionId, | ||
| abortSignal: req.abortSignal, | ||
| }); |
There was a problem hiding this comment.
registerActiveGatewayRequest() is called before the middleware chain runs, but SessionMiddleware can mutate req.sessionId (auto-reset, getOrCreateSession id fixup, workspace reset). That means the active request gets registered/released under the old sessionId, so interruptGatewaySessionExecution()/abort routing won’t work for the actual session and the old map entry may leak. Move registration until after the final sessionId is known (or re-register when sessionId changes) and ensure the abort signal passed to downstream middleware/agent uses the correctly registered entry.
|
@claude Do a thorough review of this PR in terms of: KISS, YAGNI, DRY, Fail Fast, Secure by Default |
|
Claude finished @furukama's task in 2m 55s —— View job Code Review: KISS · YAGNI · DRY · Fail Fast · Secure by Default
The middleware chain abstraction is directionally right, but there are several violations of the principles you asked about. Here they are, ranked by severity. DRY — Duplicate middleware systemThe single biggest issue: The divergence exists for a real reason — the container's YAGNI — Dead phases in the gateway chain
KISS —
|
| Principle | Issues |
|---|---|
| DRY | Entire middleware system duplicated in container; repeated state-extraction boilerplate |
| YAGNI | beforeModel/afterModel phases defined but unused in gateway; single-middleware command chain |
| KISS | GatewayMiddlewareState god object with implicit ordering; as never type cast |
| Fail Fast | registerActiveGatewayRequest uses pre-mutation sessionId |
| Secure by Default | modified flag always false for modify decisions; parseArgs({}) bypasses security checks |
6afa570 to
4d6a3bc
Compare
Summary
Testing