Node.js SDK
The Control Zero Node.js SDK provides policy enforcement for AI agents running in JavaScript and TypeScript environments.
Installation
npm install @controlzero/sdk
Requirements:
- Node.js 18 or later
- TypeScript 5.0+ (optional, for type-checked usage)
Configuration
Basic Configuration
import { ControlZero } from '@controlzero/sdk';
const client = new ControlZero({
apiKey: 'cz_live_your_api_key_here',
projectId: 'proj_abc123',
});
await client.initialize();
Environment Variables
You can configure the SDK using environment variables instead of passing arguments directly:
export CONTROLZERO_API_KEY="cz_live_your_api_key_here"
export CONTROLZERO_PROJECT_ID="proj_abc123"
import { ControlZero } from '@controlzero/sdk';
// Reads from environment variables automatically
const client = new ControlZero();
await client.initialize();
Configuration Options
| Option | Environment Variable | Type | Default | Description |
|---|---|---|---|---|
apiKey | CONTROLZERO_API_KEY | string | -- | Your project API key. Required. |
projectId | CONTROLZERO_PROJECT_ID | string | -- | Your project ID. Required. |
baseUrl | CONTROLZERO_BASE_URL | string | https://api.controlzero.dev | The Control Zero API base URL. |
cacheDir | CONTROLZERO_CACHE_DIR | string | ~/.controlzero/cache | Directory for caching policy bundles. |
refreshInterval | CONTROLZERO_REFRESH_INTERVAL | number | 60 | Seconds between policy bundle refresh checks. |
logDecisions | CONTROLZERO_LOG_DECISIONS | boolean | true | Whether to send decision logs to the server. |
failOpen | CONTROLZERO_FAIL_OPEN | boolean | false | If true, allow actions when policy evaluation fails. |
secretsEnabled | CONTROLZERO_SECRETS_ENABLED | boolean | false | Enable encrypted secrets vault access. See Secrets Management. |
Advanced Configuration
const client = new ControlZero({
apiKey: 'cz_live_your_api_key_here',
projectId: 'proj_abc123',
cacheDir: '/var/cache/controlzero',
refreshInterval: 30,
logDecisions: true,
failOpen: false,
});
await client.initialize();
Basic Usage
Checking an Action
The primary method is check(), which evaluates an action against your policies:
const decision = await client.check({
action: 'mcp.tool.call',
resource: 'mcp://filesystem/read_file',
context: {
agent_id: 'agent-001',
session_id: 'sess_xyz',
user_id: 'user-42',
},
});
if (decision.allowed) {
console.log('Action is permitted');
// Proceed with the action
} else {
console.log(`Action denied: ${decision.reason}`);
console.log(`Matched policy: ${decision.policyId}`);
}
Enforcing an Action
The enforce() method works like check() but throws an exception if the action is denied:
import { PolicyDeniedError } from '@controlzero/sdk';
try {
await client.enforce({
action: 'mcp.tool.call',
resource: 'mcp://filesystem/write_file',
context: { agent_id: 'agent-001' },
});
// Action is allowed -- proceed
await writeFile('/data/output.csv', data);
} catch (error) {
if (error instanceof PolicyDeniedError) {
console.log(`Blocked: ${error.message}`);
console.log(`Policy: ${error.policyId}`);
}
}
You can also use the convenience overload with positional arguments:
await client.enforce('mcp.tool.call', 'mcp://filesystem/write_file');
Batch Checking
Check multiple actions in a single call:
const decisions = await client.checkBatch([
{
action: 'mcp.tool.call',
resource: 'mcp://filesystem/read_file',
context: { agent_id: 'agent-001' },
},
{
action: 'api.request',
resource: 'https://api.example.com/v1/data',
context: { agent_id: 'agent-001' },
},
]);
for (const decision of decisions) {
console.log(`${decision.resource}: ${decision.allowed ? 'allowed' : 'denied'}`);
}
Manual Policy Refresh
Force an immediate refresh of the policy bundle:
await client.refreshPolicies();
API Reference
ControlZero
The main client class.
new ControlZero(options?)
Creates a new Control Zero client. See Configuration Options for available parameters.
initialize(): Promise<void>
Fetches policies from the server (or local directory) and starts the background refresh timer. Must be called before using check(), enforce(), or getSecret().
check(request): Promise<Decision>
Evaluates an action against the active policies.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
request | CheckRequest | Yes | Object with action, resource, and optional context. |
Returns: Promise<Decision>
enforce(request): Promise<void>
Like check(), but throws PolicyDeniedError if the action is denied.
Parameters: Same as check(), or positional (action, resource, context?).
Throws: PolicyDeniedError if the action is denied.
checkBatch(requests): Promise<Decision[]>
Evaluates multiple actions in a single call.
refreshPolicies(): Promise<void>
Forces an immediate download and activation of the latest policy bundle.
close(): void
Shuts down the client and releases resources. Stops the background refresh timer and clears any cached secrets from memory.
CheckRequest
interface CheckRequest {
/** The action being attempted (e.g., "mcp.tool.call"). */
action: string;
/** The target resource URI (e.g., "mcp://filesystem/read_file"). */
resource: string;
/** Additional context for condition evaluation. */
context?: Record<string, string>;
}
Decision
interface Decision {
/** Whether the action is permitted. */
allowed: boolean;
/** Human-readable explanation of the decision. */
reason: string;
/** The ID of the policy that matched, if any. */
policyId?: string;
/** The ID of the specific rule that matched, if any. */
ruleId?: string;
/** The resource that was evaluated. */
resource: string;
/** When the decision was made. */
timestamp: Date;
}
PolicyDeniedError
class PolicyDeniedError extends ControlZeroError {
readonly action: string;
readonly resource: string;
readonly policyId?: string;
readonly ruleId?: string;
}
Usage with MCP
Control Zero integrates naturally with the Model Context Protocol. Here is an example of wrapping MCP tool calls with policy enforcement:
import { ControlZero } from '@controlzero/sdk';
const cz = new ControlZero();
await cz.initialize();
async function callMCPTool(
server: string,
tool: string,
args: Record<string, unknown>
): Promise<unknown> {
const resource = `mcp://${server}/${tool}`;
// Enforce the policy before calling the tool
await cz.enforce('mcp.tool.call', resource);
// Policy check passed -- call the tool
return mcpClient.callTool(server, tool, args);
}
Secrets Management
Secrets management is entirely optional. By default, the SDK operates in policy-only mode -- you manage your own LLM provider keys through environment variables, a secrets manager, or any method you prefer. Enable the vault only if you want Control Zero to handle key storage and injection.
Policy-Only Mode (Default)
In the default mode, you manage your own provider keys. The SDK enforces policies without touching your keys:
import { ControlZero } from '@controlzero/sdk';
// You manage your own keys -- Control Zero only enforces policies
const cz = new ControlZero({
apiKey: 'cz_live_your_api_key_here',
});
await cz.initialize();
// Your key, your way
const openaiKey = process.env.OPENAI_API_KEY;
// Control Zero enforces the policy before the call
await cz.enforce('llm.generate', 'model/gpt-4');
Vault Mode (Optional)
Enable the vault to let the SDK retrieve your provider keys from Control Zero's encrypted storage. Keys are fetched once during initialize() and cached in memory. Subsequent getSecret() calls are in-memory lookups with no network overhead.
import { ControlZero } from '@controlzero/sdk';
const cz = new ControlZero({
apiKey: 'cz_live_your_api_key_here',
secretsEnabled: true, // opt into vault access
});
await cz.initialize();
// Retrieve the decrypted key from memory (no network call)
const openaiKey = cz.getSecret('openai');
// Policy enforcement works the same way in both modes
await cz.enforce('llm.generate', 'model/gpt-4');
Secrets API Reference
getSecret(provider): string
Returns the decrypted plaintext value for an LLM provider. This is an in-memory lookup -- no network call is made.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
provider | ProviderType | Yes | The provider name: "openai", "anthropic", "google", "cohere", or "custom". |
Returns: The decrypted plaintext API key.
Throws: SecretNotFoundError if secrets are not enabled or no matching secret exists.
getSecretByName(name): string
Returns the decrypted plaintext value for a secret identified by its name (as stored in the dashboard).
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The human-readable name of the secret. |
Returns: The decrypted plaintext secret value.
Throws: SecretNotFoundError if secrets are not enabled or no matching secret exists.
Supported Providers
| Provider | Value | Description |
|---|---|---|
| OpenAI | "openai" | OpenAI API keys |
| Anthropic | "anthropic" | Anthropic API keys |
"google" | Google AI (Gemini) API keys | |
| Cohere | "cohere" | Cohere API keys |
| Custom | "custom" | Any other provider |
For more details on how secrets are stored and secured, see Secrets Management.
Error Handling
import {
ControlZero,
ConnectionError,
PolicyDeniedError,
SecretNotFoundError,
NotInitializedError,
} from '@controlzero/sdk';
const client = new ControlZero();
try {
await client.initialize();
} catch (error) {
if (error instanceof ConnectionError) {
// Cannot reach Control Zero server
}
}
try {
const decision = await client.check({
action: 'mcp.tool.call',
resource: 'mcp://filesystem/read_file',
});
} catch (error) {
if (error instanceof NotInitializedError) {
// Client not initialized -- call initialize() first
}
}
CommonJS Support
The SDK supports both ESM and CommonJS:
// ESM
import { ControlZero } from '@controlzero/sdk';
// CommonJS
const { ControlZero } = require('@controlzero/sdk');
TypeScript
The SDK is written in TypeScript and ships with full type declarations. All types are exported from the main entry point:
import type { CheckRequest, Decision, ProviderType } from '@controlzero/sdk';