/** * Shared permission-check helper that applies the configured * {@link PermissionMode} so plugins don't each re-implement the * observe-vs-enforce branch. * * Plugins should call this in their pre-tool-use handler instead of * `client.checkPermission` directly. The helper: * * 1. Runs the check. * 2. On a Keto deny (`result.allowed === false`), applies the mode: * - `observe` — record a `permission.observe_deny` audit event, * return `{ kind: "observe", … }` so the caller allows the * tool through. * - `enforce` — return `{ kind: "deny", … }` so the caller blocks. * 3. On a thrown {@link OryError}, inspects the classified code: * - Infrastructure errors (`network_error`, `rate_limited`, * `not_found`, `unknown`) — returns `{ kind: "fail_open", … }`. * Callers should log and allow. * - Auth-rejected check calls (`forbidden`, `session_inactive`, * `session_aal2_required` — the check itself was rejected, so * its result cannot be trusted) — in `enforce` mode returns * `{ kind: "deny", … }` so the caller blocks; in `observe` * mode falls back to `fail_open` (observe never blocks). * * The discriminated `kind` lets plugins map cleanly to their native * decision shape (claude-code exit codes, openclaw `{ block: true }`, * opencode `throw OryDenialError`, etc.) without re-implementing the * mode logic. */ import type { OryAgentClient } from "./client.js"; import { type PermissionMode } from "./config.js"; import type { OryError, OryErrorCode, PermissionCheck, PermissionResult } from "./types.js"; /** * Why a permission check came back denied, once the block-aware OPL schema is * in play: * * - `not_granted` — compatibility for grant-based checks outside native * AgentTool/ShellTool policy (notably MCP). * - `explicit_block` — a native `use` permit denied because a * `blockedSubjects` relation matched. * * Threaded onto the decision's activity attributes and into the observe-mode * audit event so "explicitly blocked" is distinguishable from "never granted" * in activity logs, mirroring how `checkRejected` distinguishes an auth-rejected * deny. */ export type BlockReason = "not_granted" | "explicit_block"; /** * Compatibility shim for callers that still pass the former native `users` * relation. Native adapters now pass `use` directly; mapping `users` preserves * the public helper contract without reintroducing grant-based native checks. */ export declare function resolveCheckRelation(relation: string): string; /** * Attributes describing *what was checked* and *under which posture*. * Plugins spread this onto their `tool.invoke` / `tool.block` activity events so * the audit trail makes the observe-vs-enforce posture and the checked * subject visible at a glance — without that, an event with `allowed=false` * status=ok is ambiguous (observe pass-through? fail-open? bug?). */ export interface DecisionActivityAttributes { permissionMode: PermissionMode; /** Direct subject ID when the check used `subjectId`. */ subjectId?: string; /** SubjectSet rendered as `:#` when used. */ subjectSet?: string; /** * Set when the check call itself was auth-rejected and enforce mode * turned that into a deny — the classified {@link OryErrorCode} that * caused it. Makes the "denied because the check failed" case * distinguishable from a normal Keto deny in the audit trail. */ checkRejected?: OryErrorCode; /** * Shell decomposition (issue #76): the specific denied command word that * caused a `Bash` deny (`curl`), so denial text and `tool.block` events name * it. {@link shellDeniedWords} carries the full set when more than one. */ shellWord?: string; shellDeniedWords?: string[]; /** * The parsed decomposition, attached to activity so the shell breakdown is * observable in real time: every command word extracted, and how many. These * are binary/builtin *names* only — the **raw** command is NEVER placed in a * activity event; it goes only to the local debug log, gated by * `ORY_AGENT_DEBUG`. */ shellWords?: string[]; shellCommandCount?: number; /** * Set when a shell command could not be safely parsed (dynamic name / * unparseable) and enforce mode denied it, or observe logged it. */ shellTooComplex?: true; /** * Set on a deny: whether the deny was an explicit block or a plain missing * grant. See {@link BlockReason}. */ blockReason?: BlockReason; /** * The machine principal an explicit block matched — the exact subject string * (`Agent:|`, …), so the audit trail names the relation an * admin would go edit. Set whenever a principal block fired, including on a * deny the user's own check had already produced. */ blockedPrincipal?: string; /** Which level of the identity model the block was written at. */ blockedPrincipalLevel?: string; } export type PermissionDecision = { kind: "allow"; result: PermissionResult; mode: PermissionMode; activityAttributes: DecisionActivityAttributes; } | { kind: "deny"; result: PermissionResult; mode: "enforce"; activityAttributes: DecisionActivityAttributes; } | { kind: "observe"; result: PermissionResult; mode: "observe"; activityAttributes: DecisionActivityAttributes; } | { kind: "fail_open"; error: OryError; mode: PermissionMode; activityAttributes: DecisionActivityAttributes; }; /** * The "what should the caller do?" half of a decision, independent of * which kind of check produced the underlying `allowed` boolean. Used * by both the regular check path (via {@link checkAndDecide}) and the * MCP path (via {@link applyPermissionMode} on the MCP result). */ export type ModeDecision = { kind: "allow"; mode: PermissionMode; activityAttributes: DecisionActivityAttributes; } | { kind: "deny"; mode: "enforce"; activityAttributes: DecisionActivityAttributes; } | { kind: "observe"; mode: "observe"; activityAttributes: DecisionActivityAttributes; }; export interface CheckAndDecideOptions { /** * Activity attributes merged into the underlying `permission.check` event * and into the synthetic `permission.observe_deny` event when emitted. * Same shape and semantics as `client.checkPermission`'s option. */ activityAttributes?: Record; /** * Override the resolved {@link PermissionMode}. Tests use this to * exercise both branches without mutating env or config state. */ modeOverride?: PermissionMode; } export interface ApplyPermissionModeContext { /** * Namespace / object / relation that produced the `allowed` boolean. * Logged on observe-deny and attached to the audit event. All optional * — the helper still works without them, but populated values make * the audit trail searchable. */ namespace?: string; object?: string; relation?: string; /** Subject ID for the observe-deny log line, if available. */ subjectId?: string; /** SubjectSet for the observe-deny log line, if available. */ subjectSet?: PermissionCheck["subjectSet"]; /** Attributes merged into the `permission.observe_deny` activity event. */ activityAttributes?: Record; /** Override the resolved mode (tests). */ modeOverride?: PermissionMode; /** * Why the check denied, when known (block-aware schema path). When * `explicit_block`, observe mode records a distinct `permission.block_observed` * audit event instead of the generic `permission.observe_deny`, and the * reason is attached to the decision activity attributes in every mode. */ blockReason?: BlockReason; } /** * Map an `allowed` boolean from any permission check (plain or MCP) * onto a {@link ModeDecision}. When `allowed === false` and the mode * is `observe`, emits the `permission.observe_deny` audit event and * returns `{ kind: "observe" }` so the caller knows to let the action * proceed despite the deny. */ export declare function applyPermissionMode(client: OryAgentClient, allowed: boolean, context?: ApplyPermissionModeContext): ModeDecision; /** * Outcome of {@link gateToolCall}. Either Agent Security isn't connected so no * check runs, the tool is a user-interaction primitive (`AskUserQuestion`, * `ExitPlanMode`, `TodoWrite`, …) and we pass through with one audit event, * or it's a real tool execution and the caller gets the standard * {@link PermissionDecision}. */ export type ToolGateOutcome = { kind: "not_connected"; /** Attributes to attach to the caller's pass-through activity, if any. */ activityAttributes: { securityConnected: false; toolName: string; }; } | { kind: "interactive"; /** Attributes to attach to the caller's pass-through activity, if any. */ activityAttributes: { interactive: true; toolName: string; }; } | PermissionDecision; export interface GateToolCallArgs { /** Harness name (`claude-code`, `codex`, …). Used to look up the interactive-tool catalog. */ harness: string; /** The tool the agent is invoking. */ toolName: string; /** Permission check to run when the tool is a real execution. */ check: PermissionCheck; /** * Attributes merged into permission activity (real path) and the * `user.interaction` event (interactive path). */ activityAttributes?: Record; /** Override the resolved {@link PermissionMode}. Tests use this. */ modeOverride?: PermissionMode; /** * Raw shell command string(s) when {@link toolName} is the harness's shell tool * (issue #76). When present and the tool is a shell tool, the gate decomposes * each command into `ShellTool:#use` sub-checks in addition to the * top-level tool check. Absent / undefined ⇒ decomposition is skipped and the * gate behaves exactly as before. */ shellCommand?: string | readonly string[]; /** * The acting machine principals, so an explicit block on the agent install, * this session, the sub-agent kind, or this spawn can deny the call. Omitted ⇒ * only the agent principal already on the client is considered (there is no * sub-agent context to check). */ principals?: PrincipalBlockSubjects; } /** * Single entry point for the pre-tool-use gate. Checks whether Agent Security * is connected first — with no project URL there is * nothing to check against, so the caller gets `{ kind: "not_connected" }` * back and should record the invocation as an audit event and proceed. * Otherwise splits the harness's incoming "tool" into two semantic * categories: * * - **Interactive** — the tool surfaces UI to the user * (`AskUserQuestion`, `ExitPlanMode`, `TodoWrite`, plus anything * listed in `ORY_INTERACTIVE_TOOLS`). The plugin must not gate these * through Ory: the user is the decision-maker, and blocking them in * enforce mode (or logging a misleading observe-deny) hides the very * prompt the user needs to see. We record one `user.interaction` * audit event and return — no permission check, no `tool.invoke`, * no `tool.block`. * * - **Execution** — every other tool. Delegates to * {@link checkAndDecide} so observe/enforce/fail-open behavior is * identical to the legacy code path. * * The caller branches on `outcome.kind`. The four execution kinds * (`allow`, `deny`, `observe`, `fail_open`) keep their existing * semantics; the new `interactive` kind means "do nothing else." */ export declare function gateToolCall(client: OryAgentClient, args: GateToolCallArgs): Promise; /** * Gate a shell tool by decomposing its command into the program/builtin words * that will run and checking each as `ShellTool:#use`, on top of the * existing top-level tool check. * * Aggregation: **any deny → deny** (naming the offending word); else any observe * → observe (pass through, per-word `permission.observe_deny` events recorded); * else allow. A too-complex / unparseable command is **fail-closed** in enforce * (deny + alert) and logged-then-passed in observe. A parser-unavailable or * infrastructure error is **fail-open** (top-level decision only) — availability * must not hinge on the parser. */ export declare function decomposeAndCheck(client: OryAgentClient, args: GateToolCallArgs): Promise; /** * Run a permission check and resolve the configured mode against the * result. Never throws — infrastructure errors surface as a typed * `{ kind: "fail_open" }` decision, and auth-rejected check calls in * enforce mode as `{ kind: "deny" }` (see * {@link CHECK_UNTRUSTED_CODES}). */ export declare function checkAndDecide(client: OryAgentClient, check: PermissionCheck, opts?: CheckAndDecideOptions): Promise; /** * The acting machine principals for a tool call, as the subjects a `blocked` * relation can name. Built by the caller (a harness handler knows whether it is * in a sub-agent and what its type/spawn id are) and threaded into * {@link gateToolCall}. */ export interface PrincipalBlockSubjects { /** Sub-agent client id, when this call runs inside a sub-agent. */ subAgentClientId?: string; /** Sub-agent type, required for the spawn-level subject to be built. */ subAgentType?: string; /** Per-spawn id, on the harnesses that expose one. */ perSpawnId?: string; /** Override the session; defaults to the client's ambient session. */ sessionId?: string; }