@agenticcontrolplane/governance
v0.2.1
Published
Thin Node SDK for the Agentic Control Plane governance hook protocol. Wrap any tool call; ACP decides allow/deny/redact.
Maintainers
Readme
@agenticcontrolplane/governance
Thin Node SDK for the Agentic Control Plane governance hook protocol.
Wraps the two endpoints ACP exposes:
POST /govern/tool-use— pre-tool check (allow / deny / ask)POST /govern/tool-output— post-tool audit + PII scan
Same protocol Claude Code uses. Works with any Node agent runtime.
Install
npm install @agenticcontrolplane/governanceUsage
import { withContext, governed } from "@agenticcontrolplane/governance";
// Wrap your tool handlers once.
const search = governed("web_search", async ({ query }: { query: string }) => {
return doSearch(query); // your code, your credentials
});
// In each request handler, bind the end user's JWT to the async context.
app.post("/run", async (req, res) => {
const token = req.header("authorization")!.slice("Bearer ".length);
await withContext({ userToken: token }, async () => {
const result = await search({ query: req.body.q });
// result is the tool's output, or "tool_error: <reason>" on deny.
res.json({ result });
});
});What happens per call
preToolUsePOSTs to/govern/tool-usewith{ tool_name, tool_input, session_id }+Authorization: Bearer <user-jwt>.- Gateway evaluates policy, rate limits, scope, PII → returns
{ decision, reason }. - On
deny, the wrapped handler short-circuits with"tool_error: <reason>"(model can see and adapt). - On
allow, your handler runs. Result is sent to/govern/tool-outputfor audit. - If gateway returns
action: "redact", the redacted output is returned to the caller.
Fail-open
Network errors, timeouts (5s default), or a gateway answer with no decision → the tool proceeds with reason "fail-open (<cause>): <detail>", where the cause is one of not-configured (no withContext() in scope), unreachable, or gateway-error. Governance is never a single point of failure for the agent — and never silent about it: the first ungoverned call per cause per process is announced on console.warn, and every ungoverned call appends a line to ~/.acp/lapse.log (ACP_LAPSE_LOG=<path> overrides, ACP_LAPSE_LOG=off disables) — the same ledger the Claude Code hook writes. A 4xx that carries a decision (a 429 rate-limit deny, say) is honored as the verdict, not treated as an outage.
Both planes in one call
governed() covers what your agent does. The ACP proxy covers what it
spends. init() wires both — call it before you construct a model client,
since the SDKs read their config from the environment at construction time:
import { init } from "@agenticcontrolplane/governance";
import Anthropic from "@anthropic-ai/sdk";
init(); // governance + proxy
const client = new Anthropic(); // now priced and metered by ACPConstructing clients explicitly instead? Skip init() and pass the options:
import { modelClientOptions } from "@agenticcontrolplane/governance";
const client = new Anthropic(modelClientOptions("anthropic"));Set ACP_API_KEY=gsk_... from the console.
Routing is all-or-nothing per provider. init() either sets both the base
URL and the key, or leaves that provider completely alone and warns — a
half-applied provider (ACP's URL against your real vendor key) is just a 401.
So if OPENAI_BASE_URL already points at your own gateway, ACP won't silently
reroute you; it tells you those calls aren't being priced.
Three API shapes, each on its own mount — "anthropic", "openai" (chat
completions), and "openai-responses". The last two are not
interchangeable: /v1 serves chat completions, /openai/v1 serves responses.
Framework adapters
This package is the core. For framework-native ergonomics see:
@agenticcontrolplane/governance-anthropic— Anthropic Agent SDK + Messages APIacp-crewai(Python) — CrewAIacp-langchain(Python) — LangChain / LangGraph
API
configure(partial: Partial<Config>): void
getConfig(): Config
withContext(ctx, fn): Promise<T>
getContext(): GovernanceContext | undefined
preToolUse(toolName, toolInput?): Promise<{ allowed, reason, decision }>
postToolOutput(toolName, toolInput, toolOutput): Promise<PostToolOutputResponse | null>
governed<I, O>(toolName, handler, opts?): AsyncHandler<I, O | string>
// proxy plane — price and meter model calls
init(options?: InitOptions): Record<string, string> // both planes
modelBaseUrl(shape?: ModelShape): string
modelClientOptions(shape?: ModelShape): { baseURL, apiKey }
apiKey(): string // reads ACP_API_KEYLicense
MIT
