Current section

Files

Jump to
codex_sdk lib codex approvals hook.ex
Raw

lib/codex/approvals/hook.ex

defmodule Codex.Approvals.Hook do
@moduledoc """
Behaviour for implementing pluggable approval hooks.
Hooks can provide synchronous or asynchronous approval decisions for tool invocations,
command executions, file access operations, and request-permissions prompts.
## Callbacks
- `c:prepare/2` - Called before the approval review, can mutate metadata
- `c:review_tool/3` - Review a tool invocation
- `c:review_command/3` - Review a command execution (optional)
- `c:review_file/3` - Review a file access operation (optional)
- `c:review_permissions/3` - Review a request-permissions approval prompt (optional)
- `c:await/2` - Wait for an async approval decision (optional)
## Return Values
Synchronous hooks return:
- `:allow` - approve the operation
- `{:allow, opts}` - approve with additional options (used by app-server approvals)
- `{:deny, reason}` - deny with a reason string
Asynchronous hooks return:
- `{:async, ref}` - defer decision, will call `c:await/2` later
- `{:async, ref, metadata}` - defer decision with additional metadata
`{:allow, opts}` supports `:grant_root` to grant file-change approvals for the
session when requested by the app-server. Permissions approvals additionally
support `:permissions` and `:scope`.
## Example
defmodule MyApp.SlackApprovalHook do
@behaviour Codex.Approvals.Hook
@impl true
def prepare(event, context) do
# Add custom metadata before review
{:ok, Map.put(context, :slack_channel, "#approvals")}
end
@impl true
def review_tool(event, context, _opts) do
# Post to Slack and return async ref
ref = make_ref()
MyApp.SlackClient.post_approval_request(ref, event, context)
{:async, ref}
end
@impl true
def await(ref, timeout) do
# Wait for Slack response
receive do
{:approval_decision, ^ref, decision} -> {:ok, decision}
after
timeout -> {:error, :timeout}
end
end
end
"""
alias Codex.Protocol.RequestPermissions
@type event :: map()
@type context :: map()
@type opts :: keyword()
@type allow_opts :: [
for_session: boolean(),
execpolicy_amendment: [String.t()],
grant_root: String.t() | Path.t(),
permissions:
RequestPermissions.RequestPermissionProfile.t()
| RequestPermissions.GrantedPermissionProfile.t()
| map(),
scope: RequestPermissions.PermissionGrantScope.t()
]
@type decision :: :allow | {:allow, allow_opts()} | {:deny, String.t() | atom()}
@type async_ref :: reference()
@type async_result :: {:async, async_ref} | {:async, async_ref, metadata :: map()}
@type review_result :: decision() | async_result()
@doc """
Called before any review operation to prepare or augment context.
This callback can be used to add metadata, initialize state, or transform
the context before it's passed to review callbacks.
"""
@callback prepare(event(), context()) :: {:ok, context()} | {:error, term()}
@doc """
Review a tool invocation request.
## Parameters
- `event` - The tool call event (contains tool_name, arguments, call_id, etc.)
- `context` - The approval context (thread, metadata, etc.)
- `opts` - Hook-specific options
## Returns
- `:allow` - approve the tool invocation
- `{:deny, reason}` - deny with a reason
- `{:async, ref}` - defer decision, will be awaited later
- `{:async, ref, metadata}` - defer with additional metadata
"""
@callback review_tool(event(), context(), opts()) :: review_result()
@doc """
Review a command execution request (optional).
App-server command approval events include normalized snake_case fields such as
`:approval_id`, `:command`, `:cwd`, `:command_actions`,
`:network_approval_context`, `:additional_permissions`,
`:proposed_execpolicy_amendment`, `:proposed_network_policy_amendments`, and
`:available_decisions` when the connected Codex build sends them.
If not implemented, commands are allowed by default.
"""
@callback review_command(event(), context(), opts()) :: review_result()
@doc """
Review a file access request (optional).
App-server file approval events include normalized `:grant_root` when the
server suggests a session-scoped writable root.
If not implemented, file operations are allowed by default.
"""
@callback review_file(event(), context(), opts()) :: review_result()
@doc """
Review a request-permissions approval request (optional).
App-server permissions approval events expose a structured
`Codex.Protocol.RequestPermissions.RequestPermissionProfile`, including
network, filesystem, and macOS permission families when upstream provides
them.
If not implemented, permissions requests are allowed by default.
"""
@callback review_permissions(event(), context(), opts()) :: review_result()
@doc """
Wait for an async approval decision.
This callback is called when a review returned `{:async, ref}` and the
system needs to wait for the decision.
## Parameters
- `ref` - The reference returned by the review callback
- `timeout` - Maximum time to wait in milliseconds
## Returns
- `{:ok, decision}` - the approval decision
- `{:error, :timeout}` - timeout reached
- `{:error, reason}` - other error
"""
@callback await(async_ref(), timeout :: pos_integer()) ::
{:ok, decision()} | {:error, :timeout | term()}
@optional_callbacks prepare: 2,
review_command: 3,
review_file: 3,
review_permissions: 3,
await: 2
@doc """
Default prepare implementation that returns the context unchanged.
"""
def default_prepare(_event, context), do: {:ok, context}
@doc """
Default review implementation that allows all operations.
"""
def default_review(_event, _context, _opts), do: :allow
end