Specification: Policy Evaluation Semantics¶
Spec ID:
tower-fleet/policy/v1Status: Normative Version: 1.0.0 Created: 2025-12-22
Abstract¶
This specification defines the policy evaluation semantics for intent-based operations. It establishes risk classification, controls enforcement, and confirmation flows that gate execution based on operation characteristics.
Conformance¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
1. Goals¶
- Risk-proportional controls - Higher risk operations require stronger confirmation
- Fail-closed - Unknown risks default to highest controls
- Auditable decisions - Every policy decision is logged with rationale
- Pure evaluation - Policy is a function: (intent, context) → (decision, controls)
2. Risk Classification¶
2.1 Risk Levels¶
Intents MUST declare exactly one risk level:
| Level | Description | Examples |
|---|---|---|
read-only |
No state changes | observe-app, check-logs |
low |
Recoverable changes | restart-app (rolling restart) |
medium |
Significant changes | scale-app, migrate-schema |
high |
Production-affecting | deploy-app to production |
destructive |
Irreversible changes | delete-app, drop-database |
2.2 Risk Inheritance¶
If an intent does not declare a risk level:
- Intents with mode: observe default to read-only
- Intents with mode: mutate default to high (fail-closed)
- Intents touching production resources default to high
3. Controls¶
Controls are enforcement mechanisms applied based on risk level.
3.1 Control Types¶
| Control | Description | Values |
|---|---|---|
confirmation |
User must confirm before execution | none, yes, type-to-confirm |
lock |
Acquire exclusive lock before execution | lock name template |
dry_run_first |
Require dry-run before real execution | true, false |
explicit_env |
Environment must be explicit (not defaulted) | true, false |
require_clean_git |
Working directory must be clean | true, false |
cooldown |
Minimum time between executions | duration (e.g., 5m) |
3.2 Default Controls by Risk Level¶
# policy/rules.yaml
defaults:
read-only:
confirmation: none
lock: null
dry_run_first: false
low:
confirmation: yes
lock: null
dry_run_first: false
medium:
confirmation: yes
lock: "${intent}-${params.app}"
dry_run_first: false
high:
confirmation: yes
lock: "${intent}-${params.app}-${params.environment}"
dry_run_first: true
explicit_env: true
destructive:
confirmation: type-to-confirm
lock: "${intent}-${params.app}"
dry_run_first: true
cooldown: 5m
3.3 Control Override¶
Intents MAY override default controls:
# In intent definition
risk: medium
controls:
confirmation: none # Override: no confirmation needed
lock: null # Override: no locking
Override restrictions:
- destructive intents MUST NOT reduce confirmation below yes
- high intents MUST NOT disable locking when targeting production
4. Confirmation Flows¶
4.1 Confirmation: none¶
No user interaction required. Execution proceeds immediately.
4.2 Confirmation: yes¶
User must confirm with y, yes, or --confirm flag.
4.3 Confirmation: type-to-confirm¶
User must type the exact resource name to confirm.
This will DELETE deployment 'home-portal' in namespace 'home-portal'.
Type 'home-portal' to confirm: _
The confirmation target is determined by:
1. controls.confirm_target if specified
2. ${params.app} if present
3. Intent name otherwise
4.4 Dry-Run Bypass¶
In --dry-run mode, all confirmations are bypassed with audit logging:
5. Policy Evaluation¶
5.1 Evaluation Function¶
Policy evaluation is a pure function:
Where PolicyDecision is:
interface PolicyDecision {
allowed: boolean;
controls: Controls;
reasons: string[]; // Why this decision was made
warnings: string[]; // Non-blocking concerns
blocked_by?: string; // Which rule blocked (if denied)
}
5.2 Evaluation Order¶
- Parse intent risk level (default if not specified)
- Load default controls for risk level
- Apply intent control overrides
- Evaluate prereqs (existing mechanism)
- Check context constraints (time, actor, environment)
- Return decision with merged controls
5.3 Context Constraints¶
Policy MAY include context-based rules:
# policy/rules.yaml
context_rules:
- name: no_production_deploys_friday_afternoon
match:
risk: [high, destructive]
params.environment: production
when:
day_of_week: [5] # Friday
hour_gte: 14 # After 2 PM
action: deny
message: "Production deploys blocked Friday afternoon"
- name: require_approval_for_destructive
match:
risk: destructive
when:
actor_not_in: [admin, owner]
action: deny
message: "Destructive operations require admin approval"
6. Intent Schema Extension¶
6.1 Risk and Controls Fields¶
intent: deploy-app
version: v1.0.0
# Risk classification (required for mutate mode)
risk: high
# Control overrides (optional)
controls:
confirmation: yes
lock: "deploy-${params.app}"
explicit_env: true
dry_run_first: true
# Existing fields...
params:
app: ""
environment: ""
6.2 Validation Schema¶
{
"risk": {
"type": "string",
"enum": ["read-only", "low", "medium", "high", "destructive"]
},
"controls": {
"type": "object",
"properties": {
"confirmation": {
"enum": ["none", "yes", "type-to-confirm"]
},
"lock": {
"type": ["string", "null"]
},
"dry_run_first": {
"type": "boolean"
},
"explicit_env": {
"type": "boolean"
},
"require_clean_git": {
"type": "boolean"
},
"cooldown": {
"type": "string",
"pattern": "^[0-9]+[smh]$"
},
"confirm_target": {
"type": "string"
}
}
}
}
7. Audit Events¶
7.1 Policy Evaluated¶
Emitted after policy evaluation completes:
{
"event": "policy_evaluated",
"decision": "approved",
"risk_level": "medium",
"controls_applied": {
"confirmation": "yes",
"lock": "deploy-home-portal"
},
"rules_matched": ["default_medium_controls"],
"warnings": []
}
7.2 Policy Denied¶
{
"event": "policy_evaluated",
"decision": "denied",
"risk_level": "high",
"blocked_by": "no_production_deploys_friday_afternoon",
"reason": "Production deploys blocked Friday afternoon"
}
7.3 Confirmation Events¶
{"event": "confirmation_required", "type": "type-to-confirm", "target": "home-portal"}
{"event": "confirmation_received", "type": "type-to-confirm", "input": "home-portal", "valid": true}
8. Implementation¶
8.1 File Structure¶
intents/
├── policy/
│ ├── rules.yaml # Risk → controls mapping + context rules
│ └── evaluator.sh # Policy evaluation script
├── examples/
│ └── *.yaml # Intent definitions with risk levels
8.2 Evaluator Interface¶
# Evaluate policy for an intent
# Input: Intent JSON on stdin or as argument
# Output: PolicyDecision JSON on stdout
# Exit: 0 = allowed, 1 = denied, 2 = error
./scripts/policy-evaluator.sh --intent deploy-app --params '{"app":"home-portal"}'
# Output:
{
"allowed": true,
"controls": {
"confirmation": "yes",
"lock": "deploy-home-portal"
},
"reasons": ["risk:high", "default_controls"],
"warnings": []
}
8.3 Integration with run-intent.sh¶
# In run-intent.sh, after parsing intent:
policy_decision=$(./scripts/policy-evaluator.sh --intent-json "$INTENT_JSON")
if [[ $(echo "$policy_decision" | jq -r '.allowed') != "true" ]]; then
emit_event policy_evaluated "$policy_decision"
log ERROR "Policy denied: $(echo "$policy_decision" | jq -r '.blocked_by')"
exit 1
fi
# Apply controls from decision
CONTROLS=$(echo "$policy_decision" | jq -c '.controls')
9. Examples¶
9.1 Read-Only Intent¶
intent: observe-app
risk: read-only
# No controls needed - inherits defaults (confirmation: none, no lock)
9.2 Medium Risk with Lock¶
9.3 Destructive with Type-to-Confirm¶
intent: delete-namespace
risk: destructive
controls:
confirmation: type-to-confirm
confirm_target: "${params.namespace}"
lock: "delete-${params.namespace}"
cooldown: 10m
10. Security Considerations¶
- No control weakening in production - Cannot reduce controls for production targets
- Audit all decisions - Policy decisions are logged for forensics
- Fail-closed - Parse errors or unknown risks result in denial
- Context isolation - Policy evaluator has no side effects
Appendix A: Migration from Current System¶
Current intents use policy.prereqs and policy.require_confirmation. Migration:
- Add
riskfield to all intents - Move
require_confirmationlogic tocontrols.confirmation - Keep
prereqsas-is (orthogonal to risk controls) - Deprecate
policy.require_confirmationin favor ofcontrols
Backwards compatibility:
- If risk not specified, infer from mode field
- If controls not specified, use risk-level defaults
- policy.require_confirmation honored if controls.confirmation not set