firi193/HopWatch
GitHub: firi193/HopWatch
一款 AI Agent 链路的提示注入检测与监控管道,通过两级检测机制同步净化外部内容并异步推理会话攻击行为。
Stars: 0 | Forks: 0
# HopWatch
针对 AI agent pipeline 的提示注入检测与监控。
HopWatch 位于 agent 链中,监控 agent 获取的外部内容及其调用的工具。它能在毫秒级内对内容进行筛查,记录跨跳的因果链,并利用 LLM judge 推理某个会话是否看起来像攻击。
**MVP 模式仅用于检测** —— agent 始终会接收到已净化的内容;HopWatch 永远不会阻止执行。
## 工作原理
```
Agent fetches external content
│
▼
POST /analyze or @analyzeExternal
│
▼
StackOne Defender (Tier 1 patterns + Tier 2 ML)
│
├─► Return sanitized content (~5ms hot path)
├─► Write detection to Postgres (async)
├─► Archive raw content to disk (async)
├─► Enqueue to BullMQ (async)
└─► Slack alert if tier2_score > 0.7 (async)
Agent tool calls
│
▼
POST /action or @recordAction
│
└─► Write to Postgres + Redis action list
Queue worker (separate process)
│
▼
Batch detections by session → Gemini 2.5 Pro Judge
│
├─► Write verdict to Postgres
└─► Slack alert on injection or novel pattern
```
### 两级检测
| 层级 | 内容 | 时机 |
|---|---|---|
| **Defender**(同步) | 通过 `@stackone/defender` 进行模式匹配 + ML 评分 | 每次 `/analyze` 调用 |
| **Judge LLM**(异步) | Gemini 2.5 Pro 跨跃点、工具历史和先前会话摘要进行推理 | 从批量处理任务的 Worker |
### 队列路由
检测结果会被路由到两个 BullMQ 队列之一:
| 通道 | 触发条件 |
|---|---|
| `judge-high` | `tier2_score ≥ 0.7`,或低分但带有升级信号(新来源、上游标志、类指令文本) |
| `judge-medium` | 中等分数,或低分且无升级信号 |
高优先级通道在累积 10 个任务时刷新(或在 5 分钟过期保护后刷新)。中优先级通道每 15 分钟刷新一次。
## 技术栈
| 层级 | 技术 |
|---|---|
| HTTP server | Hono + `@hono/node-server` |
| Defender | `@stackone/defender`(进程内) |
| 数据库 | PostgreSQL 17, Drizzle ORM, postgres.js |
| 队列 | BullMQ(基于 Redis) |
| Judge LLM | 通过 `@google/genai` 调用 Gemini 2.5 Pro |
| 缓存 | 通过 ioredis 调用 Redis 7 |
| 校验 | Zod |
| 日志 | Pino |
| 运行时 | Node.js 22+ LTS, TypeScript 6, pnpm |
## 前置条件
- [Node.js](https://nodejs.org/) 22 或更高版本
- [pnpm](https://pnpm.io/)
- [Docker](https://www.docker.com/)(用于 Postgres 和 Redis)
## 快速开始
### 1. 安装依赖
```
pnpm install
```
### 2. 配置环境
```
cp .env.example .env
```
编辑 `.env` 并至少设置 `GEMINI_API_KEY`(队列 worker / Judge LLM 必需)。`SLACK_WEBHOOK_URL` 是可选的 —— 如果未设置,将抑制告警。
### 3. 启动基础设施
```
docker compose up -d
```
这将启动:
- **Postgres 17**,端口为 `localhost:5432`(用户/密码/数据库:`hopwatch`)
- **Redis 7**,端口为 `localhost:6379`
### 4. 运行迁移
```
pnpm db:migrate
```
创建三个表:`detections`、`verdicts` 和 `agent_actions`。
### 5. 启动服务
HopWatch 作为两个 Node 进程运行:
```
# Terminal 1 — HTTP API
pnpm dev
# Terminal 2 — 队列 worker (Judge LLM)
pnpm worker
```
API 默认监听端口 `3000`。
### 6. 验证
```
curl http://localhost:3000/health
# {"status":"ok"}
```
## 环境变量
| 变量 | 默认值 | 描述 |
|---|---|---|
| `DATABASE_URL` | `postgresql://hopwatch:hopwatch@localhost:5432/hopwatch` | Postgres 连接字符串 |
| `REDIS_URL` | `redis://localhost:6379` | Redis 连接字符串 |
| `GEMINI_API_KEY` | — | Judge LLM worker 必需 |
| `SLACK_WEBHOOK_URL` | — | Slack 入站 webhook;如果为空则跳过告警 |
| `TIER2_HIGH_THRESHOLD` | `0.7` | 分数高于此值 → 高优先级队列 + 同步 Defender 告警 |
| `TIER2_MEDIUM_THRESHOLD` | `0.3` | 分数高于此值 → 针对上游跃点标记会话 |
| `CONTENT_ARCHIVE_DIR` | `./data/archive` | 原始内容存档的本地目录 |
| `PORT` | `3000` | HTTP server 端口 |
| `DISABLE_TIER2` | 在 `.env.example` 中为 `true` | 跳过 ML 评分;Tier 1 模式仍会运行,`tier2_score` 为 `null` |
一旦 `onnxruntime-node` 可用,在生产环境中设置 `DISABLE_TIER2=false`。Tier 2 模型预热在服务器启动时进行,首次启动可能需要几秒钟。
## API
### `GET /health`
返回 `{ "status": "ok" }`。
### `POST /analyze`
通过 Defender 筛查外部内容。立即返回净化后的内容;存储和队列写入为“即发即忘”模式。
**请求:**
```
{
"content": "raw external content (any JSON value)",
"agent_id": "my-agent",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"hop": 0,
"source_url": "https://example.com/page",
"source_type": "web"
}
```
`session_id` 必须是 UUID。`hop` 从 0 开始,并在每个 agent 边界递增。
**响应:**
```
{
"sanitized": {},
"tier2_score": 0.12,
"max_sentence": null,
"allowed": true,
"sentences_removed": false,
"detection_id": "uuid"
}
```
### `POST /action`
记录工具调用。始终返回 `200` —— 存储错误会被记录,但永远不会暴露给调用者。
**请求:**
```
{
"tool_name": "sendEmail",
"agent_id": "my-agent",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"hop": 0
}
```
## 进程内装饰器
对于在同一进程中运行的 TypeScript agent,请使用装饰器而不是 HTTP:
```
import { analyzeExternal } from "./interceptor/analyze.js";
import { recordAction } from "./interceptor/action.js";
import { generateSessionId } from "./interceptor/session.js";
const sessionId = generateSessionId();
class MyAgent {
@analyzeExternal({ sessionId, hop: 0, agentId: "agent-1", sourceType: "email" })
async fetchEmail(id: string) {
return await gmailApi.get(id);
}
@recordAction({ sessionId, hop: 0, agentId: "agent-1" })
async sendEmail(args: SendEmailArgs) {
return await gmailApi.send(args);
}
}
```
两个装饰器都调用与 HTTP 路由相同的 pipeline。`@analyzeExternal` 返回净化的内容(并将 `_detection_id` 附加到对象结果中)。`@recordAction` 从方法名中推导出 `tool_name`,并在执行前记录。
## 会话模型
- **`session_id`** —— 贯穿链中所有 agent 的 UUID。由调用者生成一次(`generateSessionId()`);从不在处理程序内部自动生成。
- **`hop`** —— 从 0 开始的整数,在每个 agent 边界递增。调用者负责将这两个值贯穿整个链。
结合 `agent_actions`,这使得可以针对每个会话重建完整的因果链。
## 告警
两条 Slack 告警路径(通过 webhook):
| 路径 | 触发条件 | 时机 | 严重程度 |
|---|---|---|---|
| Defender | `tier2_score > TIER2_HIGH_THRESHOLD` | 在 `/analyze` 期间同步 | 高 |
| Judge | 判定为 `injection`,或 `novelty_flag = true` | 写入判定后异步 | 严重 / 中等 |
## 项目结构
```
src/
├── index.ts # HTTP server entry point
├── db/ # Drizzle schema and client
├── interceptor/ # Core pipelines, decorators, Defender wrapper
├── routes/ # Hono route handlers (/analyze, /action)
├── queue/ # BullMQ client, router, worker
├── judge/ # Gemini Judge LLM invocation
├── alerting/ # Slack alert builders
└── lib/ # Logger, Redis client
```
## 脚本
| 命令 | 描述 |
|---|---|
| `pnpm dev` | 启动带有热重载的 HTTP server |
| `pnpm worker` | 启动队列 worker(Judge LLM) |
| `pnpm build` | 编译 TypeScript |
| `pnpm test` | 运行测试 (Vitest) |
| `pnpm db:generate` | 从 schema 变更生成 Drizzle 迁移 |
| `pnpm db:migrate` | 应用待处理的迁移 |
## 故障排除
**Docker 连接被拒绝** —— 启动 Docker Desktop,然后运行 `docker compose up -d`。
**数据库连接错误** —— 在运行 `pnpm db:migrate` 之前,请等待 `docker compose up` 执行完毕后的几秒钟。
**Tier 2 / ONNX 错误** —— 设置 `DISABLE_TIER2=true`。Tier 1 模式匹配仍会运行。
**Judge 从未运行** —— 确认 `pnpm worker` 正在运行且已设置 `GEMINI_API_KEY`。中优先级队列上的低风险检测每 15 分钟批量处理一次。
**无 Slack 告警** —— 如果未设置 `SLACK_WEBHOOK_URL`,这是预期行为。
## 许可证
ISC
标签:BullMQ, MITM代理, PostgreSQL, 提示词注入检测, 搜索引擎查询, 测试用例, 自动化攻击, 请求拦截, 零日漏洞检测