Signal Scoring & Threat Models

How a signal is classified, what revocation follows, how agent trust scores move, and how events are clustered.

Last reviewed September 17, 2026Fresh

Overview

Every signal is routed by its event_type through a static classification, and the route decides what PasskeyBridge does on its own account. Scores come into it in three separate places, which are easy to confuse:

  • the risk_score you or a carrier lookup send with the signal, which is stored and passed on and decides nothing;
  • the trust score on each agent delegate, which the trust engine moves on soft signals and which narrows scopes;
  • the intelligence risk score, a model assessment written after the fact for Enterprise organizations.
Inbound signal  ->  POST /v1/shield-ingest/{tenant_id}
      |
      +-- classified by event_type (static list, no configuration)
             |
             +-- HARD   -> revoke agent delegates and A2A negotiations, inline
             +-- SOFT   -> dispatch one trust evaluation for the organization
             +-- other  -> stored, matched against playbooks, nothing else

Hard and soft handling both need an identifier: they only run when the signal carried phone or phone_hash. Playbook actions run either way.

Cascade classifier

The classifier is a pure function from signal_type to hard, soft or unknown. No model, no thresholds, no per-organization configuration, so the same input always gives the same class and a classification can be reproduced from the type alone. It is the deterministic core described in the PBCASCADE patent application (64/006,812).

Hard (nine types): sim_swap, sim_swap_detected, port_out, number_port, number_porting, device_compromise, ss7_intercept, account_takeover, scope_poisoning.

Soft (ten types): velocity_anomaly, behavioral_anomaly, geo_anomaly, credential_leak, credential_stuffing_attempt, suspicious_login, failed_verification, device_change, unusual_device, number_recycle.

Unknown is everything else, including test_signal and the carrier_check a clean carrier lookup forwards.

One list, and nothing else keeps a copy. The cascade endpoint classifies through this function, the ingest pipeline reaches it through a pair of predicates in the trust engine module, and both intelligence functions use it to tell the hallucination checker whether it is looking at a hard signal. Every one of those resolves to the same array. They did not always: separate copies drifted twice, and both times it was number_porting and scope_poisoning that went missing, first so that they revoked on one path and did nothing on the other, then so that an AI assessment of those two types escaped the checker's hard-signal floor. A build gate now refuses a second copy anywhere in the edge functions rather than comparing the copies it happens to know about.

Unknown types are treated differently by the two paths. Ingest stores an unrecognised type and runs its playbook. The cascade endpoint refuses it with 400 Unknown signal type: <value>.

Parametric revocation cascade

There are two revocation paths, and how many subsystems are touched depends on which one ran.

Inbound signals revoke two subsystems. A hard signal at /v1/shield-ingest revokes, in parallel:

  • agent delegates: every active delegate on the organization set to is_active = false, trust_score = 0, narrowing_reason = "Hard signal: <type>";
  • A2A negotiations: every negotiation in active, negotiated, attested or pending set to status = 'revoked', trust_state = 'auto_revoked', combined_trust_coefficient = 0, transaction limits zeroed, allowed_scopes emptied.

Shadow identities and BLAST tunnel sessions are untouched on this path. The counts are recorded on the event as a parametric_revocation action and audited as parametric_revocation.triggered.

The cascade endpoint revokes four. shield-cascade adds shadow identity freezing (is_active = false, revoked_at stamped) and BLAST session teardown (torn_down = true, torn_down_at stamped). It is Enterprise-only and takes a tenant-admin session, and it is what the Revocation tab drives. Full request and response shapes are in CASCADE signal classification.

Ordering. On the ingest path revocation runs before the playbook actions, so it never waits on a Slack post or a customer webhook, and a playbook carrying suspend_account cannot deactivate the delegates first and leave revocation reporting zero.

Counting. The cascade endpoint counts with an exact row count rather than the length of the returned array, because a large update is capped at 1,000 returned rows while the update itself touches every matching row.

Dry run. cascade_dry_run counts what each of the four subsystems would affect and writes nothing. It is the safe way to see the blast radius before running the real thing.

Graduated trust engine

Soft signals dispatch evaluate_tenant to shield-agent-trust, which re-scores up to 200 active delegates on the organization, each against its 100 most recent activity rows. The engine itself is a pure function: the calling function does every read and write.

Trust score is 0.000 to 1.000, stored per delegate and rounded to three decimals on every update.

