# Output Gate: JS policies for MCP tool calls

> Output Gate runs goja JS policies pre and post execution to validate inputs and filter outputs. ctx reference, PII redaction examples, 5s timeout.

Canonical: https://www.toolmesh.io/en/output-gate/

The Output Gate is ToolMesh's content control layer. JavaScript policies run before (pre) and after (post) every tool execution, enabling input validation, output filtering, and PII redaction.

## How It Works

Gate policies are top-level JavaScript files in the `policies/` directory. They are executed by [goja](https://github.com/dop251/goja), a Go-native JavaScript engine, with a 5-second timeout per policy.

Each policy receives a single global variable named `ctx` and runs in one of two phases:

- **`pre`** — before the tool executes. Mutate `ctx.params` to rewrite the request, or `throw` to reject it.
- **`post`** — after the tool executes. Mutate `ctx.response.content` to filter the response.

Throwing a string or `Error` rejects the call. If the script completes without throwing, the call is allowed.

### `ctx` reference

| Field | Description |
|-------|-------------|
| `ctx.phase` | `"pre"` or `"post"` |
| `ctx.tool` | Tool name with backend prefix (e.g. `github_merge_pull`) |
| `ctx.toolAccess` | DADL access tag: `"read"`, `"write"`, `"admin"`, `"dangerous"`, or empty |
| `ctx.params` | Tool parameters (mutate in `pre` to rewrite) |
| `ctx.response.content` | Tool response (mutate in `post` to filter; only populated in `post`) |
| `ctx.user.userID` | Authenticated user identifier |
| `ctx.user.callerId` | Caller identity (e.g. `cli`, `claude-desktop`) |
| `ctx.user.callerClass` | `"trusted"`, `"standard"`, or `"untrusted"` |
| `ctx.user.roles` | Array of user role strings |
| `ctx.rateLimitExceeded(n)` | Function — returns `true` if the user exceeded `n` calls/hour |

## Examples

### Reject dangerous parameters (pre)

```javascript
// policies/block-force-delete.js
if (ctx.phase === "pre" && ctx.params.force_delete === true) {
  throw "force_delete is blocked by policy";
}
```

### Redact PII from responses (post)

```javascript
// policies/redact-email.js
if (ctx.phase === "post" && ctx.response && ctx.response.content) {
  for (var i = 0; i < ctx.response.content.length; i++) {
    var block = ctx.response.content[i];
    if (block && block.type === "text" && block.text) {
      block.text = block.text.replace(
        /[\w.-]+@[\w.-]+\.\w+/g,
        "[REDACTED]"
      );
    }
  }
}
```

Mutations on `ctx.response.content` propagate back to the live response — downstream evaluators and the executor see the redacted content.

### Strip Co-Authored-By from GitHub PRs (pre)

Many AI coding agents append `Co-Authored-By` trailers to commit messages and PR descriptions. A pre-gate policy can strip them before the GitHub API call:

```javascript
// policies/strip-co-authored-by.js
if (ctx.phase === "pre") {
  var coAuthorPattern = /^[Cc]o-[Aa]uthored-[Bb]y:.*$/gm;

  function stripTrailer(text) {
    if (!text) return text;
    return text
      .replace(coAuthorPattern, "")
      .replace(/\n{3,}/g, "\n\n")
      .replace(/\n+$/, "\n");
  }

  if (ctx.tool === "github_create_pull" || ctx.tool === "github_update_pull") {
    if (ctx.params.body) {
      ctx.params.body = stripTrailer(ctx.params.body);
    }
  }
  if (ctx.tool === "github_merge_pull" && ctx.params.commit_message) {
    ctx.params.commit_message = stripTrailer(ctx.params.commit_message);
  }
  if (ctx.tool === "github_create_git_commit" && ctx.params.message) {
    ctx.params.message = stripTrailer(ctx.params.message);
  }
}
```

The AI agent never notices the change, and no `Co-Authored-By` line reaches your repository.

### Read-only enforcement for untrusted callers (pre)

```javascript
// policies/untrusted-readonly.js
if (ctx.phase === "pre" &&
    ctx.user.callerClass === "untrusted" &&
    ctx.toolAccess !== "read") {
  throw "Caller class 'untrusted' may only execute read-tagged tools";
}
```

This relies on the DADL `access` tag (`read`, `write`, `admin`, `dangerous`) declared per tool — see the [DADL spec](https://dadl.ai/spec/dadl-spec-v0.1).

## CallerClass-Based Filtering

The gate receives the CallerClass, enabling tiered policies:

| CallerClass | Typical Filtering |
|-------------|-------------------|
| `trusted` | Credentials only |
| `standard` | High-risk PII + credentials |
| `untrusted` | All PII patterns, read-only tools |

## Rate Limiting

Policies can enforce a sliding-window rate limit per user:

```javascript
if (ctx.phase === "pre" && ctx.rateLimitExceeded(100)) {
  throw "Rate limit exceeded (100/hour)";
}
```

## Configuration

```bash
GATE_EVALUATORS=goja           # Enable goja evaluator (default)
```

Place policy files in the `policies/` directory. ToolMesh loads them at startup.

## Enterprise: Compliance-LLM

The enterprise extension adds an LLM-based gate evaluator that classifies content against compliance rules. This enables policies like "block responses containing financial advice" without writing regex patterns.
