Skip to content

feat(examples): add TanStack AI chat() guardrails example - #207

Open
davidmytton wants to merge 4 commits into
mainfrom
david/cursor/tanstack-agent-guardrails
Open

feat(examples): add TanStack AI chat() guardrails example#207
davidmytton wants to merge 4 commits into
mainfrom
david/cursor/tanstack-agent-guardrails

Conversation

@davidmytton

Copy link
Copy Markdown
Collaborator

Adds examples/tanstack-agent: a small Node chat({ middleware }) support agent protected by @arcjet/guard/tanstack-ai/v0 (arcjet-js#6260).

Inbound guard() runs before chat(). guardMiddleware is first in the middleware list so onBeforeToolCall rate-limits lookups and scans free-text note args for PII. Default DENY is { type: "skip", result: ArcjetDenialResult }. Correlation is a caller-owned sessionId — never ctx.threadId.

The adapter is on main but not on npm yet, so @arcjet/guard and @arcjet/transport (./http2) are vendored from d730d57. Repin once @arcjet/guard/tanstack-ai/v0 publishes.

import { guardMiddleware, tanstackAiContext } from "@arcjet/guard/tanstack-ai/v0";
import { chat } from "@tanstack/ai";

const appContext = { sessionId: conversationId };
const ctx = tanstackAiContext({ context: appContext });

const inbound = await arcjet.guard({
  label: "message.received",
  rules: [detectInjection(text)],
  ...ctx,
});
if (inbound.conclusion === "DENY" || inbound.hasFailedOpen()) {
  return; // do not call chat()
}

const stream = chat({
  adapter,
  messages,
  tools: [lookupOrder],
  context: appContext,
  middleware: [
    guardMiddleware(arcjet, {
      action: ({ toolName }) => `${toolName}.invoked`,
      sessionId: conversationId,
      rules: ({ input }) => [
        lookupLimit({ key: `order:${input.orderId}`, requested: 1 }),
        ...(input.note ? [detectPii(input.note)] : []),
      ],
    }),
  ],
});

Docs: /guards/tanstack-ai/. Run examples/tanstack-agent with ARCJET_KEY and AI_GATEWAY_API_KEY.

Open in Web Open in Cursor 

cursoragent and others added 4 commits August 31, 2026 14:25
Add a standalone tanstack-agent that vendors unpublished
@arcjet/guard/tanstack-ai/v0 from arcjet-js@3e81a91c and demonstrates
guardMiddleware first, tanstackAiContext, and inbound guard() before chat().

Co-authored-by: David Mytton <davidmytton@users.noreply.github.com>
Use streamToText so RUN_ERROR is not an empty 200, accept TANSTACK_MODEL
via openaiCompatibleText, register the example in the root compose file,
and return 400 for invalid JSON and Zod errors.

Co-authored-by: David Mytton <davidmytton@users.noreply.github.com>
Repin the unpublished tanstack-ai/v0 adapter to the merge commit on
arcjet-js main (d730d57). npm still does not export the subpath.

Co-authored-by: David Mytton <davidmytton@users.noreply.github.com>
Guard on main imports @arcjet/transport/http2, which npm 1.11.0 does
not export. Pin both packages to the #6260 merge SHA so the example
can start.

Co-authored-by: David Mytton <davidmytton@users.noreply.github.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​tanstack/​ai-openai@​0.22.310010010098100
Addednpm/​@​tanstack/​ai@​0.52.010010010098100

View full report

@socket-security

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn Medium
Network access: npm @tanstack/ai-openai in module globalThis["fetch"]

Module: globalThis["fetch"]

Location: Package overview

From: examples/tanstack-agent/package-lock.jsonnpm/@tanstack/ai-openai@0.22.3

ℹ Read more on: This package | This alert | What is network access?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should remove all network access that is functionally unnecessary. Consumers should audit network access to ensure legitimate use.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@tanstack/ai-openai@0.22.3. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Low
Environment variable access: npm @tanstack/ai-utils

Location: Package overview

From: examples/tanstack-agent/package-lock.jsonnpm/@tanstack/ai@0.52.0npm/@tanstack/ai-openai@0.22.3npm/@tanstack/ai-utils@0.4.0

ℹ Read more on: This package | This alert | What is environment variable access?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should be clear about which environment variables they access, and care should be taken to ensure they only access environment variables they claim to.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@tanstack/ai-utils@0.4.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@arcjet-review arcjet-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arcjet Review — 🟡 Medium Risk

Decision: Approved

Rationale: Self-contained example addition. New Node HTTP server has input validation (zod), body size cap (32 KiB), message length cap (2000), and clear README warnings that this is a local demo without auth. No hardcoded secrets. The vendored @arcjet/guard and @arcjet/transport packages are pinned to a documented arcjet-js commit (d730d57) with SOURCE.txt attribution and are only used inside the example. Escalation triggers fire (Dockerfile, compose.yaml, package.json) but each is scoped to the new example directory and doesn't touch shared infra. Approving despite Medium risk because the changes are isolated to an example, security caveats are clearly documented, and the guardrails logic itself (inbound guard() + hasFailedOpen check, guardMiddleware first, default-skip DENY) matches the documented @arcjet/guard/tanstack-ai/v0 patterns.

Summary of Changes

Adds examples/tanstack-agent: a Node HTTP server + minimal HTML page demonstrating TanStack AI chat() protected by Arcjet Guard's tanstack-ai/v0 adapter. Inbound prompt-injection screening runs before chat(); guardMiddleware runs first for tool-call rate limiting and PII detection on free-text args. Because @arcjet/guard/tanstack-ai/v0 is not yet on npm, @arcjet/guard and @arcjet/transport are vendored from arcjet-js@d730d57. Also registers the example in the top-level compose.yaml, README, and prepare-to-publish.ts.

Escalation Triggers

  • Dependency Changes: New examples/tanstack-agent/package.json declares dependencies including file:./vendor/... refs and an overrides block for @arcjet/transport.
  • CI/CD Pipeline: New Dockerfile for the example (scoped to examples/tanstack-agent, not shared CI).
  • Infrastructure: New examples/tanstack-agent/compose.yaml and an include entry in the top-level compose.yaml.

Notes

PR exceeds the 1000-line threshold, but ~2500+ of the added lines are vendored README/LICENSE/SKILL.md content from arcjet-js@d730d57 (documented in vendor/SOURCE.txt), not novel logic. The reviewable new code (index.ts, lib/agent.ts, lib/arcjet.ts, index.html, tsconfig/Dockerfile/compose) is small and self-contained.

Path filtering: 95 files excluded by ignore paths. 27 of 122 files included in review.

Review: 762d75cd | Model: anthropic/claude-opus-4-7 | Powered by Arcjet Review

if (!process.env.AI_GATEWAY_API_KEY && !process.env.OPENAI_API_KEY) {
throw new Error("AI_GATEWAY_API_KEY is required");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this env-var check runs on every POST but the value only changes at process start. Consider moving the AI_GATEWAY_API_KEY/OPENAI_API_KEY check next to server.listen(...) so the process fails fast at startup instead of after the first request. Not blocking for an example.

for await (const chunk of request) {
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
size += buffer.byteLength;
if (size > MAX_JSON_BODY_BYTES) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the body exceeds MAX_JSON_BODY_BYTES you throw out of the for await, but the underlying request stream is left in a half-consumed state — the client may see a truncated response before the 413 lands. Consider request.destroy() (or request.resume() to drain) before throwing so the socket is cleaned up promptly.

content: info.result,
});
},
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

