Outcall
SpecificationsS003 · Rule Engine

S003 · Rule Engine

Specification module 003-rule-engine

S003: Rule Engine

FieldValue
SpecS003
FeatureRule Engine
Date2026-04-21
StatusDraft
Author@marktopper

Overview

The rule engine is the core policy evaluation system in Outcall. Every request from an agent container -- network connections, HTTP requests, DNS lookups, Docker API calls, command executions -- passes through the rule engine, which evaluates it against a set of YAML-defined rules and returns an allow or block decision.

Rules are written as YAML files in a rules directory. Each file contains an optional definitions section for reusable variables and a rules list where each rule has a CEL (Common Expression Language) condition, an action (allow, block, or enrich), and optional metadata. Files are evaluated in filename sort order. Within a file, rules are evaluated top-to-bottom. The first allow or block match wins. If no rule matches, the default policy is always BLOCK -- this is not configurable.

Enrich hooks (action: enrich) are special rules that execute host-side scripts to populate run.context with additional data before the allow/block decision is reached. They do not terminate evaluation.

Static analysis runs at startup: CEL parse errors abort the daemon, while warnings (e.g., unused definitions) are logged but allow startup to proceed.

Rule file format

version: "1"
definitions:
  is_github: network.hostname == "github.com"

rules:
  - id: "allow-github-api"
    condition: |
      $is_github &&
      http.method in ["GET", "POST"] &&
      http.path.startsWith("/api/v3")
    action: allow

  - id: "block-force-push"
    condition: run.tool == "git" && "-f" in run.flags
    action: block
    log: true

Context variables

The CEL evaluation context exposes these variable namespaces:

NamespaceVariablesSource
networkhostname, ip, port, protocolConnection metadata from bridge/nftables
httpmethod, path, host, headers, body_sizeHTTP request inspection
dnsquery, record_typeDNS query interception
dockerimage, command, volumes, env_keys, capabilitiesDocker API request inspection
runtool, args, flags, cwd, contextCommand execution metadata

Agent rule requests

Agents can submit rule requests through the agent-facing API. These requests queue for host operator review and approval. The host operator can approve, deny, or modify requested rules before they take effect.

User Scenarios

S003-US-001 [P1] As a host operator, I want to write YAML rules that allow specific network access for agent containers so that agents can reach required APIs while everything else is blocked.

S003-US-002 [P1] As a host operator, I want rules evaluated in a predictable order so that I can reason about which rule will match a given request.

S003-US-003 [P1] As a host operator, I want the system to block all traffic by default so that I never accidentally leave a permissive gap.

S003-US-004 [P2] As a host operator, I want to define reusable variables in my rule files so that I can avoid repeating complex conditions.

S003-US-005 [P2] As a host operator, I want enrich hooks so that I can call host-side scripts to gather context before making allow/block decisions.

S003-US-006 [P1] As a host operator, I want static analysis at startup so that typos and invalid CEL expressions are caught before the daemon accepts traffic.

S003-US-007 [P2] As a host operator, I want to reload rules without restarting the daemon so that I can update policy on the fly.

S003-US-008 [P2] As a host operator, I want to inspect loaded rules and test expressions from the CLI so that I can debug policy.

S003-US-009 [P3] As an agent, I want to request additional rules so that I can ask the host operator for access I need.

S003-US-010 [P1] As a host operator, I want to review and approve or deny agent rule requests so that agents cannot self-authorize.

Requirements Summary

