Writing Policies

Policies are YAML files that define rules for agent actions. TameFlare uses a deny-wins precedence model: if multiple policies match an action and any one returns deny, the action is denied regardless of other policies.


Policy model overview

TameFlare has two enforcement systems that work together:

| System | Mode | How it works | Best for | |---|---|---|---| | Access rules (gateway permissions) | Mode A (proxy) | Per-agent, per-connector allow/deny rules set via CLI or dashboard wizard | Simple setups: "Agent X can use GitHub but not Stripe" | | Policies (JSON) | Mode A + Mode B | Condition-based rules evaluated against the full action spec | Complex logic: "Allow PR creates, require approval for merges to main, deny branch deletes" |

Which takes precedence?

Evaluation order:

  1. Kill switch — if active (scoped or global), the action is immediately denied
  2. Access rules — gateway checks if the agent has permission for this connector. If no permission exists, the request is blocked (deny-all default). If the permission is require_approval, the proxy holds the connection.
  3. Policies — if the action passes access rules, policies are evaluated for fine-grained decisions

Access rules are the first gate. Policies provide additional refinement. An action must pass both to be allowed.

Tip
For most users, access rules (set via the dashboard wizard or tf permissions set) are sufficient. Policies are for teams that need condition-based logic, risk-based routing, or policy-as-code workflows.

When to use each

| Scenario | Use access rules | Use policies | |---|---|---| | "Agent X can use GitHub" | Yes | Not needed | | "Agent X can read GitHub but not write" | Partial (action pattern: github.*.get) | Better with policies for complex patterns | | "Block merges to main unless approved" | No | Yes — condition on parameters.branch_name | | "Require approval for transfers over $10k" | No | Yes — condition on parameters.amount | | "Block all actions during maintenance windows" | No | Yes — time-window policy pack |


Decision algorithm

When an action is evaluated, TameFlare follows this algorithm:

1. Collect all enabled policies whose scope matches the action
2. Sort by priority (highest first)
3. For each policy, evaluate rules in order:
   a. First rule whose conditions match → record its decision
   b. If no rules match → policy has no effect (skip)
4. Aggregate all decisions using deny-wins:
   - If ANY policy returned "deny" → final decision = DENY
   - Else if ANY policy returned "requires_approval" → final decision = REQUIRES_APPROVAL
   - Else if ANY policy returned "allow" → final decision = ALLOW
   - Else (no policies matched) → final decision = DENY (default)

Deny-wins precedence

The precedence order is: deny > requires_approval > allow

| Policy A says | Policy B says | Final decision | Why | |---|---|---|---| | allow | allow | allow | Both agree | | allow | requires_approval | requires_approval | Stricter wins | | allow | deny | deny | Deny always wins | | requires_approval | deny | deny | Deny always wins | | requires_approval | requires_approval | requires_approval | Both agree |

Worked example

Given these policies:

  • Policy 1 (priority 900, GitHub Safety): PR merges to mainrequires_approval
  • Policy 2 (priority 800, Payment Controls): All GitHub actions → allow
  • Policy 3 (priority 50, Risk Safety Net): Irreversible + production → requires_approval

Action: github.pr.merge to main in production, marked irreversible.

  1. Policy 1 matches → requires_approval
  2. Policy 2 matches → allow
  3. Policy 3 matches → requires_approval
  4. Aggregate: requires_approval wins over allow (stricter)
  5. Final: requires_approval

If Policy 1 had said deny instead, the final would be deny regardless of the others.


Default behavior

When no policies match an action (no scope matches, or no rule conditions match):

  • Final decision: deny — TameFlare uses a fail-closed model
  • Hint provided: the API response includes a hint field explaining which action type wasn't covered and suggesting you create a catch-all policy
  • The audit log records the denial with reason no_matching_policy

Org-level default

There is no "default allow" setting. The deny-all default is intentional and cannot be overridden globally. To allow actions without specific policies, create a low-priority catch-all policy:

