Edictum
TypeScript SDK

TypeScript SDK

Install and use the Edictum TypeScript SDK to enforce rules on AI agent tool calls in Node.js applications.

AI Assistance

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-yaml

Requires 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:

PackageDescriptionInstall
@edictum/corePipeline, rules, audit, session, YAML engine. Zero runtime deps.pnpm add @edictum/core
@edictum/vercel-aiVercel AI SDK adapterpnpm add @edictum/vercel-ai
@edictum/claude-sdkClaude Agent SDK adapterpnpm add @edictum/claude-sdk
@edictum/langchainLangChain.js adapterpnpm add @edictum/langchain
@edictum/openai-agentsOpenAI Agents SDK adapterpnpm add @edictum/openai-agents
@edictum/edictumOpenClaw adapterpnpm add @edictum/edictum
@edictum/serverServer SDK -- HTTP client, SSE hot-reload, audit sinkpnpm add @edictum/server
@edictum/otelOpenTelemetry spans and metricspnpm 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: DENY

Key Differences from Python

AspectPythonTypeScript
YAML loadingEdictum.from_yaml()Edictum.fromYaml() (camelCase)
Async YAMLNot neededEdictum.fromYamlAsync() for ESM
YAML stringEdictum.from_yaml_string()Edictum.fromYamlString()
Block exceptionEdictumDeniedEdictumDenied (same name)
Server connectionEdictum.from_server()createServerGuard() from @edictum/server
Adapter extraspip install edictum[langchain]pnpm add @edictum/langchain (separate package)

See Migration from Python for a full comparison.

Next Steps

Last updated on

On this page