screenInbound maps every thrown error to GUARD_UNAVAILABLE / reason: "ERROR". That's the right fail-closed posture for this demo, but it also swallows programming errors (bad rule config, etc.) into a generic "unavailable" message. Consider at least console.error-logging the caught exception so it's not silently hidden during development.

response.writeHead(status, { "content-type": "application/json" });
response.end(JSON.stringify(value));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asPrintableId silently drops a conversation id that fails the printable-ASCII / length check, and the run then goes uncorrelated. For a demo that's fine, but it may confuse a user who supplies an id and sees (uncorrelated) in the UI with no explanation. Consider returning a 400 for a supplied-but-invalid id instead of silently coercing to undefined.

@@ -0,0 +1,97 @@
{
"name": "@arcjet/guard",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vendored @arcjet/guard package.json declares version: "1.11.0" — the same version as the currently published npm release, even though this build is from an unreleased main commit that contains the tanstack-ai/v0 subpath the npm release lacks. If a consumer ever hoists this into a workspace that also depends on @arcjet/guard@1.11.0 from the registry, npm may treat them as satisfying the same version and de-dup unpredictably. Consider bumping to a distinguishable pre-release version (e.g. 1.11.1-vendor.<sha>) — matches the intent of the SOURCE.txt/README note that this is a temporary vendor drop.

@arcjet-review arcjet-review Bot removed ai-review-in-progress needs review Awaiting human review labels Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants