import { Logger } from '@happyvertical/logger'; import { SmrtClassOptions } from '@happyvertical/smrt-core'; import { OperationPermissionCollectionInput, OperationPermissionDecision, PermissionResolver, SessionPermissionRuntimeContext } from '@happyvertical/smrt-users'; /** * The bound principal an agent runs as. A resolved persona structurally * satisfies this once its optional `runAsUserId` has been narrowed to a * concrete id — `allowedTools` on a `ResolvedPersona` is already the persona's * tools intersected with the `TenantAgent` capability ceiling. */ export interface PrincipalBinding { /** The user whose live permissions bound this execution. Required. */ runAsUserId: string; /** Tenant the principal acts within. */ tenantId: string | null; /** * The persona's tool allow-list (already capped by the agent-class ceiling). * This is a **fail-closed** whitelist, mirroring * `@happyvertical/smrt-chat`'s `AgentSession.isToolAllowed()` (S5 #1392): an * absent (`undefined`) or empty allow-list permits **NO** tools, never all of * them, so forgetting to pass it can only tighten authority. Resolved personas * always provide a concrete `string[]`. */ allowedTools?: string[]; /** Optional acting `Bot` profile id, recorded in the audit entry. */ actsAsProfileId?: string | null; } /** * A single audit record describing an agent action performed by the bound * principal (`actorUserId`) on behalf of the originating user * (`onBehalfOfUserId`). */ export interface PrincipalAuditEntry { /** Action label, e.g. `'agent.run'`. */ action: string; /** The persona's bound user the work ran as. */ actorUserId: string; /** The user who triggered the agent, if known. */ onBehalfOfUserId: string | null; /** Tenant the action ran within. */ tenantId: string | null; /** Canonical agent class, when the caller supplies it. */ agentClass?: string; /** Acting profile id, when the persona sets one. */ actsAsProfileId?: string | null; /** Free-form additional context. */ metadata?: Record; } /** * Sink that records a {@link PrincipalAuditEntry}. Provide one to persist audit * rows (e.g. via `AuditLog.record`); when omitted, the entry is emitted as a * structured log line. */ export type PrincipalAuditSink = (entry: PrincipalAuditEntry) => void | Promise; /** * Options for {@link executeAsPrincipal}. */ export interface ExecuteAsPrincipalOptions extends SmrtClassOptions { /** The bound principal to run as. */ principal: PrincipalBinding; /** The originating user the action is performed on behalf of (for audit). */ onBehalfOfUserId?: string | null; /** Canonical agent class, recorded in the audit entry. */ agentClass?: string; /** Audit action label. Defaults to `'agent.run'`. */ action?: string; /** Extra audit metadata merged into the emitted entry. */ auditMetadata?: Record; /** * Pre-resolved permission slugs. When omitted, the principal's permissions * are resolved live so role changes reflect on the next execution. */ permissions?: string[]; /** Reuse an initialized resolver across executions. */ resolver?: PermissionResolver; /** Opt into Postgres RLS transaction wrapping (defaults to package config). */ postgresRls?: boolean; /** * Enter tenant context so tenant auto-filtering applies on every adapter. * Defaults to `true` whenever the principal has a tenant. */ enterTenantContext?: boolean; /** Audit sink. Defaults to a structured log line. */ audit?: PrincipalAuditSink; /** Logger used for the default audit sink. */ logger?: Logger; } /** * Thrown when the persona attempts a tool outside its `allowedTools`. */ export declare class PrincipalToolNotAllowedError extends Error { readonly tool: string; readonly status = 403; constructor(tool: string); } /** * The handle passed to the {@link executeAsPrincipal} body. Its * session-permission {@link context} is already published for the principal, so * data operations are bounded by RLS on Postgres. The assertions enforce the * remaining two authority dimensions. */ export interface PrincipalRun { /** The published session-permission runtime context for the principal. */ context: SessionPermissionRuntimeContext; /** The principal's published (snapshot) permission slugs. */ permissions: string[]; /** * The effective, fail-closed tool allow-list — always a concrete array (an * absent binding allow-list normalizes to `[]`, i.e. no tools). */ allowedTools: string[]; /** * Whether `tool` is within the fail-closed allow-list. An empty allow-list, * or an empty/non-string tool name, permits nothing. */ isToolAllowed(tool: string): boolean; /** Throw {@link PrincipalToolNotAllowedError} unless `tool` is allowed. */ assertToolAllowed(tool: string): void; /** * Assert the principal holds the catalog permission for `(collection, * action)`, authorizing against the **published** principal set * (`context.permissionSet`) — the same snapshot the RLS session enforces — so * the bound is adapter-independent. This is the door-agnostic teeth for the * RLS-off adapters; under Postgres RLS it is a redundant (but harmless) * second gate. Throws `OperationPermissionError` on denial. */ assertOperation(collection: OperationPermissionCollectionInput, action: string, extraOptions?: SmrtClassOptions): Promise; } /** * Run `fn` AS the persona's bound principal. * * Resolves the bound user's live permissions, publishes them onto the DB * session (so Postgres RLS bounds every query per-`(table, action)`), emits an * on-behalf-of audit entry, and hands `fn` a {@link PrincipalRun} whose * assertions enforce the persona tool ceiling and the RLS-off catalog gate. * * @example * ```typescript * await executeAsPrincipal( * { * db, * principal: { * runAsUserId: persona.runAsUserId, * tenantId: persona.tenantId, * allowedTools: persona.allowedTools, * }, * onBehalfOfUserId: triggeringUserId, * agentClass: persona.agentClass, * }, * async (run) => { * run.assertToolAllowed('articles.publish'); * await run.assertOperation('articles', 'update'); * await agent.run(); * }, * ); * ``` */ export declare function executeAsPrincipal(options: ExecuteAsPrincipalOptions, fn: (run: PrincipalRun) => Promise): Promise; //# sourceMappingURL=execute-as-principal.d.ts.map