TSG-04: Policy Lifecycle, Shadow Promotion, & Override Rejections¶
1. Symptoms & Alerts¶
- Producer calls to
POST /api/v1/notificationsor/simulatefail with422 Unprocessable Entity(PolicyOverrideErrororValidationError). - Operator calls to
POST /api/v1/policies/reloadfail with404 Not Found,409 Conflict, or422 Unprocessable Entity. - An operator wonders why a policy updated in a YAML file is not yet active, or how multi-process
uns/uns-mcpinstances synchronize policy changes.
2. Root Cause & Component Mechanics¶
2.1 Revision-Locked Durable Policy Authority (PolicyRuntime)¶
In src/uns/policy_runtime.py and src/db/policy_store.py:
1. All ACTIVE policies are stored in an immutable, revision-numbered JSON snapshot table (notification_policy_snapshots) coordinated by notification_policy_revisions.
2. Canonical Intake (Runtime.accept): Acquires the revision lock (UPDATE notification_policy_revisions SET revision = revision WHERE singleton = 1) inside its acceptance transaction so policy activation and accepted obligations linearize together.
3. Steady-State Simulation & Inspection (F-05): Warm simulate() and /ui/policies/effective calls read the current revision and immutable snapshot in a single consistent SELECT without acquiring a SQLite write lock.
4. Because policy resolution checks the database revision on every call, separate uns and uns-mcp processes automatically pick up newly persisted ACTIVE snapshots as soon as POST /api/v1/policies/reload commits.
2.2 Producer Override Enforcement & Reserved Metadata¶
- No Producer Weakening of
ACTIVEPolicy: When anACTIVEpolicy matchesnotification_type, producer-supplied overrides onSendNotificationRequest(delivery_mode,require_ack,priority, orintentifpolicy.intentis configured) that differ from the resolvedACTIVEpolicy raisePolicyOverrideError(HTTP 422). - Reserved Adapter-Control Metadata:
PolicyRuntime.resolve()rejects requests whosemetadatadictionary contains any adapter-steering key: slack_channelemail_topagerduty_severitywebsocket_user_id(plus any customtarget_user_metadata_key)- Persistent Channel Requirement for
GUARANTEEDMode: If a notification resolves todelivery_mode = "GUARANTEED"but its resolvedchannelslist contains only["WEBSOCKET"](or is empty),PolicyRuntime.resolve()raisesValueError(HTTP 422:"GUARANTEED requires a persistent channel; choose BEST_EFFORT for WebSocket-only preview").
3. Diagnostic Commands¶
Step 3.1: Inspect effective policy resolution for a signal shape¶
curl -sS -G -H "Authorization: Bearer $UNS_API_KEY" \
"$UNS_BASE_URL/ui/policies/effective" \
--data-urlencode "notification_type=system.alert" \
--data-urlencode "intent=SAFETY" \
--data-urlencode "autonomy_mode=LIVE" \
--data-urlencode "irreversible=true" | jq .
Step 3.2: Preview a producer request without side effects¶
curl -sS -X POST -H "Authorization: Bearer $UNS_API_KEY" \
-H "Content-Type: application/json" \
"$UNS_BASE_URL/api/v1/notifications/simulate" \
-d '{
"notification_type": "system.alert",
"intent": "SAFETY",
"title": "Test",
"message": "Simulation check",
"source": "diagnostics",
"autonomy_context": {"autonomy_mode": "LIVE", "agent_id": "00000000-0000-0000-0000-000000000001"}
}' | jq .
Step 3.3: Inspect persisted policy snapshots and revisions in SQLite¶
sqlite3 "$UNS_DB_FILE" "
SELECT r.revision, s.content_hash, s.created_at
FROM notification_policy_revisions r
JOIN notification_policy_snapshots s ON s.revision = r.revision
WHERE r.singleton = 1;
"
4. Safe Remediation Steps¶
Scenario A: POST /api/v1/policies/reload returns 409 Conflict¶
- Cause 1 - Re-activating a
RETIREDpolicy content hash:SQLAlchemyPolicyStore._save_policyblocks reactivating an identical content hash that was previously markedRETIRED("A retired policy version cannot be reactivated; publish a new version"). - Fix: Increment the
versionfield in the YAML policy file so its canonical content hash is new, then re-run/api/v1/policies/reload. - Cause 2 - Duplicate
(notification_type, status)in directory: Multiple YAML files define the samenotification_typeandstatus. - Fix: Ensure at most one
ACTIVEand onePREVIEWdefinition exist pernotification_type.
Scenario B: POST /api/v1/policies/reload returns 422 Unprocessable Entity¶
- Cause: YAML syntax error, invalid enum (
channels,delivery_mode,status), or invalid policy field (ack_timeout_minutes <= 0,dedup_window_seconds < 0). - Fix: Inspect the
detailmessage in the422response, correct the YAML file in your configuredUNS_POLICY_DIR, and retry with-d '{}'(note that an optional{"policy_dir": "..."}body is only for subdirectories within the server's allowed policy root):
Scenario C: Producer receives 422 on POST /api/v1/notifications¶
- If error mentions
Producer metadata cannot control adapter routing or urgency: Removeslack_channel,email_to,pagerduty_severity, orwebsocket_user_idfromrequest.metadata. Channel routing is controlled exclusively by server-side environment variables and policy configuration. - If error mentions
PolicyOverrideError: Remove explicitdelivery_mode,require_ack, orpriorityoverrides from the producer payload (or alignintentwith the policy's configuredintent) so theACTIVEpolicy governs delivery.
5. Runtime Guardrails (F-05 Disposition)¶
Note
Cross-Process Policy Consistency & Lock-Free Warm Reads (F-05):
Only policies with status: ACTIVE are written into notification_policy_snapshots and enforced by PolicyRuntime. Policies with status: PREVIEW are persisted in notification_policies for shadow comparison (load_shadow_registry()) and do not alter runtime delivery until promoted to status: ACTIVE with a bumped version. Warm simulation and effective-policy reads execute a single SELECT join without blocking concurrent SQLite writers.