# `PhoenixKitAI.GuardBlocks`
[🔗](https://github.com/BeamLabEU/phoenix_kit_ai/blob/v0.26.1/lib/phoenix_kit_ai/guard_blocks.ex#L1)

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.

# `active`

```elixir
@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`.

# `active`

```elixir
@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).

# `block`

```elixir
@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).

# `blocked?`

```elixir
@spec blocked?(String.t(), term()) :: boolean()
```

Whether `user_uuid` is blocked from `task_key` now.

# `check`

```elixir
@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`.

# `group`

```elixir
@spec group(PhoenixKitAI.TaskSpec.t()) :: String.t()
```

The block group of a task: its `block_group`, or its key.

# `list_active`

```elixir
@spec list_active(String.t()) :: [PhoenixKitAI.GuardBlock.t()]
```

The active blocks of a group, newest first (from the database).

# `may_block?`

```elixir
@spec may_block?(term(), String.t()) :: boolean()
```

Whether `scope` may block `user_uuid` (see the moduledoc).

# `may_lift?`

```elixir
@spec may_lift?(term(), PhoenixKitAI.GuardBlock.t()) :: boolean()
```

Whether `scope` may lift `block` (see the moduledoc).

# `private?`

```elixir
@spec private?(String.t()) :: boolean()
```

Whether a block of `group` is private: any of its tasks is.

# `tasks_of`

```elixir
@spec tasks_of(String.t()) :: [PhoenixKitAI.TaskSpec.t()]
```

The declared tasks of a block group.

# `unblock`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
