# @grovetech/defender

Active runtime protection for vibe-coded apps. Drop-in Express middleware that
blocks prompt injection, sensitive-path probes, and PII / API-key leaks in your
LLM responses — in real time, without touching anyone else's systems.

> Defender is the active counterpart to the [Grovetech Vibe Code Health
> Scanner](https://grovetechai.com/scanner). The scanner is a one-shot pen-test;
> Defender is permanent runtime defence.

## Install

```bash
npm install @grovetech/defender
```

Get an API key at <https://grovetechai.com/dashboard?tab=defender>. Free plan
includes 1 000 requests/month with the input guard enabled.

> **Try it without installing anything** —
> [open the live demo on Replit](https://replit.com/github/grovetechai/defender-example)
> or read the source in
> [`server.ts`](https://github.com/grovetechai/defender-example/blob/main/server.ts).

## Quick start (Express)

```ts
import express from "express";
import { defender } from "@grovetech/defender";

const app = express();
app.use(express.json());

const d = defender({ apiKey: process.env.GROVETECH_DEFENDER_KEY });

// Web layer — security headers + sensitive-path block (.env, .git/*, …)
app.use(d.web());

// AI layer — screens req.body.prompt / .messages for prompt injection
app.use("/api/chat", d.ai());

// Output guard — wrap your own LLM call
app.post("/api/chat", async (req, res) => {
  const reply = await callOpenAi(req.body.prompt);
  const out = d.guardOutput(reply);
  if (!out.allowed) return res.status(502).json({ error: "Output blocked" });
  res.json({ reply });
});

app.listen(3000);
```

## Build-time secret check

Add to `package.json`:

```json
{ "scripts": { "postbuild": "defender check ./dist" } }
```

The build fails if any leaked OpenAI / Anthropic / AWS / Stripe key, GitHub
PAT or PEM private key is found in the emitted bundle.

## What gets blocked

| Layer    | Examples                                                       |
| -------- | -------------------------------------------------------------- |
| Web      | `/.env`, `/.git/config`, `/wp-config.php`, missing CSP/HSTS     |
| AI input | "ignore previous instructions", DAN/jailbreak, token bombs      |
| AI output| Leaked `sk-…`, `AKIA…`, `ghp_…`, PEM keys, system-prompt leak   |

Detection patterns are recycled from the same engine that powers our hosted
scanner (`server/security-scan.ts`, `server/vibe-coding-scan.ts`,
`server/ai-agent/attacks.json`) so what we test for in audits is what we block
at runtime.

## AI conversation defense (new in v2)

Three opt-in helpers for chatbot and RAG apps. None of them auto-mount —
call them where you want the protection.

```ts
// 1. Multi-turn jailbreak + per-session rate limit
app.use("/api/chat", d.conversationGuard({
  sessionIdFrom: (req) => req.body.sessionId,
  // defaults: 20 msg/min, 50k tokens/h, window 10, threshold 6.0
}));

// 2. RAG poisoning guard — invisible Unicode, embedded directives,
//    markdown image exfil, base64 smuggling. Two hooks:
const rag = d.rag();

app.post("/api/docs", async (req, res) => {
  const r = await rag.inspectIngest({ id, source, content });
  if (r.blocked) return res.status(400).json({ findings: r.findings });
  await vectorDb.upsert({ id, source, content: r.sanitizedContent });
});

app.post("/api/chat", async (req, res) => {
  const chunks = await vectorDb.query({ ... });
  const { allowed } = await rag.inspectRetrieval(chunks);
  // pass only `allowed` to your LLM
});

// 3. Behavioral anomaly — z-score on message length + gap between messages
const anomaly = d.anomalyDetector();
const obs = await anomaly.observe(sessionId, { text: req.body.message });
if (obs.suspicious) { /* log, captcha, ban — your choice */ }
```

Full reference, defaults table, rollout playbook and privacy notes:
[`docs/defender/conversation-rag-anomaly.md`](https://github.com/grovetechai/defender/blob/main/docs/defender/conversation-rag-anomaly.md).

Status: **beta** — covered by 599 unit tests (`packages/defender/src/*.test.ts`),
dogfooded on the Viki chat widget at grovetechai.com in audit-only mode. Start
with audit, flip to block after a week of clean logs.

## Tool-call guard (new in 0.4.0)

Deterministic allowlist/denylist for AI agents with tools — decide which
tools the agent may call **before** execution. Glob patterns (`*` = one
segment, trailing `.*` = one or more), deny wins over allow, empty `allow`
= audit mode (nothing blocked, fail-open).

```ts
const guard = d.tool({
  allow: ["search", "db.read.*"],
  deny:  ["db.write.*", "payments.*"],
});

// Before executing a tool call:
const v = guard.check({ tool: "db.write.payments" });
// { allowed: false, rule: "tool_denylist", matched: "db.write.*" }

// Or wrap an OpenAI/Vercel AI SDK-style tools array — disallowed tools are
// removed from the list, allowed ones get a runtime check around execute():
const safeTools = guard.wrap(tools);
```

Violations report to telemetry as layer `tool` (dashboard on Pro+; local
enforcement always works regardless of plan).

## Agent layer (0.6.0 – 0.9.0)

Once the agent can *act*, screening the prompt is no longer enough. These
four additions decide on a single agent step, right before it executes.

**Argument inspection (0.6.0).** An attack rarely looks like
`payments.send`; it looks like an allowed `db.query` carrying
`DROP TABLE`. The guard now inspects what gets passed in, not just the
tool name.

```ts
guard.check({ tool: "db.query", args: { sql: "DROP TABLE users" } });
// { allowed: false, rule: "tool_args", … }
```

**Egress allowlist (0.7.0).** With agents, exfiltration usually leaves
through an HTTP request. `d.egress()` decides on the destination. Internal
ranges and cloud metadata (`169.254.169.254`, `metadata.google.internal`)
are blocked by default.

```ts
const egress = d.egress({ allow: ["api.openai.com", "*.stripe.com"] });
await egress.check("http://169.254.169.254/latest/meta-data/");
// { allowed: false, rule: "egress_internal" }
```

It is a decision function, not a global patch — call it where the outbound
request is actually made, otherwise it protects nothing.

**MCP policy + rug pull (0.8.0).** The verdict of an MCP configuration scan
gets enforced at runtime. `d.mcpPolicy()` pulls the policy on its own (ETag,
15-minute interval, on-disk cache for cold start); unknown servers stay
fail-open, `dangerous` is blocked. Pass `definition` and a server that
rewrote its tool description after approval is blocked too — **even if the
scan marked it safe**, which is exactly what a rug pull looks like.

```ts
const policy = d.mcpPolicy();
const guard  = d.tool({ mcp: () => policy.current() });   // function, not object:
                                                          // a fresh scan applies
                                                          // without a restart
```

**Destructive action budget (0.9.0).** A cap on the *number* of irreversible
steps per session — cost guards watch dollars, but five silent deletes cost
a fraction of a cent.

```ts
d.tool({ budget: { maxDestructivePerSession: 3, destructive: ["db.write.*"] } });
```

Destructive means you declared it, or the arguments gave it away. It is
**never guessed from the tool name** — `search_deleted_items` is a read. A
blocked call does not count against the budget, otherwise a series of
rejected attempts would exhaust the cap and turn the protection into a
denial-of-service lever.

## Model & provider allowlist (0.12.0)

`d.tool()` guards tools, `d.egress()` guards generic outbound calls. Neither
catches a compromised dependency rewriting your LLM base URL — at which point
your prompts, system prompt and key go to someone else's server while the app
looks fine.

```ts
const m = d.model({ allowModels: ["gpt-4o-mini"], allowHosts: ["api.openai.com"] });
m.guard({ model: "gpt-4o-mini", baseUrl: openai.baseURL }, () => openai.chat…);
```

It is a decision function, not a global patch — call it where the model is
called, or it protects nothing. Note: if you set `allowHosts` and then call
without `baseUrl`, the call is **blocked**. Passing silently would promise
protection we are not performing.

## Human approval for dangerous actions (0.12.0)

A third state between allow and deny (OWASP LLM06). The agent may delete a
customer or send a payment — but not without a human knowing.

```ts
const g = d.tool({
  allow: ["db.*", "payments.*"],
  approval: { require: ["payments.*", "db.delete.*"], onRequest: sendToSlack },
});

const v = g.check({ tool: "payments.send", session, args });
// { allowed: false, rule: "approval_required", approvalId: "apr_…" }
g.approve(v.approvalId, "ondrej");   // once a human clicks
```

The grant is **single-use, time-limited and bound to a hash of the arguments** —
approving $100 will not authorise $100,000, and one "yes" does not open the
door permanently. How you ask the human (Slack, email, a button in your admin)
is up to you.

Order inside the guard: hard rules → approval → budget. A call awaiting
approval has not run yet, so it must not consume the destructive-action budget;
otherwise three unanswered requests would exhaust the cap.

## Reverse proxy for non-Node stacks (0.11.0)

The SDK is Express middleware, so it only runs in Node. To guard a Java,
PHP or Python backend, run Defender as a reverse proxy that sits **between
your TLS terminator and the app** — not in front of it:

```
internet :443 → nginx (TLS, unchanged) → defender :8088 → your app :8080
```

```bash
defender proxy --target http://127.0.0.1:8080 --port 8088          # audit
defender proxy --target http://127.0.0.1:8080 --port 8088 --block  # enforce
```

It applies the same engine (sensitive paths, prompt injection on AI routes,
telemetry) at the HTTP level. Measured overhead on a 1 KB response: +0 ms at
the median, +2 ms at p99.

Three things worth knowing, all found by a customer and fixed in 0.11.0:

- **It binds to `127.0.0.1`.** Earlier versions bound to `0.0.0.0`, which made
  the proxy reachable from outside and thus a way around your TLS terminator.
  Use `--host` if you really need another interface — you will get a warning.
- **WebSocket is tunnelled** to the backend. Earlier versions dropped the
  connection while the docs claimed otherwise. We do not see inside the
  tunnel, so that traffic is **not counted as protected** — route it around
  Defender if its contents matter to you.
- **The original `Host` header is preserved.** Use `--rewrite-host` for the
  old behaviour.

Output guard does not work in proxy mode yet; it needs a hook inside your app.

## Cost attribution (new in 0.4.0)

Break down LLM spend by client and project in the dashboard (Pro+). Just
tag your existing `recordCost` calls:

```ts
await d.recordCost(
  { model: "gpt-4o-mini", inputTokens, outputTokens,
    tags: { client: "acme", project: "web" } },
);
```

The SDK reports only the model, token counts, cost in micro-USD and the
tags — never prompts or PII. Untagged spend shows up as "(untagged)".

## Plans & limits

| Plan   | Req/month | Output | Tool | Conv / RAG / Anomaly | Slack/Teams |
| ------ | --------- | ------ | ---- | -------------------- | ----------- |
| Free   | 1 000     | –      | –    | rate-limit only      | –           |
| Solo   | 10 000    | yes    | –    | yes                  | –           |
| Pro    | 100 000   | yes    | yes  | yes                  | yes         |
| Agency | 1 000 000 | yes    | yes  | yes + custom         | yes (custom) |

Telemetry is best-effort — if Grovetech is unreachable, Defender still blocks
locally and never crashes your app.

## Licence

MIT — © Grovetech AI s.r.o.
