Skip to main content
ast-guard allows you to write custom validation rules and combine built-in rules to match your specific security requirements.

Combining Built-in Rules

Create a custom validator by combining built-in rules:
import {
  JSAstValidator,
  DisallowedIdentifierRule,
  ForbiddenLoopRule,
  RequiredFunctionCallRule,
  UnknownGlobalRule,
} from '@enclave-vm/ast';

const customValidator = new JSAstValidator([
  new DisallowedIdentifierRule({
    disallowed: ['eval', 'Function', 'process', 'require'],
  }),
  new ForbiddenLoopRule({
    allowFor: true,
    allowWhile: false,
  }),
  new RequiredFunctionCallRule({
    required: ['callTool'],
    minCalls: 1,
  }),
  new UnknownGlobalRule({
    allowedGlobals: ['callTool', 'Math', 'JSON', 'Array', 'Object'],
    allowStandardGlobals: true,
  }),
]);

const result = await customValidator.validate(code);

Security Presets

ast-guard includes pre-built security presets:
PresetUse CaseSecurity Level
AgentScriptLLM-generated codeHighest - whitelist-only
STRICTUntrusted guest codeHigh - no loops, no async
SECUREAutomation scriptsMedium - bounded loops only
STANDARDTrusted scriptsLow - basic guardrails
PERMISSIVEInternal/test codeMinimal - eval blocked
import { JSAstValidator, createAgentScriptPreset, Presets } from '@enclave-vm/ast';

// AgentScript (recommended for LLM code)
const agentScript = new JSAstValidator(createAgentScriptPreset());

// STRICT preset
const strict = new JSAstValidator(Presets.strict({
  requiredFunctions: ['callTool'],
  minFunctionCalls: 1,
}));

// SECURE preset
const secure = new JSAstValidator(Presets.secure({
  allowedLoops: { allowForOf: true },
}));

// STANDARD preset
const standard = new JSAstValidator(Presets.standard());

Writing Custom Rules

Rule Interface

interface ValidationRule {
  name: string;
  validate(context: ValidationContext): ValidationIssue[];
}

interface ValidationContext {
  ast: Node;           // Parsed AST (acorn)
  code: string;        // Original source code
  options: object;     // Validation options
}

interface ValidationIssue {
  rule: string;
  message: string;
  severity: 'error' | 'warning';
  location?: { line: number; column: number };
}

Example: Custom Rule

import { ValidationRule, ValidationContext, ValidationIssue } from '@enclave-vm/ast';
import { walk } from 'estree-walker';

class NoMagicNumbersRule implements ValidationRule {
  name = 'no-magic-numbers';
  private allowedNumbers: number[];

  constructor(options: { allowed?: number[] } = {}) {
    this.allowedNumbers = options.allowed || [0, 1, -1];
  }

  validate(context: ValidationContext): ValidationIssue[] {
    const issues: ValidationIssue[] = [];

    walk(context.ast, {
      enter: (node) => {
        if (node.type === 'Literal' && typeof node.value === 'number') {
          if (!this.allowedNumbers.includes(node.value)) {
            issues.push({
              rule: this.name,
              message: `Magic number ${node.value} should be a named constant`,
              severity: 'warning',
              location: node.loc?.start,
            });
          }
        }
      },
    });

    return issues;
  }
}

// Usage
const validator = new JSAstValidator([
  ...createAgentScriptPreset().rules,
  new NoMagicNumbersRule({ allowed: [0, 1, -1, 10, 100] }),
]);

Example: Block Specific API Calls

class BlockedApiCallsRule implements ValidationRule {
  name = 'blocked-api-calls';
  private blockedApis: string[];

  constructor(options: { blocked: string[] }) {
    this.blockedApis = options.blocked;
  }

  validate(context: ValidationContext): ValidationIssue[] {
    const issues: ValidationIssue[] = [];

    walk(context.ast, {
      enter: (node) => {
        if (
          node.type === 'CallExpression' &&
          node.callee.type === 'Identifier' &&
          node.callee.name === 'callTool'
        ) {
          const firstArg = node.arguments[0];
          if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {
            if (this.blockedApis.some(api => firstArg.value.startsWith(api))) {
              issues.push({
                rule: this.name,
                message: `API "${firstArg.value}" is blocked`,
                severity: 'error',
                location: node.loc?.start,
              });
            }
          }
        }
      },
    });

    return issues;
  }
}

// Usage
const validator = new JSAstValidator([
  ...createAgentScriptPreset().rules,
  new BlockedApiCallsRule({
    blocked: ['admin:', 'system:', 'internal:'],
  }),
]);

Extending Presets

Add rules to an existing preset:
import { JSAstValidator, createAgentScriptPreset } from '@enclave-vm/ast';

const preset = createAgentScriptPreset();

const validator = new JSAstValidator([
  ...preset.rules,
  new CustomRule1(),
  new CustomRule2(),
]);

Removing Rules from Presets

Filter out rules you don’t need:
const preset = createAgentScriptPreset();

const validator = new JSAstValidator(
  preset.rules.filter(rule => rule.name !== 'no-regex-literal')
);

Rule Ordering

Rules are executed in array order. For performance, order rules by:
  1. Fast rejections first - Rules that quickly identify invalid code
  2. Complex analysis last - Rules that traverse the entire AST
const validator = new JSAstValidator([
  // Fast: Check for blocked identifiers
  new DisallowedIdentifierRule({ disallowed: ['eval'] }),

  // Medium: Check call structure
  new RequiredFunctionCallRule({ required: ['callTool'] }),

  // Slower: Full AST traversal
  new UnknownGlobalRule({ allowedGlobals: ['Math', 'JSON', 'console', 'callTool'] }),
]);

Testing Custom Rules

import { JSAstValidator } from '@enclave-vm/ast';
import { describe, it, expect } from 'vitest';

describe('CustomRule', () => {
  const validator = new JSAstValidator([new CustomRule()]);

  it('should block dangerous pattern', async () => {
    const result = await validator.validate('dangerous code');
    expect(result.valid).toBe(false);
    expect(result.issues[0].rule).toBe('custom-rule');
  });

  it('should allow safe pattern', async () => {
    const result = await validator.validate('safe code');
    expect(result.valid).toBe(true);
  });
});