DMZAgentDocumentation
Sign in Start free
DMZAGENT · SDK SPEC v0.6.0 · PYTHON · TYPESCRIPT · C# · JAVA

DMZAgent SDK reference

One specification. Four SDKs. The same constructor shape, the same method names (in each language's idiomatic casing), the same return types, the same error hierarchy, and the same wire protocol. Pick your language; the code on this page tracks your choice.

Language

Section 1 · What DMZAgent is

DMZAgent is a governance layer for production AI agents. Your agent emits a stream of events — utterances, tool calls, observations. DMZAgent scores each event against the policy Canons installed in your workspace, trips a circuit breaker on risk, and writes a signed, hash-chained ledger entry that audit can verify offline.

The SDK is the customer-facing surface. You install one package, configure an API key, and wrap your existing agent code in two patterns: emit events for everything the agent does, and guard sensitive actions with a circuit-breaker check before they commit.

Section 2 · The three modules

DMZAgent has three modules. The SDK talks to all three. You do not need to choose between them — they share the same workspace, the same ledger, and the same API key.

ModuleRoleWhat the SDK does with it
Record Record & risk Every event you emit is recorded by DMZAgent and scored against your installed policies, producing a signed ledger entry.
Recognize Recognize Reads the behavior history DMZAgent has built and forecasts whether the next action is on-trend, drifting, or out of distribution.
Enforce Enforce Runs the Recognize score against your policies; returns allow, review, or block. The SDK surfaces this as a circuit breaker.

Section 3 · One spec, four SDKs

Every DMZAgent SDK is generated from a single language-agnostic specification — dmzagent-sdk-spec — with a shared contract-test corpus. A given spec version (for example, 0.6.0) corresponds to release tags in every SDK repo. No SDK ships a version the spec has not blessed.

LanguagePackageRuntimeRepo
Pythondmzagent on PyPIPython 3.10+dmzagent-sdk-python
TypeScript / Node@dmzagent/sdk on npmNode 18.17+dmzagent-sdk-typescript
C#DMZAgent.Sdk on NuGetnet8.0+dmzagent-sdk-csharp
Javadev.dmzagent:dmzagent-sdk on Maven CentralJava 17+dmzagent-sdk-java

Section 4 · Install the SDK

Pick the language and run one command.

Python
pip install dmzagent
TypeScript
npm install @dmzagent/sdk
C#
dotnet add package DMZAgent.Sdk
Javapom.xml
<dependency>
  <groupId>dev.dmzagent</groupId>
  <artifactId>dmzagent-sdk</artifactId>
  <version>0.6.0</version>
</dependency>

Section 5 · Get an API key

API keys are issued from the dashboard at /settings/api-keys. Keys are scoped to a single workspace, carry a role (tenant_admin, analyst, viewer, or auditor), and start with the prefix ck_. Every SDK rejects an API key without the ck_ prefix at construction time.

Tip

Treat the key like a database password. Store it in a secret manager, inject it via environment variable, and rotate it on personnel changes. Cookies from the dashboard are not accepted on SDK endpoints — only Bearer keys (per #101).

Subject type

Every ingestion call requires a subject_type — one of "chat", "sensor", "lead", "ticket", or "journey". This tells DMZAgent which trace pattern (§C.1) to use for grouping frames and evaluating deviations. A subject can appear with different types across calls — each ingestion tags the subject's role in that specific event.

Section 6 · Send your first event

The smallest useful program: construct the client, call capture() to ingest a single utterance, then await_outcome() to retrieve reasoning results. The SDK returns a lightweight CaptureResult immediately (accepted-only async per §1.6) and an OutcomeResult after reasoning completes.

Pythonfirst_event.py
from dmzagent import DMZAgent

cx = DMZAgent(api_key="ck_live_...")

r = cx.capture(
    kind="subject_says",
    subject_id="cust",
    subject_type="chat",
    agent_subject_id="bot",
    payload={"text": "I want a refund."},
)
print(r.frame_id, r.accepted, r.follow_my_data)
TypeScriptfirst-event.ts
import { DMZAgent } from "@dmzagent/sdk";

const cx = new DMZAgent({ apiKey: "ck_live_..." });

const r = await cx.capture({
  kind:           "subject_says",
  subjectId:      "cust",
  subjectType:    "chat",
  agentSubjectId: "bot",
  payload:        { text: "I want a refund." },
});
console.log(r.frameId, r.accepted, r.followMyData);
C#FirstEvent.cs
using DMZAgent.Sdk;

using var cx = new DMZAgentClient(apiKey: "ck_live_...");

var r = await cx.CaptureAsync(
    kind:           "subject_says",
    subjectId:      "cust",
    subjectType:    "chat",
    agentSubjectId: "bot",
    payload:        new Dictionary<string, object?> { ["text"] = "I want a refund." });
Console.WriteLine($"{r.FrameId} {r.Accepted}");
JavaFirstEvent.java
import dev.dmzagent.sdk.DMZAgentClient;

try (var cx = new DMZAgentClient("ck_live_...")) {
    var r = cx.capture(
        "subject_says",                     // kind
        "cust",                               // subject_id
        "chat",                               // subject_type
        Map.of("text", "I want a refund."), // payload
        "bot");                                // agent_subject_id
    System.out.println(r.frameId() + " " + r.accepted());
}

Section 7 · Check the circuit breaker

Before a consequential action — issuing a refund, sending a payment, exposing PII — ask DMZAgent whether the agent is still in good standing. The result carries allow, a reason, the policies that fired, and an audit anchor.

Python
g = cx.check(subject_id="bot")
if not g.allow:
    log_blocked(g.reason, g.fired_policies, g.anchor)
    return
issue_refund()
TypeScript
const g = await cx.check({ subjectId: "bot" });
if (!g.allow) {
  logBlocked(g.reason, g.firedPolicies, g.anchor);
  return;
}
await issueRefund();
C#
var g = await cx.CheckAsync(subjectId: "bot");
if (!g.Allow)
{
    LogBlocked(g.Reason, g.FiredPolicies, g.Anchor);
    return;
}
await IssueRefundAsync();
Java
var g = cx.check("bot");
if (!g.allow()) {
    logBlocked(g.reason(), g.firedPolicies(), g.anchor());
    return;
}
issueRefund();

Section 8 · Conversation helper

For multi-turn flows, the conversation helper threads an interaction_id through every event so the audit view stitches them into one timeline. The single says() call works whether the speaker is human, agent, or anything else — the participants roster carries the role.

Python
with cx.conversation(participants=[
    {"subject_id": "bot",  "role": "agent",    "kind": "agent",    "subject_type": "chat"},
    {"subject_id": "cust", "role": "customer", "kind": "human", "subject_type": "chat"},
]) as conv:
    conv.says("cust", "I want a refund.", subject_type="chat")
    conv.says("bot",  "I can help with that.", subject_type="chat")

    with conv.guard("bot", raise_on_open=True):
        conv.tool_call("bot", tool="refund.issue", subject_type="chat",
                       args={"amount": 9900})
TypeScript
const conv = cx.conversation({
  participants: [
    { subject_id: "bot",  role: "agent",    kind: "agent",    subject_type: "chat" },
    { subject_id: "cust", role: "customer", kind: "human", subject_type: "chat" },
  ],
});

await conv.says("cust", "I want a refund.", { subjectType: "chat" });
await conv.says("bot",  "I can help with that.", { subjectType: "chat" });

{
  using g = await conv.guard("bot", { raiseOnOpen: true });
  await conv.toolCall("bot", "refund.issue", { subjectType: "chat", amount: 9900 });
}
C#
using var conv = cx.Conversation(participants: new[]
{
    new Dictionary<string, object?> { ["subject_id"] = "bot",  ["role"] = "agent",    ["kind"] = "agent",    ["subject_type"] = "chat" },
    new Dictionary<string, object?> { ["subject_id"] = "cust", ["role"] = "customer", ["kind"] = "human", ["subject_type"] = "chat" },
});

await conv.SaysAsync("cust", "I want a refund.", subjectType: "chat");
await conv.SaysAsync("bot",  "I can help.", subjectType: "chat");

using (var g = await conv.GuardAsync("bot", raiseOnOpen: true))
{
    await conv.ToolCallAsync("bot", "refund.issue",
        subjectType: "chat",
        args: new Dictionary<string, object?> { ["amount"] = 9900 });
}
Java
var participants = List.<Map<String, Object>>of(
    Map.of("subject_id", "bot",  "role", "agent",    "kind", "agent",    "subject_type", "chat"),
    Map.of("subject_id", "cust", "role", "customer", "kind", "human", "subject_type", "chat"));

try (var conv = cx.conversation(participants)) {
    conv.says("cust", "I want a refund.", "chat");
    conv.says("bot",  "I can help with that.", "chat");

    try (var g = conv.guard("bot", null, true)) {
        conv.toolCall("bot", "refund.issue", "chat", Map.of("amount", 9900));
    }
}

Section 9 · Notification preferences

Every API key carries its own notification preferences: email cadence, push, SMS, WhatsApp, and a custom webhook URL. Read and update them through the SDK.

Python
# Read current preferences
prefs = cx.get_notification_prefs()
print(prefs.email_cadence, prefs.push_enabled)

# Update — only supplied fields are touched
prefs = cx.update_notification_prefs(
    email_cadence="daily",
    push_enabled=True,
    phone="+14155551234",
    sms_enabled=False,
)
TypeScript
// Read current preferences
const prefs = await cx.getNotificationPrefs();
console.log(prefs.emailCadence, prefs.pushEnabled);

// Update
prefs = await cx.updateNotificationPrefs({
  emailCadence: "daily",
  pushEnabled:  true,
  phone:        "+14155551234",
  smsEnabled:   false,
});
C#
// Read current preferences
var prefs = await cx.GetNotificationPrefsAsync();
Console.WriteLine($"{prefs.EmailCadence} {prefs.PushEnabled}");

// Update
prefs = await cx.UpdateNotificationPrefsAsync(
    emailCadence: "daily",
    pushEnabled:  true,
    phone:        "+14155551234",
    smsEnabled:   false);
Java
// Read current preferences
var prefs = cx.getNotificationPrefs();
System.out.println(prefs.emailCadence() + " " + prefs.pushEnabled());

// Update
prefs = cx.updateNotificationPrefs(
    "daily",      // emailCadence
    null,          // emailPausedUntil (unchanged)
    true,          // pushEnabled
    "+14155551234", // phone
    false);        // smsEnabled

Section 10 · Division configuration

Each division carries an arbitrary JSON configuration blob. The canonical key is reasoning_mode (§1.7) — operators set it to "per_frame" (default) or "per_trace" (batch reasoning). Read and write the config through the SDK (write requires tenant_admin+ permissions).

Python
# Read division config
cfg = cx.get_division_config("div:abc")
print(cfg.config)  # {"reasoning_mode": "per_trace"}

# Replace entire config blob
cfg = cx.update_division_config(
    division_id="div:abc",
    config={"reasoning_mode": "per_frame"},
)
TypeScript
// Read division config
const cfg = await cx.getDivisionConfig("div:abc");
console.log(cfg.config);

// Replace entire config blob
cfg = await cx.updateDivisionConfig({
  divisionId: "div:abc",
  config:     { reasoningMode: "per_frame" },
});
C#
// Read division config
var cfg = await cx.GetDivisionConfigAsync("div:abc");
Console.WriteLine(cfg.Config);

// Replace entire config blob
cfg = await cx.UpdateDivisionConfigAsync(
    divisionId: "div:abc",
    config:     new Dictionary<string, object?>
                { ["reasoning_mode"] = "per_frame" });
Java
// Read division config
var cfg = cx.getDivisionConfig("div:abc");
System.out.println(cfg.config());

// Replace entire config blob
cfg = cx.updateDivisionConfig(
    "div:abc",
    Map.of("reasoning_mode", "per_frame"));

Section 11 · Pattern: one agent, one customer

The most common shape: a chat between a single agent and a single customer. The conversation helper carries the roster; each says() emits a subject_says event under the same interaction_id. The audit timeline reconstructs the dialogue from those events alone.

See the conversation helper example above — the same pattern.

Section 12 · Pattern: multi-agent handoff

When an agent escalates to another agent (or to a human), extend the roster mid-flight. The new participant gets the same interaction_id, the soul of every participant continues to accumulate, and the audit view shows the handoff explicitly.

Python
conv.add_subject("senior-bot", role="agent", kind="agent", subject_type="chat")
conv.says("bot", "Handing this off.", subject_type="chat")
conv.says("senior-bot", "I'll take it from here.", subject_type="chat")
TypeScript
conv.addSubject({ subject_id: "senior-bot", role: "agent", kind: "agent", subjectType: "chat" });
await conv.says("bot", "Handing this off.", { subjectType: "chat" });
await conv.says("senior-bot", "I'll take it from here.", { subjectType: "chat" });
C#
conv.AddSubject("senior-bot", role: "agent", kind: "agent", subjectType: "chat");
await conv.SaysAsync("bot", "Handing this off.", subjectType: "chat");
await conv.SaysAsync("senior-bot", "I'll take it from here.", subjectType: "chat");
Java
conv.addSubject("senior-bot", "agent", "agent", "chat");
conv.says("bot", "Handing this off.", "chat");
conv.says("senior-bot", "I'll take it from here.", "chat");

Section 13 · Pattern: tool gating with guard()

Wrap any consequential tool call in guard(). If the breaker is open, the guard either returns a falsy result (the result-style form) or raises a typed exception (the raise_on_open / raiseOnOpen form). The breaker decision is itself written to the ledger, so the audit trail records both the request and DMZAgent's response.

Python
from dmzagent import CBOpenError

try:
    with cx.guard(subject_id="bot", raise_on_open=True):
        do_sensitive_thing()
except CBOpenError as e:
    log_blocked(e.reason, e.scope_ref, e.anchor)
TypeScript
import { CBOpenError } from "@dmzagent/sdk";

try {
  using g = await cx.guard({ subjectId: "bot", raiseOnOpen: true });
  await doSensitiveThing();
} catch (e) {
  if (e instanceof CBOpenError) {
    logBlocked(e.reason, e.scopeRef, e.anchor);
  } else throw e;
}
C#
try
{
    using var g = await cx.GuardAsync(subjectId: "bot", raiseOnOpen: true);
    DoSensitiveThing();
}
catch (CircuitBreakerOpenException ex)
{
    LogBlocked(ex.Reason, ex.ScopeRef, ex.Anchor);
}
Java
try (var g = cx.guard("bot", null, true)) {
    doSensitiveThing();
} catch (CircuitBreakerOpenException e) {
    logBlocked(e.reason(), e.scopeRef(), e.anchor());
}

Section 14 · Pattern: sensor and observation events

Not every event is an utterance. Structured observations — video keyframes, IoT readings, telemetry samples — go through observation(). The payload is whatever JSON shape makes sense for your domain; DMZAgent stores it verbatim and scores it against any Canon whose schema matches.

Python
cx.observation(
    agent_subject_id="sensor-1",
    subject_type="sensor",
    subjects=[{"subject_id": "sensor-1", "role": "service", "kind": "sensor"}],
    payload={"kind": "video_keyframe", "frame_index": 42},
)
TypeScript
await cx.observation({
  agentSubjectId: "sensor-1",
  subjectType:    "sensor",
  subjects: [{ subject_id: "sensor-1", role: "service", kind: "sensor" }],
  payload: { kind: "video_keyframe", frame_index: 42 },
});
C#
await cx.ObservationAsync(
    agentSubjectId: "sensor-1",
    subjectType:    "sensor",
    subjects:       new[] { new Dictionary<string, object?> { ["subject_id"] = "sensor-1", ["role"] = "service", ["kind"] = "sensor" } },
    payload:        new Dictionary<string, object?> { ["kind"] = "video_keyframe", ["frame_index"] = 42 });
Java
cx.observation(
    "sensor-1",
    "sensor",
    List.of(Map.of("subject_id", "sensor-1",
                  "role",       "service",
                  "kind",       "sensor")),
    Map.of("kind", "video_keyframe", "frame_index", 42));

Section 15 · Pattern: verify inbound webhooks

DMZAgent signs every outbound webhook with HMAC-SHA256 over <unix_seconds>.<payload>. Verify in your handler before trusting the body. The helper returns false (never throws) for malformed headers, expired timestamps, or signature mismatches. Default tolerance is 300 seconds.

Python
from dmzagent import verify_webhook_signature

ok = verify_webhook_signature(
    payload          = request.body,
    signature_header = request.headers["DMZAgent-Signature"],
    secret           = WEBHOOK_SUBSCRIPTION_SECRET,
    tolerance_seconds = 300,
)
if not ok:
    return Response(status=401)
TypeScript
import { verifyWebhookSignature } from "@dmzagent/sdk";

const payload = await req.text();
const header  = req.headers.get("DMZAgent-Signature") ?? "";
const ok = verifyWebhookSignature(payload, header, process.env.DMZAGENT_WEBHOOK_SECRET!);
if (!ok) return new Response("bad signature", { status: 401 });
C#
using DMZAgent.Sdk.Webhook;

var ok = WebhookSignature.Verify(
    payload:          rawRequestBody,
    signatureHeader:  Request.Headers["DMZAgent-Signature"]!,
    secret:           "whsec_...",
    toleranceSeconds: 300);
if (!ok) return Unauthorized();
Java
import dev.dmzagent.sdk.WebhookSignature;

boolean valid = WebhookSignature.verify(
    rawRequestBody,
    request.getHeader("DMZAgent-Signature"),
    System.getenv("DMZAGENT_WEBHOOK_SECRET"));
if (!valid) { response.setStatus(401); return; }

Section 16 · Pattern: subscribe to outbound webhooks

Outbound webhooks are configured per workspace at /settings/webhooks. Each subscription has a target URL, a set of event kinds (frame_scored, cb_state_changed, ledger_anchored, …), and its own signing secret. DMZAgent retries with exponential backoff on non-2xx and rolls dead-letters into the workspace inbox.

Section 17 · Pattern: async mode

For high-throughput emitters that don't need the rich envelope back in line, set async_mode (or send the X-DMZAgent-Async: true header). The server queues the event, returns immediately with queued=true and no frame_id, and processes it in the background.

Python
r = cx.emit_event("subject_says", subject_type="chat", ..., async_mode=True)
r.queued     # True
r.frame_id   # None — server processing in background
TypeScript
const r = await cx.emitEvent("subject_says", { subjectType: "chat", ..., asyncMode: true });
r.queued;    // true
r.frameId;   // undefined — server processing in background
C#
var r = await cx.EmitEventAsync("subject_says", subjectType: "chat", ..., asyncMode: true);
// r.Queued == true; r.FrameId is null while server processes
Java
var r = cx.emitEvent("subject_says", "chat", args, EmitOptions.asyncMode(true));
// r.queued() == true; r.frameId() is null until processed

Section 18 · Pattern: SDK ack envelopes

Every emit returns an ack envelope with the bare minimum your client needs to confirm the event landed and to find it later: frame_id (where the server stored it), interaction_id (the conversation thread it belongs to), ledger_index (its position in the audit chain), and follow_my_data (a URL that opens the per-frame view in the dashboard). Persist these alongside your own logs so a support engineer can jump from a customer ticket straight to the frame.

Section 19 · Pattern: audit replay by hash

The workspace ledger is hash-chained: every entry carries the previous entry's hash, so the whole chain is verifiable offline. Export the chain as JSON Lines from the dashboard (/settings/audit/export), then verify locally by walking prev_hash → hash. Any tampering after the fact appears as a mismatch.

Section 20 · Pattern: error handling

All SDKs share the same five-class error hierarchy with a single base type. Catch the base to handle any production failure mode; catch CBOpenError separately when it's a meaningful control-flow seam. See the error hierarchy reference for the full table.

Section 21 · Pattern: browse the Canon Library

Canons — installable governance packs — live in the public Library at /library. The SDK does not name canons in code; canons are installed at the workspace level via the dashboard or POST /v1/corpus/install. Once installed, every event you emit is automatically scored against them.

Section 22 · Pattern: Enforce MCP (preview)

Preview · not yet live

The MCP 1.0 specification is locked (see CONCORDIA_MCP.md in the spec repo) and the per-workspace endpoint slot is provisioned on every signup, but the JSON-RPC handlers (enforce_covenant, record_decision, query_corpus, get_subject_soul) are tracked for an upcoming release and not yet serving traffic. See task #212 for status. Until it ships, use the standard SDK above — guard() and check() exercise the same circuit-breaker substrate that MCP will expose.

Reference 1 · Event kinds

Four canonical wire kinds. The SDK method names follow each language's idiomatic casing; the on-wire kind string is always snake_case (per spec §8.6).

Wire kind Python TS / Node C# Java
subject_says subject_says() subjectSays() SubjectSaysAsync() subjectSays()
tool_call tool_call() toolCall() ToolCallAsync() toolCall()
tool_result tool_result() toolResult() ToolResultAsync() toolResult()
observation observation() observation() ObservationAsync() observation()

Reference 2 · Error hierarchy

Same five concepts in every SDK; only the type names follow language conventions. All inherit from a single base type, so catching the base in a try/except covers every production failure mode.

Situation Python TypeScript C# Java
Base type DMZAgentError DMZAgentError DMZAgentException DMZAgentException
401 — auth AuthError AuthError DMZAgentAuthException DMZAgentAuthException
403 — permission PermissionError PermissionError DMZAgentPermissionException DMZAgentPermissionException
400 — validation ValidationError ValidationError DMZAgentValidationException DMZAgentValidationException
5xx / network / timeout ServerError ServerError DMZAgentServerException DMZAgentServerException
Circuit breaker open CBOpenError CBOpenError CircuitBreakerOpenException CircuitBreakerOpenException

Reference 3 · Environments

EnvironmentBase URLUse for
Productionhttps://api.dmzagent.comLive workspaces. Default for every SDK.
Staginghttps://staging.api.dmzagent.comPre-production. Schema-compatible with prod but data is reset on each deploy.
Localhttp://localhost:8080Running prothinker-server on your machine. Pass via the base_url / baseUrl constructor option.

Reference 4 · Configuration

Every SDK accepts the same four configuration knobs. The defaults below apply when you do not pass an override.

KnobDefaultNotes
api_key / apiKey(required)Must start with ck_. Constructor rejects anything else.
base_url / baseUrlhttps://api.dmzagent.comOverride for staging / on-prem / local.
timeout10 secondsPer-request. Tune up for high-latency networks; tune down for sub-second guard checks.
user_agent / userAgentdmzagent-<lang>/0.6.0Appears in server-side audit logs. Useful for tracing a specific deployment.

Reference 5 · Spec and SDK versions

Every SDK pins to a single spec version. A spec tag like v0.6.0 corresponds 1:1 to release tags in every SDK repo. No SDK ships a version the spec has not blessed. Pre-1.0, MINOR bumps MAY include breaking wire changes; PATCH bumps remain backwards-compatible at both API and wire level. (See spec §11.)

LayerCurrentLockstep
Specv0.6.0
Python dmzagent0.6.0v0.6.0
TypeScript @dmzagent/sdk0.6.0v0.6.0
C# DMZAgent.Sdk0.6.0v0.6.0
Java dev.dmzagent:dmzagent-sdk0.6.0v0.6.0