From be967c254e694ea241d0c5855cdbeb434f2b18e5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 08:00:37 +0000 Subject: [PATCH] feat(skills): sync vendored skills with skills main 8287f83a Vendor-sync plugins/arcjet/skills/ from arcjet/skills main at 8287f83abb43775c49ed8b53d3cdc64bb230da18 (skills#62). Adds integrate-arcjet-guard-agent-framework-go; stamps Go SDK v1.0.0 and agentframework v0.1.0; CorrelationID rename. Apply this repo's dprint formatter after the copy. Co-authored-by: David Mytton --- CHANGELOG.md | 20 ++ plugins/arcjet/skills/arcjet/SKILL.md | 14 +- .../skills/arcjet/references/guards_go.md | 26 +- .../skills/arcjet/references/requests_go.md | 4 +- .../SKILL.md | 246 ++++++++++++++++++ 5 files changed, 300 insertions(+), 10 deletions(-) create mode 100644 plugins/arcjet/skills/integrate-arcjet-guard-agent-framework-go/SKILL.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 74bb1e6..baa8df2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,26 @@ this project adheres to [Semantic Versioning](https://semver.org/). ### Changed +- Synced the vendored skill tree with + [arcjet/skills](https://github.com/arcjet/skills) `main` at + `8287f83a` (2026-09-17, + [arcjet/skills#62](https://github.com/arcjet/skills/pull/62)). Follow-up to + the #23 sync of `f8959e14` (skills#63). Files were fetched from GitHub at + that SHA, then this repo’s `dprint` formatter was applied. The tip is + the Microsoft Agent Framework for Go skill plus the Go SDK **v1.0.0** + release: new `integrate-arcjet-guard-agent-framework-go` skill, + `agentframework` module **v0.1.0** (Go 1.26+), `GuardAction` in the Go + Guard reference, and the `CorrelationId` → `CorrelationID` / + `WithCorrelationId` → `WithCorrelationID` rename. Skills `main` also + includes [arcjet/skills#64](https://github.com/arcjet/skills/pull/64) + (singular Claude Managed Agents example URL), already applied on this + repo in #23. Canonical copy is `plugins/arcjet/skills/` (`skills/` is + the inbound symlink). Skills `main` still has no JS + `integrate-arcjet-guard-*` dirs — JS adapters stay in `arcjet/`. Did + not wait for open + [arcjet/skills#59](https://github.com/arcjet/skills/pull/59). Evals + were not copied (prior syncs do not vendor `evals/`). Deprecated alias + skill directories are unchanged. - Synced the vendored skill tree with [arcjet/skills](https://github.com/arcjet/skills) `main` at `f8959e14` (2026-09-16, diff --git a/plugins/arcjet/skills/arcjet/SKILL.md b/plugins/arcjet/skills/arcjet/SKILL.md index b03a589..babb4ae 100644 --- a/plugins/arcjet/skills/arcjet/SKILL.md +++ b/plugins/arcjet/skills/arcjet/SKILL.md @@ -1,7 +1,7 @@ --- name: arcjet license: Apache-2.0 -description: Add Arcjet security protection to HTTP routes, AI agent tool calls, MCP servers, background jobs, and queue workers. Covers rate limiting, bot detection, email validation, prompt injection, sensitive information blocking (including Rampart NER), content moderation, capture/flush, remote Guard policies, typed inputs, and abuse prevention. Works in JavaScript/TypeScript, Python, and Go. HTTP frameworks share this skill. JS Guard adapters (Vercel AI SDK, Eve, Mastra, LangChain, LangGraph, OpenAI Agents, Genkit, Google ADK, Strands, TanStack AI, Claude Agent SDK, Claude Managed Agents) are per-adapter reference files loaded from Step 3 — do not keep the whole JS Guard reference in context. Official Python LangChain, CrewAI, OpenAI Agents, Claude Agent SDK, Claude Managed Agents, and Strands Agents have dedicated integrate-arcjet-guard skills. Use when the user wants security, rate limiting, bot protection, or abuse prevention – "protect my API," "rate limit tool calls," "block bots," "secure my endpoint," or "prevent abuse" – even without naming Arcjet. +description: Add Arcjet security protection to HTTP routes, AI agent tool calls, MCP servers, background jobs, and queue workers. Covers rate limiting, bot detection, email validation, prompt injection, sensitive information blocking (including Rampart NER), content moderation, capture/flush, remote Guard policies, typed inputs, and abuse prevention. Works in JavaScript/TypeScript, Python, and Go. HTTP frameworks share this skill. JS Guard adapters (Vercel AI SDK, Eve, Mastra, LangChain, LangGraph, OpenAI Agents, Genkit, Google ADK, Strands, TanStack AI, Claude Agent SDK, Claude Managed Agents) are per-adapter reference files loaded from Step 3 — do not keep the whole JS Guard reference in context. Official Python LangChain, CrewAI, OpenAI Agents, Claude Agent SDK, Claude Managed Agents, and Strands Agents have dedicated integrate-arcjet-guard skills. Microsoft Agent Framework for Go has a dedicated integrate-arcjet-guard-agent-framework-go skill. Use when the user wants security, rate limiting, bot protection, or abuse prevention – "protect my API," "rate limit tool calls," "block bots," "secure my endpoint," or "prevent abuse" – even without naming Arcjet. metadata: author: arcjet --- @@ -61,7 +61,7 @@ See [references/cli.md](references/cli.md) for install options beyond `npx`, age #### Install the SDK with the project's package manager -Once you know which SDK you need (see Step 3), install it with the package manager the project already uses: `npm install`, `pnpm add`, `yarn add`, `bun add`, `pip install`, `uv add`, `poetry add`, or `go get`. Don't hand-edit `package.json` / `requirements.txt` / `go.mod` and guess a version: typed versions go stale (`@arcjet/next` is currently `1.12.0`; Python `arcjet` is `1.1.0`; Go must use the module tag `@v1.0.0-rc.2`, not a copied pseudo-version), and the lockfile/module metadata won't get updated. Let the package manager pick the real version and pin it. +Once you know which SDK you need (see Step 3), install it with the package manager the project already uses: `npm install`, `pnpm add`, `yarn add`, `bun add`, `pip install`, `uv add`, `poetry add`, or `go get`. Don't hand-edit `package.json` / `requirements.txt` / `go.mod` and guess a version: typed versions go stale (`@arcjet/next` is currently `1.12.0`; Python `arcjet` is `1.1.0`; Go must use a module tag, not a copied pseudo-version), and the lockfile/module metadata won't get updated. Let the package manager pick the real version and pin it. ### Step 3: Detect protection type and read the reference @@ -75,7 +75,7 @@ Determine which protection type applies: | **Go SDK** | `github.com/arcjet/arcjet-go` (with `NewClient`) | `github.com/arcjet/arcjet-go` (with `NewGuardClient`) | | **Entry point** | `protect(request)` / `Protect(ctx, r)` | `guard(label, rules)` / `Guard(ctx, request)` | -A single project can use both – for example, request-based on API routes and Guard on agent tool calls. If the project already uses a supported agent framework, prefer the official wrapper over hand-wrapping every tool. In Python, load the dedicated skill (table below) — not a raw `guard()` around every callable, and not the JS `@arcjet/guard/...` path. In JavaScript, load fundamentals plus **exactly one** adapter file from the JS table — not the sibling adapters. +A single project can use both – for example, request-based on API routes and Guard on agent tool calls. If the project already uses a supported agent framework, prefer the official wrapper over hand-wrapping every tool. In Python, load the dedicated skill (table below) — not a raw `guard()` around every callable, and not the JS `@arcjet/guard/...` path. In JavaScript, load fundamentals plus **exactly one** adapter file from the JS table — not the sibling adapters. In Go, load the Microsoft Agent Framework skill when that framework is present; otherwise use `GuardAction` from the Go Guard reference. **Common misclassifications to watch for:** @@ -106,6 +106,12 @@ When the project already uses an official Python agent framework, load the dedic | Claude Managed Agents | `arcjet.guard.claude_managed_agents` | [integrate-arcjet-guard-claude-managed-agents-py](../integrate-arcjet-guard-claude-managed-agents-py/SKILL.md) | | Strands Agents | `arcjet.guard.strands_agents` | [integrate-arcjet-guard-strands-agents-py](../integrate-arcjet-guard-strands-agents-py/SKILL.md) | +When the project is Go and already uses Microsoft Agent Framework for Go, load the dedicated skill: + +| Go framework | Import | Skill | +| ----------------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| Microsoft Agent Framework (`functool`, `mcptool`, `agent.Config.Middlewares`) | `github.com/arcjet/arcjet-go/agentframework` | [integrate-arcjet-guard-agent-framework-go](../integrate-arcjet-guard-agent-framework-go/SKILL.md) | + These references explain architectural decisions and patterns that can't be inferred from the source code alone. For exact API signatures, read the installed package's types and doc comments. ### Step 4: Implement protection @@ -197,7 +203,7 @@ For exact API signatures, parameter names, and the full set of rules and helpers - **Python SDK**: https://github.com/arcjet/arcjet-py – `arcjet` package (request protection) and `arcjet.guard` subpackage (non-HTTP guard). - **Python Guard integration skills**: [integrate-arcjet-guard-langchain-py](../integrate-arcjet-guard-langchain-py/SKILL.md), [integrate-arcjet-guard-crewai](../integrate-arcjet-guard-crewai/SKILL.md), [integrate-arcjet-guard-openai-agents-py](../integrate-arcjet-guard-openai-agents-py/SKILL.md), [integrate-arcjet-guard-claude-agent-sdk-py](../integrate-arcjet-guard-claude-agent-sdk-py/SKILL.md), [integrate-arcjet-guard-claude-managed-agents-py](../integrate-arcjet-guard-claude-managed-agents-py/SKILL.md), [integrate-arcjet-guard-strands-agents-py](../integrate-arcjet-guard-strands-agents-py/SKILL.md). - **JavaScript / TypeScript SDK**: https://github.com/arcjet/arcjet-js – monorepo with framework-specific packages (`@arcjet/next`, `@arcjet/node`, `@arcjet/fastify`, `@arcjet/sveltekit`, `@arcjet/guard`). JS Guard adapter files: [references/guards_js_vercel_ai.md](references/guards_js_vercel_ai.md) and siblings listed in Step 3. -- **Go SDK**: https://github.com/arcjet/arcjet-go – `github.com/arcjet/arcjet-go` module with request and guard clients. Pin `go get github.com/arcjet/arcjet-go@v1.0.0-rc.2` (Go 1.25+). `go get ...@latest` may still resolve **v0.1.0**. +- **Go SDK**: https://github.com/arcjet/arcjet-go – `github.com/arcjet/arcjet-go` module with request and guard clients. `go get github.com/arcjet/arcjet-go` resolves **v1.0.0** (Go 1.25+). Microsoft Agent Framework helpers live in a separate module: `go get github.com/arcjet/arcjet-go/agentframework` resolves **v0.1.0** and needs Go 1.26+. - **Guard policies**: author and publish via MCP (`list-guard-policies` / `describe-guard-policy` / `validate-guard-policy` / `put-guard-policy`). The CLI has no policy commands. Application policies select by `label` / `action`. Coding-agent policies attach by **Execute on** (Tool call or Prompt); publishing turns them on — the hook URL must not name a policy. - **Docs**: https://docs.arcjet.com – narrative guides, blueprints, and product reference. - **Console**: https://console.arcjet.com – sites, keys, and decision history. diff --git a/plugins/arcjet/skills/arcjet/references/guards_go.md b/plugins/arcjet/skills/arcjet/references/guards_go.md index 2c4269f..5790322 100644 --- a/plugins/arcjet/skills/arcjet/references/guards_go.md +++ b/plugins/arcjet/skills/arcjet/references/guards_go.md @@ -6,7 +6,9 @@ Guard protects code paths that do not have an HTTP request – agent tool calls, ## Installation -The current documented pre-release is **`github.com/arcjet/arcjet-go` v1.0.0-rc.2** (requires Go 1.25+). `go get ...@latest` may still resolve **v0.1.0** (June 30, 2026) — pin `@v1.0.0-rc.2`. Capture, Rampart, nested metadata, `WithIPSrc`, threat/billing, `GuardModerateContent`, and required Guard `Mode` are on that rc. If the project uses an older Go toolchain, warn the user and stop until it is upgraded. +The current release is **`github.com/arcjet/arcjet-go` v1.0.0** (September 17, 2026), which `go get ...@latest` resolves. It requires Go 1.25+. Capture, Rampart, nested metadata, `WithIPSrc`, threat/billing, `GuardModerateContent`, and required Guard `Mode` are all in it. If the project uses an older Go toolchain, warn the user and stop until it is upgraded. + +The Microsoft Agent Framework helpers are a separate module, **`github.com/arcjet/arcjet-go/agentframework` v0.1.0**, which requires Go 1.26+ because the framework does. Install with Go tooling: @@ -105,6 +107,22 @@ An empty `Bucket` defaults to `default-token-bucket`, `default-fixed-window`, or Go has no registration / free `guard()` API. Pass the client. +## Fail-closed helpers: `GuardAction` + +For a sensitive tool call or action, wrap it in `arcjet.GuardAction` instead of hand-writing the `HasFailedOpen()` gate. It calls Guard (always, even with no rules), denies with `*arcjet.GuardDeniedError`, fails closed with `*arcjet.GuardUnavailableError` when policy could not be evaluated, runs the function otherwise, and records one capture event whose metadata `outcome` is `success`, `degraded`, `denied`, `error`, or `unavailable`. + +```go +out, err := arcjet.GuardAction(ctx, guard, arcjet.GuardActionPolicy{ + Action: "refund.issued", + Actor: userID, + Rules: []arcjet.GuardRuleInput{refundLimit.Key(userID, 1)}, +}, func(ctx context.Context) (Receipt, error) { return refundPayment(ctx, id) }) +``` + +Distinguish the two errors with `errors.As`. `OnGuardError: arcjet.OnGuardErrorAllow` opts a call site back into fail-open; a `DENY` still blocks. Use `arcjet.NewGuardDenialResult(decision)` and `arcjet.NewGuardUnavailableResult()` when the caller is a model and needs a JSON result rather than a Go error. Available from `arcjet-go` v1.0.0. + +For Microsoft Agent Framework for Go, load [integrate-arcjet-guard-agent-framework-go](../../integrate-arcjet-guard-agent-framework-go/SKILL.md) instead of wrapping tools by hand. + ## Capture and flush `Capture` records that an action happened. It is not a security decision – it never denies, never returns an error, and never sets `HasFailedOpen()`. @@ -112,8 +130,8 @@ Go has no registration / free `guard()` API. Pass the client. ```go guard.Capture(arcjet.CaptureEvent{ Action: "refund.issued", - CorrelationId: runID, - DecisionId: decision.ID, + CorrelationID: runID, + DecisionID: decision.ID, Metadata: arcjet.Metadata{ "invoice": map[string]any{"id": "inv_123", "amount": 4200}, "refunded": true, @@ -143,7 +161,7 @@ for _, w := range decision.Warnings { ## Correlation IDs -Set `GuardRequest.CorrelationId` to correlate this guard call with HTTP requests, workflow runs, or agent traces. It is a dedicated field, not metadata, and does not affect the decision. +Set `GuardRequest.CorrelationID` to correlate this guard call with HTTP requests, workflow runs, or agent traces. It is a dedicated field, not metadata, and does not affect the decision. The agent helpers read `arcjet.ContextWithCorrelationID(ctx, id)`; `Guard` and `Capture` do not, so pass `CorrelationID` to them explicitly. ## Metadata diff --git a/plugins/arcjet/skills/arcjet/references/requests_go.md b/plugins/arcjet/skills/arcjet/references/requests_go.md index 0ef7e77..401609a 100644 --- a/plugins/arcjet/skills/arcjet/references/requests_go.md +++ b/plugins/arcjet/skills/arcjet/references/requests_go.md @@ -6,7 +6,7 @@ Request protection inspects `net/http` requests – headers, IP, body – to enf ## Installation -The current documented pre-release is **`github.com/arcjet/arcjet-go` v1.0.0-rc.2** (requires Go 1.25+). `go get ...@latest` may still resolve **v0.1.0** (June 30, 2026) — pin `@v1.0.0-rc.2`. Nested `Metadata`, `WithIPSrc`, Rampart, `decision.IP.Threat`, and Protect transport-failure ERROR decisions are on that rc. If the project uses an older Go toolchain, warn the user and stop until it is upgraded. +The current release is **`github.com/arcjet/arcjet-go` v1.0.0** (September 17, 2026), which `go get ...@latest` resolves. It requires Go 1.25+. Nested `Metadata`, `WithIPSrc`, Rampart, `decision.IP.Threat`, and Protect transport-failure ERROR decisions are all in it. If the project uses an older Go toolchain, warn the user and stop until it is upgraded. Install with Go tooling, not by editing `go.mod` directly: @@ -90,7 +90,7 @@ For user-based characteristics, use identity established by trusted authenticati ## Correlation IDs -Pass `arcjet.WithCorrelationId(id)` to `Protect` to correlate this decision with guard calls, workflow runs, or agent traces. It is a dedicated field, not `WithExtra` or `Metadata`, and does not affect the decision. +Pass `arcjet.WithCorrelationID(id)` to `Protect` to correlate this decision with guard calls, workflow runs, or agent traces. It is a dedicated field, not `WithExtra` or `Metadata`, and does not affect the decision. ## Explicit client IP diff --git a/plugins/arcjet/skills/integrate-arcjet-guard-agent-framework-go/SKILL.md b/plugins/arcjet/skills/integrate-arcjet-guard-agent-framework-go/SKILL.md new file mode 100644 index 0000000..d604453 --- /dev/null +++ b/plugins/arcjet/skills/integrate-arcjet-guard-agent-framework-go/SKILL.md @@ -0,0 +1,246 @@ +--- +name: integrate-arcjet-guard-agent-framework-go +description: Integrate Arcjet Guard into Microsoft Agent Framework for Go (github.com/microsoft/agent-framework-go). Wrap a functool or MCP tool with GuardTool, guard every tool an agent can see and screen inbound text with GuardMiddleware, or guard any Go function with arcjet.GuardAction. Use when asked to add Arcjet to a Go agent built on Microsoft Agent Framework, rate limit its tools, block prompt injection, or fail closed on tool calls. This is the Go framework, not the .NET or Python Microsoft Agent Framework. +license: Apache-2.0 +compatibility: Requires Go >= 1.26 and github.com/arcjet/arcjet-go/agentframework v0.1.0 or later, which requires github.com/arcjet/arcjet-go v1.0.0 or later and github.com/microsoft/agent-framework-go v0.1.0 or later. +metadata: + author: arcjet + type: core + library: arcjet +--- + +# Integrate Arcjet Guard into Microsoft Agent Framework for Go + +`github.com/arcjet/arcjet-go/agentframework` wraps the application's existing +`arcjet.GuardClient`. Shared Guard fundamentals (client, rules, labels, +decisions, capture) live in +[../arcjet/references/guards_go.md](../arcjet/references/guards_go.md). Load +that reference for anything that is not framework-specific. + +Three surfaces, one decision rule: + +- **Any Go function** → `arcjet.GuardAction` in the root module. No + framework dependency. Returns `*arcjet.GuardDeniedError` or + `*arcjet.GuardUnavailableError`. +- **A `tool.FuncTool` you can name at wiring time** (from `functool.New`, + `mcptool.ListTools`, or `agenttool.New`) → `agentframework.GuardTool`, or + `GuardTools` for a list. The result is still a `tool.FuncTool`. +- **An agent whose tools the model picks, or user text to screen** → + `agentframework.GuardMiddleware` in `agent.Config.Middlewares`. + +This is the Go framework. The .NET and Python Microsoft Agent Framework +implementations have no Arcjet adapter. Do not import `@arcjet/guard` or +`arcjet.guard` here. + +## Denials are tool results, never errors + +The framework replaces a tool error with `Error: Function failed.` and +allows three consecutive rounds of failing tool calls before the fourth ends +the run. `GuardTool` therefore returns `arcjet.GuardDenialResult` as a +successful result. Do not "fix" this by returning an error, and do not set +`IncludeDetailedErrors` to make errors carry the denial. + +## Fail closed by default + +`GuardTool`, `GuardTools`, `GuardMiddleware`, and `arcjet.GuardAction` deny +when policy cannot be evaluated. That is a different outcome from a policy +`DENY`, and the two return different results to the model. + +- Policy could not be evaluated: `arcjet.NewGuardUnavailableResult()`, reason + `ERROR`, five second retry hint. +- Policy denied the call, such as an exhausted rate limit or detected prompt + injection: `arcjet.NewGuardDenialResult(decision)`, or whatever + `ToolPolicy.OnDeny` returns in its place. + +An inbound denial ends the run with its message. Set +`OnGuardError: arcjet.OnGuardErrorAllow` only where availability matters more +than enforcement, such as a read-only lookup; it changes the unavailable path +alone, and a `DENY` always blocks. + +## Human approval is not policy + +`tool.ApprovalRequiredFunc` and the `toolapproval` middleware ask a person. +`GuardTool` keeps a wrapped tool's approval status, and there is no adapter +that feeds an Arcjet decision into an auto-approval rule. Hosted tools +(`hostedtool.*`) run at the provider and cannot be guarded. + +## Workflows need no separate helper + +`workflow/` is not a third surface. An agent hosted with `agentworkflow` runs +through `agent.Agent.Run`, so `GuardMiddleware` and `GuardTool` apply inside a +workflow unchanged. A bare executor is ordinary Go code: call +`arcjet.GuardAction` in its handler, and decide there what a denial does to the +run, because nothing downstream will decide it for you. + +Correlation does not survive the default execution environment. `inproc.Default` +is `inproc.OffThread`, whose run loop builds its own context, so an ID placed +with `arcjet.ContextWithCorrelationID` before `Run` never reaches an executor +and correlates nothing. Only `inproc.Lockstep` passes the caller's context +through. Set `GuardActionPolicy.CorrelationID` explicitly inside the executor +instead; it wins over the context in any case. + +## Questions to ask the human first + +Ask only what you cannot infer from the code; suggest defaults. + +1. Which tools are **risky** (side effects, irreversible, spends money, + sends messages)? Those get a fail-closed `ToolPolicy`. Read-only tools + may use `OnGuardErrorAllow`. +2. What **limits**? ("5 refunds per hour per user" → `GuardTokenBucket` + with a hardcoded `Bucket`.) +3. Who is the **user** for `Actor` and `SecurityMetadata.User`: an opaque + ID from the authenticated request, never PII. +4. What is the **correlation ID**: the conversation or request ID the + application already has. Put it on the context with + `arcjet.ContextWithCorrelationID` before `Run`. Never mint one. +5. Should **inbound text** be screened? If yes, `GuardMiddleware` with an + `InboundPolicy`. Failing closed there stops the agent for the duration + of an outage, so `OnGuardErrorAllow` is a legitimate choice at that one + site. + +## The things readers get wrong + +1. **Labels are hardcoded.** `Action: "refund.issued"`, never + `fmt.Sprintf`. In a `GuardTools` policy function, `switch t.Name()`. +2. **`Action`, not `Label`.** Wrappers take `Action`; the raw + `GuardRequest` takes `Label`. Same slug. +3. **Denial is a result.** See above. +4. **Correlation is caller-owned.** Put it on the context, or store it on + the session under `agentframework.CorrelationIDStateKey`. Never + `agent.Session.ServiceID`: providers rewrite it mid-run. Nothing + generates an ID. +5. **`Args` decodes the tool's typed input.** For a struct input the + arguments object is the value; for a scalar the framework wraps it. +6. **Wrap once.** `GuardTools` and `GuardMiddleware` skip a tool already + wrapped by `GuardTool`, so composing them costs one evaluation per call. +7. **Rules are usually resolved from the arguments**, so `ToolPolicy.Rules` + is a function, not a slice. +8. **`success` on capture means policy judged the action**, `degraded` + means it ran under `OnGuardErrorAllow` without a full judgement. + +## Step 1: Install and find the guard client + +```bash +go get github.com/arcjet/arcjet-go@latest +go get github.com/arcjet/arcjet-go/agentframework@latest +``` + +The module requires Go 1.26. If the project is on an older Go, tell the user +and stop. Create one `arcjet.NewGuardClient` at package scope; it reads +`ARCJET_KEY` when `Key` is empty. + +## Step 2: Gate a tool you can name: `GuardTool` + +```go +guarded := agentframework.MustGuardTool(guard, issueRefund, agentframework.ToolPolicy{ + Action: "refund.issued", + Actor: func(ctx context.Context, _ json.RawMessage) (string, error) { + return userIDFromContext(ctx) + }, + Rules: agentframework.Args(func(ctx context.Context, in refundArgs) ([]arcjet.GuardRuleInput, error) { + userID, err := userIDFromContext(ctx) + if err != nil { + return nil, err + } + return []arcjet.GuardRuleInput{refundLimit.Key(userID, 1)}, nil + }), + Metadata: arcjet.SecurityMetadata{Reversibility: "irreversible"}.Metadata(), +}) +``` + +`Actor` and `Rules` run per call and receive the call's context, so read the +caller's identity from there. A package-level or captured variable holds one +user for the life of the process: every caller would share one rate-limit +bucket and every decision would name the same actor. `Metadata` is read once +when the tool is wrapped, so per-call identity does not belong in it; `Actor` +already carries the user. + +`GuardTool` returns `(tool.FuncTool, error)`; `MustGuardTool` panics on a +configuration error and suits package-level initialization. For MCP tools, +wrap the output of `mcptool.ListTools` with `GuardTools` and a policy +function that switches on the tool name. + +## A policy function that returns false leaves the tool unguarded + +`GuardTools` appends a tool unchanged when the policy function returns +`false`, and does the same for a tool that is not a `tool.FuncTool`. Nothing +fails, nothing logs, and the tool runs unprotected. A `switch` on the tool +name therefore guards exactly the names it lists: add a tool later and it is +unguarded until someone adds a case. + +Decide which of the two you want, and write it down: + +- **Deny by default.** Return a policy for every tool, with a restrictive one + for names the switch does not recognize. Nothing new is ever unguarded. +- **Allow by default.** Return `false` for unrecognized names, and assert the + set of guarded tools in a test so an addition is caught there. + +A test for the second form passes the policy a tool it has no case for. +`GuardTools` returns the same value it was given when it declines, so an +identity comparison is what detects an unguarded tool: + +```go +tools := []tool.Tool{lookupOrder, issueRefund, unrecognizedTool} +guarded, err := agentframework.GuardTools(client, tools, toolPolicy) +if err != nil { + t.Fatal(err) +} +for i, g := range guarded { + if g == tools[i] { + t.Errorf("tool %q came back unwrapped", g.Name()) + } +} +``` + +A client built with `arcjet.GuardConfig{Key: "ajkey_test"}` is enough here. +The test never reaches the Arcjet API, so this is a literal in a test rather +than a key in application code. + +## Step 3: Gate an agent: `GuardMiddleware` + +```go +mw, err := agentframework.GuardMiddleware(guard, agentframework.MiddlewareConfig{ + Tools: func(t tool.Tool) (agentframework.ToolPolicy, bool) { + switch t.Name() { + case "issue_refund": + return agentframework.ToolPolicy{Action: "refund.issued", /* ... */}, true + } + return agentframework.ToolPolicy{}, false + }, + Inbound: &agentframework.InboundPolicy{ + Action: "message.received", + Rules: func(_ context.Context, text string) ([]arcjet.GuardRuleInput, error) { + return []arcjet.GuardRuleInput{promptScan.Text(text)}, nil + }, + }, +}) +a := anthropicprovider.NewAgent(client, anthropicprovider.AgentConfig{ + Config: agent.Config{Tools: tools, Middlewares: []agent.Middleware{mw}}, +}) +``` + +The middleware reaches tools from `agent.Config.Tools` and from a per-run +`agent.WithTool`. + +## Step 4: Correlate + +```go +ctx = arcjet.ContextWithCorrelationID(ctx, conversationID) +resp, err := a.RunText(ctx, prompt).Collect() +``` + +## Verify the integration + +1. `go vet ./...` passes and the project builds. +2. Exercise: an allowed tool call, a rate-limited call (the model receives + `arcjetDenied: true` and explains it), an inbound prompt-injection + denial (the run ends before the provider is called), and fail-closed + (an unreachable Arcjet returns `reason: "ERROR"`). +3. Confirm in the Arcjet Console or CLI that decisions share the + caller-owned correlation ID. +4. Manual E2E with a real `ARCJET_KEY` is still-to-verify until you run it. + +Worked example: +[`examples/agentframework`](https://github.com/arcjet/arcjet-go/tree/main/examples/agentframework). +Do not invent a second example name. Do not add an example in this skills +repo.