@roxy-agent/agents 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/dist/approvals.js +143 -0
- package/dist/approvals.js.map +1 -0
- package/dist/classifier.js +436 -0
- package/dist/classifier.js.map +1 -0
- package/dist/dashboard/client.js +2057 -0
- package/dist/dashboard/client.js.map +1 -0
- package/dist/dashboard/html.js +57 -0
- package/dist/dashboard/html.js.map +1 -0
- package/dist/dashboard/icons.js +18 -0
- package/dist/dashboard/icons.js.map +1 -0
- package/dist/dashboard/server.js +423 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/dashboard/styles.js +1685 -0
- package/dist/dashboard/styles.js.map +1 -0
- package/dist/dashboard.js +2 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/db.js +526 -0
- package/dist/db.js.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/license.js +257 -0
- package/dist/license.js.map +1 -0
- package/dist/logger.js +44 -0
- package/dist/logger.js.map +1 -0
- package/dist/ml/bash-classifier.js +121 -0
- package/dist/ml/bash-classifier.js.map +1 -0
- package/dist/ml/embedder.js +79 -0
- package/dist/ml/embedder.js.map +1 -0
- package/dist/ml/prototypes.js +707 -0
- package/dist/ml/prototypes.js.map +1 -0
- package/dist/policies.js +289 -0
- package/dist/policies.js.map +1 -0
- package/dist/slack.js +149 -0
- package/dist/slack.js.map +1 -0
- package/dist/tools/bash.js +134 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/conversation.js +36 -0
- package/dist/tools/conversation.js.map +1 -0
- package/dist/tools/filesystem.js +243 -0
- package/dist/tools/filesystem.js.map +1 -0
- package/dist/tools/introspect.js +187 -0
- package/dist/tools/introspect.js.map +1 -0
- package/dist/tools/network.js +152 -0
- package/dist/tools/network.js.map +1 -0
- package/dist/tools/policies.js +107 -0
- package/dist/tools/policies.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Wang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# @roxy-agent/agents
|
|
2
|
+
|
|
3
|
+
An MCP server that proxies all agent actions — bash, filesystem, network — through a risk classifier and a local SQLite audit log, with a live web dashboard at `http://localhost:4242`.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
Add this to your MCP config (`~/.cursor/mcp.json` for Cursor, `~/Library/Application Support/Claude/claude_desktop_config.json` for Claude Desktop, the equivalent for Codex):
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"agent-proxy": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "@roxy-agent/agents"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then reload MCP servers (or restart the host). On first launch `npx` downloads the package, the SQLite audit DB is created at `~/.agent-proxy/audit.db`, the dashboard starts at [`http://localhost:4242`](http://localhost:4242), and the ML model (~25 MB) downloads to `~/.agent-proxy/models/` in the background.
|
|
21
|
+
|
|
22
|
+
No clone, no global install, no path configuration required.
|
|
23
|
+
|
|
24
|
+
## What it does
|
|
25
|
+
|
|
26
|
+
Every tool call is:
|
|
27
|
+
|
|
28
|
+
1. **Classified** — assigned a risk level (`low` / `medium` / `high`) and a decision (`allowed` / `flagged` / `denied`).
|
|
29
|
+
2. **Logged** — written to `data/audit.db` before execution, then updated with the result, duration, and error.
|
|
30
|
+
3. **Mirrored** — printed to stderr and surfaced in the live dashboard so you can see what the agent is doing in real time.
|
|
31
|
+
|
|
32
|
+
Hard‑deny patterns (e.g. `rm -rf /`, `curl … | bash`, `mkfs.*`) are never executed — the call returns immediately with `blocked: true`.
|
|
33
|
+
|
|
34
|
+
## Bash classifier — rules + ML
|
|
35
|
+
|
|
36
|
+
Bash commands go through a two-stage classifier:
|
|
37
|
+
|
|
38
|
+
1. **Regex hard-deny pre-filter** (deterministic, ~µs). Patterns like `rm -rf /`,
|
|
39
|
+
fork bombs, `dd of=/dev/…`, and `curl … | bash` are *always* denied. ML never
|
|
40
|
+
sees them and never has the chance to downgrade them.
|
|
41
|
+
2. **Embedding-based k-NN classifier** (real ML, fully local). Commands are
|
|
42
|
+
embedded with [`Xenova/all-MiniLM-L6-v2`](https://huggingface.co/Xenova/all-MiniLM-L6-v2)
|
|
43
|
+
(~25 MB, quantized ONNX) running in-process via
|
|
44
|
+
[`@huggingface/transformers`](https://github.com/huggingface/transformers.js)
|
|
45
|
+
and scored against ~110 labeled prototype commands in
|
|
46
|
+
[`src/ml/prototypes.ts`](src/ml/prototypes.ts). The model and prototypes
|
|
47
|
+
load in the background; calls that arrive before it's ready transparently
|
|
48
|
+
fall back to the rule-based classifier.
|
|
49
|
+
|
|
50
|
+
The ML output and the rule output are then combined by taking **whichever is
|
|
51
|
+
more conservative**, so the model can escalate but never silently downgrade.
|
|
52
|
+
|
|
53
|
+
This catches commands that look benign character-by-character but are
|
|
54
|
+
clearly destructive in intent. For example:
|
|
55
|
+
|
|
56
|
+
| Command | Rules | ML |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `please nuke every file in this folder` | low / allowed | **high / flagged** (≈ "wipe everything in this folder") |
|
|
59
|
+
| `recursively obliterate node_modules` | low / allowed | **high / flagged** (≈ "rm -rf node_modules") |
|
|
60
|
+
| `publish this package to npm` | low / allowed | **high / flagged** (≈ "npm publish") |
|
|
61
|
+
| `drop all rows from the users table` | low / allowed | **high / flagged** (≈ "TRUNCATE TABLE users") |
|
|
62
|
+
| `wipe the entire git history and force push` | low / allowed | **high / flagged** (≈ "git push --force") |
|
|
63
|
+
|
|
64
|
+
The model files are cached in `data/models/` after first run. To disable ML
|
|
65
|
+
entirely (and use only the regex rules), set `AGENT_PROXY_ML=0`.
|
|
66
|
+
|
|
67
|
+
Try it:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm run build
|
|
71
|
+
node scripts/ml-smoke.mjs
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Policies — natural-language allow / block rules
|
|
75
|
+
|
|
76
|
+
Teams can attach an arbitrary number of allow / block policies through the
|
|
77
|
+
dashboard. Policies are written in plain English, embedded with the same
|
|
78
|
+
sentence-transformer used by the bash classifier, and matched against every
|
|
79
|
+
incoming bash / filesystem / network action via cosine similarity.
|
|
80
|
+
|
|
81
|
+
Precedence inside the classifier:
|
|
82
|
+
|
|
83
|
+
1. **Regex hard-deny** (`rm -rf /`, `curl … | bash`, etc.) — never overridable.
|
|
84
|
+
2. **BLOCK policy** match — denies the call.
|
|
85
|
+
3. **ALLOW policy** match — forces low / allowed (de-escalates flagged actions).
|
|
86
|
+
4. **Rules + ML** (the existing combined classifier).
|
|
87
|
+
|
|
88
|
+
Examples that work today:
|
|
89
|
+
|
|
90
|
+
| Policy | What it does |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `"Never destroy infrastructure with terraform or pulumi."` | Blocks `terraform destroy -auto-approve` (sim ≈ 0.69). |
|
|
93
|
+
| `"Always allow git status, git log, git diff, and git branch."` | De-escalates inspection-only git commands (sim ≈ 0.69). |
|
|
94
|
+
| `"Block any kubectl command."` | Blocks `kubectl get pods -n production` (sim ≈ 0.49). |
|
|
95
|
+
| `"It's fine to read any file inside the project's data folder."` | Allows reads of `data/**`. |
|
|
96
|
+
|
|
97
|
+
Tips for writing good policies:
|
|
98
|
+
|
|
99
|
+
- **Be focused.** "Block all AWS commands" matches `aws s3 ls` better than
|
|
100
|
+
"Never run any AWS or kubectl commands against production." — fewer
|
|
101
|
+
concepts per policy keeps the embedding tight.
|
|
102
|
+
- **Use the dashboard tester.** Type a candidate command and watch the live
|
|
103
|
+
similarity scores update. Anything ≥ 0.40 by default will fire.
|
|
104
|
+
- **Tune the threshold.** Set `AGENT_PROXY_POLICY_THRESHOLD=0.35` to be
|
|
105
|
+
more permissive, or `0.55` to require very tight matches.
|
|
106
|
+
|
|
107
|
+
REST API (also used by the dashboard):
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
GET /api/policies # list all
|
|
111
|
+
POST /api/policies # { kind, description, applies_to?, scope? }
|
|
112
|
+
PATCH /api/policies/:id # partial update — re-embeds if description changes
|
|
113
|
+
DELETE /api/policies/:id
|
|
114
|
+
POST /api/policies/test # { text, tool } → top-N similarities (no side effects)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
To disable policy matching entirely set `AGENT_PROXY_POLICIES=0`. To scope
|
|
118
|
+
policies (e.g. one Cursor session is "team:eng" while another is "team:sec"),
|
|
119
|
+
set `AGENT_PROXY_SCOPES=team:eng,global` — only policies whose scope is in
|
|
120
|
+
that set will be evaluated.
|
|
121
|
+
|
|
122
|
+
## Tools exposed over MCP
|
|
123
|
+
|
|
124
|
+
**Side-effect tools** — every call is classified and audited:
|
|
125
|
+
|
|
126
|
+
| Tool | Purpose |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `bash` | Run a shell command. Classified per command. |
|
|
129
|
+
| `read_file` | Read a UTF‑8 file. Sensitive paths flagged. |
|
|
130
|
+
| `write_file` | Write/append a file. Writes to sensitive paths denied. |
|
|
131
|
+
| `delete_file` | Delete a file. Always flagged. |
|
|
132
|
+
| `list_directory` | List a directory. |
|
|
133
|
+
| `fetch_url` | Make an HTTP(S) request. External hosts flagged, executable script downloads denied. |
|
|
134
|
+
|
|
135
|
+
**Introspection tools** — read-only, no audit entry, designed for the agent to plan and self-audit:
|
|
136
|
+
|
|
137
|
+
| Tool | Purpose |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| `classify` | Pre-flight risk check. Returns the decision (`allowed`/`flagged`/`denied`) the proxy *would* return for a hypothetical bash command, filesystem path, or URL — without executing it. Use this before committing to a destructive op. |
|
|
140
|
+
| `recent_events` | Read the audit log. Filter by tool, decision, or session. Pass `session_id: "current"` to summarize what the agent has done so far. |
|
|
141
|
+
| `proxy_stats` | Heartbeat / aggregate counters (totals, ML readiness, current session, dashboard URL). |
|
|
142
|
+
|
|
143
|
+
**Policy management tools** — persist user-stated guardrails across sessions:
|
|
144
|
+
|
|
145
|
+
| Tool | Purpose |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| `add_policy` | Create a natural-language allow or block rule. Use when the user says "never push to main" or "always allow npm install in this repo" — the description is embedded and matched semantically against future calls. |
|
|
148
|
+
| `list_policies` | List every persisted policy. With `against: "<text>"` it scores all policies against a hypothetical action and returns them sorted by similarity, so the agent can see which rule will fire and at what threshold. |
|
|
149
|
+
| `update_policy` | Modify an existing policy by id (toggle `enabled`, retighten the description, scope it to a tool, etc.). |
|
|
150
|
+
| `delete_policy` | Delete a policy by id. Prefer disabling via `update_policy` so it can be re-enabled later. |
|
|
151
|
+
|
|
152
|
+
The agent is encouraged to use `classify` proactively whenever it's about to run a command that *might* be destructive or surprising. Pre-flight is much cheaper than rolling back a denied tool call mid-task.
|
|
153
|
+
|
|
154
|
+
## Run from source (for local development)
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
git clone https://github.com/<you>/agent-proxy.git
|
|
158
|
+
cd agent-proxy
|
|
159
|
+
npm install
|
|
160
|
+
npm run build
|
|
161
|
+
npm start
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Or for development with auto-reload:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npm run dev
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The dashboard starts at `http://localhost:4242` (override with `AGENT_PROXY_PORT`). When run from a source checkout, the audit DB and model cache stay in `<repo>/data/` to keep your dev install separate from the user-level install at `~/.agent-proxy/`.
|
|
171
|
+
|
|
172
|
+
The dashboard is a four-tab single-page app:
|
|
173
|
+
|
|
174
|
+
- **Overview** — today's KPIs, 24h sparkline, recent activity, top firing policies
|
|
175
|
+
- **Activity** — full event log with search, tool/decision filters, time range, and a drawer that shows the full payload, classifier reasoning, and adjacent session events
|
|
176
|
+
- **Policies** — manage natural-language allow/block rules; inline tester scores any command against every policy with a similarity bar
|
|
177
|
+
- **Insights** — stacked area chart of allowed/flagged/denied per hour, tool & decision distributions, ML model stats, top denied/flagged actions, recent sessions
|
|
178
|
+
|
|
179
|
+
Set in [Geist Sans + Geist Mono + Geist Pixel](https://vercel.com/font), self-hosted from the `geist` npm package — no Google Fonts dependency, works offline. Includes a `⌘K` command palette and `g o`/`g a`/`g p`/`g i` keyboard navigation.
|
|
180
|
+
|
|
181
|
+
## MCP config — alternatives
|
|
182
|
+
|
|
183
|
+
The recommended config is `npx -y @roxy-agent/agents` (see Quick start above). You can also point your MCP host at a local checkout:
|
|
184
|
+
|
|
185
|
+
**Built from source:**
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"mcpServers": {
|
|
190
|
+
"agent-proxy": {
|
|
191
|
+
"command": "node",
|
|
192
|
+
"args": ["/absolute/path/to/agent-proxy/dist/index.js"]
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Source mode (no build step), useful while iterating:**
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"mcpServers": {
|
|
203
|
+
"agent-proxy": {
|
|
204
|
+
"command": "npx",
|
|
205
|
+
"args": ["tsx", "/absolute/path/to/agent-proxy/src/index.ts"]
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Pin a specific version** (recommended for teams to avoid surprise updates):
|
|
212
|
+
|
|
213
|
+
```json
|
|
214
|
+
{
|
|
215
|
+
"mcpServers": {
|
|
216
|
+
"agent-proxy": {
|
|
217
|
+
"command": "npx",
|
|
218
|
+
"args": ["-y", "@roxy-agent/agents@0.1.0"]
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Project rules snippet
|
|
225
|
+
|
|
226
|
+
Drop this into a project's `AGENTS.md` or `.cursor/rules/` to nudge the agent toward the proxy:
|
|
227
|
+
|
|
228
|
+
```markdown
|
|
229
|
+
You have access to an `agent-proxy` MCP server. Use it for all side effects:
|
|
230
|
+
|
|
231
|
+
- Use the `bash` tool from agent-proxy instead of running shell commands directly.
|
|
232
|
+
- Use `read_file`, `write_file`, `delete_file`, `list_directory` instead of direct file ops.
|
|
233
|
+
- Use `fetch_url` for any HTTP requests.
|
|
234
|
+
|
|
235
|
+
All actions are logged. The audit dashboard is at http://localhost:4242.
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Environment variables
|
|
239
|
+
|
|
240
|
+
| Variable | Default | Purpose |
|
|
241
|
+
| --- | --- | --- |
|
|
242
|
+
| `AGENT_PROXY_PORT` | `4242` | Dashboard HTTP port. |
|
|
243
|
+
| `AGENT_PROXY_DATA_DIR` | `~/.agent-proxy` | Where the audit DB, license, and model cache live. Falls back to `<repo>/data/` if you launched from a source checkout that already has one. |
|
|
244
|
+
| `AGENT_PROXY_SESSION_ID` | random UUID | Override the session id (handy for per‑task traceability). |
|
|
245
|
+
| `AGENT_PROXY_DISABLE_DASHBOARD` | unset | Set to `1` to skip starting the dashboard. |
|
|
246
|
+
| `AGENT_PROXY_ML` | enabled | Set to `0` to disable the embedding-based bash classifier and use rules only. |
|
|
247
|
+
| `AGENT_PROXY_ML_MODEL` | `Xenova/all-MiniLM-L6-v2` | Hugging Face model id used for the bash classifier embedder. |
|
|
248
|
+
| `AGENT_PROXY_ML_CACHE` | `~/.agent-proxy/models` | Where transformers.js caches model files. |
|
|
249
|
+
| `AGENT_PROXY_POLICIES` | enabled | Set to `0` to disable natural-language policies. |
|
|
250
|
+
| `AGENT_PROXY_POLICY_THRESHOLD` | `0.40` | Minimum cosine similarity for a policy to match. |
|
|
251
|
+
| `AGENT_PROXY_SCOPES` | `global` | Comma-separated list of scopes whose policies are active in this session. |
|
|
252
|
+
|
|
253
|
+
## Layout
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
src/
|
|
257
|
+
index.ts # MCP server entry point
|
|
258
|
+
db.ts # SQLite + queries
|
|
259
|
+
classifier.ts # rule-based + ML + policy combined classifier
|
|
260
|
+
policies.ts # natural-language allow/block policies (CRUD + matching)
|
|
261
|
+
logger.ts # logEvent + updateEventResult, mirrors to stderr
|
|
262
|
+
dashboard.ts # Express dashboard (HTML + JSON API)
|
|
263
|
+
tools/
|
|
264
|
+
bash.ts
|
|
265
|
+
filesystem.ts
|
|
266
|
+
network.ts
|
|
267
|
+
ml/
|
|
268
|
+
embedder.ts # transformers.js feature-extraction pipeline
|
|
269
|
+
prototypes.ts # labeled bash command prototypes
|
|
270
|
+
bash-classifier.ts # k-NN classifier over embedded prototypes
|
|
271
|
+
data/
|
|
272
|
+
audit.db # auto-created (events + policies tables)
|
|
273
|
+
models/ # cached ONNX model files
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Audit DB schema
|
|
277
|
+
|
|
278
|
+
```sql
|
|
279
|
+
CREATE TABLE events (
|
|
280
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
281
|
+
timestamp TEXT NOT NULL,
|
|
282
|
+
session_id TEXT NOT NULL,
|
|
283
|
+
tool TEXT NOT NULL,
|
|
284
|
+
action_type TEXT NOT NULL,
|
|
285
|
+
payload TEXT NOT NULL, -- JSON
|
|
286
|
+
risk_level TEXT NOT NULL, -- low|medium|high
|
|
287
|
+
decision TEXT NOT NULL, -- allowed|denied|flagged
|
|
288
|
+
result TEXT, -- summary string
|
|
289
|
+
duration_ms INTEGER,
|
|
290
|
+
error TEXT,
|
|
291
|
+
reason TEXT -- classifier reason
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
CREATE TABLE policies (
|
|
295
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
296
|
+
kind TEXT NOT NULL, -- allow|block
|
|
297
|
+
description TEXT NOT NULL, -- natural language
|
|
298
|
+
scope TEXT NOT NULL DEFAULT 'global',
|
|
299
|
+
applies_to TEXT NOT NULL DEFAULT '*', -- bash|filesystem|network|*
|
|
300
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
301
|
+
match_count INTEGER NOT NULL DEFAULT 0,
|
|
302
|
+
embedding BLOB, -- 384-dim float32, lazily filled
|
|
303
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
304
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
305
|
+
);
|
|
306
|
+
```
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Human-in-the-loop approval orchestrator.
|
|
2
|
+
//
|
|
3
|
+
// When the classifier returns `pending_approval`, the calling tool blocks here
|
|
4
|
+
// until a reviewer approves or denies the action via the dashboard, Slack, or
|
|
5
|
+
// the configured timeout fires. Pending state is persisted to SQLite so the
|
|
6
|
+
// dashboard can list it; resolution is signaled in-process via a Promise map.
|
|
7
|
+
//
|
|
8
|
+
// IMPORTANT: pending approvals do NOT survive a server restart (the awaiting
|
|
9
|
+
// promise dies with the process). On boot, `expireOrphanApprovals` marks any
|
|
10
|
+
// stale rows as `timeout` so the dashboard reflects reality.
|
|
11
|
+
import { createApprovalRow, setApprovalStatus, getApproval, countPendingApprovals, expireOrphanApprovals, updateEventDecision, } from "./db.js";
|
|
12
|
+
const pending = new Map();
|
|
13
|
+
function approvalTimeoutMs() {
|
|
14
|
+
const env = Number(process.env.AGENT_PROXY_APPROVAL_TIMEOUT_MS);
|
|
15
|
+
if (Number.isFinite(env) && env > 0)
|
|
16
|
+
return env;
|
|
17
|
+
return 5 * 60 * 1000;
|
|
18
|
+
}
|
|
19
|
+
export function initApprovals() {
|
|
20
|
+
const expired = expireOrphanApprovals();
|
|
21
|
+
if (expired > 0) {
|
|
22
|
+
process.stderr.write(`[agent-proxy] expired ${expired} orphan approval(s) from previous run\n`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function pendingCount() {
|
|
26
|
+
return countPendingApprovals();
|
|
27
|
+
}
|
|
28
|
+
// Block the calling tool until a reviewer (dashboard / Slack) decides.
|
|
29
|
+
// Resolves with `{ approved: true }` to let the tool proceed, or
|
|
30
|
+
// `{ approved: false }` if denied or timed out — tool should treat it as
|
|
31
|
+
// blocked and surface the reason to the agent.
|
|
32
|
+
export async function awaitApproval(input) {
|
|
33
|
+
const id = createApprovalRow({
|
|
34
|
+
event_id: input.event_id ?? null,
|
|
35
|
+
tool: input.tool,
|
|
36
|
+
summary: input.summary,
|
|
37
|
+
reason: input.reason,
|
|
38
|
+
payload: input.payload,
|
|
39
|
+
session_id: input.session_id ?? null,
|
|
40
|
+
});
|
|
41
|
+
// Notify Slack best-effort (don't block on it).
|
|
42
|
+
void (async () => {
|
|
43
|
+
try {
|
|
44
|
+
const { notifyApprovalRequest } = await import("./slack.js");
|
|
45
|
+
const row = getApproval(id);
|
|
46
|
+
if (row)
|
|
47
|
+
await notifyApprovalRequest(row);
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
})();
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
if (!pending.has(id))
|
|
54
|
+
return;
|
|
55
|
+
pending.delete(id);
|
|
56
|
+
clearInterval(poller);
|
|
57
|
+
const ok = setApprovalStatus(id, "timeout", "timeout");
|
|
58
|
+
if (!ok)
|
|
59
|
+
return;
|
|
60
|
+
try {
|
|
61
|
+
updateEventDecision(input.event_id, "denied", `approval timed out after ${Math.round(approvalTimeoutMs() / 1000)}s`);
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
resolve({
|
|
65
|
+
approved: false,
|
|
66
|
+
status: "timeout",
|
|
67
|
+
by: "timeout",
|
|
68
|
+
reason: `approval timed out after ${Math.round(approvalTimeoutMs() / 1000)}s`,
|
|
69
|
+
});
|
|
70
|
+
}, approvalTimeoutMs());
|
|
71
|
+
// Cross-process fallback: if some other process (e.g. a separately-running
|
|
72
|
+
// dashboard) resolved the approval directly in the DB, the in-memory
|
|
73
|
+
// resolve() above is never called. Poll the row every 500ms and resolve
|
|
74
|
+
// ourselves if we see a non-pending status.
|
|
75
|
+
const poller = setInterval(() => {
|
|
76
|
+
const row = getApproval(id);
|
|
77
|
+
if (!row || row.status === "pending")
|
|
78
|
+
return;
|
|
79
|
+
const entry = pending.get(id);
|
|
80
|
+
if (!entry)
|
|
81
|
+
return;
|
|
82
|
+
pending.delete(id);
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
clearInterval(poller);
|
|
85
|
+
const status = row.status;
|
|
86
|
+
const by = row.decided_by ?? "unknown";
|
|
87
|
+
try {
|
|
88
|
+
updateEventDecision(input.event_id, status === "approved" ? "allowed" : "denied", status === "approved" ? `approved by ${by}` : `${status} by ${by}`);
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
resolve({
|
|
92
|
+
approved: status === "approved",
|
|
93
|
+
status,
|
|
94
|
+
by,
|
|
95
|
+
reason: status === "approved" ? `approved by ${by}` : `${status} by ${by}`,
|
|
96
|
+
});
|
|
97
|
+
}, 500);
|
|
98
|
+
pending.set(id, { timer, poller, input, resolve });
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Called by the dashboard / Slack receiver to resolve a pending approval.
|
|
102
|
+
// Returns the updated row, or undefined if the id was unknown / already
|
|
103
|
+
// resolved.
|
|
104
|
+
export function decideApproval(id, decision, by) {
|
|
105
|
+
const row = getApproval(id);
|
|
106
|
+
if (!row)
|
|
107
|
+
return undefined;
|
|
108
|
+
if (row.status !== "pending")
|
|
109
|
+
return row;
|
|
110
|
+
const ok = setApprovalStatus(id, decision, by);
|
|
111
|
+
if (!ok)
|
|
112
|
+
return getApproval(id);
|
|
113
|
+
const entry = pending.get(id);
|
|
114
|
+
if (entry) {
|
|
115
|
+
clearTimeout(entry.timer);
|
|
116
|
+
if (entry.poller)
|
|
117
|
+
clearInterval(entry.poller);
|
|
118
|
+
pending.delete(id);
|
|
119
|
+
try {
|
|
120
|
+
updateEventDecision(entry.input.event_id, decision === "approved" ? "allowed" : "denied", decision === "approved"
|
|
121
|
+
? `approved by ${by}`
|
|
122
|
+
: `denied by ${by}`);
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
125
|
+
entry.resolve({
|
|
126
|
+
approved: decision === "approved",
|
|
127
|
+
status: decision,
|
|
128
|
+
by,
|
|
129
|
+
reason: decision === "approved" ? `approved by ${by}` : `denied by ${by}`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
else if (row.event_id) {
|
|
133
|
+
// No in-process awaiter (e.g. cross-process resolution after a restart).
|
|
134
|
+
try {
|
|
135
|
+
updateEventDecision(row.event_id, decision === "approved" ? "allowed" : "denied", decision === "approved"
|
|
136
|
+
? `approved by ${by}`
|
|
137
|
+
: `denied by ${by}`);
|
|
138
|
+
}
|
|
139
|
+
catch { }
|
|
140
|
+
}
|
|
141
|
+
return getApproval(id);
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=approvals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approvals.js","sourceRoot":"","sources":["../src/approvals.ts"],"names":[],"mappings":"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"}
|