Outcall
SpecificationsS003 · Rule Engine

S003 · Rule Engine

Specification module 003-rule-engine

S003: Rule Engine

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

Overview

The rule engine is the application-level policy evaluator in Outcall. Managed DNS, proxy, agent-API, and host-broker enforcement points submit typed context to it and receive an allow or block decision. The bridge's kernel policy is a separate fail-closed layer; Outcall does not claim that arbitrary container processes are transparently intercepted as tool or file requests.

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 or block), 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.

The enrich action is reserved for a future broker-mediated design. Current versions reject it because running project scripts inside the privileged daemon would expose the Docker socket and daemon mounts outside the host-tool policy boundary.

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, contextAgent shim or host-broker request metadata
agentnameHost-verified managed-container identity

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 [P3, Deferred] As a host operator, I want policy-checked enrich tools so that I can gather context without executing project scripts in the privileged daemon.

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 formatImplemented
S003-FR-002FunctionalP1Version field requiredImplemented
S003-FR-003FunctionalP1Rule structureImplemented
S003-FR-004FunctionalP1CEL expression evaluationImplemented
S003-FR-005FunctionalP1Context variable schemaImplemented
S003-FR-006FunctionalP2Definition variable substitutionImplemented
S003-FR-007FunctionalP1Filename sort orderImplemented
S003-FR-008FunctionalP1First match winsImplemented
S003-FR-009FunctionalP1Default BLOCK policyImplemented
S003-FR-010FunctionalP1Default policy not configurableImplemented
S003-FR-011FunctionalP3Broker-mediated enrich designDeferred
S003-FR-012FunctionalP3Enrich bounds and timeoutDeferred
S003-FR-013FunctionalP3Enrich populates run.contextDeferred
S003-FR-014FunctionalP1Static analysis at startupImplemented
S003-FR-015FunctionalP1Startup abort on errorsImplemented
S003-FR-016FunctionalP1Startup continue on warningsImplemented
S003-FR-017FunctionalP1CEL parsing via cel-interpreterImplemented
S003-FR-018FunctionalP1YAML parsing via serde_yamlImplemented
S003-FR-019FunctionalP1Rule evaluation responseImplemented
S003-FR-020FunctionalP1Log flag supportImplemented
S003-FR-021FunctionalP2Rule reload without restartImplemented
S003-FR-022FunctionalP2File watch or API trigger reloadImplemented
S003-FR-023FunctionalP2Atomic rule reloadImplemented
S003-FR-024FunctionalP3Agent rule request submissionImplemented
S003-FR-025FunctionalP3Agent rule request queueImplemented
S003-FR-026FunctionalP2Host approval of agent requestsImplemented
S003-FR-027FunctionalP1Rules directory configurationImplemented
S003-FR-028FunctionalP1Rule ID uniquenessImplemented
S003-FR-029FunctionalP1Structured logging for decisionsImplemented
S003-FR-030FunctionalP1Typed errorsImplemented
S003-FR-031FunctionalP1Evaluation latency budgetImplemented
S003-FR-032FunctionalP2Rule priority/weight fieldImplemented
S003-FR-033FunctionalP1Bridge-up prerequisiteImplemented
S003-FR-034FunctionalP2CLI reload and request commandsImplemented
S003-FR-035FunctionalP2Future CLI list and expression testDraft
S003-FR-036FunctionalP1Host API endpointsImplemented
S003-FR-037FunctionalP2Rule description fieldImplemented
S003-FR-038FunctionalP1Empty rules directoryImplemented
S003-FR-039FunctionalP3Enrich resource validationDeferred
S003-FR-041FunctionalP1Reject unsafe enrich rulesImplemented
S003-FR-040FunctionalP1Evaluation must be synchronous per requestImplemented
S003-AS-001AcceptanceP1Allow rule matches requestImplemented
S003-AS-002AcceptanceP1Block rule matches requestImplemented
S003-AS-003AcceptanceP1No rule matches (default block)Implemented
S003-AS-004AcceptanceP1First match wins orderingImplemented
S003-AS-005AcceptanceP2Definition variable expansionImplemented
S003-AS-006AcceptanceP1Unsafe enrich action is rejectedImplemented
S003-AS-007AcceptanceP1Startup CEL parse error abortsImplemented
S003-AS-008AcceptanceP1Startup unused definition warnsImplemented
S003-AS-009AcceptanceP2Hot reload via APIImplemented
S003-AS-010AcceptanceP2Hot reload via file watchDraft
S003-AS-011AcceptanceP1Multi-file filename sort orderImplemented
S003-AS-012AcceptanceP3Agent submits rule requestImplemented
S003-AS-013AcceptanceP2Host approves rule requestImplemented
S003-AS-014AcceptanceP2Host rejects rule requestImplemented
S003-AS-015AcceptanceP1CLI lists loaded rulesDraft
S003-AS-016AcceptanceP2CLI tests expression against mock contextDraft
S003-AS-017AcceptanceP1Log flag produces audit log entryImplemented
S003-AS-018AcceptanceP1Daemon not running (CLI)Implemented
S003-IF-001InterfaceP1POST /api/v1/rule/evaluateImplemented
S003-IF-002InterfaceP1GET /api/v1/rulesImplemented
S003-IF-003InterfaceP1GET /api/v1/rule/:idImplemented
S003-IF-004InterfaceP2POST /api/v1/rules/reloadImplemented
S003-IF-005InterfaceP2POST /api/v1/rule/testImplemented
S003-IF-006InterfaceP3Agent POST /v1/requests/rulesImplemented
S003-IF-007InterfaceP2Host GET /api/v1/requests/rulesImplemented
S003-IF-008InterfaceP2Host POST request approvalImplemented
S003-IF-009InterfaceP2Host POST request rejectionImplemented
S003-IF-010InterfaceP1CLI commandsImplemented
S003-IF-011InterfaceP1CLI output formatImplemented
S003-IF-012InterfaceP1YAML rule file schemaImplemented
S003-IF-013InterfaceP1Context variable typesImplemented
S003-EC-001Edge CaseP1Invalid CEL expressionImplemented
S003-EC-002Edge CaseP1Missing context variableImplemented
S003-EC-003Edge CaseP2Enrich hook timeoutDeferred
S003-EC-004Edge CaseP2Enrich hook nonexistent scriptDeferred
S003-EC-005Edge CaseP2Enrich hook non-zero exitDeferred
S003-EC-006Edge CaseP1Empty rules directoryImplemented
S003-EC-007Edge CaseP1Duplicate rule ID across filesImplemented
S003-EC-008Edge CaseP1Unsupported version fieldImplemented
S003-EC-009Edge CaseP2Undefined definition variableImplemented
S003-EC-010Edge CaseP2Circular definition referenceImplemented
S003-EC-011Edge CaseP2Rule file with only definitionsImplemented
S003-EC-012Edge CaseP2CEL expression returns non-booleanImplemented
S003-EC-013Edge CaseP1Rules directory does not existImplemented
S003-EC-014Edge CaseP2File permissions prevent readingImplemented
S003-EC-015Edge CaseP2Reload with invalid new rulesImplemented
S003-EC-016Edge CaseP2Concurrent evaluation during reloadImplemented
S003-EC-017Edge CaseP3Agent requests rule for already-allowed actionImplemented
S003-EC-018Edge CaseP1Malformed YAML fileImplemented
S003-EC-019Edge CaseP2Very large rule set (performance)Draft
S003-EC-020Edge CaseP2Non-YAML files in rules directoryImplemented
S003-SC-001SuccessP1Allow rule evaluated correctlyImplemented
S003-SC-002SuccessP1Block rule evaluated correctlyImplemented
S003-SC-003SuccessP1Default block on no matchImplemented
S003-SC-004SuccessP1Filename sort order verifiedImplemented
S003-SC-005SuccessP1First match wins verifiedImplemented
S003-SC-006SuccessP1Static analysis catches errorsImplemented
S003-SC-007SuccessP2Definition expansion worksImplemented
S003-SC-008SuccessP1Unsafe enrich action is rejectedImplemented
S003-SC-009SuccessP2Hot reload applies new rulesImplemented
S003-SC-010SuccessP1Evaluation latency under budgetDraft
S003-SC-011SuccessP1Audit log entries for logged rulesImplemented
S003-SC-012SuccessP1Bridge and network commands unaffectedImplemented
S003-SC-013SuccessP2Agent rule requests queue correctlyImplemented
S003-SC-014SuccessP1All context variables accessible in CELImplemented

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>,
}

#[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