Acceptance scenarios
S003 Acceptance Scenarios
S003-AS-001 Evaluate: allow rule matches [P1]
Given the outcalld daemon is running and the bridge is up
And a rule file contains:
rules:
- id: "allow-github"
condition: network.hostname == "github.com" && http.method == "GET"
action: allowWhen an agent container makes an HTTP GET request to github.com
Then the rule engine returns decision: allow with matched_rule: "allow-github"
And the request is permitted.
S003-AS-002 Evaluate: block rule matches [P1]
Given the outcalld daemon is running and the bridge is up
And a rule file contains:
rules:
- id: "block-force-push"
condition: run.tool == "git" && "-f" in run.flags
action: block
log: trueWhen an agent executes git push -f
Then the rule engine returns decision: block with matched_rule: "block-force-push"
And a structured log entry is emitted with the rule ID, decision, and context summary
And the request is denied.
S003-AS-003 Evaluate: no rule matches (default block) [P1]
Given the outcalld daemon is running and the bridge is up
And no rule condition matches the incoming request
When an agent container makes an HTTP request to evil.example.com
Then the rule engine returns decision: block with matched_rule: null
And the request is denied.
S003-AS-004 Evaluate: first match wins ordering [P1]
Given the outcalld daemon is running and the bridge is up
And file 00-base.yaml contains:
rules:
- id: "allow-all-github"
condition: network.hostname == "github.com"
action: allowAnd file 10-restrictions.yaml contains:
rules:
- id: "block-github-admin"
condition: network.hostname == "github.com" && http.path.startsWith("/admin")
action: blockWhen an agent makes a request to github.com/admin/settings
Then the rule engine returns decision: allow with matched_rule: "allow-all-github"
Because 00-base.yaml sorts before 10-restrictions.yaml and the first match wins.
S003-AS-005 Evaluate: definition variable expansion [P2]
Given the outcalld daemon is running and the bridge is up
And a rule file contains:
definitions:
is_github: network.hostname == "github.com"
rules:
- id: "allow-github-api"
condition: $is_github && http.path.startsWith("/api/v3")
action: allowWhen an agent makes a GET request to github.com/api/v3/repos
Then the rule engine expands $is_github to (network.hostname == "github.com")
And evaluates the full condition
And returns decision: allow.
S003-AS-006 Evaluate: enrich hook populates context [P2]
Given the outcalld daemon is running and the bridge is up
And a rule file contains:
rules:
- id: "enrich-git-context"
condition: run.tool == "git"
action: enrich
enrich:
script: hooks/check-repo.sh
- id: "allow-git-on-main"
condition: run.tool == "git" && run.context.branch == "main"
action: allowAnd the script hooks/check-repo.sh outputs {"branch": "main"}
When an agent runs a git command
Then the enrich hook executes first, populating run.context.branch
And the next rule evaluates with the enriched context
And returns decision: allow.
S003-AS-007 Startup: CEL parse error aborts [P1]
Given a rule file contains an invalid CEL expression:
rules:
- id: "bad-rule"
condition: "network.hostname =="
action: allowWhen outcalld starts
Then static analysis detects the CEL parse error
And the daemon logs the error with the file name, rule ID, and parse details
And the daemon aborts startup with a non-zero exit code.
S003-AS-008 Startup: unused definition warns [P1]
Given a rule file contains:
definitions:
unused_var: network.hostname == "example.com"
rules:
- id: "allow-all"
condition: "true"
action: allowWhen outcalld starts
Then static analysis detects that unused_var is never referenced
And the daemon logs a warning
And the daemon starts successfully.
S003-AS-009 Hot reload: via API [P2]
Given the outcalld daemon is running with rules loaded
And the host operator has modified a rule file on disk
When the host operator calls POST /api/v1/rules/reload
Then outcalld re-reads the rules directory
And validates the new rule set
And atomically swaps the new rules into the active set
And returns the count of files and rules loaded.
S003-AS-010 Hot reload: via file watch [P2]
Given the outcalld daemon is running with file watch enabled
When a rule file is modified, added, or removed in the rules directory
Then outcalld detects the change
And performs the same validation and atomic swap as API-triggered reload
And logs the reload result.
S003-AS-011 Evaluate: multi-file filename sort order [P1]
Given the rules directory contains files: 50-custom.yaml, 00-base.yaml, 25-team.yaml
When outcalld loads rules
Then files are loaded in order: 00-base.yaml, 25-team.yaml, 50-custom.yaml
And rules from 00-base.yaml are evaluated before rules from 25-team.yaml
And rules from 25-team.yaml are evaluated before rules from 50-custom.yaml.
S003-AS-012 Agent: submits rule request [P3]
Given the outcalld daemon is running
When an agent submits a rule request via POST /api/v1/agent/rule-request with:
{
"description": "Need access to PyPI for package installation",
"requested_access": "HTTPS to pypi.org",
"suggested_condition": "network.hostname == \"pypi.org\" && http.method == \"GET\""
}Then the request is queued with status pending
And the agent receives a request ID
And the request does not affect rule evaluation.
S003-AS-013 Host: approves rule request [P2]
Given a pending rule request exists with ID req-001
When the host operator calls POST /api/v1/rule-request/req-001/approve
Then outcalld writes a new rule file to the rules directory
And triggers an automatic reload
And the new rule is active for subsequent evaluations.
S003-AS-014 Host: denies rule request [P2]
Given a pending rule request exists with ID req-001
When the host operator calls POST /api/v1/rule-request/req-001/deny
Then the request status changes to denied
And no rule file is written
And active rules are unchanged.
S003-AS-015 CLI: lists loaded rules [P1]
Given the outcalld daemon is running with rules loaded
When the user runs outcall rule list
Then the CLI prints a table of all loaded rules with their IDs, files, actions, and condition previews
And the command exits with code 0.
S003-AS-016 CLI: tests expression against mock context [P2]
Given the outcalld daemon is running
When the user runs:
outcall rule test --expr 'network.hostname == "github.com"' --context '{"network":{"hostname":"github.com","ip":"1.2.3.4","port":443,"protocol":"tcp"}}'Then the CLI prints Result: true
And the command exits with code 0.
S003-AS-017 Evaluate: log flag produces audit entry [P1]
Given a rule with log: true matches a request
When the evaluation completes
Then outcalld emits a structured log entry at info level containing:
- rule ID
- decision (allow or block)
- timestamp
- context summary (hostname, path, tool, or other relevant fields)
S003-AS-018 CLI: daemon not running [P1]
Given the outcalld daemon is not running
When the user runs any outcall rule subcommand
Then the CLI prints Error: cannot connect to outcalld at <socket> -- is it running?
And the command exits with code 1.