Policy

policy.yaml reference

Rules are evaluated in file order and the first match wins, so the file reads top to bottom exactly the way it executes.

Kams is fully useful with every rule deleted

Observation and enforcement are separate concerns. Findings that match no rule are still detected, still emitted as span events, still counted, and still logged. Policy only decides what to do about them.

Shape

yaml
version: 1

defaults:
  action: allow
  fail_mode: open

thresholds:
  integrity.description_delta:
    high: 0.55
    medium: 0.30

rules:
  - name: quarantine-poisoned-pinned-definition
    match:
      kind: INTEGRITY_DRIFT
      severity: [CRITICAL]
      min_confidence: 0.6
    action: quarantine_server
    ttl: 1h
    reason: >-
      A pinned tool definition changed into text that reads as prompt injection.

Ordering is file order rather than a specificity or priority score. That is more predictable: an operator can reason about the file without running it.

Actions

ActionEnforcingScopeEffect
allowExplicitly permit; stops rule evaluation
warnRecord only. Nothing is enforced
redact_argstoolReplace sensitive spans with typed placeholders
rate_limittoolSliding window; requires rate
block_tooltoolReject calls to that tool
quarantine_serverserverReject every call to the server

A quarantine deliberately covers the whole server (tool is set to null); every other action is scoped to the tool that triggered it.

Matchers

Every field inside match is ANDed. An empty matcher matches everything.

FieldTypeMatches
kindstring / listINTEGRITY_DRIFT, INTEGRITY_INJECTION, EGRESS_SENSITIVE, COST_SPIKE, BEHAVIOURAL_THRASH, DETECTOR_ERROR
detectorstring / listStable detector ID, e.g. integrity.result_injection
severitystring / listExact severity membership
min_severitystringseverity >= X, using INFO < LOW < MEDIUM < HIGH < CRITICAL
serverstring / listLogical server name
toolstring / listTool name
classeslistIntersection with evidence.classes (egress)
min_confidencefloatfinding.confidence >= X

unless

A second matcher that excludes. This is how destination-aware policy is written:

yaml
- name: redact-pii-to-untrusted
  match:
    kind: EGRESS_SENSITIVE
    classes: [email, phone]
  unless:
    server: [crm-mcp]
  action: redact_args

An empty `unless` is ignored

unless: {} would otherwise exclude everything, which is never what an operator means.

Durations and rates

ttl accepts 30s, 15m, 1h, 7d, or a bare number of seconds.

rate is required for rate_limit and parses as <count>/<unit>, where unit is s/sec/second, m/min/minute, or h/hour:

yaml
- name: throttle-retry-storm
  match:
    detector: behavioural.retry_storm
    min_severity: HIGH
  action: rate_limit
  rate: 4/min
  ttl: 10m

This is a real sliding window, not a fixed bucket: timestamps older than the window are evicted on each check, so a limit recovers gradually rather than resetting on a boundary.

A malformed rate raises at load time rather than silently disabling the rule.

Fail open

fail_mode: open is the default and encodes the project's first principle.

  • A malformed policy.yaml falls back to a permissive policy, printed loudly to stderr.
  • A rule that raises during evaluation is logged and skipped.
  • A detector that raises is logged and treated as producing no finding.
  • An unreadable shared-state file is treated as empty.

The one deliberate exception

Quarantine. It is always explicit, always attributable to a named rule, and always TTL-bound.

The shipped default policy

The rules Kams ships with, in order:

RuleMatchAction
quarantine-poisoned-pinned-definitionCritical drift, confidence ≥ 0.6quarantine_server 1h
quarantine-homoglyph-squattingintegrity.tool_squatting ≥ HIGHquarantine_server 1h
block-tool-on-result-injectionintegrity.result_injection ≥ HIGHblock_tool 15m
warn-on-provisional-driftDrift at HIGH/MEDIUMwarn
block-credential-egressCredential classes, not vault-mcpblock_tool
redact-pii-to-untrustedemail/phone, not crm-mcpredact_args
throttle-retry-stormbehavioural.retry_storm ≥ HIGHrate_limit 4/min 10m
warn-on-agent-thrashbehavioural.thrashwarn
warn-on-error-ratebehavioural.error_ratewarn
throttle-context-cost-runawayCOST_SPIKE ≥ HIGHrate_limit 6/min 10m

Note what is deliberately not enforced. Agent thrash is a prompt or planning problem, not a server problem, so Kams reports rather than intervenes. And a failing dependency is throttled rather than blocked — the server may recover, and a TTL'd rate limit degrades gracefully where a block does not.

Loading

bash
kams shim --policy ./policy.yaml -- <command>     # default: ./policy.yaml
kams shim --no-enforce -- <command>               # observe only, no engine at all
kams shim --no-reflex -- <command>                # permissive policy, still honours SigNoz

See Shared state & TTL for how installed restrictions are stored and expired.