IDTypePriorityTitleStatus
S003-FR-001FunctionalP1YAML rule file formatDraft
S003-FR-002FunctionalP1Version field requiredDraft
S003-FR-003FunctionalP1Rule structureDraft
S003-FR-004FunctionalP1CEL expression evaluationDraft
S003-FR-005FunctionalP1Context variable schemaDraft
S003-FR-006FunctionalP2Definition variable substitutionDraft
S003-FR-007FunctionalP1Filename sort orderDraft
S003-FR-008FunctionalP1First match winsDraft
S003-FR-009FunctionalP1Default BLOCK policyDraft
S003-FR-010FunctionalP1Default policy not configurableDraft
S003-FR-011FunctionalP2Enrich hook executionDraft
S003-FR-012FunctionalP2Enrich hook timeoutDraft
S003-FR-013FunctionalP2Enrich populates run.contextDraft
S003-FR-014FunctionalP1Static analysis at startupDraft
S003-FR-015FunctionalP1Startup abort on errorsDraft
S003-FR-016FunctionalP1Startup continue on warningsDraft
S003-FR-017FunctionalP1CEL parsing via cel-interpreterDraft
S003-FR-018FunctionalP1YAML parsing via serde_yamlDraft
S003-FR-019FunctionalP1Rule evaluation responseDraft
S003-FR-020FunctionalP1Log flag supportDraft
S003-FR-021FunctionalP2Rule reload without restartDraft
S003-FR-022FunctionalP2File watch or API trigger reloadDraft
S003-FR-023FunctionalP2Atomic rule reloadDraft
S003-FR-024FunctionalP3Agent rule request submissionDraft
S003-FR-025FunctionalP3Agent rule request queueDraft
S003-FR-026FunctionalP2Host approval of agent requestsDraft
S003-FR-027FunctionalP1Rules directory configurationDraft
S003-FR-028FunctionalP1Rule ID uniquenessDraft
S003-FR-029FunctionalP1Structured logging for decisionsDraft
S003-FR-030FunctionalP1Typed errorsDraft
S003-FR-031FunctionalP1Evaluation latency budgetDraft
S003-FR-032FunctionalP2Rule priority/weight fieldDraft
S003-FR-033FunctionalP1Bridge-up prerequisiteDraft
S003-FR-034FunctionalP2CLI rule inspection commandsDraft
S003-FR-035FunctionalP2CLI expression test commandDraft
S003-FR-036FunctionalP1Host API endpointsDraft
S003-FR-037FunctionalP2Rule description fieldDraft
S003-FR-038FunctionalP1Empty rules directoryDraft
S003-FR-039FunctionalP2Enrich hook script validationDraft
S003-FR-040FunctionalP1Evaluation must be synchronous per requestDraft
S003-AS-001AcceptanceP1Allow rule matches requestDraft
S003-AS-002AcceptanceP1Block rule matches requestDraft
S003-AS-003AcceptanceP1No rule matches (default block)Draft
S003-AS-004AcceptanceP1First match wins orderingDraft
S003-AS-005AcceptanceP2Definition variable expansionDraft
S003-AS-006AcceptanceP2Enrich hook populates contextDraft
S003-AS-007AcceptanceP1Startup CEL parse error abortsDraft
S003-AS-008AcceptanceP1Startup unused definition warnsDraft
S003-AS-009AcceptanceP2Hot reload via APIDraft
S003-AS-010AcceptanceP2Hot reload via file watchDraft
S003-AS-011AcceptanceP1Multi-file filename sort orderDraft
S003-AS-012AcceptanceP3Agent submits rule requestDraft
S003-AS-013AcceptanceP2Host approves rule requestDraft
S003-AS-014AcceptanceP2Host denies rule requestDraft
S003-AS-015AcceptanceP1CLI lists loaded rulesDraft
S003-AS-016AcceptanceP2CLI tests expression against mock contextDraft
S003-AS-017AcceptanceP1Log flag produces audit log entryDraft
S003-AS-018AcceptanceP1Daemon not running (CLI)Draft
S003-IF-001InterfaceP1POST /api/v1/rule/evaluateDraft
S003-IF-002InterfaceP1GET /api/v1/rulesDraft
S003-IF-003InterfaceP1GET /api/v1/rule/:idDraft
S003-IF-004InterfaceP2POST /api/v1/rules/reloadDraft
S003-IF-005InterfaceP2POST /api/v1/rule/testDraft
S003-IF-006InterfaceP3POST /api/v1/agent/rule-requestDraft
S003-IF-007InterfaceP2GET /api/v1/rule-requestsDraft
S003-IF-008InterfaceP2POST /api/v1/rule-request/:id/approveDraft
S003-IF-009InterfaceP2POST /api/v1/rule-request/:id/denyDraft
S003-IF-010InterfaceP1CLI commandsDraft
S003-IF-011InterfaceP1CLI output formatDraft
S003-IF-012InterfaceP1YAML rule file schemaDraft
S003-IF-013InterfaceP1Context variable typesDraft
S003-EC-001Edge CaseP1Invalid CEL expressionDraft
S003-EC-002Edge CaseP1Missing context variableDraft
S003-EC-003Edge CaseP2Enrich hook timeoutDraft
S003-EC-004Edge CaseP2Enrich hook nonexistent scriptDraft
S003-EC-005Edge CaseP2Enrich hook non-zero exitDraft
S003-EC-006Edge CaseP1Empty rules directoryDraft
S003-EC-007Edge CaseP1Duplicate rule ID across filesDraft
S003-EC-008Edge CaseP1Unsupported version fieldDraft
S003-EC-009Edge CaseP2Undefined definition variableDraft
S003-EC-010Edge CaseP2Circular definition referenceDraft
S003-EC-011Edge CaseP2Rule file with only definitionsDraft
S003-EC-012Edge CaseP2CEL expression returns non-booleanDraft
S003-EC-013Edge CaseP1Rules directory does not existDraft
S003-EC-014Edge CaseP2File permissions prevent readingDraft
S003-EC-015Edge CaseP2Reload with invalid new rulesDraft
S003-EC-016Edge CaseP2Concurrent evaluation during reloadDraft
S003-EC-017Edge CaseP3Agent requests rule for already-allowed actionDraft
S003-EC-018Edge CaseP1Malformed YAML fileDraft
S003-EC-019Edge CaseP2Very large rule set (performance)Draft
S003-EC-020Edge CaseP2Non-YAML files in rules directoryDraft
S003-SC-001SuccessP1Allow rule evaluated correctlyDraft
S003-SC-002SuccessP1Block rule evaluated correctlyDraft
S003-SC-003SuccessP1Default block on no matchDraft
S003-SC-004SuccessP1Filename sort order verifiedDraft
S003-SC-005SuccessP1First match wins verifiedDraft
S003-SC-006SuccessP1Static analysis catches errorsDraft
S003-SC-007SuccessP2Definition expansion worksDraft
S003-SC-008SuccessP2Enrich hook executes and populates contextDraft
S003-SC-009SuccessP2Hot reload applies new rulesDraft
S003-SC-010SuccessP1Evaluation latency under budgetDraft
S003-SC-011SuccessP1Audit log entries for logged rulesDraft
S003-SC-012SuccessP1Bridge and network commands unaffectedDraft
S003-SC-013SuccessP2Agent rule requests queue correctlyDraft
S003-SC-014SuccessP1All context variables accessible in CELDraft

