Skip to content
Browse documentation

Node SDK

`@cirvix/agent-control` wraps your tool-executor boundary and throws a typed `CirvixDenied` your agent can plan around.

Install

TerminalShell
$ npm install @cirvix/agent-control

Zero runtime dependencies. Node 20 or later. ESM only.

guard.wrap()

agent.tsTypeScript
import { readFile } from "node:fs/promises";import { guard, parseRules, CirvixDenied, CirvixHeld } from "@cirvix/agent-control"; const rules = parseRules(JSON.parse(await readFile("cirvix.policy.json", "utf8"))); const tools = guard.wrap(myTools, {  agent: "pr-triage",  environment: process.env.CIRVIX_ENV ?? "local",  rules,}); try {  await agent.invoke(input);} catch (err) {  if (err instanceof CirvixHeld) {    console.log(err.approvers);   // ["platform-oncall"] — a person can release this  } else if (err instanceof CirvixDenied) {    console.log(err.policy);      // "deny-dotenv-read"    console.log(err.remediation); // 'secrets.get("STRIPE_KEY")'    console.log(err.decisionId);  // pass to `cirvix why`  }  throw err;}
The option is rules, not policyDir
wrap forwards its options straight to the Guard constructor, which takes an in-memory rule array. Passing an option Guard does not have leaves rules empty — and an empty rule set denies everything. Load from disk with parseRules, or use loadPolicy from the testing entry point.

What wrap accepts, and what it gives up

It returns the same shape it was given — an object of name → function, an array of tool objects carrying a callable (func, invoke, call, execute, handler, _call, run), or a single function. Descriptions, schemas, and framework metadata are preserved by reference, and fn.name survives, because frameworks introspect both.

What you give up versus the gateway is the property that matters most: wrap governs the tools you hand it. A tool the agent reaches directly is never evaluated. That trade is stated rather than glossed, because an operator who believes they are equivalent will not understand why a call was never seen.

CirvixDenied and CirvixHeld

PropertyTypeMeaning
policystring \| nullThe rule that decided the call
decisionIdstring \| nullPass to cirvix why for the full record
reasonstring \| nullHuman-readable explanation
remediationstring \| nullA suggested next step, when one exists
appealablebooleanWhether re-requesting with an approval could succeed
approversstring[]CirvixHeld only — who can release the call

They are distinct types because they call for different behaviour: a denial means re-plan, a hold means this exact call may still happen once somebody says yes. Collapsing them teaches agents to treat both as failure.

Testing a policy

policy.test.tsTypeScript
import { evaluate } from "@cirvix/agent-control/testing"; test("production writes are held for a human", async () => {  const decision = await evaluate({    policyDir: "./policies",    agent: "deploy-bot",    action: "k8s.apply",    resource: "production/checkout",    context: { environment: "production" },  });   expect(decision.verdict).toBe("hold");  expect(decision.approvers).toContain("platform-oncall");});

evaluate accepts one rule source: rules, policy/policyFile, or policyDir. The default context is permissive on purpose — inside the workspace, no external egress, no secret touched — so a test asserting a denial is denied by the rule it is testing rather than by a restrictive default.

Secret handles

secrets.tsTypeScript
import { SecretsClient } from "@cirvix/agent-control/secrets"; const secrets = new SecretsClient({  apiUrl: process.env.CIRVIX_API_URL,  apiKey: process.env.CIRVIX_API_KEY,   // cvx_…  agent: "billing-bot",}); const handle = await secrets.get("STRIPE_RESTRICTED_KEY");// "sec_handle_9f2c…" — opaque, and useless anywhere the secret does not sanction

SecretsClient is a class, not a module-level singleton. Pass it to a Guard as secrets and substitution happens inside authorize — all or nothing, because partial substitution would send a real credential alongside a literal handle.

Subpath exports

/policy · /guard · /gateway · /daemon · /audit · /secrets · /testing

Edit this page on GitHub
Was this page helpful?