Skip to main content

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

OptionEnvironment VariableTypeDefaultDescription
apiKeyCONTROLZERO_API_KEYstring--Your project API key. Required.
projectIdCONTROLZERO_PROJECT_IDstring--Your project ID. Required.
baseUrlCONTROLZERO_BASE_URLstringhttps://api.controlzero.devThe Control Zero API base URL.
cacheDirCONTROLZERO_CACHE_DIRstring~/.controlzero/cacheDirectory for caching policy bundles.
refreshIntervalCONTROLZERO_REFRESH_INTERVALnumber60Seconds between policy bundle refresh checks.
logDecisionsCONTROLZERO_LOG_DECISIONSbooleantrueWhether to send decision logs to the server.
failOpenCONTROLZERO_FAIL_OPENbooleanfalseIf true, allow actions when policy evaluation fails.
secretsEnabledCONTROLZERO_SECRETS_ENABLEDbooleanfalseEnable 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:

NameTypeRequiredDescription
requestCheckRequestYesObject 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

Optional

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:

NameTypeRequiredDescription
providerProviderTypeYesThe 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:

NameTypeRequiredDescription
namestringYesThe 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

ProviderValueDescription
OpenAI"openai"OpenAI API keys
Anthropic"anthropic"Anthropic API keys
Google"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';