SignalWindowTriggerWeight
Velocity spike5 minutesMore than 50 actionsup to 0.30
Error burst2 minutesMore than 10 non-success resultsup to 0.25
Scope violationAll recorded activityAny result of scope_violationup to 0.35
IP rotation10 minutesMore than 5 distinct address digestsup to 0.20
Burst after inactivity24-hour gapMore than 10 actions in 5 minutes0.15
AI behavioural deltaOn assessmentIntelligence worker deltaup to 0.30
Normal usageAny activity, no anomalyRecovery+0.05

Each negative weight is scaled by severity, so a delegate at 51 actions in the window loses almost nothing and one at 100 loses the full 0.30.

Narrowing ladder, applied after the new score is computed:

ScoreActionScopes kept
Below the revocation floorDelegate deactivated, scopes emptiednone
0.7 and aboveNone, and previously narrowed scopes are restoredOriginal set
0.4 to 0.699Narrow to read-onlyingest:read, events:read, playbook:read, intelligence:read, verify
0.2 to 0.399Narrow to verify-onlyverify

The floor is per delegate. auto_revoke_below defaults to 0.2, which is why the ladder above reads as a 0.2 cut-off, and it is checked before the ladder so an operator can set a higher floor. A value outside 0 to 1 falls back to 0.2.

Recovery is deliberately slow. Only +0.05 per evaluation with no anomalies, and an evaluation only happens when a soft signal arrives or the intelligence worker dispatches one, so a narrowed delegate climbs back over several evaluations rather than in one.

Activity has to be recorded to be scored. The signals above are computed from shield_agent_activity rows, written through the service-role record_activity action. A delegate with no recorded activity produces no signals and its score does not move.

A hard signal skips all of this. evaluate_tenant called with a hard type answers hard_signal_bypass and does nothing, because the ingest pipeline has already revoked. More detail in Agent monitoring and guardrails.

Behavioral clustering

Clustering groups recent events for one organization by similarity, to surface coordinated activity that a single event cannot show. The engine is pure; the calling function supplies the events.

Features per event: type, the first 8 characters of the identifier hash, the first 8 of the signal hash, a 4-hour band of the UTC hour, the day of week, a risk bucket, a latency bucket, an IP hash prefix, a user-agent class and whether a playbook matched.

Similarity is 70% Jaccard overlap of that feature set plus 30% temporal proximity within 30 minutes. Single-linkage merging at 0.55, minimum cluster size 3.

Labels are inferred from the dominant pattern: sim_farm_pattern, credential_stuffing_campaign, targeted_attack, high_risk_<type>_cluster or <type>_cluster. Threat probability is the high-risk ratio times 0.5, plus cohesion times 0.3, plus a size bonus capped at 0.2, and clusters are returned sorted by it.

One feature is still always empty on ingest events. The user-agent class has no writer, so it adds nothing to similarity. The other two do: the ingest pipeline fills metadata with risk_score, correlation_id, the HIBP fields, playbook_matched, and ip_prefix, which is the first 8 characters of the keyed IP digest rather than the address or the whole digest. Until 2026-09-17 all three were empty and the "few addresses" half of the sim_farm_pattern test was satisfied by their absence, so the label was applied to any cluster holding more than three distinct identifier prefixes. It now requires at least one address to have been observed, and falls through to a label that claims less when none was.

Where to see it. Dashboard > Intelligence > Clusters, Enterprise only, over a window of up to 168 hours. Clustering is not part of the ingest decision and nothing revokes on a cluster.

Intelligence layer

For Enterprise organizations, the ingest pipeline dispatches shield-intelligence-worker after the event row is written, fire and forget, so the response never waits on it.

The worker reads the event and the organization's 25 most recent live events, asks Claude Haiku for one JSON assessment, clamps the score, cross-checks it against the deterministic signal and the organization's own baseline, clusters the window, and writes a row to shield_intelligence.

It is not part of the decision. The only state it changes outside its own table is at an assessed risk above 0.95, where shadow identities matching the event's identifier hash are deactivated, and a delegate trust evaluation when the model returns a trust delta of more than 0.05 either way.

Live traffic only. Test-mode and sandbox events are not dispatched and are refused by the worker if they arrive another way. Their assessments used to dominate the baseline the cross-check measures against, which pointed the check the wrong way.

Corrected classifications are stored rather than learned from. A reviewer's correction is written beside the original assessment for the record. Nothing feeds it back into the model. The full pipeline is in AI threat intelligence.

Related from the blog