feat(examples): add LangChain createAgent guardrails example - #199
feat(examples): add LangChain createAgent guardrails example#199davidmytton wants to merge 6 commits into
Conversation
Add a standalone examples/langchain-agent matching eve/mastra/langgraph/claude. Pins unpublished @arcjet/guard/langchain/v1 from arcjet-js@ce4051a6 (david/cursor/guard-langchain-v1-f9b3) via file:./vendor/arcjet-guard. Co-authored-by: David Mytton <davidmytton@users.noreply.github.com>
Re-vendor unpublished @arcjet/guard/langchain/v1 from 8005e9a17d63bf051bdda88b7f307b940b7b15ca and slim the example around createAgent, the two denial envelopes, and caller-owned thread_id. Co-authored-by: David Mytton <davidmytton@users.noreply.github.com>
Rebuild @arcjet/guard/langchain/v1 from main @ c49abcc1. The langchain/v1 subpath is still unpublished on npm, so keep the file:./vendor/arcjet-guard pin. Co-authored-by: David Mytton <davidmytton@users.noreply.github.com>
Drop the model parameter and AgentModel alias left over from verification, use import type for BaseMessage (biome useImportType), derive the LangChain context once per request, and split the inbound DENY and fail-open branches back apart so each reads on its own. Co-authored-by: David Mytton <davidmytton@users.noreply.github.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
All alerts resolved. Learn more about Socket for GitHub. This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored. Ignoring alerts on:
|
There was a problem hiding this comment.
Arcjet Review — 🟡 Medium Risk
Decision: Needs Review
Rationale: Author explicitly marks the PR as Draft pending a published @arcjet/guard release to replace the vendored file: dependency, so this is not ready to merge on that ground alone. Beyond the vendor-pin gate, the change is a new self-contained example (mirrors existing langgraph-agent / claude-agent siblings), introduces a new package.json and Dockerfile (dependency-changes and ci-cd-changes triggers), and ships an intentionally unauthenticated /api/agent demo endpoint. Diff size (>1000 lines) is dominated by the vendored README and SKILL.md rather than executable code. Code-level review of index.ts / lib/agent.ts / lib/arcjet.ts shows input validation, fail-closed defaults on the guard, correct two-envelope denial handling, and no hardcoded secrets — but a human should confirm the vendored dist matches the recorded SHA and repin before merge.
Summary of Changes
Adds examples/langchain-agent, a new standalone LangChain JS createAgent example protected by Arcjet Guard, with an unauthenticated demo HTTP server, a Dockerfile, and a vendored @arcjet/guard build (file:./vendor/arcjet-guard pinned to arcjet/arcjet-js c49abcc1, pending npm release). Registered in the root README and scripts/prepare-to-publish.ts.
Escalation Triggers
- Dependency Changes: New examples/langchain-agent/package.json with langchain, @langchain/core, @langchain/openai, zod, and a file:./vendor/arcjet-guard dependency for an unreleased subpath.
- CI/CD Pipeline: New examples/langchain-agent/Dockerfile (author notes it is unverified because no Docker daemon is available in the devcontainer).
Security Flags
- [LOW] Authentication & Authorization (examples/langchain-agent/index.ts:65): The /api/agent endpoint is unauthenticated and the server binds to 0.0.0.0. This is explicitly documented as a local demo pattern (matching sibling examples) and the README warns against deploying without adding auth/rate limiting, so it is intended behaviour — flagging so a human reader can confirm the demo scope is acceptable for this repo.
- [MEDIUM] Vulnerable Components / Supply Chain (examples/langchain-agent/package.json:29): @arcjet/guard is pinned to a vendored directory (file:./vendor/arcjet-guard) built from arcjet/arcjet-js@c49abcc1 rather than a published npm release. The recorded SHA in vendor/SOURCE.txt should be re-verified against the vendored dist/ before merge, and the pin swapped to the published version once shipped. The Files Changed list does not include vendor/arcjet-guard/dist/**, so a human should confirm the dist/ tree is present in the branch and matches the SHA the author claims to have byte-compared.
- [LOW] Insufficient Logging / Error Handling (examples/langchain-agent/index.ts:78): The /api/agent catch block echoes
error.messageback to the client with status 500 (e.g. an 'AI_GATEWAY_API_KEY is required' string), which can leak internal configuration hints. Acceptable for a local demo but worth flagging.
Review Focus Areas
- Before merge, confirm the vendor pin has been swapped for a published @arcjet/guard version that actually exports the langchain/v1 subpath, and that peer ranges (langchain >=1.2.0 <2, @langchain/core >=1 <2) still hold.
Author explicitly states the PR is draft pending this repin; merging with a file: dependency to a vendored monorepo subdirectory is not appropriate for a published example. - Verify the recorded SHA c49abcc1f9afce7d284b6c294d0dcee5916ada86 matches the vendored dist/ contents (which are not visible in this diff) and that this is a merge commit on arcjet-js main, not a dangling branch build.
The dist/ tree is what runs; the author's byte-identity claim is the only assurance without a diff of the compiled output. - Is the unauthenticated /api/agent endpoint plus 0.0.0.0 bind acceptable given how this example is distributed (docker-build.yml matrix excluded, README warning present)?
Matches sibling langgraph-agent / claude-agent pattern, but worth a conscious human sign-off since it forwards user text into a paid model call. - In guardMiddleware's rules callback, when readOrderId(input) is undefined the token bucket key falls back to
order:${toolName}(i.e.order:notify_warehouse). Is that the intended shared-bucket-per-tool behaviour, or should the middleware skip rate limiting when no orderId is present?
Silent fallback keys can make a rate limit look scoped-per-order when in fact all callers share one bucket.
Notes
Diff exceeds the 1000-line size threshold, but the bulk is the vendored @arcjet/guard README (~2100 lines) and SKILL.md (~300 lines). Executable example code (index.ts, lib/agent.ts, lib/arcjet.ts, index.html) is well under 800 lines and reads cleanly. vendor/arcjet-guard/dist/** is referenced by package.json's main/types/exports but does not appear in the Files Changed list — either the path filter excluded it or the dist is missing; a human should confirm on the branch.
Path filtering: 77 files excluded by ignore paths. 22 of 99 files included in review.
Review: 2abe7c2f | Model: anthropic/claude-opus-4-7 | Powered by Arcjet Review
|
@SocketSecurity ignore npm/langsmith@0.9.0 |
arcjet-rei
left a comment
There was a problem hiding this comment.
Do we want to vendor the SDK in the GH tree, or create a build job to bootstrap a vendor directory when the build is run on the example on download? The way this is set up right now, a very small percentage of the code is actually the example, and the rest is vendored SDK code.
|
It's vendored temporarily until we do the SDK release. We can remove it all afterwards |
Per PR review feedback — the devcontainer adds dependency surface without value to end users. Co-authored-by: David Mytton <davidmytton@users.noreply.github.com>
|
@SocketSecurity ignore npm/@langchain/openai@1.2.8 |
arcjet-rei
left a comment
There was a problem hiding this comment.
Now that we have a published version of the SDK that includes the LangChain helplers, should this be regenerated to not vendor the SDK?
Drop the vendored arcjet-js build now that @arcjet/guard/langchain/v1 ships on npm. Refresh lockfile and align Dockerfile/gitignore with other published examples. Co-authored-by: David Mytton <davidmytton@users.noreply.github.com>
Yep - removed in c04c433 |
|
@SocketSecurity ignore npm/@langchain/langgraph-checkpoint@1.1.5 |
Adds
examples/langchain-agent: a LangChain JScreateAgentsupport agent protected by Arcjet Guard, alongside themastra-agent/langgraph-agent/claude-agentsiblings. Registered in the root README andscripts/prepare-to-publish.ts.This is
createAgent+wrapToolCall, not LangGraph'sStateGraph+ToolNode(that islanggraph-agent). Docs slug is/guards/langchain-js/.The two denial envelopes stay distinct:
guardToolreturns a plain object,wrapToolCallmust return a realToolMessage(a bare object crashes the messages reducer). Neither setsstatus: "error"and neither throws, so denials are read asarcjetDeniedon the payload.humanInTheLoopMiddlewareis HITL, not a policy gate — it appears only as a comment, and nothing denies inafterModel.thread_idis caller-owned and never minted.Vendor pin.
@arcjet/guard/langchain/v1merged in arcjet-js#6248 (c49abcc1) but is not on npm yet, so@arcjet/guardis pinned tofile:./vendor/arcjet-guard, same aslanggraph-agent/claude-agent. SHA is invendor/SOURCE.txt. Draft until that ships.Verified. biome lint,
tsc --noEmit,npm audit, andnpm ciall clean. Behaviour exercised through a realcreateAgentloop withFakeToolCallingModel: inbound fail-closed, both DENY envelopes with the handler never running, ALLOW still reaching it, and a branded tool guarded exactly once. Also assertedrulesreceives real tool args — otherwise the rate-limit key would silently beorder:undefinedand the PII scan would never run.