/** * The permission preset, as a thing a chat can see and change. * * The host owns both knobs — how far the sandbox lets a command reach, and * whether an action that needs approval gets to ask — and pairs them into * named presets switched by `/permission `. The command already takes * that argument; a chat could always switch, it just had to know the name and * type it. So this module is not new capability, it is the same capability * with a surface: read the current preset, offer the others, and switch by * running the host's own command. * * One property of the shipped table has to reach the person pressing the * button. `danger-full-access` is not only "stop confining" — it also sets the * approval policy to `never`, and `never` means an action that still needs * approval is REFUSED rather than waved through. Someone who reads the name as * "allow everything" would be half right and half wrong, which is the worst * way to be wrong about a permission. * @module dsh-lark-channel/permission */ import type { HostAgent, HostCommands, HostPermissionPresets, HostSessionProjections } from './host.ts'; import type { ConversationSubject } from './session.ts'; /** * The `permissions` projection the host publishes for every session: which * preset is in force and which ones exist. */ export interface PermissionsProjection { readonly currentValue?: unknown; readonly options?: readonly { readonly value?: unknown; readonly name?: unknown; readonly description?: unknown; }[]; } /** * One preset a deployment offers, as the host describes it. * * The host names and explains its own presets, including ones a deployment * added. Keeping only the value would leave a card able to explain exactly the * two names this plugin happens to hardcode. */ export interface PresetOption { /** The name `/permission` takes, and what a button carries. */ readonly value: string; /** What to show a reader; the value itself when the host offered no other. */ readonly name: string; /** The host's own explanation, when it published one. */ readonly description?: string | undefined; /** * What the preset actually does, from the deployment's own table. * * Carried so that what a card SAYS and what this channel ENFORCES read the * same source. A deployment defines its own table: a preset called * `workspace-write` can be unconfined underneath, and a card that described * it from the name would be asking someone to authorize one thing while * granting another — in the one place where that is least acceptable, a * consent screen. Absent when the table cannot be read. */ readonly sandbox?: string | undefined; readonly approval?: string | undefined; } /** The host command that switches presets. Channel-driven, host-owned. */ export declare const PERMISSION_COMMAND = "permission"; /** Marks this plugin's preset buttons apart from other card actions. */ export declare const PERMISSION_ACTION = "dsh-lark-channel/permission"; /** The preset this channel treats as the loud one, whatever else a deployment defines. */ export declare const UNCONFINED_PRESET = "danger-full-access"; /** What one conversation's presets look like right now. */ export interface PresetState { /** The preset in force, when the host reported one. */ readonly current?: string | undefined; /** Every preset this deployment offers, in the host's own order. */ readonly available: readonly PresetOption[]; } /** * Whether switching to one preset loosens what the conversation may reach. * * The one asymmetry this channel's authorization rests on: taking the sandbox * off is a grant, and putting it back on is not. A rule that gated both the * same way would stop an ordinary member from making their own conversation * SAFER, which is the wrong thing to make hard. * * Judged by what the preset DOES, not by what it is called. A deployment * defines its own table — `unrestricted-prod: { sandbox: danger-full-access, * approval: never }` is a preset a name check would wave straight through — so * the knobs decide: removing the confinement or removing the asking is a * grant. Where the table cannot be read the answer is yes, because "I could * not tell" is not a reason to skip an approver. * @param preset - the preset being switched to. * @param presets - the deployment's preset table, when composed. * @returns true when the switch removes confinement or stops the asking. */ export declare function loosensSandbox(preset: string, presets: HostPermissionPresets | undefined): boolean; /** * Whether one preset both removes the sandbox and stops the asking — the two * things this channel's loudest copy promises, and the pair a button that * offers to "stop asking" must actually deliver. * @param option - the option to judge, as read. * @returns true only when the deployment's table says both. */ export declare function isUnconfined(option: PresetOption | undefined): boolean; /** * Card payload carried by one preset button. * * It names the CONVERSATION, not just the chat. A chat outlives its sessions — * `/new` and `/cd` each move it onto a different session id, and the ones it * left behind stay in the bridge's tables — so a click that carries only a * chat id has to guess which of them it meant. The conversation key is the * thing that survives all of it: the live session is derived from it, the same * way every message does. */ export interface PermissionActionValue extends ConversationSubject { readonly kind: typeof PERMISSION_ACTION; /** The preset to switch to. */ readonly preset: string; } /** * Narrow an arbitrary card-action value to this module's payload. * @param value - raw button value from a card action event. * @returns the typed payload, or undefined for foreign card actions. */ export declare function permissionActionValue(value: unknown): PermissionActionValue | undefined; /** * Read which preset one conversation runs under. * * Through the host's own `permissions` projection, for three reasons that the * first two attempts each missed. It is a READ: running `/permission` to find * out appends `command/run` and `command/done` to the session log, which made * a status card a second writer beside the agent's. It is CHEAP: a projection * is folded incrementally and cached, while folding the log per read costs the * whole log every time a card is drawn. And it is PUBLISHED: the projection * carries a registered schema, unlike a service method or a sentence meant for * a human to read. * @param projections - the projection registry, when composed. * @param agent - the conversation's live agent. * @returns the state, empty where nothing published one. */ export declare function readPresets(projections: HostSessionProjections | undefined, agent: HostAgent | undefined, presets?: HostPermissionPresets | undefined): PresetState; /** * Switch one conversation to a preset, through the host's own command. * @param agent - the conversation's live agent. * @param commands - the host command runtime, when composed. * @param preset - the preset name to switch to. * @param signal - cancellation for the host execution. * @returns whether the switch landed, and what the host said about it. */ export declare function switchPreset(agent: HostAgent, commands: HostCommands | undefined, preset: string, signal: AbortSignal): Promise<{ readonly ok: boolean; readonly detail?: string; }>; /** * The sandbox mode one tool call asked to be raised to, read from the exact * arguments the call carried. * * The host's approval request names the tool and the reason but not the * escalation — that travels in the call's own `sandbox_permissions` argument, * which this channel already snapshots at ask time. Reading it there is how * the card can say what is actually being granted. * @param callArguments - the snapshotted arguments, as the model produced them. * @returns the requested mode, or undefined when the call asked for none. */ export declare function requestedEscalation(callArguments: string | undefined): string | undefined; //# sourceMappingURL=permission.d.ts.map