TypeScript SDK
Install and use the Edictum TypeScript SDK to enforce rules 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 ruleset 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 rules enforcement as Python agents. The TypeScript SDK (@edictum/core) is a full port of the Python library with identical ruleset 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 rulesets.
Write a Ruleset
Save this as rules.yaml:
apiVersion: edictum/v1
kind: Ruleset
metadata:
name: agent-safety
defaults:
mode: enforce
rules:
- id: block-sensitive-reads
type: pre
tool: readFile
when:
args.path:
contains_any: [".env", ".secret", "credentials", ".pem", "id_rsa"]
then:
action: block
message: "Sensitive file '{args.path}' blocked."
- 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:
action: block
message: "Destructive command blocked: {args.command}"
- id: session-limits
type: session
limits:
max_tool_calls: 50
max_attempts: 120
then:
action: block
message: "Session limit reached."Run It
import { Edictum, EdictumDenied } from '@edictum/core'
const guard = Edictum.fromYaml('rules.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}`)
// BLOCKED: agent tries to read .env
try {
await guard.run('readFile', { path: '.env' }, readFile)
} catch (e) {
if (e instanceof EdictumDenied) {
console.log(`BLOCKED: ${e.reason}`)
}
}Expected output:
OK: contents of readme.txt
BLOCKED: Sensitive file '.env' blocked.The .env file was never read. The ruleset 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, rules, 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/edictum | OpenClaw adapter | pnpm add @edictum/edictum |
@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 Rules
Define rules in code when you need dynamic logic:
import { Decision, Edictum } from '@edictum/core'
import type { Precondition } from '@edictum/core'
const noRm: Precondition = {
tool: 'Bash',
check: async toolCall => {
if (toolCall.bashCommand?.includes('rm -rf')) {
return Decision.fail('Cannot run rm -rf')
}
return Decision.pass_()
},
}
const guard = new Edictum({ rules: [noRm] })Postconditions must set contractType: 'post' to avoid misclassification:
import type { Postcondition } from '@edictum/core'
const checkPII: Postcondition = {
tool: 'searchDatabase',
contractType: 'post',
check: async (_toolCall, output) => {
const text = String(output)
if (/\b\d{3}-\d{2}-\d{4}\b/.test(text)) {
return Decision.fail('SSN detected in output')
}
return Decision.pass_()
},
}Observe Mode
Test rules in production without blocking any tool calls:
const guard = Edictum.fromYaml('rules.yaml', { mode: 'observe' })Calls that would be blocked are logged as CALL_WOULD_DENY audit events but allowed to proceed. Review the decision log, tune rules, then switch to enforce.
See observe mode for the full workflow.
Dry-Run Evaluation
Test rules without executing anything:
const result = await guard.evaluate('readFile', { path: '.env' })
console.log(result.decision) // "deny"
console.log(result.denyReasons[0]) // "Sensitive file '.env' blocked."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.decision.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() |
| Block 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