feat(examples): add TanStack AI chat() guardrails example - #207
feat(examples): add TanStack AI chat() guardrails example#207davidmytton wants to merge 4 commits into
Conversation
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>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
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.
|
There was a problem hiding this comment.
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"); | ||
| } | ||
|
|
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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, | ||
| }); | ||
| }, | ||
| }, |
There was a problem hiding this comment.
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)); | ||
| } | ||
|
|
There was a problem hiding this comment.
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", | |||
There was a problem hiding this comment.
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.
Adds
examples/tanstack-agent: a small Nodechat({ middleware })support agent protected by@arcjet/guard/tanstack-ai/v0(arcjet-js#6260).Inbound
guard()runs beforechat().guardMiddlewareis first in the middleware list soonBeforeToolCallrate-limits lookups and scans free-textnoteargs for PII. Default DENY is{ type: "skip", result: ArcjetDenialResult }. Correlation is a caller-ownedsessionId— neverctx.threadId.The adapter is on
mainbut not on npm yet, so@arcjet/guardand@arcjet/transport(./http2) are vendored fromd730d57. Repin once@arcjet/guard/tanstack-ai/v0publishes.Docs:
/guards/tanstack-ai/. Runexamples/tanstack-agentwithARCJET_KEYandAI_GATEWAY_API_KEY.