Blocks: a person may not call a group of AI tasks until the block is lifted
(PhoenixKitAI.GuardBlock). A person sets them — there is no automatic
block.
Groups
A block is on a task's block_group, or on the task's key when it names
none, so tasks that take the same kind of free text (free_text: the
Playground and an assistant's chat) share one block. A block of a group is
private when any task of the group is (private: true): its reason is
shown only to the holders of ai.manage.
The check
check/2 runs on every call of a declared task that has a user, before
the model check (see PhoenixKitAI.Guard), and in PhoenixKitAI.Guard.admit/4:
a blocked call is refused with {:error, {:task_blocked, block_group}},
which names the group and never the reason (the refusal is a journal row
every holder of ai reads). A turn admitted before the block runs to its
end. A dry run (process_image(dry_run: true)) is not checked.
Who may
| set a block | lift a block | |
|---|---|---|
| an Owner | anybody | any block, their own too (kept as a record) |
a holder of ai.manage | anybody but an Owner | another person's, not an Owner's |
a holder of agent.manage | — | another person's free_text block, not an Owner's |
| anybody else | — | — |
Nobody but an Owner lifts their own block. agent.manage is checked by
its string; this module does not depend on the assistant's.
Each block and lift is a blocked / unblocked event, a notification to
the watchers (PhoenixKitAI.GuardEvents), an in-app notice to the person,
and an Activity entry with the event as its resource. None of them carries
the reason or the note.
The cache
The active blocks of every group are kept on each node (the request
cache's flags table), loaded with one query, so a call without a block
costs none. A block or a lift drops this node's copy at once and
broadcasts {:guard_blocks_changed, block_group} on
PhoenixKitAI.TaskLimits.topic/0,
which PhoenixKitAI.GuardWatch turns into the same drop on every node; a
restarted watch drops it too. The copy is also read again after 60
seconds. The check fails open: a database error reads as no block.
Summary
Functions
The active block of user_uuid in task_key's group, or nil — without
its reason and note, for a page or notice that shows the state. With the
reason: active/3.
The active block as viewer (a scope) may see it: its reason only when
PhoenixKitAI.Guard.reasons_visible?/2 allows it (any viewer of a
block that is not private; ai.manage or an Owner for a private one).
Blocks user_uuid from the group of task_or_group (a task key, or a
block group's name). reason is kept, cut to 500 characters; origin
may name the flag it came from (%{event_uuid: uuid}, kept when the flag
is that person's). Needs ai.manage, and an Owner to block an Owner (see
the moduledoc).
Whether user_uuid is blocked from task_key now.
:ok, or {:error, {:task_blocked, block_group}} when user_uuid is
blocked from the task's group. A call without a task or a user is :ok.
The block group of a task: its block_group, or its key.
The active blocks of a group, newest first (from the database).
Whether scope may block user_uuid (see the moduledoc).
Whether scope may lift block (see the moduledoc).
Whether a block of group is private: any of its tasks is.
The declared tasks of a block group.
Lifts block (a %GuardBlock{} or its uuid), keeping note (cut to 500
characters) and who lifted it. Who may: see the moduledoc.
Functions
@spec active(String.t(), term()) :: PhoenixKitAI.GuardBlock.t() | nil
The active block of user_uuid in task_key's group, or nil — without
its reason and note, for a page or notice that shows the state. With the
reason: active/3.
@spec active(String.t(), term(), term()) :: PhoenixKitAI.GuardBlock.t() | nil
The active block as viewer (a scope) may see it: its reason only when
PhoenixKitAI.Guard.reasons_visible?/2 allows it (any viewer of a
block that is not private; ai.manage or an Owner for a private one).
@spec block(term(), String.t(), term(), term(), map() | keyword() | nil) :: {:ok, PhoenixKitAI.GuardBlock.t()} | {:error, :forbidden | :owner_only | :unknown_task | :invalid_user | :unknown_user | :already_blocked | Ecto.Changeset.t()}
Blocks user_uuid from the group of task_or_group (a task key, or a
block group's name). reason is kept, cut to 500 characters; origin
may name the flag it came from (%{event_uuid: uuid}, kept when the flag
is that person's). Needs ai.manage, and an Owner to block an Owner (see
the moduledoc).
Whether user_uuid is blocked from task_key now.
@spec check(PhoenixKitAI.TaskSpec.t() | nil, term()) :: :ok | {:error, {:task_blocked, String.t()}}
:ok, or {:error, {:task_blocked, block_group}} when user_uuid is
blocked from the task's group. A call without a task or a user is :ok.
@spec group(PhoenixKitAI.TaskSpec.t()) :: String.t()
The block group of a task: its block_group, or its key.
@spec list_active(String.t()) :: [PhoenixKitAI.GuardBlock.t()]
The active blocks of a group, newest first (from the database).
Whether scope may block user_uuid (see the moduledoc).
@spec may_lift?(term(), PhoenixKitAI.GuardBlock.t()) :: boolean()
Whether scope may lift block (see the moduledoc).
Whether a block of group is private: any of its tasks is.
@spec tasks_of(String.t()) :: [PhoenixKitAI.TaskSpec.t()]
The declared tasks of a block group.
@spec unblock(term(), PhoenixKitAI.GuardBlock.t() | String.t(), term()) :: {:ok, PhoenixKitAI.GuardBlock.t()} | {:error, :forbidden | :owner_only | :own_block | :not_blocked | :not_found}
Lifts block (a %GuardBlock{} or its uuid), keeping note (cut to 500
characters) and who lifted it. Who may: see the moduledoc.