apiVersion: TameFlare/v1
kind: Policy
metadata:
  id: pol_catch_all
  name: Allow All (development only)
  priority: 1
  enabled: true
spec:
  scope: {}
  rules:
    - name: allow-everything
      conditions:
        all:
          - field: type
            operator: exists
      decision: allow
      reason: Catch-all allow for development
Warning
A catch-all allow policy defeats the purpose of TameFlare. Use it only in development or as a temporary measure while building your policy set.

Full YAML schema

Every policy follows this structure:

apiVersion: TameFlare/v1
kind: Policy
metadata:
  id: pol_unique_id
  name: Human-readable name
  description: What this policy does
  tags: [production, safety]
  priority: 100
  enabled: true
spec:
  scope:
    action_types: [github.pr.merge, github.branch.delete]
    resources:
      providers: [github]
      environments: [production, staging]
      targets: [my-org/api-service]
    agents:
      ids: [agent_deploy_bot]
      environments: [production]
  rules:
    - name: rule-name
      conditions:
        all:
          - field: type
            operator: eq
            value: github.pr.merge
      decision: allow | deny | requires_approval
      reason: Human-readable explanation
      approver_groups: [engineering-leads]
      evidence_required: [ticket_url]

metadata

| Field | Required | Default | Description | |---|---|---|---| | id | Yes | — | Unique identifier (e.g., pol_github_safety) | | name | Yes | — | Display name shown in dashboard and audit trail | | description | No | — | Longer explanation of the policy's purpose | | tags | No | [] | Labels for filtering and organization | | priority | No | 100 | Higher priority policies are evaluated first | | enabled | No | true | Set to false to disable without deleting |

scope

The scope determines which actions this policy applies to. If an action doesn't match the scope, the policy is skipped entirely. All scope fields are optional — an empty scope matches everything.

  • action_types — List of action types this policy applies to. If omitted, matches all types.
  • resources.providers — Only match actions targeting these providers (e.g., github, stripe, aws).
  • resources.environments — Only match actions in these environments (e.g., production).
  • resources.targets — Only match actions targeting these specific resources.
  • agents.ids — Only apply to these specific agents.
  • agents.environments — Only apply to agents in these environments.
Tip
Use scopes to keep policies focused. A policy scoped to providers: [github] won't slow down evaluation of Stripe actions.

rules

Rules are evaluated in order within a policy. The first rule whose conditions match determines the decision. If no rules match, the policy has no effect on the action.

Each rule has:

  • conditions — An all (AND) or any (OR) group of conditions. Can be nested.
  • decisionallow, deny, or requires_approval.
  • reason — Shown to the agent and logged in the audit trail.
  • approver_groups — Required when decision is requires_approval. Specifies who can approve.

Conditions and operators

Conditions match fields from the action spec using dot-path notation. For example, parameters.amount accesses the amount field inside parameters.

Available fields

| Field path | Example value | Description | |---|---|---| | type | github.pr.merge | The action type | | resource.provider | github | Service provider | | resource.environment | production | Target environment | | resource.target | my-org/api-service | Specific resource | | parameters.* | Any value | Action-specific parameters | | risk_hints.production_target | true | Risk hint flags | | risk_hints.financial_impact | true | Risk hint flags | | risk_hints.irreversible | true | Risk hint flags | | risk_hints.estimated_blast_radius | high | Risk hint level | | agent_id | agent_deploy_bot | The requesting agent | | agent_environment | production | Agent's environment |

Operators

| Operator | Description | Example | |---|---|---| | eq | Equals | field: type, operator: eq, value: github.pr.merge | | neq | Not equals | field: resource.environment, operator: neq, value: development | | in | Value is in list | field: parameters.currency, operator: in, value: [USD, EUR, GBP] | | not_in | Value is not in list | field: resource.environment, operator: not_in, value: [production] | | contains | String contains substring | field: parameters.body, operator: contains, value: BREAKING | | matches | Glob pattern match | field: resource.target, operator: matches, value: "my-org/*" | | not_matches | Does not match glob | field: type, operator: not_matches, value: "*.delete" | | regex | Regular expression | field: parameters.email, operator: regex, value: ".*@company\\.com$" | | gt | Greater than (numbers) | field: parameters.amount, operator: gt, value: 10000 | | gte | Greater than or equal | field: risk_hints.estimated_blast_radius, operator: gte, value: 5 | | lt | Less than | field: parameters.instance_count, operator: lt, value: 100 | | lte | Less than or equal | field: parameters.amount, operator: lte, value: 500 | | exists | Field is present | field: parameters.ticket_url, operator: exists | | not_exists | Field is absent | field: parameters.approval_override, operator: not_exists |

Nested conditions

You can nest all and any groups to build complex logic:

conditions:
  all:
    - field: type
      operator: eq
      value: github.pr.merge
    - any:
        - field: resource.target
          operator: matches
          value: "my-org/api-*"
        - field: resource.target
          operator: matches
          value: "my-org/web-*"

This matches: action is a PR merge AND (target starts with api- OR starts with web-).

Decisions

| Decision | Behavior | Token issued? | |---|---|---| | allow | Action proceeds immediately | Yes | | deny | Action is blocked with a reason | No | | requires_approval | Slack notification sent, agent waits | Yes, after approval |

Warning
Deny wins. If multiple policies match and any one returns deny, the action is denied — even if higher-priority policies say allow. This is a fail-closed security model.

Real-world examples

1. GitHub: Protect production branches

Block deletion of protected branches and require approval for production merges.

apiVersion: TameFlare/v1
kind: Policy
metadata:
  id: pol_github_branch_safety
  name: GitHub Branch Safety
  tags: [github, production]
  priority: 900
spec:
  scope:
    resources:
      providers: [github]
  rules:
    - name: block-protected-branch-deletion
      conditions:
        all:
          - field: type
            operator: eq
            value: github.branch.delete
          - field: parameters.branch_name
            operator: in
            value: [main, master, develop, staging, release]
      decision: deny
      reason: Cannot delete protected branches
 
    - name: require-approval-for-production-merges
      conditions:
        all:
          - field: type
            operator: eq
            value: github.pr.merge
          - field: resource.environment
            operator: eq
            value: production
      decision: requires_approval
      reason: Production merges require engineering lead approval
      approver_groups:
        - engineering-leads
 
    - name: allow-dev-actions
      conditions:
        all:
          - field: resource.environment
            operator: eq
            value: development
      decision: allow
      reason: Development actions are auto-approved

2. Payments: Control financial transactions

Require approval for large transfers, block transfers to unknown accounts, and deny high-risk currencies.

apiVersion: TameFlare/v1
kind: Policy
metadata:
  id: pol_payment_controls
  name: Payment Controls
  tags: [payments, financial, compliance]
  priority: 800
spec:
  scope:
    action_types:
      - payment.transfer.initiate
      - payment.refund.create
      - payment.subscription.cancel
  rules:
    - name: block-sanctioned-currencies
      conditions:
        all:
          - field: type
            operator: eq
            value: payment.transfer.initiate
          - field: parameters.currency
            operator: in
            value: [KPW, SYP, IRR]
      decision: deny
      reason: Transfers in sanctioned currencies are prohibited
 
    - name: require-approval-large-transfers
      conditions:
        all:
          - field: type
            operator: eq
            value: payment.transfer.initiate
          - field: parameters.amount
            operator: gt
            value: 10000
      decision: requires_approval
      reason: "Transfers over $10,000 require finance team approval"
      approver_groups:
        - finance-team
 
    - name: require-approval-large-refunds
      conditions:
        all:
          - field: type
            operator: eq
            value: payment.refund.create
          - field: parameters.amount
            operator: gt
            value: 500
      decision: requires_approval
      reason: "Refunds over $500 require manager approval"
      approver_groups:
        - support-managers
 
    - name: allow-small-transactions
      conditions:
        all:
          - field: parameters.amount
            operator: lte
            value: 500
      decision: allow
      reason: Small transactions are auto-approved
Tip
The action types above (payment.transfer.initiate, payment.refund.create) are examples. You define your own action types to match your domain. TameFlare doesn't enforce a naming scheme.

3. Infrastructure: Guard production deployments

Prevent destructive infrastructure changes and require approval for production modifications.

apiVersion: TameFlare/v1
kind: Policy
metadata:
  id: pol_infra_safety
  name: Infrastructure Safety
  tags: [infrastructure, aws, production]
  priority: 850
spec:
  scope:
    action_types:
      - infra.server.terminate
      - infra.server.provision
      - infra.dns.modify
      - infra.database.drop
      - infra.cluster.scale
  rules:
    - name: block-database-drops
      conditions:
        all:
          - field: type
            operator: eq
            value: infra.database.drop
          - field: resource.environment
            operator: eq
            value: production
      decision: deny
      reason: Production database drops are permanently blocked
 
    - name: require-approval-server-termination
      conditions:
        all:
          - field: type
            operator: eq
            value: infra.server.terminate
          - field: resource.environment
            operator: in
            value: [production, staging]
      decision: requires_approval
      reason: Server termination in production/staging requires SRE approval
      approver_groups:
        - sre-team
 
    - name: limit-cluster-scaling
      conditions:
        all:
          - field: type
            operator: eq
            value: infra.cluster.scale
          - field: parameters.instance_count
            operator: gt
            value: 50
      decision: requires_approval
      reason: "Scaling beyond 50 instances requires infrastructure lead approval"
      approver_groups:
        - infra-leads
 
    - name: allow-dev-infra
      conditions:
        all:
          - field: resource.environment
            operator: eq
            value: development
      decision: allow
      reason: Development infrastructure changes are auto-approved

4. Communications: Control outbound messages

Prevent agents from sending emails to external recipients without approval, and block mass communications.

apiVersion: TameFlare/v1
kind: Policy
metadata:
  id: pol_comms_safety
  name: Communications Safety
  tags: [email, slack, communications]
  priority: 700
spec:
  scope:
    action_types:
      - email.send
      - slack.message.post
      - notification.broadcast
  rules:
    - name: block-mass-emails
      conditions:
        all:
          - field: type
            operator: eq
            value: email.send
          - field: parameters.recipient_count
            operator: gt
            value: 100
      decision: deny
      reason: Mass emails (100+ recipients) are blocked. Use the marketing platform instead.
 
    - name: require-approval-external-emails
      conditions:
        all:
          - field: type
            operator: eq
            value: email.send
          - field: risk_hints.external_recipient
            operator: eq
            value: true
      decision: requires_approval
      reason: Emails to external recipients require compliance review
      approver_groups:
        - compliance-team
 
    - name: require-approval-announcements
      conditions:
        all:
          - field: type
            operator: eq
            value: notification.broadcast
      decision: requires_approval
      reason: Broadcast notifications require comms team approval
      approver_groups:
        - comms-team
 
    - name: allow-internal-messages
      conditions:
        all:
          - field: risk_hints.external_recipient
            operator: eq
            value: false
      decision: allow
      reason: Internal messages are auto-approved

5. Data access: Protect sensitive information

Control access to PII, prevent bulk data exports, and require approval for cross-environment data movement.

apiVersion: TameFlare/v1
kind: Policy
metadata:
  id: pol_data_access
  name: Data Access Controls
  tags: [data, privacy, compliance]
  priority: 900
spec:
  scope:
    action_types:
      - data.query.execute
      - data.export.create
      - data.record.delete
  rules:
    - name: block-bulk-deletes
      conditions:
        all:
          - field: type
            operator: eq
            value: data.record.delete
          - field: parameters.record_count
            operator: gt
            value: 1000
      decision: deny
      reason: Bulk deletes (1000+ records) are blocked. Use the admin console.
 
    - name: require-approval-pii-access
      conditions:
        all:
          - field: risk_hints.contains_pii
            operator: eq
            value: true
      decision: requires_approval
      reason: Actions involving PII require privacy team approval
      approver_groups:
        - privacy-team
 
    - name: require-approval-large-exports
      conditions:
        all:
          - field: type
            operator: eq
            value: data.export.create
          - field: parameters.row_count
            operator: gt
            value: 10000
      decision: requires_approval
      reason: "Large data exports (10k+ rows) require data team approval"
      approver_groups:
        - data-team
 
    - name: allow-small-queries
      conditions:
        all:
          - field: type
            operator: eq
            value: data.query.execute
          - field: risk_hints.contains_pii
            operator: eq
            value: false
      decision: allow
      reason: Non-PII queries are auto-approved

6. Risk-based: Universal safety net

A catch-all policy that uses risk hints to require approval for any high-risk action, regardless of domain.

apiVersion: TameFlare/v1
kind: Policy
metadata:
  id: pol_risk_safety_net
  name: Risk-Based Safety Net
  tags: [universal, risk, safety]
  priority: 50
spec:
  scope: {}
  rules:
    - name: require-approval-irreversible-production
      conditions:
        all:
          - field: risk_hints.irreversible
            operator: eq
            value: true
          - field: risk_hints.production_target
            operator: eq
            value: true
      decision: requires_approval
      reason: Irreversible production actions require team lead approval
      approver_groups:
        - team-leads
 
    - name: require-approval-high-blast-radius
      conditions:
        all:
          - field: risk_hints.estimated_blast_radius
            operator: eq
            value: high
          - field: risk_hints.financial_impact
            operator: eq
            value: true
      decision: requires_approval
      reason: High blast radius actions with financial impact require approval
      approver_groups:
        - team-leads
Tip
Set this policy to a low priority (e.g., 50) so domain-specific policies take precedence. It acts as a safety net for any action that slips through without a dedicated policy.

Policy scoping guide

Scopes control which actions a policy applies to. Use scopes to keep policies focused and efficient.

Scope levels

| Scope | Field | Effect | |---|---|---| | Global | scope: {} | Matches every action. Use for safety nets. | | Per-connector | scope.resources.providers: [github] | Only evaluates for actions targeting this provider | | Per-action-type | scope.action_types: [github.pr.merge] | Only evaluates for specific action types | | Per-agent | scope.agents.ids: [agent_deploy_bot] | Only applies to a specific agent | | Per-environment | scope.resources.environments: [production] | Only applies in specific environments | | Combined | Multiple fields | All scope fields must match (AND logic) |

Priority ordering

Policies are evaluated in priority order (highest number first). Use priority to control which policies are checked first:

| Priority range | Recommended use | |---|---| | 900-1000 | Hard blocks (deny rules that should never be overridden) | | 700-899 | Domain-specific policies (GitHub safety, payment controls) | | 400-699 | Team-level policies (per-agent restrictions) | | 100-399 | Default/general policies | | 1-99 | Safety nets and catch-all rules |

Priority does not override deny-wins. A priority-50 deny still beats a priority-900 allow. Priority only controls evaluation order, which matters when you want a specific policy's requires_approval to be recorded first (for audit trail clarity).


Policy-as-code workflow

Policies are YAML files that can be stored in git, reviewed in PRs, and deployed via CI.

Recommended workflow

1. Create a policy via the dashboard Policy Builder (or POST JSON to the API)
2. Test with the dry-run sandbox against sample actions
3. Enable the policy and monitor via the audit log
4. Iterate: adjust scope, conditions, and decisions as needed
5. Dashboard shows version history for each policy

Deploying policies via API

