|
| 1 | +/** |
| 2 | + * Whether a row in the trail is something this deployment turned away, something that was allowed |
| 3 | + * and then did not happen, or something that went through. |
| 4 | + * |
| 5 | + * THE TWO PLACES THAT DECIDE THIS HAVE TO BE ONE PLACE. The audit page asks the question twice: once |
| 6 | + * to colour and label a row, and once to build the `eventType` list behind the `Blocked` and |
| 7 | + * `Did not happen` saved views. They were two hand-written lists, and they had already drifted — a |
| 8 | + * refusal missing from the first is drawn as "Allowed", a refusal missing from the second is |
| 9 | + * missing from the view somebody clicks to ask what this deployment refused, and neither omission |
| 10 | + * says anything. So the lists live here, the page derives both from them, and a new refusal is added |
| 11 | + * in one place or in none. |
| 12 | + * |
| 13 | + * WHY "ALLOWED" IS THE ONE WRONG ANSWER. The page falls back to it for anything it does not |
| 14 | + * recognise, which is the right default for the many rows that are neither a refusal nor a failure — |
| 15 | + * a credential saved, a component published, a person's role changed. For a refusal it is not a |
| 16 | + * missing label, it is the opposite of what happened, on the screen an administrator opens to find |
| 17 | + * out what happened. A trail that is confidently wrong is worse than a silent one. |
| 18 | + */ |
| 19 | + |
| 20 | +/** |
| 21 | + * Refused: this deployment declined, and nothing was attempted. |
| 22 | + * |
| 23 | + * `mcp.callback_refused`, `routines.dispatch_refused` and `session.refused` are here even though no |
| 24 | + * policy judged anybody and, in the last two, no Bot was involved at all. Somebody filtering for |
| 25 | + * "what did this deployment turn away" wants them, and for each of the three this row is the only |
| 26 | + * evidence anywhere that anything was attempted: the wire answers all of them with the same opaque |
| 27 | + * refusal on purpose. |
| 28 | + */ |
| 29 | +export const REFUSED_EVENT_TYPES = [ |
| 30 | + "computer.action_refused", |
| 31 | + "component.refused", |
| 32 | + "component.function_refused", |
| 33 | + "mcp.call_rejected", |
| 34 | + "mcp.callback_refused", |
| 35 | + "routines.dispatch_refused", |
| 36 | + /* |
| 37 | + * A hop one Bot was not allowed to make. `server/src/audit.ts` calls this "the more important of |
| 38 | + * the pair": a hop that happened is visible in the transcript anyway, and a hop that was refused |
| 39 | + * is invisible everywhere else. |
| 40 | + */ |
| 41 | + "agent.handoff_refused", |
| 42 | + /** An endpoint a stored agent tried to reach and the deployment would not dial. */ |
| 43 | + "agent.dial_refused", |
| 44 | + /** A rotation aimed at a key the credential does not belong to, or at a revoked one. */ |
| 45 | + "credential.rotation_refused", |
| 46 | + /** A revoked person still holding a bookmark, or an address outside the deployment. */ |
| 47 | + "session.refused", |
| 48 | +] as const; |
| 49 | + |
| 50 | +/** |
| 51 | + * Did not happen: nothing was refused, and nothing came of it either. |
| 52 | + * |
| 53 | + * Its own family because the difference is what somebody came to the row to find out. A boundary |
| 54 | + * holding and a Bot that was asked and never answered are different faults with different fixes, and |
| 55 | + * only one of them is the deployment working as configured. |
| 56 | + */ |
| 57 | +export const DID_NOT_HAPPEN_EVENT_TYPES = [ |
| 58 | + "computer.action_failed", |
| 59 | + "agent.stream_stalled", |
| 60 | + /** A hop that was accepted, ran out of attempts, and never became the other Bot's turn. */ |
| 61 | + "agent.handoff_failed", |
| 62 | + /** A question that reached nobody: the Bot stopped, and the person was never asked. */ |
| 63 | + "agent.escalation_failed", |
| 64 | +] as const; |
| 65 | + |
| 66 | +export type AuditOutcome = "refused" | "did-not-happen" | "allowed"; |
| 67 | + |
| 68 | +const REFUSED = new Set<string>(REFUSED_EVENT_TYPES); |
| 69 | +const DID_NOT_HAPPEN = new Set<string>(DID_NOT_HAPPEN_EVENT_TYPES); |
| 70 | + |
| 71 | +/** What kind of thing this row is, for the label and the colour it is drawn in. */ |
| 72 | +export function outcomeOf(eventType: string): AuditOutcome { |
| 73 | + if (REFUSED.has(eventType)) return "refused"; |
| 74 | + if (DID_NOT_HAPPEN.has(eventType)) return "did-not-happen"; |
| 75 | + return "allowed"; |
| 76 | +} |
| 77 | + |
| 78 | +/** The `eventType` query one of the saved views filters by. */ |
| 79 | +export function eventTypeFilter( |
| 80 | + types: readonly string[], |
| 81 | +): `?eventType=${string}` { |
| 82 | + return `?eventType=${types.join(",")}`; |
| 83 | +} |
0 commit comments