Skip to content

Specification: Policy Evaluation Semantics

Spec ID: tower-fleet/policy/v1 Status: 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

  1. Risk-proportional controls - Higher risk operations require stronger confirmation
  2. Fail-closed - Unknown risks default to highest controls
  3. Auditable decisions - Every policy decision is logged with rationale
  4. 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.

Apply migrations for home-portal to production? [y/N]

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:

{"event": "confirmation_bypassed", "reason": "dry_run_mode"}


5. Policy Evaluation

5.1 Evaluation Function

Policy evaluation is a pure function:

evaluate_policy(intent, params, context) → PolicyDecision

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

  1. Parse intent risk level (default if not specified)
  2. Load default controls for risk level
  3. Apply intent control overrides
  4. Evaluate prereqs (existing mechanism)
  5. Check context constraints (time, actor, environment)
  6. 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

intent: scale-app
risk: medium
controls:
  lock: "scale-${params.app}"
  confirmation: yes

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

  1. No control weakening in production - Cannot reduce controls for production targets
  2. Audit all decisions - Policy decisions are logged for forensics
  3. Fail-closed - Parse errors or unknown risks result in denial
  4. Context isolation - Policy evaluator has no side effects

Appendix A: Migration from Current System

Current intents use policy.prereqs and policy.require_confirmation. Migration:

  1. Add risk field to all intents
  2. Move require_confirmation logic to controls.confirmation
  3. Keep prereqs as-is (orthogonal to risk controls)
  4. Deprecate policy.require_confirmation in favor of controls

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