TypeScript SDK
Install and use the Edictum TypeScript SDK to enforce contracts on AI agent tool calls in Node.js applications.
Right page if: you want to install Edictum in a TypeScript or Node.js project, see the package overview, or write your first contract in TypeScript. Wrong page if: you are using Python -- see https://docs.edictum.ai/docs/quickstart. For a specific TS adapter, see https://docs.edictum.ai/docs/typescript/adapters. Gotcha: `fromYaml()` is synchronous. Use `fromYamlAsync()` in ESM contexts if you hit module-loading issues. YAML parsing requires `js-yaml` as an optional peer dependency.
Agents running in Node.js need the same contract enforcement as Python agents. The TypeScript SDK (@edictum/core) is a full port of the Python library with identical contract semantics -- same YAML, same pipeline, same deterministic enforcement.
Getting Started
Install
pnpm add @edictum/core js-yamlRequires Node 22+. The js-yaml peer dependency is needed to load YAML contract bundles.
Write a Contract
Save this as contracts.yaml:
apiVersion: edictum/v1
kind: ContractBundle
metadata:
name: agent-safety
defaults:
mode: enforce
contracts:
- id: deny-sensitive-reads
type: pre
tool: readFile
when:
args.path:
contains_any: [".env", ".secret", "credentials", ".pem", "id_rsa"]
then:
effect: deny
message: "Sensitive file '{args.path}' denied."
- id: block-destructive-commands
type: pre
tool: runCommand
when:
any:
- args.command: { starts_with: "rm " }
- args.command: { starts_with: "DROP " }
- args.command: { contains: "mkfs" }
then:
effect: deny
message: "Destructive command denied: {args.command}"
- id: session-limits
type: session
limits:
max_tool_calls: 50
max_attempts: 120
then:
effect: deny
message: "Session limit reached."Run It
import { Edictum, EdictumDenied } from '@edictum/core'
const guard = Edictum.fromYaml('contracts.yaml')
const readFile = async (args: Record<string, unknown>) => {
return `contents of ${args.path}`
}
// Allowed: normal file read
const result = await guard.run('readFile', { path: 'readme.txt' }, readFile)
console.log(`OK: ${result}`)
// DENIED: agent tries to read .env
try {
await guard.run('readFile', { path: '.env' }, readFile)
} catch (e) {
if (e instanceof EdictumDenied) {
console.log(`DENIED: ${e.reason}`)
}
}Expected output:
OK: contents of readme.txt
DENIED: Sensitive file '.env' denied.The .env file was never read. The contract was evaluated in JavaScript, outside the LLM. The agent cannot talk its way past this check.
Packages
The TypeScript SDK is a monorepo of focused packages:
| Package | Description | Install |
|---|---|---|
@edictum/core | Pipeline, contracts, audit, session, YAML engine. Zero runtime deps. | pnpm add @edictum/core |
@edictum/vercel-ai | Vercel AI SDK adapter | pnpm add @edictum/vercel-ai |
@edictum/claude-sdk | Claude Agent SDK adapter | pnpm add @edictum/claude-sdk |
@edictum/langchain | LangChain.js adapter | pnpm add @edictum/langchain |
@edictum/openai-agents | OpenAI Agents SDK adapter | pnpm add @edictum/openai-agents |
@edictum/openclaw | OpenClaw adapter | pnpm add @edictum/openclaw |
@edictum/server | Server SDK -- HTTP client, SSE hot-reload, audit sink | pnpm add @edictum/server |
@edictum/otel | OpenTelemetry spans and metrics | pnpm add @edictum/otel |
All packages ship dual ESM + CJS builds.
Programmatic Contracts
Define contracts in code when you need dynamic logic:
import { Edictum, Verdict } from '@edictum/core'
import type { Precondition } from '@edictum/core'
const noRm: Precondition = {
tool: 'Bash',
check: async (envelope) => {
if (envelope.bashCommand?.includes('rm -rf')) {
return Verdict.fail('Cannot run rm -rf')
}
return Verdict.pass()
},
}
const guard = new Edictum({ contracts: [noRm] })Postconditions must set contractType: 'post' to avoid misclassification:
import type { Postcondition } from '@edictum/core'
const checkPII: Postcondition = {
tool: 'searchDatabase',
contractType: 'post',
check: async (envelope, output) => {
const text = String(output)
if (/\b\d{3}-\d{2}-\d{4}\b/.test(text)) {
return Verdict.fail('SSN detected in output')
}
return Verdict.pass()
},
}Observe Mode
Test contracts in production without denying any tool calls:
const guard = Edictum.fromYaml('contracts.yaml', { mode: 'observe' })Calls that would be denied are logged as CALL_WOULD_DENY audit events but allowed to proceed. Review the audit trail, tune contracts, then switch to enforce.
See observe mode for the full workflow.
Dry-Run Evaluation
Test contracts without executing anything:
const result = await guard.evaluate('readFile', { path: '.env' })
console.log(result.verdict) // "deny"
console.log(result.denyReasons[0]) // "Sensitive file '.env' denied."Batch evaluation:
const results = await guard.evaluateBatch([
{ tool: 'readFile', args: { path: '.env' } },
{ tool: 'readFile', args: { path: 'readme.txt' } },
{ tool: 'runCommand', args: { command: 'rm -rf /' } },
])
for (const r of results) {
console.log(`${r.toolName}: ${r.verdict.toUpperCase()}`)
}
// readFile: DENY
// readFile: ALLOW
// runCommand: DENYKey Differences from Python
| Aspect | Python | TypeScript |
|---|---|---|
| YAML loading | Edictum.from_yaml() | Edictum.fromYaml() (camelCase) |
| Async YAML | Not needed | Edictum.fromYamlAsync() for ESM |
| YAML string | Edictum.from_yaml_string() | Edictum.fromYamlString() |
| Denial exception | EdictumDenied | EdictumDenied (same name) |
| Server connection | Edictum.from_server() | createServerGuard() from @edictum/server |
| Adapter extras | pip install edictum[langchain] | pnpm add @edictum/langchain (separate package) |
See Migration from Python for a full comparison.
Next Steps
Last updated on