SDK Reference
Complete reference for @blacklake-systems/surface-sdk (current version: 0.1.4).
The SDK is for developers building agents who want to integrate governance directly into their code. If you are using MCP-compatible tools, you may not need the SDK. The MCP proxy handles governance automatically for any tool call that passes through it. The SDK is most useful when you are building your own agent runtime and want to call bl.govern() explicitly before each tool execution.
npm install @blacklake-systems/surface-sdk@0.1.4
Constructor
import { BlackLake } from '@blacklake-systems/surface-sdk';
const bl = new BlackLake(config: BlackLakeConfig);
BlackLakeConfig
interface BlackLakeConfig {
apiKey: string; // Required. Your organisation's API key.
baseUrl?: string; // Optional. Defaults to https://api.blacklake.systems.
// Set to 'http://localhost:3100' for local mode.
}
Local mode: when running npx @blacklake-systems/surface-cli, the API is available at http://localhost:3100. No API key is required in local mode — you can pass any non-empty string.
// Local mode
const bl = new BlackLake({
apiKey: 'local',
baseUrl: 'http://localhost:3100',
});
// Cloud mode
const bl = new BlackLake({
apiKey: process.env.BLACKLAKE_API_KEY!,
// baseUrl defaults to https://api.blacklake.systems
});
All methods on the SDK instance share a single HTTP client configured with the apiKey and baseUrl provided here.
bl.govern()
Evaluate whether an agent is permitted to invoke a tool. Call this before every tool execution.
bl.govern(request: GovernRequest): Promise<GovernResponse>
GovernRequest
interface GovernRequest {
agent: string; // Agent name (as registered)
tool: string; // Tool name (as registered)
action?: Record<string, unknown>; // Optional. The action the agent wants to take. Logged with the evaluation.
context?: Record<string, unknown>; // Optional. Caller-supplied context. Logged with the evaluation.
}
GovernResponse
interface GovernResponse {
decision: 'allow' | 'deny' | 'approval_required' | 'default_deny';
evaluation_id: string; // ID of the recorded evaluation.
policy_id: string | null; // ID of the matched policy, or null.
reason: string; // Human-readable explanation of the decision.
evaluated_at: string; // ISO 8601 UTC timestamp.
approval_id?: string; // Present only when decision === 'approval_required'.
}
Example
const result = await bl.govern({
agent: 'data-pipeline-agent',
tool: 'write-to-database',
action: { table: 'orders', operation: 'insert' },
context: { run_id: 'run_abc123' },
});
if (result.decision === 'allow') {
await writeToDatabase(payload);
} else {
throw new Error(`Blocked by governance: ${result.reason} (${result.evaluation_id})`);
}
bl.agents
bl.agents.create()
Register a new agent.
bl.agents.create(data: {
name: string;
description?: string;
environment: 'development' | 'staging' | 'production';
risk_classification: 'low' | 'medium' | 'high' | 'critical';
approval_mode?: 'auto_approve' | 'require_approval' | 'block';
}): Promise<Agent>
const agent = await bl.agents.create({
name: 'fraud-detection-agent',
environment: 'production',
risk_classification: 'high',
});
bl.agents.list()
List all agents in the organisation, with optional filters.
bl.agents.list(params?: {
environment?: 'development' | 'staging' | 'production';
status?: 'active' | 'suspended' | 'disabled';
}): Promise<Agent[]>
// All active production agents
const agents = await bl.agents.list({
environment: 'production',
status: 'active',
});
bl.agents.get()
Retrieve a single agent by ID.
bl.agents.get(id: string): Promise<Agent>
const agent = await bl.agents.get('agent_01j...');
bl.agents.update()
Update agent fields. Only provided fields are changed.
bl.agents.update(
id: string,
data: Partial<{
name: string;
description: string;
environment: 'development' | 'staging' | 'production';
risk_classification: 'low' | 'medium' | 'high' | 'critical';
status: 'active' | 'suspended' | 'disabled';
approval_mode: 'auto_approve' | 'require_approval' | 'block';
}>
): Promise<Agent>
const updated = await bl.agents.update('agent_01j...', {
risk_classification: 'critical',
});
bl.agents.suspend()
Set agent status to suspended. Governance calls for this agent will return deny.
bl.agents.suspend(id: string): Promise<Agent>
await bl.agents.suspend('agent_01j...');
bl.agents.activate()
Set agent status back to active.
bl.agents.activate(id: string): Promise<Agent>
await bl.agents.activate('agent_01j...');
bl.agents.bindTool()
Create a binding between an agent and a tool, granting the agent access to be governed against that tool.
bl.agents.bindTool(
agentId: string,
toolId: string
): Promise<{ id: string; agent_id: string; tool_id: string; created_at: string }>
await bl.agents.bindTool('agent_01j...', 'tool_01j...');
bl.agents.listTools()
List all tools bound to an agent. Each item includes the binding metadata and the full tool object.
bl.agents.listTools(agentId: string): Promise<ToolBinding[]>
interface ToolBinding {
binding_id: string;
binding_created_at: string;
tool: Tool;
}
const bindings = await bl.agents.listTools('agent_01j...');
for (const b of bindings) {
console.log(b.tool.name, b.binding_created_at);
}
bl.agents.unbindTool()
Remove a binding between an agent and a tool. Returns void. After this call, governance requests from this agent for this tool will be denied.
bl.agents.unbindTool(agentId: string, toolId: string): Promise<void>
await bl.agents.unbindTool('agent_01j...', 'tool_01j...');
bl.tools
bl.tools.create()
Register a new tool.
bl.tools.create(data: {
name: string;
description?: string;
risk_classification: 'low' | 'medium' | 'high' | 'critical';
}): Promise<Tool>
const tool = await bl.tools.create({
name: 'delete-record',
description: 'Permanently deletes a database record',
risk_classification: 'critical',
});
bl.tools.list()
List all tools in the organisation.
bl.tools.list(): Promise<Tool[]>
const tools = await bl.tools.list();
bl.tools.get()
Retrieve a single tool by ID.
bl.tools.get(id: string): Promise<Tool>
const tool = await bl.tools.get('tool_01j...');
bl.policies
bl.policies.create()
Create a new policy.
bl.policies.create(data: {
name: string;
priority: number;
agent_selector?: Record<string, unknown>;
tool_selector?: Record<string, unknown>;
outcome: 'allow' | 'deny' | 'approval_required';
enabled?: boolean;
}): Promise<Policy>
const policy = await bl.policies.create({
name: 'block-high-risk-tools-in-prod',
priority: 1,
agent_selector: { environment: 'production' },
tool_selector: { risk_classification: 'high' },
outcome: 'deny',
});
bl.policies.list()
List all policies, ordered by priority ascending.
bl.policies.list(): Promise<Policy[]>
const policies = await bl.policies.list();
bl.policies.get()
Retrieve a single policy by ID.
bl.policies.get(id: string): Promise<Policy>
const policy = await bl.policies.get('pol_01j...');
bl.policies.update()
Update policy fields. Only provided fields are changed. Updating a policy flushes the policy cache immediately.
bl.policies.update(
id: string,
data: Partial<{
name: string;
priority: number;
agent_selector: Record<string, unknown>;
tool_selector: Record<string, unknown>;
outcome: 'allow' | 'deny' | 'approval_required';
enabled: boolean;
}>
): Promise<Policy>
// Disable a policy without deleting it
const updated = await bl.policies.update('pol_01j...', { enabled: false });
bl.policies.delete()
Permanently delete a policy. Returns void. This flushes the policy cache immediately.
bl.policies.delete(id: string): Promise<void>
await bl.policies.delete('pol_01j...');
bl.evaluations
bl.evaluations.list()
List evaluation records with optional filters. Returns a paginated response.
bl.evaluations.list(params?: {
agent_id?: string;
tool_id?: string;
outcome?: 'allow' | 'deny' | 'approval_required' | 'default_deny';
limit?: number; // Default: 50
offset?: number; // Default: 0
}): Promise<PaginatedResponse<Evaluation>>
// Last 10 denied evaluations for a specific agent
const { data, total } = await bl.evaluations.list({
agent_id: 'agent_01j...',
outcome: 'deny',
limit: 10,
});
console.log(`Showing ${data.length} of ${total} denials`);
bl.evaluations.get()
Retrieve a single evaluation by ID.
bl.evaluations.get(id: string): Promise<Evaluation>
const evaluation = await bl.evaluations.get('eval_01j...');
bl.organisation
Manage the current organisation. The organisation is derived from the API key — there is no organisation ID parameter.
bl.organisation.get()
Fetch the current organisation.
bl.organisation.get(): Promise<Organisation>
const org = await bl.organisation.get();
console.log(org.name, org.contact_email);
bl.organisation.delete()
Permanently delete the organisation and all associated data: agents, tools, policies, bindings, the evaluation audit log, and every API key. The deletion is irreversible. The caller must pass the organisation's exact name as confirmation — any other value is rejected with CONFIRMATION_MISMATCH.
bl.organisation.delete(confirmation: string): Promise<void>
const org = await bl.organisation.get();
await bl.organisation.delete(org.name);
bl.approvals
bl.approvals.list()
List approval records with optional filters. Returns a paginated response.
bl.approvals.list(params?: {
status?: 'pending' | 'approved' | 'rejected' | 'expired';
agent_id?: string;
tool_id?: string;
limit?: number; // Default: 50
offset?: number; // Default: 0
}): Promise<PaginatedResponse<Approval>>
// All pending approvals
const { data, total } = await bl.approvals.list({ status: 'pending' });
console.log(`${data.length} of ${total} pending approvals`);
bl.approvals.get()
Retrieve a single approval by ID.
bl.approvals.get(id: string): Promise<Approval>
const approval = await bl.approvals.get('approval_01j...');
console.log(approval.status, approval.expires_at);
bl.approvals.status()
Fetch only the status fields for an approval. Cheaper than a full get() when polling.
bl.approvals.status(id: string): Promise<ApprovalStatusResponse>
const { status, decided_at, expires_at } = await bl.approvals.status('approval_01j...');
bl.approvals.approve()
Approve a pending approval. Throws APPROVAL_ALREADY_DECIDED if it is not in pending status. Throws APPROVAL_EXPIRED if it has passed its expires_at.
bl.approvals.approve(
id: string,
data: { decided_by: string; reason?: string }
): Promise<Approval>
const approval = await bl.approvals.approve('approval_01j...', {
decided_by: 'ops-team',
reason: 'Verified vendor and amount are correct',
});
console.log(approval.status); // 'approved'
bl.approvals.reject()
Reject a pending approval. Same error conditions as approve().
bl.approvals.reject(
id: string,
data: { decided_by: string; reason?: string }
): Promise<Approval>
const approval = await bl.approvals.reject('approval_01j...', {
decided_by: 'ops-team',
reason: 'Amount exceeds policy limit without CFO sign-off',
});
console.log(approval.status); // 'rejected'
bl.approvals.wait()
Poll the approval until it reaches a terminal status (approved, rejected, or expired) and return the final Approval object. Throws BlackLakeError with code APPROVAL_WAIT_TIMEOUT (status 408) if the timeout elapses before a decision is made.
bl.approvals.wait(
id: string,
options?: {
interval?: number; // Poll interval in ms. Default: 2000.
timeout?: number; // Total wait time in ms. Default: 300000 (5 minutes).
}
): Promise<Approval>
try {
const approval = await bl.approvals.wait('approval_01j...', {
interval: 3000,
timeout: 60_000, // 1 minute
});
if (approval.status === 'approved') {
await executeAction();
} else {
console.log(`Not approved: ${approval.decision_reason}`);
}
} catch (err) {
if (err instanceof BlackLakeError && err.code === 'APPROVAL_WAIT_TIMEOUT') {
console.log('Timed out waiting for approval. Use webhooks for long-running workflows.');
}
throw err;
}
bl.webhooks
bl.webhooks.list()
List all webhooks registered for the organisation.
bl.webhooks.list(): Promise<{ webhooks: Webhook[] }>
const { webhooks } = await bl.webhooks.list();
const active = webhooks.filter(w => w.enabled);
bl.webhooks.create()
Register a new webhook. The secret field in the response is the raw signing secret — it is shown once and cannot be retrieved again. Store it securely.
bl.webhooks.create(data: {
url: string;
events: Array<'approval.created' | 'approval.approved' | 'approval.rejected'>;
enabled?: boolean; // Default: true
}): Promise<CreatedWebhook>
const wh = await bl.webhooks.create({
url: 'https://myapp.example/webhooks/blacklake',
events: ['approval.created', 'approval.approved', 'approval.rejected'],
});
console.log(wh.secret); // save this — not shown again
bl.webhooks.get()
Retrieve a single webhook by ID. Does not return the secret.
bl.webhooks.get(id: string): Promise<Webhook>
const wh = await bl.webhooks.get('wh_01j...');
console.log(wh.url, wh.enabled);
bl.webhooks.update()
Update webhook fields. Only provided fields are changed.
bl.webhooks.update(
id: string,
data: Partial<{
url: string;
events: Array<'approval.created' | 'approval.approved' | 'approval.rejected'>;
enabled: boolean;
}>
): Promise<Webhook>
// Disable a webhook temporarily
const wh = await bl.webhooks.update('wh_01j...', { enabled: false });
bl.webhooks.delete()
Permanently delete a webhook. Returns void.
bl.webhooks.delete(id: string): Promise<void>
await bl.webhooks.delete('wh_01j...');
bl.webhooks.listDeliveries()
List delivery attempts for a webhook, ordered by delivered_at descending. Useful for debugging failed deliveries.
bl.webhooks.listDeliveries(
id: string,
params?: { limit?: number; offset?: number }
): Promise<PaginatedResponse<WebhookDelivery>>
const { data, total } = await bl.webhooks.listDeliveries('wh_01j...', { limit: 10 });
for (const d of data) {
console.log(`${d.event} status=${d.status_code ?? 'error'} ${d.delivered_at}`);
}
Webhook verification
Verify incoming webhook signatures before trusting the payload. Use the signing secret returned at creation time.
import { createHmac, timingSafeEqual } from 'crypto';
export function verifyWebhook(
secret: string,
headers: Record<string, string>,
rawBody: string,
): boolean {
const timestamp = headers['x-blacklake-timestamp'];
const signature = headers['x-blacklake-signature'];
if (!timestamp || !signature) return false;
const expected = 'sha256=' + createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const provided = Buffer.from(signature);
const calculated = Buffer.from(expected);
if (provided.length !== calculated.length) return false;
return timingSafeEqual(provided, calculated);
}
bl.apiKeys
Manage the API keys for the current organisation.
bl.apiKeys.list()
List all API keys (active and revoked). Never returns the raw key or its hash — only the last four characters as key_suffix for identification.
bl.apiKeys.list(): Promise<{ keys: ApiKey[] }>
interface ApiKey {
id: string;
name: string;
key_suffix: string | null;
created_at: string;
revoked_at: string | null;
}
const { keys } = await bl.apiKeys.list();
const active = keys.filter(k => !k.revoked_at);
bl.apiKeys.create()
Generate a new API key. The raw key is returned once in the key field — store it immediately. After this call, the raw key cannot be retrieved again.
bl.apiKeys.create(name: string): Promise<CreatedApiKey>
interface CreatedApiKey {
id: string;
name: string;
key: string; // The raw bl_-prefixed key. Shown ONCE.
created_at: string;
warning: string;
}
const newKey = await bl.apiKeys.create('production-rotated');
process.env.BLACKLAKE_API_KEY = newKey.key; // store securely
bl.apiKeys.revoke()
Revoke an API key by ID. Subsequent requests using that key will return 401 UNAUTHORIZED. The API rejects attempts to revoke the key currently being used to make the call (CANNOT_REVOKE_SELF).
bl.apiKeys.revoke(id: string): Promise<void>
await bl.apiKeys.revoke('key_01j...');
Types
interface Agent {
id: string;
organisation_id: string;
name: string;
description: string | null;
environment: 'development' | 'staging' | 'production';
risk_classification: 'low' | 'medium' | 'high' | 'critical';
status: 'active' | 'suspended' | 'disabled';
approval_mode: 'auto_approve' | 'require_approval' | 'block';
created_at: string;
updated_at: string;
}
interface Tool {
id: string;
organisation_id: string;
name: string;
description: string | null;
risk_classification: 'low' | 'medium' | 'high' | 'critical';
created_at: string;
}
interface Policy {
id: string;
organisation_id: string;
name: string;
priority: number;
agent_selector: Record<string, unknown>;
tool_selector: Record<string, unknown>;
outcome: 'allow' | 'deny' | 'approval_required';
enabled: boolean;
created_at: string;
updated_at: string;
}
interface Evaluation {
id: string;
organisation_id: string;
agent_id: string;
tool_id: string;
policy_id: string | null;
action_payload: Record<string, unknown> | null;
outcome: 'allow' | 'deny' | 'approval_required' | 'default_deny';
evaluated_at: string;
request_context: Record<string, unknown> | null;
}
interface PaginatedResponse<T> {
data: T[];
total: number;
}
interface Organisation {
id: string;
name: string;
contact_email: string | null;
created_at: string;
}
interface ToolBinding {
binding_id: string;
binding_created_at: string;
tool: Tool;
}
interface ApiKey {
id: string;
name: string;
key_suffix: string | null;
created_at: string;
revoked_at: string | null;
}
interface CreatedApiKey {
id: string;
name: string;
key: string;
created_at: string;
warning: string;
}
type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'expired';
interface Approval {
id: string;
organisation_id: string;
evaluation_id: string;
agent_id: string;
tool_id: string;
policy_id: string | null;
action_payload: Record<string, unknown> | null;
request_context: Record<string, unknown> | null;
status: ApprovalStatus;
decided_by: string | null;
decision_reason: string | null;
decided_at: string | null;
created_at: string;
expires_at: string;
}
interface ApprovalStatusResponse {
status: ApprovalStatus;
decided_at: string | null;
expires_at: string;
}
type WebhookEvent = 'approval.created' | 'approval.approved' | 'approval.rejected';
interface Webhook {
id: string;
organisation_id: string;
url: string;
secret_suffix: string | null;
events: WebhookEvent[];
enabled: boolean;
created_at: string;
}
interface CreatedWebhook {
id: string;
url: string;
secret: string; // Raw signing secret. Shown ONCE.
secret_suffix: string;
events: WebhookEvent[];
enabled: boolean;
created_at: string;
warning: string;
}
interface WebhookDelivery {
id: string;
webhook_id: string;
event: WebhookEvent;
payload: Record<string, unknown>;
status_code: number | null; // null on network error
response_body: string | null; // truncated to 500 chars
error: string | null; // network/timeout error message
delivered_at: string;
duration_ms: number | null;
}
Error Handling
All SDK methods throw BlackLakeError on non-2xx responses.
class BlackLakeError extends Error {
readonly status: number; // HTTP status code
readonly code: string; // Machine-readable error code
readonly message: string; // Human-readable explanation
}
import { BlackLake, BlackLakeError } from '@blacklake-systems/surface-sdk';
try {
await bl.agents.get('agent_does_not_exist');
} catch (err) {
if (err instanceof BlackLakeError) {
console.error(`[${err.status}] ${err.code}: ${err.message}`);
// [404] AGENT_NOT_FOUND: Agent agent_does_not_exist not found
} else {
throw err;
}
}
Common error codes:
| Code | Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid API key. |
VALIDATION_ERROR | 400 | Request body failed schema validation. The message identifies the field. |
AGENT_NOT_FOUND | 404 | The agent ID or name does not exist in this organisation. |
TOOL_NOT_FOUND | 404 | The tool ID or name does not exist in this organisation. |
POLICY_NOT_FOUND | 404 | The policy ID does not exist in this organisation. |
EVALUATION_NOT_FOUND | 404 | The evaluation ID does not exist in this organisation. |
ORG_NOT_FOUND | 404 | The organisation does not exist (typically only after deletion). |
API_KEY_NOT_FOUND | 404 | The API key ID does not exist in this organisation. |
BINDING_EXISTS | 409 | The agent-tool binding already exists. |
CONFIRMATION_MISMATCH | 400 | The deletion confirmation string does not match the organisation name. |
CANNOT_REVOKE_SELF | 400 | Cannot revoke the API key currently being used to make the request. |
APPROVAL_NOT_FOUND | 404 | The approval ID does not exist in this organisation. |
APPROVAL_ALREADY_DECIDED | 400 | The approval has already been approved or rejected. |
APPROVAL_EXPIRED | 400 | The approval has passed its 24-hour expiry and can no longer be decided. |
WEBHOOK_NOT_FOUND | 404 | The webhook ID does not exist in this organisation. |
APPROVAL_WAIT_TIMEOUT | 408 | bl.approvals.wait() timed out before the approval reached a terminal status. |
RATE_LIMITED | 429 | Per-key or per-IP rate limit exceeded. Retry after the Retry-After header. |
PAYLOAD_TOO_LARGE | 413 | Request body exceeds the per-route size limit (8KB on /v1/govern, 32KB elsewhere). |
INTERNAL_SERVER_ERROR | 500 | Unexpected server error. Should not happen in normal operation. |