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.
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.
| Module | Role | What 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.
| Language | Package | Runtime | Repo |
|---|---|---|---|
| Python | dmzagent on PyPI | Python 3.10+ | dmzagent-sdk-python |
| TypeScript / Node | @dmzagent/sdk on npm | Node 18.17+ | dmzagent-sdk-typescript |
| C# | DMZAgent.Sdk on NuGet | net8.0+ | dmzagent-sdk-csharp |
| Java | dev.dmzagent:dmzagent-sdk on Maven Central | Java 17+ | dmzagent-sdk-java |
Section 4 · Install the SDK
Pick the language and run one command.
pip install dmzagent
npm install @dmzagent/sdk
dotnet add package DMZAgent.Sdk
<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.
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).
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.
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)
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);
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}");
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.
g = cx.check(subject_id="bot")
if not g.allow:
log_blocked(g.reason, g.fired_policies, g.anchor)
return
issue_refund()
const g = await cx.check({ subjectId: "bot" });
if (!g.allow) {
logBlocked(g.reason, g.firedPolicies, g.anchor);
return;
}
await issueRefund();
var g = await cx.CheckAsync(subjectId: "bot");
if (!g.Allow)
{
LogBlocked(g.Reason, g.FiredPolicies, g.Anchor);
return;
}
await IssueRefundAsync();
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.
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})
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 });
}
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 });
}
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.
# 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,
)
// 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,
});
// 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);
// 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).
# 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"},
)
// 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" },
});
// 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" });
// 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.
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")
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" });
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");
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.
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)
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;
}
try
{
using var g = await cx.GuardAsync(subjectId: "bot", raiseOnOpen: true);
DoSensitiveThing();
}
catch (CircuitBreakerOpenException ex)
{
LogBlocked(ex.Reason, ex.ScopeRef, ex.Anchor);
}
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.
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},
)
await cx.observation({
agentSubjectId: "sensor-1",
subjectType: "sensor",
subjects: [{ subject_id: "sensor-1", role: "service", kind: "sensor" }],
payload: { kind: "video_keyframe", frame_index: 42 },
});
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 });
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.
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)
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 });
using DMZAgent.Sdk.Webhook;
var ok = WebhookSignature.Verify(
payload: rawRequestBody,
signatureHeader: Request.Headers["DMZAgent-Signature"]!,
secret: "whsec_...",
toleranceSeconds: 300);
if (!ok) return Unauthorized();
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.
r = cx.emit_event("subject_says", subject_type="chat", ..., async_mode=True)
r.queued # True
r.frame_id # None — server processing in background
const r = await cx.emitEvent("subject_says", { subjectType: "chat", ..., asyncMode: true });
r.queued; // true
r.frameId; // undefined — server processing in background
var r = await cx.EmitEventAsync("subject_says", subjectType: "chat", ..., asyncMode: true);
// r.Queued == true; r.FrameId is null while server processes
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)
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
| Environment | Base URL | Use for |
|---|---|---|
| Production | https://api.dmzagent.com | Live workspaces. Default for every SDK. |
| Staging | https://staging.api.dmzagent.com | Pre-production. Schema-compatible with prod but data is reset on each deploy. |
| Local | http://localhost:8080 | Running 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.
| Knob | Default | Notes |
|---|---|---|
api_key / apiKey | (required) | Must start with ck_. Constructor rejects anything else. |
base_url / baseUrl | https://api.dmzagent.com | Override for staging / on-prem / local. |
timeout | 10 seconds | Per-request. Tune up for high-latency networks; tune down for sub-second guard checks. |
user_agent / userAgent | dmzagent-<lang>/0.6.0 | Appears 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.)
| Layer | Current | Lockstep |
|---|---|---|
| Spec | v0.6.0 | — |
Python dmzagent | 0.6.0 | v0.6.0 |
TypeScript @dmzagent/sdk | 0.6.0 | v0.6.0 |
C# DMZAgent.Sdk | 0.6.0 | v0.6.0 |
Java dev.dmzagent:dmzagent-sdk | 0.6.0 | v0.6.0 |