Functional requirements
S003 Functional Requirements
YAML rule file format
S003-FR-001 [P1] outcalld MUST load rule files from a configurable rules directory. Each file MUST have a .yaml extension. Files without .yaml extension MUST be ignored.
S003-FR-002 [P1] Each rule file MUST contain a version field at the top level. The only supported value is "1". An unsupported or missing version MUST cause a startup error.
S003-FR-003 [P1] Each rule in the rules list MUST have:
S003-FR-003.a id -- a string identifier unique across all loaded rule files
S003-FR-003.b condition -- a CEL expression that evaluates to a boolean
S003-FR-003.c action -- one of allow, block, or enrich
S003-FR-037 [P2] Each rule MAY have a description field containing a human-readable explanation. This field is for documentation and CLI display only; it has no effect on evaluation.
S003-FR-020 [P1] Each rule MAY have a log: true field. When a rule with log: true matches, outcalld MUST emit a structured log entry containing the rule ID, the decision, and the request context summary.
CEL expression evaluation
S003-FR-004 [P1] outcalld MUST evaluate rule conditions using CEL (Common Expression Language). Conditions MUST be parsed and compiled at load time, not at evaluation time.
S003-FR-017 [P1] CEL evaluation MUST use the cel-interpreter crate.
S003-FR-005 [P1] The CEL evaluation context MUST expose the following variable namespaces:
S003-FR-005.a network -- hostname (string, optional), ip (string), port (int), protocol (string)
S003-FR-005.b http -- method (string), path (string), host (string), headers (map<string,string>), body_size (int)
S003-FR-005.c dns -- query (string), record_type (string)
S003-FR-005.d docker -- image (string), command (list), volumes (list), env_keys (list), capabilities (list)
S003-FR-005.e run -- tool (string), args (list), flags (list), cwd (string), context (map<string,any>). Note: the run.context map is populated from agent-provided metadata key-value pairs (see S004-FR-013).
S003-FR-005.f Each namespace is optional. When a namespace is absent from the evaluation request (e.g., a DNS query has no http context), any CEL expression that references a field in the absent namespace MUST evaluate to false, not error. The implementation MUST inject absent namespaces as objects with all fields set to their zero values (empty string, 0, empty list/map) so that CEL field access succeeds without special error handling.
S003-FR-040 [P1] The rule set MUST be immutable during evaluation of a single request. Concurrent evaluations from multiple containers against the same immutable rule set are permitted and expected. Rule reloads MUST swap the entire rule set atomically (new requests see the new set, in-flight evaluations complete against the old set).
Definition variable substitution
S003-FR-006 [P2] Rule files MAY contain a definitions section mapping names to CEL sub-expressions. In rule conditions, $name references MUST be replaced with the corresponding definition expression (parenthesized) before CEL compilation.
S003-FR-006.a Definitions MUST be expanded recursively -- a definition MAY reference another definition.
S003-FR-006.b An undefined $name reference MUST cause a startup error.
S003-FR-006.c A circular reference chain MUST be detected and cause a startup error.
S003-FR-006.d Definitions are scoped to the file that declares them. A definition in one file is not visible to rules in another file.
Evaluation order
S003-FR-007 [P1] Rule files MUST be loaded in filename lexicographic sort order. Within a file, rules MUST be evaluated in the order they appear in the rules list. This is the base evaluation order when no priority field (S003-FR-032) is present. When priorities are used, priority takes precedence and filename/position is the tiebreaker within the same priority level.
S003-FR-008 [P1] Evaluation MUST follow first-match-wins semantics. The first rule whose condition evaluates to true with action allow or block determines the decision. Evaluation MUST stop after the first terminal match.
S003-FR-009 [P1] If no allow or block rule matches, the decision MUST be block.
S003-FR-010 [P1] The default BLOCK policy MUST NOT be configurable. There is no default_action setting.
S003-FR-032 [P2] Rules MAY include an optional numeric priority field (integer, default 100). When present, rules are sorted globally by priority (ascending — lower number = higher priority). Filename/position order is the tiebreaker within the same priority level. This means a priority: 1 rule in 99-custom.yaml evaluates before a priority: 50 rule in 00-base.yaml. Rules without a priority field are assigned the default (100) and sort among themselves by filename/position.
Enrich hooks
S003-FR-011 [P2] When evaluation reaches a rule with action: enrich, outcalld MUST execute the specified host-side script. The script path is relative to the rules directory.
S003-FR-012 [P2] Enrich hooks MUST have a configurable timeout. The default timeout is 5000ms. Per-rule timeout overrides MAY be specified via the enrich.timeout_ms field.
S003-FR-013 [P2] The enrich script receives the evaluation context as JSON on stdin and MUST write a JSON object to stdout. The output is merged into run.context for subsequent rules. Enrich rules do not terminate evaluation -- processing continues with the next rule.
S003-FR-039 [P2] At startup, outcalld SHOULD verify that enrich hook scripts exist and are executable. A missing or non-executable script SHOULD produce a warning.
Static analysis at startup
S003-FR-014 [P1] outcalld MUST perform static analysis on all rule files at startup before accepting any traffic.
S003-FR-015 [P1] Static analysis MUST treat the following as errors that abort startup:
S003-FR-015.a CEL parse/compile failure
S003-FR-015.b Missing or unsupported version field
S003-FR-015.c Duplicate rule IDs across all files
S003-FR-015.d Undefined $name variable references
S003-FR-015.e Circular definition references
S003-FR-015.f Malformed YAML (parse failure)
S003-FR-016 [P1] Static analysis MUST treat the following as warnings that are logged but do not prevent startup:
S003-FR-016.a Unused definitions (defined but never referenced)
S003-FR-016.b Enrich hook scripts that are missing or not executable
S003-FR-016.c Rule files with a definitions section but no rules section
YAML parsing
S003-FR-018 [P1] YAML parsing MUST use the serde and serde_yaml crates. The rule file schema MUST be represented as Rust types with Serialize and Deserialize derives.
Evaluation response
S003-FR-019 [P1] Every evaluation MUST return a result containing:
S003-FR-019.a The decision (allow or block)
S003-FR-019.b The ID of the matched rule, or null if the default block applied
S003-FR-019.c The filename of the matched rule, or null if the default block applied
S003-FR-019.d Whether the match was logged
Rule reload
S003-FR-021 [P2] outcalld MUST support reloading rules without restarting the daemon.
S003-FR-022 [P2] Reload MAY be triggered by a host API endpoint or by file system change detection. At minimum, the API trigger MUST be supported.
S003-FR-023 [P2] Reload MUST be atomic: the new rule set is fully parsed and validated before it replaces the old set. If the new set fails validation, the old rules remain active and the reload returns an error.
Agent rule requests
S003-FR-024 [P3] Agents MAY submit rule requests through the agent-facing API. A request includes a description, the access being requested, and an optional suggested CEL condition.
S003-FR-025 [P3] Rule requests MUST be queued with status pending. They are not evaluated or applied until a host operator acts on them.
S003-FR-026 [P2] The host operator MUST be able to approve or deny pending rule requests via the host API. Approving a request writes a new rule file to the rules directory and triggers a reload.
Configuration and integration
S003-FR-027 [P1] The rules directory MUST be configurable via --rules-dir flag. The default is /etc/outcall/rules.d.
S003-FR-028 [P1] Rule IDs MUST be unique across all loaded files. Duplicate IDs MUST be a startup error.
S003-FR-029 [P1] outcalld MUST emit structured log entries for every evaluation when the matched rule has log: true, and SHOULD emit debug-level log entries for all evaluations. Log entries MUST include: rule ID (or "default-block"), decision, timestamp, and a context summary.
S003-FR-030 [P1] Rule engine errors MUST use typed errors consistent with the existing outcalld error system.
S003-FR-031 [P1] CEL evaluation for a single request MUST complete within 50ms under normal conditions. If evaluation exceeds this budget, outcalld MUST log a warning.
S003-FR-033 [P1] The rule engine MUST verify that the bridge is up before evaluating requests. If the bridge is not up, evaluation MUST return an error (not a block decision).
S003-FR-034 [P2] The outcall CLI MUST add a rule subcommand with sub-subcommands: list, show, reload, test, requests.
S003-FR-035 [P2] The outcall rule test command MUST accept a CEL expression and a JSON context, evaluate the expression, and print the result.
S003-FR-036 [P1] Rule management endpoints MUST be served on the host unix socket only. The agent-facing socket MUST only expose the rule request submission endpoint (S003-IF-006).
S003-FR-038 [P1] If the rules directory exists but contains no .yaml files, the daemon MUST start successfully. All evaluations MUST return the default block decision.