# Create a new policy
curl -X POST http://localhost:3000/api/v1/policies \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "GitHub Branch Safety",
    "yaml": "apiVersion: TameFlare/v1\nkind: Policy\nmetadata:\n  id: pol_github_branch_safety\n  name: GitHub Branch Safety\n  priority: 900\nspec:\n  scope:\n    resources:\n      providers: [github]\n  rules:\n    - name: block-branch-delete\n      conditions:\n        all:\n          - field: type\n            operator: eq\n            value: github.branch.delete\n      decision: deny\n      reason: Branch deletion is blocked"
  }'

CI pipeline example (GitHub Actions)

- name: Deploy policies
  run: |
    for policy in policies/*.yaml; do
      # Dry-run first
      curl -sf -X POST http://localhost:3000/api/v1/actions/dry-run \
        -H "Authorization: Bearer $AAF_API_KEY" \
        -H "Content-Type: application/json" \
        -d "{\"action_spec\": {\"type\": \"github.pr.merge\"}}" \
        || echo "Warning: dry-run failed for $policy"
 
      # Deploy
      YAML_CONTENT=$(cat "$policy" | jq -Rs .)
      curl -X POST http://localhost:3000/api/v1/policies \
        -H "Authorization: Bearer $AAF_API_KEY" \
        -H "Content-Type: application/json" \
        -d "{\"name\": \"$(basename $policy .yaml)\", \"yaml\": $YAML_CONTENT}"
    done

Version history

Every policy change is recorded in the policy_versions table:

  • Version number incremented on each update
  • Changed by — user who made the change
  • Change typecreated, updated, enabled, disabled
  • Full YAML snapshot — the complete policy at that version

View version history in the dashboard: Policies → (select policy) → Version History


Risk model

Connectors assign risk hints to parsed actions based on the action type and parameters. Risk hints are metadata that policies can use for condition matching.

Built-in risk hints

| Hint | Type | Set by | Example | |---|---|---|---| | production_target | boolean | Connector (from environment field) | true if resource.environment == "production" | | financial_impact | boolean | Connector (from action type) | true for payment.transfer.*, stripe.charge.* | | irreversible | boolean | Connector (from action type) | true for *.delete, *.terminate, *.drop | | external_recipient | boolean | Agent (in action spec) | true if action sends data outside the organization | | contains_pii | boolean | Agent (in action spec) | true if action involves personally identifiable information | | estimated_blast_radius | string | Connector (heuristic) | low, medium, or high based on action scope |

How risk hints affect evaluation

Risk hints do not automatically change the decision. They are fields that policies can match on:

# This policy uses risk hints for condition matching
rules:
  - name: require-approval-high-risk
    conditions:
      all:
        - field: risk_hints.irreversible
          operator: eq
          value: true
        - field: risk_hints.production_target
          operator: eq
          value: true
    decision: requires_approval

Agent-provided vs connector-provided hints

  • Connector-provided: production_target, financial_impact, irreversible, estimated_blast_radius — set automatically by the connector based on the parsed action
  • Agent-provided: external_recipient, contains_pii, and any custom hints — set by the agent in the risk_hints field of the action spec

Agents can add custom risk hint fields. Policies can match on any field path under risk_hints.* using the standard operators.

Risk scoring

TameFlare calculates a risk score (0-100) for each action based on the combination of risk hints:

| Factor | Score contribution | |---|---| | production_target: true | +20 | | financial_impact: true | +25 | | irreversible: true | +30 | | external_recipient: true | +15 | | estimated_blast_radius: high | +20 | | estimated_blast_radius: medium | +10 |

The risk score is logged in the audit trail and shown in the dashboard action detail page. It does not directly affect the decision — only policies determine allow/deny/require_approval.


Policy packs

Three built-in packs ship in policies/ and can be installed from the dashboard Policies page with one click:

  • github-safe-defaults — Branch protection, PR approval requirements, and safe defaults for all GitHub actions
  • rate-limits — Action frequency limits to prevent runaway agents
  • time-windows — Restrict destructive actions to business hours

Testing policies (dry-run and simulation)

Dry-run endpoint

Test policies without affecting live traffic or counting toward action limits:

curl -X POST http://localhost:3000/api/v1/actions/dry-run \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "action_spec": {
      "type": "payment.transfer.initiate",
      "resource": {
        "provider": "stripe",
        "account": "acct_123",
        "target": "transfers",
        "environment": "production"
      },
      "parameters": {
        "amount": 25000,
        "currency": "USD",
        "recipient": "acct_external"
      },
      "risk_hints": {
        "financial_impact": true,
        "external_recipient": true,
        "production_target": true,
        "irreversible": true
      }
    }
  }'

Dry-run response

The response includes the full evaluation trace:

{
  "decision": "requires_approval",
  "risk_score": 90,
  "matched_policies": [
    {
      "id": "pol_payment_controls",
      "name": "Payment Controls",
      "matched_rule": "require-approval-large-transfers",
      "decision": "requires_approval",
      "reason": "Transfers over $10,000 require finance team approval"
    },
    {
      "id": "pol_risk_safety_net",
      "name": "Risk-Based Safety Net",
      "matched_rule": "require-approval-irreversible-production",
      "decision": "requires_approval",
      "reason": "Irreversible production actions require team lead approval"
    }
  ],
  "evaluated_policies": 5,
  "skipped_policies": 2,
  "hint": null
}

Dashboard dry-run UI

The Policies page includes an interactive dry-run panel:

  1. Navigate to Policies in the dashboard
  2. Click Test Policy (or use the dry-run panel)
  3. Enter an action spec (JSON) or select from templates
  4. Click Evaluate — see the decision, matched rules, and full trace
  5. Edit the action spec and re-evaluate to test edge cases

The dashboard shows live YAML validation with inline errors as you type.

Simulation endpoint

For batch testing, use the simulation endpoint to evaluate an action against a specific set of policies:

curl -X POST http://localhost:3000/api/v1/policies/simulate \
  -H "Authorization: Bearer <api_key>" \
  -H "Content-Type: application/json" \
  -d '{
    "action_spec": { "type": "github.pr.merge" },
    "policy_ids": ["pol_github_branch_safety", "pol_risk_safety_net"]
  }'

This evaluates only the specified policies, useful for testing a new policy before enabling it.


Policy hit analytics

Identify which policies and rules are triggered most frequently to optimize your policy set.

Finding noisy rules

Query the audit log for the most frequently matched policies:

# Top 10 most-hit policies (last 7 days)
sqlite3 apps/web/local.db "
  SELECT json_extract(details, '$.matched_policy') as policy,
         json_extract(details, '$.matched_rule') as rule,
         json_extract(details, '$.decision') as decision,
         COUNT(*) as hits
  FROM audit_events
  WHERE event_type IN ('action.allowed', 'action.denied', 'action.pending_approval')
    AND created_at > date('now', '-7 days')
  GROUP BY policy, rule, decision
  ORDER BY hits DESC
  LIMIT 10;
"

What to look for

| Pattern | Meaning | Action | |---|---|---| | One rule has 90%+ of all hits | Catch-all rule is too broad | Narrow the scope or add more specific rules above it | | A deny rule fires constantly | Agents keep attempting blocked actions | Check if the agent is in a retry loop, or if the policy is too strict | | An approval rule fires frequently | Too many actions need human approval | Consider allowing low-risk variants automatically | | A policy has zero hits | Policy scope doesn't match any real traffic | Review scope — it may be misconfigured or unnecessary |

Dashboard analytics

The dashboard overview page shows:

  • Actions over time — stacked area chart by decision (allow/deny/require_approval)
  • Top agents — which agents generate the most traffic
  • Top denied action types — which actions are most frequently blocked

Use these charts to identify patterns and tune policies accordingly.

Prometheus metrics

If you have Prometheus configured, use the aaf_requests_total metric:

# Top 5 most-denied action types
topk(5, sum(rate(aaf_requests_denied_total[7d])) by (action_type))

See Observability for the full metrics reference.


Next steps