Package Exports
- @roxy-agent/agents
- @roxy-agent/agents/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@roxy-agent/agents) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
@roxy-agent/agents
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.
Quick start
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):
{
"mcpServers": {
"agent-proxy": {
"command": "npx",
"args": ["-y", "@roxy-agent/agents"]
}
}
}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, and the ML model (25 MB) downloads to `/.agent-proxy/models/` in the background.
No clone, no global install, no path configuration required.
What it does
Every tool call is:
- Classified — assigned a risk level (
low/medium/high) and a decision (allowed/flagged/denied). - Logged — written to
data/audit.dbbefore execution, then updated with the result, duration, and error. - Mirrored — printed to stderr and surfaced in the live dashboard so you can see what the agent is doing in real time.
Hard‑deny patterns (e.g. rm -rf /, curl … | bash, mkfs.*) are never executed — the call returns immediately with blocked: true.
Bash classifier — rules + ML
Bash commands go through a two-stage classifier:
- Regex hard-deny pre-filter (deterministic, ~µs). Patterns like
rm -rf /, fork bombs,dd of=/dev/…, andcurl … | bashare always denied. ML never sees them and never has the chance to downgrade them. - Embedding-based k-NN classifier (real ML, fully local). Commands are
embedded with
Xenova/all-MiniLM-L6-v2(~25 MB, quantized ONNX) running in-process via@huggingface/transformersand scored against ~110 labeled prototype commands insrc/ml/prototypes.ts. The model and prototypes load in the background; calls that arrive before it's ready transparently fall back to the rule-based classifier.
The ML output and the rule output are then combined by taking whichever is more conservative, so the model can escalate but never silently downgrade.
This catches commands that look benign character-by-character but are clearly destructive in intent. For example:
| Command | Rules | ML |
|---|---|---|
please nuke every file in this folder |
low / allowed | high / flagged (≈ "wipe everything in this folder") |
recursively obliterate node_modules |
low / allowed | high / flagged (≈ "rm -rf node_modules") |
publish this package to npm |
low / allowed | high / flagged (≈ "npm publish") |
drop all rows from the users table |
low / allowed | high / flagged (≈ "TRUNCATE TABLE users") |
wipe the entire git history and force push |
low / allowed | high / flagged (≈ "git push --force") |
The model files are cached in data/models/ after first run. To disable ML
entirely (and use only the regex rules), set AGENT_PROXY_ML=0.
Try it:
npm run build
node scripts/ml-smoke.mjsPolicies — natural-language allow / block rules
Teams can attach an arbitrary number of allow / block policies through the dashboard. Policies are written in plain English, embedded with the same sentence-transformer used by the bash classifier, and matched against every incoming bash / filesystem / network action via cosine similarity.
Precedence inside the classifier:
- Regex hard-deny (
rm -rf /,curl … | bash, etc.) — never overridable. - BLOCK policy match — denies the call.
- ALLOW policy match — forces low / allowed (de-escalates flagged actions).
- Rules + ML (the existing combined classifier).
Examples that work today:
| Policy | What it does |
|---|---|
"Never destroy infrastructure with terraform or pulumi." |
Blocks terraform destroy -auto-approve (sim ≈ 0.69). |
"Always allow git status, git log, git diff, and git branch." |
De-escalates inspection-only git commands (sim ≈ 0.69). |
"Block any kubectl command." |
Blocks kubectl get pods -n production (sim ≈ 0.49). |
"It's fine to read any file inside the project's data folder." |
Allows reads of data/**. |
Tips for writing good policies:
- Be focused. "Block all AWS commands" matches
aws s3 lsbetter than "Never run any AWS or kubectl commands against production." — fewer concepts per policy keeps the embedding tight. - Use the dashboard tester. Type a candidate command and watch the live similarity scores update. Anything ≥ 0.40 by default will fire.
- Tune the threshold. Set
AGENT_PROXY_POLICY_THRESHOLD=0.35to be more permissive, or0.55to require very tight matches.
REST API (also used by the dashboard):
GET /api/policies # list all
POST /api/policies # { kind, description, applies_to?, scope? }
PATCH /api/policies/:id # partial update — re-embeds if description changes
DELETE /api/policies/:id
POST /api/policies/test # { text, tool } → top-N similarities (no side effects)To disable policy matching entirely set AGENT_PROXY_POLICIES=0. To scope
policies (e.g. one Cursor session is "team:eng" while another is "team:sec"),
set AGENT_PROXY_SCOPES=team:eng,global — only policies whose scope is in
that set will be evaluated.
Tools exposed over MCP
Side-effect tools — every call is classified and audited:
| Tool | Purpose |
|---|---|
bash |
Run a shell command. Classified per command. |
read_file |
Read a UTF‑8 file. Sensitive paths flagged. |
write_file |
Write/append a file. Writes to sensitive paths denied. |
delete_file |
Delete a file. Always flagged. |
list_directory |
List a directory. |
fetch_url |
Make an HTTP(S) request. External hosts flagged, executable script downloads denied. |
Introspection tools — read-only, no audit entry, designed for the agent to plan and self-audit:
| Tool | Purpose |
|---|---|
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. |
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. |
proxy_stats |
Heartbeat / aggregate counters (totals, ML readiness, current session, dashboard URL). |
Policy management tools — persist user-stated guardrails across sessions:
| Tool | Purpose |
|---|---|
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. |
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. |
update_policy |
Modify an existing policy by id (toggle enabled, retighten the description, scope it to a tool, etc.). |
delete_policy |
Delete a policy by id. Prefer disabling via update_policy so it can be re-enabled later. |
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.
Run from source (for local development)
git clone https://github.com/<you>/agent-proxy.git
cd agent-proxy
npm install
npm run build
npm startOr for development with auto-reload:
npm run devThe 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/.
The dashboard is a four-tab single-page app:
- Overview — today's KPIs, 24h sparkline, recent activity, top firing policies
- 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
- Policies — manage natural-language allow/block rules; inline tester scores any command against every policy with a similarity bar
- Insights — stacked area chart of allowed/flagged/denied per hour, tool & decision distributions, ML model stats, top denied/flagged actions, recent sessions
Set in Geist Sans + Geist Mono + Geist Pixel, 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.
MCP config — alternatives
The recommended config is npx -y @roxy-agent/agents (see Quick start above). You can also point your MCP host at a local checkout:
Built from source:
{
"mcpServers": {
"agent-proxy": {
"command": "node",
"args": ["/absolute/path/to/agent-proxy/dist/index.js"]
}
}
}Source mode (no build step), useful while iterating:
{
"mcpServers": {
"agent-proxy": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/agent-proxy/src/index.ts"]
}
}
}Pin a specific version (recommended for teams to avoid surprise updates):
{
"mcpServers": {
"agent-proxy": {
"command": "npx",
"args": ["-y", "@roxy-agent/agents@0.1.0"]
}
}
}Project rules snippet
Drop this into a project's AGENTS.md or .cursor/rules/ to nudge the agent toward the proxy:
You have access to an `agent-proxy` MCP server. Use it for all side effects:
- Use the `bash` tool from agent-proxy instead of running shell commands directly.
- Use `read_file`, `write_file`, `delete_file`, `list_directory` instead of direct file ops.
- Use `fetch_url` for any HTTP requests.
All actions are logged. The audit dashboard is at http://localhost:4242.Environment variables
| Variable | Default | Purpose |
|---|---|---|
AGENT_PROXY_PORT |
4242 |
Dashboard HTTP port. |
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. |
AGENT_PROXY_SESSION_ID |
random UUID | Override the session id (handy for per‑task traceability). |
AGENT_PROXY_DISABLE_DASHBOARD |
unset | Set to 1 to skip starting the dashboard. |
AGENT_PROXY_ML |
enabled | Set to 0 to disable the embedding-based bash classifier and use rules only. |
AGENT_PROXY_ML_MODEL |
Xenova/all-MiniLM-L6-v2 |
Hugging Face model id used for the bash classifier embedder. |
AGENT_PROXY_ML_CACHE |
~/.agent-proxy/models |
Where transformers.js caches model files. |
AGENT_PROXY_POLICIES |
enabled | Set to 0 to disable natural-language policies. |
AGENT_PROXY_POLICY_THRESHOLD |
0.40 |
Minimum cosine similarity for a policy to match. |
AGENT_PROXY_SCOPES |
global |
Comma-separated list of scopes whose policies are active in this session. |
Layout
src/
index.ts # MCP server entry point
db.ts # SQLite + queries
classifier.ts # rule-based + ML + policy combined classifier
policies.ts # natural-language allow/block policies (CRUD + matching)
logger.ts # logEvent + updateEventResult, mirrors to stderr
dashboard.ts # Express dashboard (HTML + JSON API)
tools/
bash.ts
filesystem.ts
network.ts
ml/
embedder.ts # transformers.js feature-extraction pipeline
prototypes.ts # labeled bash command prototypes
bash-classifier.ts # k-NN classifier over embedded prototypes
data/
audit.db # auto-created (events + policies tables)
models/ # cached ONNX model filesAudit DB schema
CREATE TABLE events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
session_id TEXT NOT NULL,
tool TEXT NOT NULL,
action_type TEXT NOT NULL,
payload TEXT NOT NULL, -- JSON
risk_level TEXT NOT NULL, -- low|medium|high
decision TEXT NOT NULL, -- allowed|denied|flagged
result TEXT, -- summary string
duration_ms INTEGER,
error TEXT,
reason TEXT -- classifier reason
);
CREATE TABLE policies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL, -- allow|block
description TEXT NOT NULL, -- natural language
scope TEXT NOT NULL DEFAULT 'global',
applies_to TEXT NOT NULL DEFAULT '*', -- bash|filesystem|network|*
enabled INTEGER NOT NULL DEFAULT 1,
match_count INTEGER NOT NULL DEFAULT 0,
embedding BLOB, -- 384-dim float32, lazily filled
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);