Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ jobs:
node --test 'arcjet-guard/src/vercel-eve/**/*.test.ts'
working-directory: ${{ github.workspace }}

# The mastra namespace imports @mastra/core for types only. A static scan
# proves the imports are written `import type`; this proves the
# consequence, catching a build step that re-emits one as a value import.
#
# Runs before the eve-absent step because that one deletes a different
# optional peer. Typecheck legitimately fails without @mastra/core.
- name: Unit tests with mastra absent
Comment thread
davidmytton marked this conversation as resolved.
run: |
rm -rf node_modules/@mastra/core arcjet-guard/node_modules/@mastra/core
node -e "try { require.resolve('@mastra/core', { paths: ['arcjet-guard/src/mastra/v1'] }); console.error('mastra still resolves'); process.exit(1) } catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error }"
count=$(find arcjet-guard/src/mastra -name '*.test.ts' | wc -l)
test "$count" -ge 5 || { echo "only $count mastra test files matched; the glob has gone stale" >&2; exit 1; }
node --test 'arcjet-guard/src/mastra/**/*.test.ts'
working-directory: ${{ github.workspace }}

# The vercel-eve namespace imports eve for types only, which is what lets
# guard support Node 22 at all — eve declares engines.node ">=24". A static
# scan proves the imports are written `import type`; this proves the
Expand Down
36 changes: 36 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Agent guidance

## Examples live in `arcjet/examples`

Do not add application examples under `examples/` in this repository.

