PhoenixKit.API.Approvals (phoenix_kit v2.63.0)

Copy Markdown View Source

The approval queue of dangerous API operations (the approval path of the dangerous-operation flow): the calls of a key marked "requires approval" that would be dangerous wait here for a person.

Life of an approval

pending -> approved -> executing -> executed | failed
pending -> rejected | stale | expired

request/2 creates the pending row (the danger gate calls it with the preview the handler produced in dry-run mode). An agent follows its approval with system.get_approval (get_for_key/2 and status_view/1).

Rules

  • An identical pending request (same key, tool, arguments) returns the existing approval.
  • A key holds at most api_approval_pending_max pending approvals; one more is {:error, :pending_cap}. Both rules run under a per-key advisory lock.
  • input is the arguments the approval will run with; at most 1 MB; cleared when the approval is decided or expires.
  • Who may decide: an Owner, or a person who holds api, created the key and holds the tool's permission (may_decide?/2).
  • An approved operation is run once: only an approved row starts, and it moves to executing first. If it runs for more than 15 minutes without calling ctx.progress, the stuck-operation sweep marks it failed (interrupted) although the process is still working; when the handler then succeeds, the row is corrected to executed (with a note). Until then, and if the process really died, the row stays failed and part of the operation may have been applied: an agent must check the data and not queue the operation again before it has.

Changes are announced on the PubSub topic "phoenix_kit:api:approvals" as {:api_approval_changed, approval_uuid}, which the approvals page listens to.

Summary

Functions

Approves approval_id and runs it now, in the calling process: the decision, the checks of the request path repeated against the key as it stands, the values fingerprint, then the operation. On {:ok, row} the row is final: executed, failed or stale.

The active people who may decide approval: the Owners and the key's creator, if they qualify.

The approval approval_id if it belongs to the key key_uuid, else nil (an unknown id, a malformed id, another key's approval and a confirmation code all read the same). A pending approval past its time reads as expired.

The pending approvals scope may decide, oldest first, with their keys.

The latest decided approvals scope may see, newest change first.

Whether the person behind scope may approve or reject approval: an Owner, or a holder of api who created the key and holds the tool's permission.

How many pending approvals scope may decide.

Rejects approval_id: nothing runs and its input is discarded.

Queues an approval. args is %{actor, tool, input, input_hash, preview, fingerprint, context}: the key's actor, the tool name, the validated arguments without the control fields, their canonical hash, the preview and fingerprint of the dry run, and the request context (language currency hosts base_url request_id) the operation will run with.

The JSON-able view system.get_approval answers: id, status, tool, preview, times, and by status the result, or the error and the partial results. Never the input.

The PubSub topic of approval changes.

Functions

approve(approval_id, scope, now \\ DateTime.utc_now())

@spec approve(term(), PhoenixKit.Users.Auth.Scope.t(), DateTime.t()) ::
  {:ok, PhoenixKit.API.Confirm.Confirmation.t()}
  | {:error, :not_found | :forbidden | :not_pending | :expired}

Approves approval_id and runs it now, in the calling process: the decision, the checks of the request path repeated against the key as it stands, the values fingerprint, then the operation. On {:ok, row} the row is final: executed, failed or stale.

deciders(approval)

@spec deciders(PhoenixKit.API.Confirm.Confirmation.t()) :: [
  PhoenixKit.Users.Auth.User.t()
]

The active people who may decide approval: the Owners and the key's creator, if they qualify.

get_for_key(approval_id, key_uuid)

@spec get_for_key(term(), term()) :: PhoenixKit.API.Confirm.Confirmation.t() | nil

The approval approval_id if it belongs to the key key_uuid, else nil (an unknown id, a malformed id, another key's approval and a confirmation code all read the same). A pending approval past its time reads as expired.

list_pending(scope, now \\ DateTime.utc_now())

@spec list_pending(PhoenixKit.Users.Auth.Scope.t(), DateTime.t()) :: [
  %{
    approval: PhoenixKit.API.Confirm.Confirmation.t(),
    key: PhoenixKit.API.Keys.Key.t()
  }
]

The pending approvals scope may decide, oldest first, with their keys.

list_recent(scope, limit)

@spec list_recent(PhoenixKit.Users.Auth.Scope.t(), pos_integer()) :: [
  %{
    approval: PhoenixKit.API.Confirm.Confirmation.t(),
    key: PhoenixKit.API.Keys.Key.t()
  }
]

The latest decided approvals scope may see, newest change first.

may_decide?(scope, approval)

@spec may_decide?(
  PhoenixKit.Users.Auth.Scope.t(),
  PhoenixKit.API.Confirm.Confirmation.t()
) :: boolean()

Whether the person behind scope may approve or reject approval: an Owner, or a holder of api who created the key and holds the tool's permission.

pending_count(scope)

@spec pending_count(PhoenixKit.Users.Auth.Scope.t()) :: non_neg_integer()

How many pending approvals scope may decide.

reject(approval_id, scope, now \\ DateTime.utc_now())

@spec reject(term(), PhoenixKit.Users.Auth.Scope.t(), DateTime.t()) ::
  {:ok, PhoenixKit.API.Confirm.Confirmation.t()}
  | {:error, :not_found | :forbidden | :not_pending | :expired}

Rejects approval_id: nothing runs and its input is discarded.

request(args, now \\ DateTime.utc_now())

@spec request(map(), DateTime.t()) ::
  {:ok, PhoenixKit.API.Confirm.Confirmation.t(), :created | :existing}
  | {:error, :pending_cap | :input_too_large | Ecto.Changeset.t()}

Queues an approval. args is %{actor, tool, input, input_hash, preview, fingerprint, context}: the key's actor, the tool name, the validated arguments without the control fields, their canonical hash, the preview and fingerprint of the dry run, and the request context (language currency hosts base_url request_id) the operation will run with.

status_view(row)

@spec status_view(PhoenixKit.API.Confirm.Confirmation.t()) :: map()

The JSON-able view system.get_approval answers: id, status, tool, preview, times, and by status the result, or the error and the partial results. Never the input.

topic()

@spec topic() :: String.t()

The PubSub topic of approval changes.