/** * Governance record for an org service principal (`svc-@service.`). * * A service token proves WHO is calling; this record says who is ACCOUNTABLE * for the principal, whether it may run at all, and which actions it may call. * Every entry point that admits a service identity (MCP admission, MCP tool * dispatch, agent run start) reads it through `evaluateServicePrincipal` so * suspend and the action grant are enforced in one place. * * Constraints that must survive future edits: * * 1. No row means a legacy principal minted before governance existed: it * stays active and unrestricted ("ungoverned"). Tightening it is an explicit * admin act, never a side effect of upgrading. * 2. An unreadable row is NOT an absent row. A read failure resolves to * "unavailable" and callers refuse the request (503), because answering * "ungoverned" would let a suspended principal back in during an outage. * 3. A grant is deny-by-default once `allowedActions` is a list: an empty list * grants nothing, and `null` is the only unrestricted value. */ import { type DbExec } from "../db/client.js"; export declare const SERVICE_PRINCIPAL_LIFECYCLES: readonly ["active", "suspended", "retired"]; export type ServicePrincipalLifecycle = (typeof SERVICE_PRINCIPAL_LIFECYCLES)[number]; export declare const SERVICE_PRINCIPAL_RISK_TIERS: readonly ["low", "medium", "high"]; export type ServicePrincipalRiskTier = (typeof SERVICE_PRINCIPAL_RISK_TIERS)[number]; export declare const MAX_SERVICE_PRINCIPAL_ACTIONS = 200; export declare function isServicePrincipalActionPattern(value: string): boolean; export interface ServicePrincipalPolicy { orgId: string; /** Normalized service name, the `` in `svc-@service.`. */ serviceName: string; /** Accountable human. Always an email, never the service identity. */ ownerEmail: string | null; /** Operational team or on-call queue that answers for the principal. */ team: string | null; riskTier: ServicePrincipalRiskTier; /** What the principal is for, in one line. */ purpose: string | null; lifecycle: ServicePrincipalLifecycle; /** `null` is unrestricted. A list is the only set of actions it may call. */ allowedActions: string[] | null; lifecycleReason: string | null; lifecycleChangedBy: string | null; lifecycleChangedAt: number | null; createdAt: number | null; updatedAt: number | null; } export type ServicePrincipalState = { status: "not-service"; } | { status: "ungoverned"; orgId: string; serviceName: string; } | { status: "active"; policy: ServicePrincipalPolicy; } | { status: "suspended" | "retired"; policy: ServicePrincipalPolicy; } /** No verified org matches the address, so this is not the principal. */ | { status: "org-mismatch"; } | { status: "unavailable"; }; export declare const servicePrincipalPoliciesStore: import("../db/store-registry.js").Store; export declare function ensureTable(): Promise; export declare function servicePrincipalLifecycleLockKey(orgId: string, serviceName: string): string; export declare function lockServicePrincipalLifecycle(db: Pick, orgId: string, serviceName: string): Promise; export declare function withServicePrincipalLifecycleLock(orgId: string, serviceName: string, work: (db: DbExec) => Promise): Promise; /** Throws when the store cannot be read; `null` means there is no record. */ export declare function getServicePrincipalPolicy(orgId: string, serviceName: string, db?: Pick): Promise; export declare function listServicePrincipalPolicies(orgId: string): Promise; export interface ServicePrincipalPolicyInput { ownerEmail?: string | null; team?: string | null; riskTier?: ServicePrincipalRiskTier; purpose?: string | null; allowedActions?: string[] | null; } /** * Create or update the governance fields. Fields left `undefined` keep their * stored value; `null` clears one. Lifecycle changes go through * `setServicePrincipalLifecycle` so they always carry an actor and a reason. */ export declare function upsertServicePrincipalPolicy(orgId: string, serviceName: string, input: ServicePrincipalPolicyInput): Promise; export declare class ServicePrincipalRetiredError extends Error { readonly statusCode = 409; constructor(); } export declare function setServicePrincipalLifecycle(orgId: string, serviceName: string, lifecycle: ServicePrincipalLifecycle, change: { actorEmail: string; reason?: string | null; }, db?: Pick): Promise; /** * Resolve whether a caller may run from its identity email and verified org. * Non-service identities return `not-service` without touching the database, * so the hot path for human callers is unchanged. */ export declare function evaluateServicePrincipal(email: string | null | undefined, orgId?: string | null, db?: Pick): Promise; /** Whether the principal may call `actionName`. `null` grants everything. */ export declare function isActionGranted(allowedActions: string[] | null, actionName: string): boolean; /** Message returned to a refused caller; never echoes the stored reason. */ export declare const SERVICE_PRINCIPAL_SUSPENDED_MESSAGE = "This service principal is suspended or retired. Ask an organization admin to resume it.";