# Signals and Revocation

Tokens have a finite lifetime, but trust can be lost before a token expires. Continuous Access Evaluation (CAE) signals give you a way to communicate that something has changed — immediately — without waiting for the next token refresh cycle.

### The Problem with Relying on TTL Alone

Short TTLs reduce the blast radius of a compromised credential. But they do not eliminate it. A 15-minute token can still cause damage for 15 minutes after you discover a problem. In a multi-agent system, that window multiplies across every active session.

CAE signals close that window. When you ingest a signal against an identity, ZeroID marks its sessions as inactive. Any downstream service that verifies the token via introspection will immediately see `active: false` instead of the cached valid state, regardless of when the token expires.

This lets you move from "wait for expiry" to "revoke now, enforce everywhere."

### Signal Types

Each signal type describes a specific kind of event. Use the most specific type that matches your situation.

| Signal Type          | When to Use                                                                                                                                                                                   |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session_revoked`    | You want to explicitly end an active session immediately. Use for emergency shutdowns, forced logouts, or after detecting a breach.                                                           |
| `credential_change`  | A key has been rotated, a secret has been updated, or credentials associated with the identity have changed. Signals that existing tokens may no longer reflect the current credential state. |
| `ip_change`          | The identity is operating from an unexpected IP address or network. Use to flag out-of-policy network behavior.                                                                               |
| `anomalous_behavior` | The identity is behaving in a way that deviates from its baseline — unusual request volume, unexpected scope usage, or timing anomalies.                                                      |
| `policy_violation`   | The identity attempted or performed something that violates its defined operating policy. Use after a policy enforcement event.                                                               |
| `retirement`         | The identity is being decommissioned. Signals that no new sessions should be accepted and existing ones should be wound down.                                                                 |
| `owner_change`       | The human or system entity responsible for this identity has changed. Use when an agent changes hands or its controlling principal is updated.                                                |

### Severity Levels

Every signal carries a severity that downstream consumers can use to calibrate their response.

| Severity   | Meaning                                                                         |
| ---------- | ------------------------------------------------------------------------------- |
| `low`      | Informational. Something changed, but no immediate action is required.          |
| `medium`   | Warrants investigation or monitoring. Active sessions may continue under watch. |
| `high`     | Warrants immediate review. Consider suspending or restricting the identity.     |
| `critical` | Treat as a breach or emergency. Revoke immediately.                             |

ZeroID does not automatically take different actions based on severity — it records the signal and marks sessions appropriately. Severity is a signal to your system and operators.

### Ingesting a Signal

#### Python

```python
# Emergency revocation
client.signals.ingest(
    signal_type="session_revoked",
    severity="critical",
    source="security-team",
    identity_id="agt-prod-orchestrator",
    payload={"reason": "credential exposure detected"},
)

# Credential rotation event
client.signals.ingest(
    signal_type="credential_change",
    severity="medium",
    source="key-rotation-service",
    identity_id="agt-prod-orchestrator",
    payload={"rotated_key_id": "key-abc123"},
)

# Anomalous behavior detected by a monitoring agent
client.signals.ingest(
    signal_type="anomalous_behavior",
    severity="high",
    source="anomaly-detector",
    identity_id="agt-tool-search",
    payload={
        "detail": "request volume 40x above baseline",
        "window_seconds": 60,
        "observed_requests": 2400,
    },
)

# Policy violation
client.signals.ingest(
    signal_type="policy_violation",
    severity="high",
    source="policy-enforcer",
    identity_id="agt-tool-writer",
    payload={"violated_rule": "no-external-data-egress"},
)

# Unexpected source IP
client.signals.ingest(
    signal_type="ip_change",
    severity="medium",
    source="network-monitor",
    identity_id="agt-prod-orchestrator",
    payload={"observed_ip": "203.0.113.42", "expected_cidr": "10.0.0.0/8"},
)

# Agent retirement
client.signals.ingest(
    signal_type="retirement",
    severity="low",
    source="lifecycle-manager",
    identity_id="agt-legacy-importer",
    payload={"replacement_identity_id": "agt-v2-importer"},
)

# Owner change
client.signals.ingest(
    signal_type="owner_change",
    severity="medium",
    source="identity-admin",
    identity_id="agt-internal-reporter",
    payload={"previous_owner": "team-data", "new_owner": "team-analytics"},
)
```

#### TypeScript

```typescript
// Emergency revocation
await client.signals.ingest({
  signal_type: "session_revoked",
  severity: "critical",
  source: "security-team",
  identity_id: "agt-prod-orchestrator",
  payload: { reason: "credential exposure detected" },
});

// Credential rotation event
await client.signals.ingest({
  signal_type: "credential_change",
  severity: "medium",
  source: "key-rotation-service",
  identity_id: "agt-prod-orchestrator",
  payload: { rotated_key_id: "key-abc123" },
});

// Anomalous behavior detected by a monitoring agent
await client.signals.ingest({
  signal_type: "anomalous_behavior",
  severity: "high",
  source: "anomaly-detector",
  identity_id: "agt-tool-search",
  payload: {
    detail: "request volume 40x above baseline",
    window_seconds: 60,
    observed_requests: 2400,
  },
});

// Policy violation
await client.signals.ingest({
  signal_type: "policy_violation",
  severity: "high",
  source: "policy-enforcer",
  identity_id: "agt-tool-writer",
  payload: { violated_rule: "no-external-data-egress" },
});

// Unexpected source IP
await client.signals.ingest({
  signal_type: "ip_change",
  severity: "medium",
  source: "network-monitor",
  identity_id: "agt-prod-orchestrator",
  payload: { observed_ip: "203.0.113.42", expected_cidr: "10.0.0.0/8" },
});

// Agent retirement
await client.signals.ingest({
  signal_type: "retirement",
  severity: "low",
  source: "lifecycle-manager",
  identity_id: "agt-legacy-importer",
  payload: { replacement_identity_id: "agt-v2-importer" },
});

// Owner change
await client.signals.ingest({
  signal_type: "owner_change",
  severity: "medium",
  source: "identity-admin",
  identity_id: "agt-internal-reporter",
  payload: { previous_owner: "team-data", new_owner: "team-analytics" },
});
```

### What Happens After a Signal Is Ingested

When ZeroID receives a signal:

1. The signal is recorded against the identity.
2. Active sessions for that identity are marked inactive.
3. Token introspection for any token belonging to that identity returns `active: false`.
4. The signal is available in the identity's event history for audit purposes.

Propagation is immediate within ZeroID. There is no polling delay between signal ingestion and the change in introspection behavior. The gap is whatever latency exists between your downstream service calling introspection and ZeroID processing the signal — typically sub-second.

Tokens themselves are not modified or deleted. They are not invalidated at the JWT level. What changes is the state that introspection reflects. Downstream services that rely on local JWT signature verification only (without calling introspection) will not see the revocation. See the section on common mistakes below.

### How Downstream Services Should Respond

The correct pattern for a downstream service receiving a token from an agent:

1. Verify the JWT signature using the JWKS endpoint.
2. Check the standard JWT claims (expiry, issuer, audience).
3. For sensitive operations, call token introspection and check that `active: true`.
4. On any `401 Unauthorized` response from a resource server, re-verify via introspection before retrying.

When introspection returns `active: false`, the downstream service should:

* reject the request
* return `401 Unauthorized` to the caller
* not cache the valid result from a previous introspection call

The `session.active` field on a retrieved session object reflects the same state. If you are building an orchestrator that holds references to sub-agent sessions, check `session.active` before dispatching work to a sub-agent. Do not assume a session you opened earlier is still valid.

### REST Example

```bash
curl http://localhost:8899/api/v1/signals \
  -H "Content-Type: application/json" \
  -H "X-Account-ID: acct-demo" \
  -H "X-Project-ID: proj-demo" \
  -d '{
    "signal_type": "session_revoked",
    "severity": "critical",
    "source": "security-team",
    "identity_id": "agt-prod-orchestrator",
    "payload": {
      "reason": "credential exposure detected"
    }
  }'
```

### Practical Patterns

#### Emergency Shutdown

Use when you need an identity stopped immediately — breach suspected, runaway agent, or unexpected behavior at production scale.

```python
# Step 1: Deactivate the identity so no new sessions can start
client.identities.deactivate(identity_id="agt-prod-orchestrator")

