Outcall
SpecificationsS003 · Rule Engine

Interface requirements

S003 Interface Requirements

Unless stated otherwise, /api/v1/... endpoints are available only on the operator-owned host Unix socket. Agent endpoints use the separately mounted agent Unix socket and require a valid host-derived container identity and check-in session.

S003-IF-001 POST /api/v1/rule/evaluate [P1]

Evaluate a typed context against the loaded rules. The host endpoint first requires the managed bridge to be up.

{
  "context": {
    "network": {
      "hostname": "github.com",
      "ip": "140.82.121.4",
      "port": 443,
      "protocol": "tcp"
    },
    "http": {
      "method": "GET",
      "path": "/api/v3/repos",
      "host": "github.com",
      "headers": {},
      "body_size": 0
    },
    "agent": { "name": "review-agent" }
  }
}

Success returns ApiResponse<EvaluateResult>:

{
  "success": true,
  "data": {
    "decision": "allow",
    "matched_rule": "allow-github-api",
    "file": "00-base.yaml",
    "logged": false
  }
}

No match returns a successful block decision with matched_rule and file set to null. A bridge or evaluation precondition failure returns success: false and no data.

S003-IF-002 GET /api/v1/rules [P1]

Return ApiResponse<Vec<RuleSummary>> in effective evaluation order: ascending priority, with filename and source position preserving order among equal priorities.

{
  "success": true,
  "data": [{
    "id": "allow-github-api",
    "file": "00-base.yaml",
    "action": "allow",
    "condition_preview": "network.hostname == \"github.com\"",
    "description": "Allow GitHub API access"
  }]
}

S003-IF-003 GET /api/v1/rule/:id [P1]

Return ApiResponse<RuleDetail> with the fully expanded CEL condition.

{
  "success": true,
  "data": {
    "id": "allow-github-api",
    "condition": "network.hostname == \"github.com\"",
    "action": "allow",
    "log": false,
    "description": "Allow GitHub API access",
    "priority": 100
  }
}

An unknown ID returns success: false with rule not found: "<id>".

S003-IF-004 POST /api/v1/rules/reload [P2]

Trigger an atomic disk reload. The request has no body.

{
  "success": true,
  "data": {
    "files_loaded": 3,
    "rules_loaded": 12,
    "warnings": []
  }
}

Validation failure returns success: false; the previous rule snapshot and derived bridge policy remain active.

S003-IF-005 POST /api/v1/rule/test [P2]

Compile and evaluate one CEL expression without changing active policy.

{
  "expression": "network.hostname == \"github.com\"",
  "context": {
    "network": {
      "hostname": "github.com",
      "ip": "140.82.121.4",
      "port": 443,
      "protocol": "tcp"
    }
  }
}

The response is ApiResponse<TestExpressionResult>. CEL syntax or runtime errors are returned in data.error with data.result set to false.

S003-IF-006 POST /v1/requests/rules [P3]

Submit a complete rule file on the agent socket:

{
  "rule_file": "version: \"1\"\nrules:\n  - id: allow-pypi\n    condition: 'network.hostname == \"pypi.org\"'\n    action: allow\n"
}

The daemon validates the file before durably queuing it. Success is HTTP 201 with ApiResponse<RuleRequestResponse>:

{
  "success": true,
  "data": {
    "id": "rr-aabbcc112233",
    "status": "pending",
    "reason": null
  }
}

GET /v1/requests/rules/:id lets the submitting container inspect its own request status. Requests belonging to another container are not disclosed.

S003-IF-007 GET /api/v1/requests/rules [P2]

Return pending requests on the host socket as ApiResponse<Vec<PendingRuleRequest>>. Each item includes id, the verified container_id, the submitted rule_file, status, and optional reason. Completed requests are retained for bounded durable audit state but omitted from this queue view.

S003-IF-008 POST /api/v1/requests/rules/:id/approve [P2]

Approve a pending request. The request has no body. Outcall creates agent-<id>.yaml atomically with mode 0600, reloads policy, and only then marks the request approved.

{
  "success": true,
  "data": {
    "id": "rr-aabbcc112233",
    "rules_loaded": 12
  }
}

Unknown or completed IDs return HTTP 404 or 409. A write, reload, or durable state failure returns HTTP 500 and rolls policy back where possible.

S003-IF-009 POST /api/v1/requests/rules/:id/reject [P2]

Reject a pending request with an optional bounded reason:

{ "reason": "Use the existing package mirror rule." }

Success returns ApiResponse<RejectRuleResult> containing the request id. The reason is limited to 1024 bytes and may not be empty or contain control characters.

S003-IF-010 CLI commands [P1]

outcall rules reload
outcall requests list
outcall requests approve <request-id>
outcall requests reject <request-id> [--reason <text>]

All commands accept the global --socket <path> option. Raw rule inspection and CEL testing remain available through S003-IF-002, S003-IF-003, and S003-IF-005; dedicated CLI wrappers are tracked separately by S003-FR-035.

S003-IF-011 CLI output format [P1]

The CLI prints concise human-readable results and writes errors to stderr. Representative success output is:

Reloaded 12 rule(s) from 3 file(s).
Rule request "rr-aabbcc112233" approved; 12 rule(s) loaded.
Rule request "rr-aabbcc112233" rejected.

requests list prints ID, CONTAINER, and STATUS, or No pending rule requests. Exit status is zero on success and non-zero on transport or API failure.

S003-IF-012 YAML rule file schema [P1]

version: "1"
definitions:                     # Optional, file-scoped CEL expressions.
  <name>: <CEL expression>
rules:                           # Optional; defaults to an empty list.
  - id: <string>                 # Required and globally unique.
    condition: <CEL expression>  # Required and must evaluate to bool to match.
    action: <allow|block>        # enrich is reserved and currently rejected.
    log: <boolean>               # Optional; default false.
    description: <string>        # Optional.
    priority: <integer>          # Optional; lower runs first, default 100.
    egress:                      # Optional and valid only for allow rules.
      mode: <proxy|direct_ip>
      ports: [<port>, ...]       # direct_ip only; defaults to 80 and 443.
      allow_private_ips: false   # Explicit opt-in for internal destinations.

Both .yaml and .yml files are accepted. Unknown fields, unsafe enrich, and unimplemented egress.mode: intercept settings are rejected.

S003-IF-013 Context variable types [P1]

VariableCEL TypeNotes
network.hostnamestringEmpty when unresolved or absent
network.ipstringEmpty when absent
network.portintZero when absent
network.protocolstringUsually tcp or udp
http.methodstringUppercase for proxy requests
http.pathstringIncludes leading / when present
http.hoststringValidated request authority
http.headersmap(string, string)Header names are normalized
http.body_sizeintBytes
dns.querystringNormalized queried name
dns.record_typestringFor example A or AAAA
docker.imagestringFull image reference
docker.commandlist(string)Command and arguments
docker.volumeslist(string)Requested mount paths
docker.env_keyslist(string)Names only, never values
docker.capabilitieslist(string)Requested Linux capabilities
run.toolstringTool identifier
run.argslist(string)Positional arguments
run.flagslist(string)Flags including dashes
run.cwdstringWorking directory
run.contextmap(string, dyn)Typed request metadata
agent.namestringHost-verified managed agent name

On this page