Examples were removed from this SDK in
[#6217](https://github.com/arcjet/arcjet-js/pull/6217). New and remaining
examples belong in [`arcjet/examples`](https://github.com/arcjet/examples).
The current migration PR is
[arcjet/examples#193](https://github.com/arcjet/examples/pull/193) — add
Guard / adapter demos there (or a follow-up on that repo), not here.

That includes:

- Framework apps (`nextjs-*`, `express-*`, …)
- Guard integration demos (`nextjs-ai-agent`, `eve-agent`, `mastra-agent`)

Follow `arcjet/examples` CONTRIBUTING.md and its canonical example pattern
(`package.json` metadata, templated README, Dockerfile, `compose.yaml`,
devcontainer, LICENSE). Pin Arcjet packages to published versions. Standalone
AI examples that need a model key stay out of the default compose/CI matrix.

Do not restore `.github/workflows/reusable-examples.yml` or re-add an
`examples` CI job in this repo for new work.

README and skill links should point at
`https://github.com/arcjet/examples/tree/main/examples/<name>`, not at a
path under this repository.

## Integration work: review before a PR

For Guard vendor integrations and other integration work, keep going on a
branch and push it. Do not open a pull request until David confirms after
reviewing the code. The final report must include the exact branch name,
commit SHA, and a concise diff summary.
24 changes: 14 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ New adapters are added to the root of this monorepo in the format of
For example, `arcjet-sveltekit` is the directory for the `@arcjet/sveltekit`
package.

Each new adapter should come with an example application in this repository. See
[Examples](#examples) for guidance on creating an example.
Each new adapter should come with an example application in
[`arcjet/examples`](https://github.com/arcjet/examples), not in this
repository. See [Examples](#examples).

New adapters (and any other new package) must also be wired into our release and
publish pipeline before they can ship. See [Adding a new
Expand All @@ -59,6 +60,9 @@ change them:
filesystem-first, with one `defineTool` per file and no author-controlled call
site, so its enforcement points are a channel-boundary screen and a
connection-level approval gate that `vercel-ai/v7` has no equivalent of.
`mastra/v1` is the same idea on a different SDK: Mastra already runs
channels through `processInput` and treats `requireApproval` as human HITL,
so its helpers are `guardTool`, `guardProcessor`, and `guardHooks`.
- **Flat** — a single level under `@arcjet/guard`, no further nesting.
- **Explicitly versioned, with no unversioned alias.** `@arcjet/guard/vercel-ai`
does not resolve, and neither does a wildcard `./vercel-ai/*`. An alias would
Expand Down Expand Up @@ -114,15 +118,15 @@ is picked up with its structure preserved.

## Examples

Examples should be scaffolded using the scaffolding tool recommended by the
framework. Generally, we choose all defaults for the example applications in
this repository, but that is not a strict rule.
Do not add application examples under `examples/` in this repository. They
live in [`arcjet/examples`](https://github.com/arcjet/examples) (moved in
[#6217](https://github.com/arcjet/arcjet-js/pull/6217); remaining examples
are landing in [arcjet/examples#193](https://github.com/arcjet/examples/pull/193)).
Agents: see the root [AGENTS.md](./AGENTS.md) for the same rule.

When adding an example, it needs to be added to the
[dependabot.yml](./.github/dependabot.yml) file and the
[reusable-examples.yml](./.github/workflows/reusable-examples.yml) workflow. If
the example does not have a build process to run in CI, it can be excluded from
the workflow file.
Scaffold new examples in that repo with the framework's recommended tool,
following its CONTRIBUTING.md and the canonical example pattern. Do not
restore `.github/workflows/reusable-examples.yml` here.

## Publish

Expand Down
104 changes: 93 additions & 11 deletions arcjet-guard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,56 @@ helper. Currently available:
export default defineHook(arcjetHooks(arcjet));
```

- **`@arcjet/guard/mastra/v1`** — Mastra v1 integration. Exports `guardTool`,
`guardProcessor`, `guardHooks`, and `mastraAgentContext`. There is no
`guardInbound` (channels already hit `processInput`) and no `guardApproval`
(Mastra `requireApproval` is human HITL, not policy):

```ts
import { launchArcjet, detectPromptInjection, tokenBucket } from "@arcjet/guard";
import { guardTool, guardProcessor, guardHooks } from "@arcjet/guard/mastra/v1";
import { Agent } from "@mastra/core/agent";
import { createTool } from "@mastra/core/tools";
import { z } from "zod";

const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
const limit = tokenBucket({
refillRate: 10,
intervalSeconds: 60,
maxTokens: 10,
});

const lookupOrder = guardTool(
arcjet,
createTool({
id: "lookup-order",
description: "Look up an order",
inputSchema: z.object({ orderNumber: z.string() }),
execute: async ({ orderNumber }) => ({ orderNumber, status: "shipped" }),
}),
{
action: "order.looked-up",
onGuardError: "deny",
rules: (input) => [limit({ key: input.orderNumber, requested: 1 })],
},
);

export const agent = new Agent({
id: "support-agent",
name: "support-agent",
instructions: "Help the user.",
model: "openai/gpt-4o",
tools: { lookupOrder },
inputProcessors: [
guardProcessor(arcjet, {
action: "message.received",
rules: ({ text }) => [detectPromptInjection()(text)],
}),
],
hooks: guardHooks(arcjet),
});
```

### Naming and versions

Integration paths are `@arcjet/guard/<vendor-sdk>/v<major>` — the SDK being
Expand Down Expand Up @@ -933,15 +983,34 @@ importing only core guards are not forced to install unneeded packages:
only to use `@arcjet/guard/vercel-eve/v0`). **Eve requires Node.js >= 24**,
which is higher than `@arcjet/guard`'s own floor of >= 22. If you are using
Eve, ensure your deployment environment and CI both run Node 24 or later.
- **`@arcjet/guard/mastra/v1`** requires `@mastra/core` (optional peer,
installed only to use `@arcjet/guard/mastra/v1`). The peer range is `>=1 <2`.

**pnpm caveat**: pnpm does not reliably honour
`peerDependenciesMeta.*.optional` (pnpm#5152, #8142), especially with
`--strict-peer-dependencies` enabled. If `pnpm install` fails with missing
peers, either install them explicitly or relax strict peer checking:

Install only the peer for the integration you use — not a combined set.
Users pick one of these; Eve and Mastra are not installed together:

```sh
pnpm install ai @ai-sdk/provider-utils eve
# or
# @arcjet/guard/vercel-ai/v7
pnpm install ai @ai-sdk/provider-utils
```

```sh
# @arcjet/guard/vercel-eve/v0 (Node.js >= 24)
pnpm install eve
```

```sh
# @arcjet/guard/mastra/v1
pnpm install @mastra/core
```

```sh
# or skip the peer install and relax the check:
pnpm install --no-strict-peer-dependencies
```

Expand All @@ -952,11 +1021,11 @@ are not tied to any AI SDK, and internally they are kept that way — nothing
they import reaches `ai`. They are published on each vendor namespace, so there
is one path to learn and no layering to reason about.

Both `@arcjet/guard/vercel-ai/v7` and `@arcjet/guard/vercel-eve/v0` now export
these helpers. The open next step is promoting them to the root `@arcjet/guard`
export so a caller can get the agnostic layer without installing a vendor peer.
Eve was the second integration and exercised the shape without changing it,
which is the evidence that promotion was waiting on.
`@arcjet/guard/vercel-ai/v7`, `@arcjet/guard/vercel-eve/v0`, and
`@arcjet/guard/mastra/v1` now export these helpers. The open next step is
promoting them to the root `@arcjet/guard` export so a caller can get the
agnostic layer without installing a vendor peer. That change is a follow-up
with its own ADR; there is still no public `@arcjet/guard/agents`.

### `onGuardError`: handling evaluation failures

Expand All @@ -972,6 +1041,7 @@ which is the evidence that promotion was waiting on.
| `guard()` (core) | Allow (fail open), `hasFailedOpen()===true` | gate manually on `hasFailedOpen()` |
| `guardTool` / `guardAction` | Deny (fail closed) | `onGuardError: "allow"` |
| Eve `guardInbound` / `guardApproval` | Deny (fail closed) | `onGuardError: "allow"` |
| Mastra `guardProcessor` / `guardHooks` | Deny (fail closed) | `onGuardError: "allow"` |

`onGuardError` is broader than Arcjet Cloud availability. It governs both an
unexpected throw from `guard()` and an ALLOW decision whose `hasFailedOpen()`
Expand Down Expand Up @@ -1316,13 +1386,15 @@ Use `securityMetadata()` keys consistently across your app:

## Example

For a complete working example integrating `@arcjet/guard` with the Vercel AI SDK, see [examples/nextjs-ai-agent](https://github.com/arcjet/arcjet-js/tree/main/examples/nextjs-ai-agent), which demonstrates wrapping agent tools with guard checks, enforcing rules on application-invoked actions, and emitting audit events joined by correlation ID.
For a complete working example integrating `@arcjet/guard` with the Vercel AI SDK, see [`nextjs-ai-agent`](https://github.com/arcjet/examples/tree/main/examples/nextjs-ai-agent) in [`arcjet/examples`](https://github.com/arcjet/examples), which demonstrates wrapping agent tools with guard checks, enforcing rules on application-invoked actions, and emitting audit events joined by correlation ID.

For an example with Vercel Eve, see [`eve-agent`](https://github.com/arcjet/examples/tree/main/examples/eve-agent), which shows how to protect tools, connections, and channels with Arcjet guards, and record agent lifecycle events with hooks.

For an example with Vercel Eve, see [examples/eve-agent](https://github.com/arcjet/arcjet-js/tree/main/examples/eve-agent), which shows how to protect tools, connections, and channels with Arcjet guards, and record agent lifecycle events with hooks.
For an example with Mastra, see [`mastra-agent`](https://github.com/arcjet/examples/tree/main/examples/mastra-agent), which shows inbound prompt-injection screening, guarded tools (deny, PII on args, rate limit, fail-closed), hooks for unwrapped tools, and thread/resource correlation. These Guard examples land with [arcjet/examples#193](https://github.com/arcjet/examples/pull/193).

## Agent skill

For integration help in Claude Code or other AI coding agents, two skill files are packaged with `@arcjet/guard`:
For integration help in Claude Code or other AI coding agents, three skill files are packaged with `@arcjet/guard`:

**For Vercel AI SDK:**

Expand All @@ -1346,7 +1418,17 @@ ln -s /path/to/node_modules/@arcjet/guard/skills/integrate-arcjet-guard-eve ~/.c

In Claude Code, use `/integrate-arcjet-guard-eve` to start an integration session.

Each skill guides you through wrapping tools, gating connections, screening inbound messages, and recording lifecycle events joined by correlation ID.
**For Mastra:**

```bash
cp -r node_modules/@arcjet/guard/skills/integrate-arcjet-guard-mastra ~/.claude/skills/
# or
ln -s /path/to/node_modules/@arcjet/guard/skills/integrate-arcjet-guard-mastra ~/.claude/skills/
```

In Claude Code, use `/integrate-arcjet-guard-mastra` to start an integration session.

Each skill guides you through wrapping tools, screening inbound messages, and recording lifecycle events joined by correlation ID.

Note: `npx skills add arcjet/skills` refers to the separate Anthropic skills marketplace, not the packaged file.

Expand Down
9 changes: 9 additions & 0 deletions arcjet-guard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@
"./vercel-eve/v0": {
"types": "./dist/vercel-eve/v0/index.d.ts",
"import": "./dist/vercel-eve/v0/index.js"
},
"./mastra/v1": {
"types": "./dist/mastra/v1/index.d.ts",
"import": "./dist/mastra/v1/index.js"
}
},
"publishConfig": {
Expand Down Expand Up @@ -104,6 +108,7 @@
},
"devDependencies": {
"@ai-sdk/provider-utils": "5.0.13",
"@mastra/core": "1.58.0",
"@types/node": "22.20.1",
"ai": "7.0.38",
"eve": "0.31.0",
Expand All @@ -114,6 +119,7 @@
},
"peerDependencies": {
"@ai-sdk/provider-utils": ">=5 <6",
"@mastra/core": ">=1 <2",
"ai": ">=7 <8",
"eve": ">=0.25.1 <1"
},
Expand All @@ -126,6 +132,9 @@
},
"eve": {
"optional": true
},
"@mastra/core": {
"optional": true
}
},
"engines": {
Expand Down
Loading
Loading