UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

62 lines • 3.72 kB
/** * verify/personTest — only a person's token is a person (rule 5). * * A verified signature proves who SIGNED a token, not that it stands for a * person. On Entra ID any application registered in the tenant can obtain an * app-only token for this API by the client-credentials grant — right issuer, * right audience, a valid signature and an `oid` — and Microsoft says so: * "app-only tokens can be issued without a `roles` claim. Applications that * expose APIs must implement permission checks in order to accept tokens." * A verifier that accepted it would hand a service principal a `principal`, * conversations, a model budget and the agent's tools. * * Five checks, in this order, because each names a different shape: * * c. `idtyp: "app"` — Entra's own marker, refused whenever present. * d. `oid` equal to `sub` — Microsoft's documented app-only test. * b. roles and NO scope claim at all — a daemon's token carries app roles * instead of delegated scopes. * a. the required scope in the scope claim — Microsoft: `scp` is "Only * included for user tokens". * e. the client that obtained the token is one this deployment lists — on * Okta and Keycloak a client-credentials token may carry scopes too, and * this is the check that refuses it there. * * a–d fail as `not-a-user-token`; e fails as `wrong-client`. `scp` alone is * Microsoft's test, not an IdP-neutral one; the checks together are. * * Check b is DEFENCE IN DEPTH, kept on purpose: today no token's outcome * depends on it, because check a refuses every token with no scope claim in * the same words. It stays so that a later change to a — an optional scope, a * scope read from elsewhere — cannot silently admit a daemon's roles-only * token. (A mutation that deletes it survives the suite, and that is why.) * * **The client check is load-bearing on Keycloak and Okta**, where a service * account's client-credentials token carries your API's scope: every client in * `allowedClients` must have service accounts / client credentials turned OFF, * and `'any'` is refused in production by `identityFromConfig`. */ import type { IdentityFailureClass } from '../../../hosting/errors.js'; import { type ClaimPath } from './claims.js'; /** What the person test is configured with. */ export interface PersonTestOptions { /** The OAuth scope only this API's person tokens carry. One scope, exact. */ readonly requiredScope: string; /** Where the scopes are: `'scp'` (Entra, AD FS) or `'scope'` (RFC 9068, Keycloak, Okta). */ readonly scopeClaim: ClaimPath; /** Clients allowed to obtain a person token for this API, or `'any'` (the check is off). */ readonly allowedClients: readonly string[] | 'any'; /** The configured roles claim — part of check b's "roles without a scope". */ readonly rolesClaim: ClaimPath; } /** The failure class, or `undefined` when the token stands for a person. */ export declare function personTestFailure(payload: Readonly<Record<string, unknown>>, options: PersonTestOptions): Extract<IdentityFailureClass, 'not-a-user-token' | 'wrong-client'> | undefined; /** * Unknown is not none (rule 14). When the roles claim was REPLACED by a * pointer to somewhere else — OIDC's distributed claims (`_claim_names`), which * is what Entra sends instead of `groups` above 200 groups, or Entra's * `hasgroups: true` — the person's roles are not in this token. Reading them * as "no roles" would strip a person silently; this says so instead. */ export declare function rolesAreElsewhere(payload: Readonly<Record<string, unknown>>, rolesClaim: ClaimPath): boolean; //# sourceMappingURL=personTest.d.ts.map