# Step 2: Ingest a session_revoked signal to terminate existing sessions
client.signals.ingest(
    signal_type="session_revoked",
    severity="critical",
    source="security-team",
    identity_id="agt-prod-orchestrator",
    payload={"reason": "emergency shutdown initiated"},
)
```

Deactivating the identity prevents new token issuance. The signal terminates existing sessions. Both steps are needed for a complete shutdown.

#### Suspicious Behavior — Monitor Mode

Use when you want to flag an identity for closer inspection without immediately terminating it. Ingest a signal at `medium` or `high` severity, but do not revoke the session. This leaves the identity running while giving your monitoring systems a signal to increase scrutiny.

```python
client.signals.ingest(
    signal_type="anomalous_behavior",
    severity="high",
    source="anomaly-detector",
    identity_id="agt-tool-search",
    payload={
        "detail": "unexpected scope usage: storage:delete not in baseline",
        "monitor_mode": True,
    },
)
```

Your downstream services or monitoring layer can read this signal from the event history and apply stricter rate limiting or logging without forcing a re-auth cycle.

#### Key Rotation Event

Use when a credential backing an identity has been rotated. Existing tokens were issued under the old key. Ingesting a `credential_change` signal tells downstream systems to re-verify those tokens rather than trust cached state.

```python
# Rotate the key first
new_key = client.keys.rotate(identity_id="agt-prod-orchestrator")

# Then signal that credentials have changed
client.signals.ingest(
    signal_type="credential_change",
    severity="medium",
    source="key-rotation-service",
    identity_id="agt-prod-orchestrator",
    payload={"rotated_key_id": new_key.id},
)
```

After this signal, any downstream service calling introspection on a token issued before the rotation will get `active: false`. The agent will need to re-authenticate to receive a token issued under the new key.

#### Agent Retirement

Use when decommissioning an identity permanently. The `retirement` signal communicates intent to other services and audit consumers. Pair it with `deactivate` to enforce it.

```python
# Signal retirement intent first — audit trail and downstream awareness
client.signals.ingest(
    signal_type="retirement",
    severity="low",
    source="lifecycle-manager",
    identity_id="agt-legacy-importer",
    payload={
        "reason": "replaced by agt-v2-importer",
        "replacement_identity_id": "agt-v2-importer",
    },
)

# Then deactivate to enforce it
client.identities.deactivate(identity_id="agt-legacy-importer")
```

Signal before deactivating so that downstream systems that consume the event history have a record of the intent, not just the state change.

### Common Mistakes

Avoid:

* **Relying only on token expiry for revocation.** A 15-minute TTL is not a revocation mechanism. If you discover a problem at minute zero, you still have a 15-minute exposure window. Ingest a signal immediately.
* **Verifying tokens only via local JWT signature checks.** Local verification confirms the signature is valid. It does not check whether the session is still active. For any sensitive operation, call introspection.
* **Caching introspection results for too long.** If you cache a `active: true` result for several minutes, you have recreated the TTL problem at a different layer. Keep introspection cache TTLs short, or skip caching entirely for high-value operations.
* **Not checking `session.active` before dispatching to sub-agents.** In orchestrator patterns, sessions you opened for sub-agents can be revoked while your orchestrator is still running. Always check `session.active` before dispatching new work.
* **Ingesting signals without a meaningful `source`.** The `source` field is part of the audit record. A value like `"my-service"` is more useful than `"unknown"` when you are tracing an incident after the fact.
* **Skipping the `retirement` signal and deactivating directly.** Deactivation changes state without explanation. The `retirement` signal provides context to audit consumers and downstream systems that may be reading the event history.

#### What's Next?

* Continue to [**Downstream Authorization**](/agent-identity-zeroid/guides/downstream-authorization.md) to understand how services should enforce policy once they have verified a token.
* Revisit [**Credential Policies**](/agent-identity-zeroid/guides/credential-policies.md) to constrain what tokens can be issued in the first place.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.highflame.ai/agent-identity-zeroid/guides/signals-and-revocation.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
