OpenBox governance + observability for Agent-RT TypeScript
agents. A thin adapter over the base SDK
@openbox-ai/openbox-sdk-ts,
mirroring the architecture and wire behavior of openbox-agentrt-sdk-python.
Status: active development; APIs may still change.
This package exposes two independent entry points. They are not interchangeable:
| Surface | Import | Role |
|---|---|---|
| Governed agent loop | @openbox-ai/openbox-agentrt-governance/governance |
Enforcement — createOpenBoxAgentRT, GovernedAgentLoop, and the provider/tool wrappers block model/tool calls that fail governance (throws before the wrapped call). This is the only surface that enforces. |
| Import-light root | @openbox-ai/openbox-agentrt-governance |
Helpers + observability only — SDK identity, event builders, message/redaction helpers, and a telemetry-only evaluator. It never blocks execution and never loads agent-rt or the base-SDK runtime. |
The split is deliberate: the root stays safe to import anywhere, while the
runtime-bound surface (instrumentation, network, approval polling) lives behind
the /governance subpath.
npm install @openbox-ai/openbox-agentrt-governance agent-rt@0.0.3agent-rt@0.0.3 is an exact peer dependency (used for types only). The OpenBox
base SDK is installed transitively. Requires Node 24.10+.
import { AgentLoop, ToolRegistry, loadModel, type AgentConfig } from "agent-rt";
import { createOpenBoxAgentRT } from "@openbox-ai/openbox-agentrt-governance/governance";
const governance = await createOpenBoxAgentRT({
apiUrl: process.env.OPENBOX_URL ?? "https://core.openbox.ai",
apiKey: process.env.OPENBOX_API_KEY,
agentName: "content-builder"
});
const provider = await loadModel(); // reads OPENAI_MODEL and OPENAI_API_KEY
const registry = new ToolRegistry(); // register tools as usual
const loop = governance.governLoop(
new AgentLoop(provider, undefined, registry)
);
const agent: AgentConfig = {
name: "content-builder",
instructions: "Be brief.",
model: { model: process.env.OPENAI_MODEL ?? "gpt-4o-mini" }
};
try {
const result = await loop.run(agent, [
{ role: "user", content: [{ type: "text", text: "Hello" }] }
]);
console.log(result.terminationReason);
} finally {
// Flushes in-flight hook telemetry, then shuts down instrumentation +
// runtime. Always await it.
await governance.close();
}governLoop returns a GovernedAgentLoop whose run(...) / runStreaming(...)
take the same arguments as AgentLoop. The original loop is not mutated.
| Governed operation | Wrapper | Events |
|---|---|---|
| Run | governance.runScope(prompt, body) |
WorkflowStarted → SignalReceived (enforced) → pre-screen (enforced) → WorkflowCompleted / WorkflowFailed |
| Model | governance.wrapProvider(provider) |
ActivityStarted(llm_call) (enforced, redaction applied) → ActivityCompleted |
| Tools | governance.wrapToolRegistry(registry) / wrapToolExecutor(executor) |
ActivityStarted(<tool>) (enforced) → ActivityCompleted |
AgentLoop runs registered tools through the registry and only falls back to
the executor, so governLoop wraps both. A wrapped provider/tool called outside
a run scope fails closed. Tool activities carry an __openbox sentinel
(tool_type from toolTypeMap, plus the tool's Agent-RT sideEffect) so Rego
policies can classify calls.
Every started activity is closed before the workflow is — including when a run deadline or abort signal abandons a pending call, a stream is stopped early, or a gate blocks — and completion telemetry is best-effort: a failed send is logged, never thrown over a tool or model result. See docs/governance-model.md.
Model calls without non-empty user text still pass through the model-start gate (with an empty prompt field).
import { evaluateLifecycleTelemetryOnly } from "@openbox-ai/openbox-agentrt-governance";
const verdict = await evaluateLifecycleTelemetryOnly(runtime, event, {
logger: console
});The root surface sends telemetry and builds events only. A returned BLOCK/HALT verdict is never thrown. To enforce governance you must use the
/governancesurface.
Each model and tool call runs inside an OpenBox activity scope (AsyncLocalStorage) with a trace-map fallback, so HTTP/DB/file spans captured by base instrumentation resolve to the enclosing LLM/tool activity. This is the primary, supported correlation path.
Because the sync fs wrapper fires its telemetry after returning,
await governance.close() (which drains in-flight hook telemetry) so the last
fs event is durable.
A fully offline smoke example (scripted model, in-memory tool, fake Core — no
network, no secrets) lives in examples/content-builder-agent,
next to the full content-writer port:
npm run build && npm run example:smokeFrom the TypeScript SDK root, configure your API keys in
examples/content-builder-agent/.env, then run:
npm ci
npm run build
cd examples/content-builder-agent
npm ci
npm run start -- "Write a blog post about prompt engineering"OPENBOX_URL is optional and defaults to https://core.openbox.ai.
Requires Node.js 24.10 or later.
Environment prefix OPENBOX_AGENTRT_* layered over global OPENBOX_*. See
docs/api-reference.md for the full configuration
surface. On-API-error posture defaults to fail_open; set
onApiError: "fail_closed" for destructive agents.
createOpenBoxAgentRT forwards the tagged identity configuration straight to
the base SDK's OpenBoxClient.fromConfig — this package never mints a
signature, assertion, or token itself. One runtime and one client serve startup
validation, every gate, approval polling, HTTP/DB/file hooks, and completion
telemetry.
OpenBox DID (v1, default):
const governance = await createOpenBoxAgentRT({
apiUrl: process.env.OPENBOX_URL ?? "https://core.openbox.ai",
apiKey: process.env.OPENBOX_API_KEY,
agentDid: process.env.OPENBOX_AGENT_DID,
agentPrivateKey: process.env.OPENBOX_AGENT_PRIVATE_KEY
});Okta AI Agent (v2):
const governance = await createOpenBoxAgentRT({
apiUrl: process.env.OPENBOX_URL ?? "https://core.openbox.ai",
apiKey: process.env.OPENBOX_API_KEY,
agentId: process.env.OPENBOX_AGENT_ID,
organizationId: process.env.OPENBOX_ORGANIZATION_ID,
deploymentId: process.env.OPENBOX_DEPLOYMENT_ID,
agentProofAudience: process.env.OPENBOX_AGENT_PROOF_AUDIENCE,
oktaAgentId: process.env.OPENBOX_OKTA_AGENT_ID,
oktaAgentKeyId: process.env.OPENBOX_OKTA_AGENT_KEY_ID,
oktaAgentPrivateKey: process.env.OPENBOX_OKTA_AGENT_PRIVATE_KEY, // PKCS8 PEM, keep in a secret store
oktaAgentAlgorithm: "RS256"
});Keycloak workload identity (IAM v3, keycloak_workload):
const governance = await createOpenBoxAgentRT({
apiUrl: process.env.OPENBOX_URL ?? "https://core.openbox.ai",
apiKey: process.env.OPENBOX_API_KEY,
identityMethod: "keycloak_workload", // recommended: a missing key is then an error
workloadPrivateKey: process.env.OPENBOX_WORKLOAD_PRIVATE_KEY, // PKCS8 PEM RSA, keep in a secret store
onApiError: "fail_closed"
});Every field above may also be set via the corresponding OPENBOX_AGENTRT_* or
global OPENBOX_* environment variable (framework-prefixed wins). The identity
modes are mutually exclusive — the base SDK rejects combining them before any
request.
Injected runtime. When you pass runtime, it wins: the identity/config
options are ignored (no second client is built), and close() closes that
runtime and its client — coordinate shutdown if you share it.
Contributions and bug reports are welcome. Install development dependencies
with npm ci, then run the checks from the SDK directory:
npm run lint
npm run typecheck
npm test
npm run build
npm run import:checkFor the content-builder example, run npm ci inside
examples/content-builder-agent, then npm run typecheck there.
Include tests for behavior changes and update public API documentation.
MIT