Out of Scope

  • nftables rule generation -- the bridge spec (S001) handles the nftables layer; S003 evaluates policy at the application level
  • Container lifecycle management -- starting/stopping agent containers
  • TLS or authentication on the host socket
  • Rule versioning or rollback -- only the current file set is active
  • Multi-tenancy -- all rules apply to all agent containers on the same bridge
  • Custom CEL functions -- only built-in CEL operators and standard functions
  • GUI for rule management -- CLI and API only

Cross-Spec Dependencies

  • Depends on: S001 (bridge must be up for rule evaluation to be meaningful -- S003-FR-033)
  • Depends on: S002 (network context variables populated from network management layer)
  • Required by: S004 (agent permission requests flow through the rule engine), S005, S006, S007, S009

Shared Types (outcall-api)

Constants

pub const RULES_DIR_DEFAULT: &str = "/etc/outcall/rules.d";
pub const RULE_FILE_EXTENSION: &str = ".yaml";
pub const RULE_VERSION_SUPPORTED: &str = "1";
pub const EVALUATION_TIMEOUT_MS: u64 = 50;
pub const ENRICH_HOOK_TIMEOUT_MS: u64 = 5000;

New types (added to outcall-api)

Note: RuleAction, RuleRequestStatus, Verdict, and RuleRequest are already defined in S000. The types below extend the shared library for rule engine support.

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleFile {
    pub version: String,
    pub definitions: Option<HashMap<String, String>>,
    pub rules: Vec<Rule>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Rule {
    pub id: String,
    pub condition: String,
    pub action: RuleAction,          // from S000
    pub priority: Option<i32>,       // S003-FR-032: default 100, lower = higher priority
    pub log: Option<bool>,
    pub description: Option<String>,
    pub enrich: Option<EnrichConfig>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnrichConfig {
    pub script: String,
    pub timeout_ms: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvaluateRequest {
    pub context: EvaluationContext,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvaluationContext {
    pub network: Option<NetworkContext>,
    pub http: Option<HttpContext>,
    pub dns: Option<DnsContext>,
    pub docker: Option<DockerContext>,
    pub run: Option<RunContext>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkContext {
    pub hostname: Option<String>,
    pub ip: String,
    pub port: u16,
    pub protocol: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpContext {
    pub method: String,
    pub path: String,
    pub host: String,
    pub headers: HashMap<String, String>,
    pub body_size: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DnsContext {
    pub query: String,
    pub record_type: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DockerContext {
    pub image: String,
    pub command: Vec<String>,
    pub volumes: Vec<String>,
    pub env_keys: Vec<String>,
    pub capabilities: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunContext {
    pub tool: String,
    pub args: Vec<String>,
    pub flags: Vec<String>,
    pub cwd: String,
    pub context: HashMap<String, serde_json::Value>,
}

/// Internal rule engine result. Translated to Verdict (S000) for agent API responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvaluateResult {
    pub decision: Decision,
    pub matched_rule: Option<String>,
    pub file: Option<String>,
    pub logged: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Decision {
    Allow,
    Block,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleSummary {
    pub id: String,
    pub file: String,
    pub action: RuleAction,
    pub condition_preview: String,
    pub description: Option<String>,
}

/// Agent-submitted rule request (via agent API).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuleRequestSubmission {
    pub description: String,
    pub requested_access: String,
    pub suggested_condition: Option<String>,
}

/// Server-side stored rule request (enriched with ID, timestamp, status).
/// Host API returns this via ApiResponse<StoredRuleRequest>.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredRuleRequest {
    pub id: String,
    pub submitted_at: String,
    pub status: RuleRequestStatus,   // from S000
    pub description: String,
    pub requested_access: String,
    pub suggested_condition: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReloadResult {
    pub files_loaded: usize,
    pub rules_loaded: usize,
    pub warnings: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestExpressionRequest {
    pub expression: String,
    pub context: EvaluationContext,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestExpressionResult {
    pub result: bool,
    pub error: Option<String>,
}

On this page