Files
james.maesandClaude Fable 5.2 a9ebe64883
maven / build (push) Successful in 7m48s
build / maven (push) Successful in 7m48s
fix(process): only a reviewed process runs; a person confirms what they were shown; an independent review of the process tools closed
After an independent review of the process tools. Each change is described
by the guarantee it gives.

- Only a reviewed process runs, decided when the application runs. A host
  registers the processes it reviewed for the assistant in
  AssistantQBitConfig (withReviewedProcesses, or
  withReviewedProcessesResource for a file on its classpath). With no list,
  only a table's own bulk edit and bulk delete are runnable: they act on
  records of a table the person may already change through the assistant.
  With a list: exactly the listed processes and those two, each still
  subject to every never-runnable rule, to an administrator's narrowing row
  and to the person's own permission. RunnableProcessReview stays as the
  way a host notices drift in its build, and now also fails when the
  reviewed file is not the registered list.
- An input is only a field a screen asks a person for. A screen's field
  whose name the process keeps for its own working is never an input
  (ProcessControlNames: the inputs of the process's backend steps, and a
  fixed set this QBit owns, checked by a test against the QQQ version it
  builds with). The person is told the field cannot be set through the
  assistant; the process stays runnable for its other fields.
- A person confirms what they were shown. A run stops at every later screen
  that has something to show (a form, values to read, a text, a list of
  records), and going on is a second card that shows it. Only a screen with
  nothing to show is passed. The screen a run starts at is on the first
  card.
- A run is over exactly the records its card counted. A filter is resolved
  when the run is proposed, to at most 500 primary keys (more is refused,
  for the person to narrow); the confirmed run is sent those keys and does
  not start when one of them is gone.
- A process that may run as a durable job is created waiting, or not run:
  where the application does not offer the operation that creates a job,
  the run is refused.
- Text of the application reaches a model only inside the data envelope
  every tool result uses: a process's messages, what a screen shows, the
  application's message for a refusal or a failure. That holds for a
  result, for a card's lines, for the summary a later request keeps of a
  process step, for what a later request is told became of a confirmed run,
  and for the thread's own message about it (ToolLabels.forModel,
  MessageParts.modelText).
- A card goes on only with the run as it stood when the card was made: a
  run counts a version up at every step and stop, and an older card of the
  same run is refused.

One expectation of ChatToolSetTest changes: the scenario
everythingAsAnotherPerson no longer holds the four process tools. That host
registered no reviewed list, and its person may run processes of another
QBit and change no table, so the assistant runs no process for them.
ToolCallChecksTest is unchanged. No new tool. AssistantStore is still the
only code that calls data actions.

README, UPGRADE.md, CHANGELOG.md, CLAUDE.md and section 4.5 of
docs/actions-and-mcp.md say the same.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 17:51:29 -06:00

811 lines
58 KiB
Markdown

# Upgrading qbit-assistant
## The assistant runs a process the signed-in person may run (Unreleased, UI API extension assistant 1.10.0)
**After this upgrade, with nothing else changed, the assistant can run two kinds of process for a person who may run them themselves, once that person confirmed the run on a card: a table's own bulk edit and its bulk delete, on a table the assistant may already change for that person.** Every other process stays closed until the application's code names it on a reviewed list. No DDL changes.
### What a host should do before deploying
1. **Decide whether the two bulk actions should run.** They act on records of a table the person may change through the assistant already (one confirmed record at a time, until now). To keep them away: `AssistantQBitConfig.withDefaultProcessAccess(DataAccessMode.NONE)`, or `withProcessAccess("orders.bulkDelete", DataAccessMode.NONE)` for one, or `READ` on the table.
2. **To let the assistant run other processes, register a reviewed list.** Nothing else opens one:
```java
new AssistantQBitConfig().withReviewedProcessesResource("assistant-runnable-processes.txt") // src/main/resources, one process name per line
```
Then add a test to the application that calls `RunnableProcessReview.assertReviewed(qInstance, person, Path.of("src/main/resources/assistant-runnable-processes.txt"))` (see the README, "Noticing drift"). Its first run fails and prints every process that could be run through the assistant. For each: accept it (its line goes in the file, which registers it) or say in code that the assistant stays away from it, so the test stops naming it:
- a process of your own that a model should never drive (running SQL, restoring a backup, changing identity or permissions, acting on another person's work): `QPermissionRules.withDeniedClients(Set.of("assistant"))` on the process;
- a process of a QBit you do not own: `ClientChannelsMetaData` (by name or pattern), or `AssistantQBitConfig.withProcessAccess(name, DataAccessMode.NONE)`.
From then on the test fails whenever a process is added that nobody decided about, when a listed process is gone, and when the file is not the list the application registered. An unlisted process is not runnable in the meantime, with or without the test.
3. **Nothing to change for an administrator's rows.** A `PROCESS` row on Assistant > Data Access can only narrow: `NONE` (or `READ`) keeps the assistant from running that process. A `READ_WRITE` row opens nothing: not an unlisted process, and nothing the code or the person's permissions do not.
### What is never runnable through the assistant
A process that is not on the application's reviewed list (except a table's own bulk edit and bulk delete); a process denied to the `assistant` channel; the assistant's own processes; a hidden process (except those two); a process no screen offers (in no app and on no table); a process on a table the assistant may not change; a process with a screen the assistant cannot present (a file upload, a required password, a download, a bulk-load form, a folder picker, a widget, custom content). The README has the list with its reasons.
### What a person sees
The person asks; the assistant looks up the process and its inputs, asks for a required value it was not given (it never makes one up), and proposes the run. A card shows the process, every input, what the first screen says, the records (how many, of which table) and the steps that will stop for the person. Nothing runs until they confirm. A process that may run as a durable job is created as a job that waits for the person: they start it on its job record (core does not let the assistant start a job); where the application does not offer the operation that creates a job, such a process is not run through the assistant. Any other process runs until it ends or reaches a screen with something for the person that their card did not show (a review, a form, values to read, a text, a list of records): it stops, and going on takes a second card that shows what that screen shows.
Four limits a person can meet:
- **A field that cannot be set.** A screen's field whose name the process keeps for its own working (`ProcessControlNames`: the inputs of its backend steps, and the names QQQ's streamed and bulk steps use) is not an input through the assistant. The person is told; they set it in the application. A process of your own that declares a screen's field as an input of a backend step as well is affected: that field is set in the application.
- **A filter over more than 500 records** is refused, for the person to narrow: the records of a run are fixed, by primary key, when it is proposed.
- **An older card of the same run** is refused once the run moved on.
- **A screen that lists records the assistant cannot show** (the application does not offer the operation that reads them, or they are of a table the assistant may not read) ends the run's way through the assistant there.
**A released panel** draws the card as a record change (heading from the summary, one line per input, the button of an update), because it knows only the three record tools; the run works. The process card proper is the panel's package.
### Other changes in this release
- **The write tools no longer say confirmation can be turned off.** The descriptions of `create_record`, `update_record` and `delete_record` ended "unless the administrator turned confirmation off". Tool descriptions are part of the cached request prefix: **the prefix changes once with this release** (one uncached request per conversation in flight), and again for a person who is offered the process tools.
- **The routing classifier asks whether a spending limit is in force**, like every other model call. Without one it is not called and the heuristic chooses the route. Only a host whose limits are all scoped to single models or providers that do not include the classifier's notices a difference.
- **Test connection stays available without a spending limit**, by decision, and is documented as the one exemption (README, "No answer without a spending limit").
- **Scheduled tasks**: `run_process` is offered to a scheduled task only with its write approval, like the record write tools.
- UI API extension `assistant` 1.10.0: `AssistantActionV1` gains `processName` and `processLabel`, and `changes` carries the card's lines for a `process_run`. No operation changed.
### Java API
- New: `tools.process.ProcessTools`, `ProcessToolFamily`, `ProcessRunnability`, `ProcessControlNames`, `RunnableProcessReview`; `ActionKinds.PROCESS_RUN`; `AssistantQBitConfig.withReviewedProcesses(Collection)`, `withReviewedProcessesResource(String)`, `getReviewedProcesses()` and `setReviewedProcesses(Set)`; `DataAccessService.forProcessInCode`, `forTableInCode` (what versioned code alone allows, with no administrator's row) and `reviewedProcesses()`.
- `engine.ToolLabels.forModel(toolName, summary)` and `summaryIsData(toolName)`, `MessageParts.addToolText(toolName, text)` and `modelText()`: the summary of a process step, and the thread's message about a confirmed run, are told to a model inside the data envelope. A text part of a message may now carry `source` (the tool whose outcome it repeats); a panel shows it as any text.
## Writes follow the signed-in person; the assistant needs a spending limit (Unreleased)
**After this upgrade the assistant proposes changes on every table the signed-in person may change themselves, with no settings row, and it refuses to answer anybody for whom no spending limit is in force.** Both are owner decisions of 2026-11-12. Every change still waits for the person's confirmation card, runs as that person through the UI API, and is refused there, with the screen's own message, when they may not make it. No DDL changes.
### What a host must apply
1. **Include the migration changeset** in the master changelog, once, after the assistant's tables:
```yaml
- include: { file: db/liquibase/qbit-assistant/assistant-data-access-follow-user.changelog.sql }
```
It deletes the rows of `assistant_data_access` that the retired table action "Add rows for every table" wrote and nobody changed since: type `TABLE`, mode `READ`, the action's fixed note, and a modify date less than one second after the create date. It touches no other row: not a row an administrator wrote or changed, not a `NONE` or `READ_WRITE` row, not a `PROCESS` row. Its rollback is empty. A host that never ran that action has nothing to remove and may still include it. A host with its own migration tool runs the one `DELETE` of that file.
Without the changeset nothing breaks: each of those rows now reads as a restriction, and the assistant stays read-only on that table until the row is deleted.
**Before upgrading, look at the rows the changeset will remove. A table that an administrator deliberately left read-only for the assistant by running "Add rows for every table" and never editing its row becomes writable through the assistant after the upgrade** (for the people who may change it themselves, each change on a confirmation card): the changeset cannot tell such a row from one nobody meant. This query, which has the changeset's own predicate, shows them. Run it in every environment before the changeset runs:
```sql
SELECT object_name, mode, notes, create_date, modify_date
FROM assistant_data_access
WHERE object_type = 'TABLE'
AND mode = 'READ'
AND notes = 'Added by ''Add rows for every table'': the assistant''s default for this table. The user''s own permissions still apply.'
AND create_date IS NOT NULL
AND modify_date IS NOT NULL
AND modify_date < create_date + INTERVAL '1' SECOND
ORDER BY object_name;
```
To keep a table read-only for the assistant, edit its row on Assistant > Data Access before upgrading: change its note to one of your own and save. An edited row is an administrator's row; the changeset leaves it, and after the upgrade it reads as the restriction you meant. A row the query does not show is not touched.
2. **Review the rows that remain** on Assistant > Data Access in every environment, before deploying. A row no longer opens anything. What each row means now:
| Row | Before | After |
|---|---|---|
| none | the assistant may read the table | the assistant may read it and propose what the person may change |
| `READ` | the same as no row | **a restriction**: the assistant never proposes a change to this table |
| `NONE` | the assistant never sees the table | unchanged |
| `READ_WRITE` | the assistant may propose changes to this table | no restriction, the same as no row: delete it or keep it |
Which rows exist in a deployed database cannot be read from code; an administrator has to look.
3. **Make sure a spending limit is in force**, or the assistant refuses every question after the upgrade. One is, when a budget row on Assistant > Budgets is switched on, has a limit above zero and applies to the person (a whole-instance budget applies to everybody), or when the code configuration or the policy row declares a hard limit above zero (`AssistantBudgets` user daily or instance monthly). The starter defaults changelog seeds a monthly whole-instance budget on a host that had no budget; a host that deleted or switched off its budgets, or never included the starter defaults and declares no hard limit, must add one. The Setup card's Budgets line says so.
### What changes for administrators
- **Data Access rows only restrict.** Add a row to make a table read-only for the assistant (`READ`) or to hide it (`NONE`). There is no longer a way to open a table from that screen, because nothing needs opening: the assistant follows each person's own permissions. The mode's help text and the Data access card on the Overview say what decided for each table ("default", "pinned in code", "administrator setting", "sensitive table").
- **The table action "Add rows for every table" is gone.** The Data access card shows the effective access of every table, and its source, without storing anything.
- **People who may change a table are offered the write tools at once**, on every table they may change. A person who may only read is not offered them. Tell the people who use the assistant: the card is the moment to check.
- **With no budget in force the assistant says so and stops**: "The assistant is not answering because no spending limit is in force. An administrator must set or switch on a budget (Assistant > Budgets) before it can be used." Switching a budget back on restores it with the next question. A budget that is switched off, or whose limit is zero, does not count. A temporary "unlimited until" override does not take a budget out of force: it is a dated exception to a budget that stays in place.
- **Scheduled tasks** are refused the same way, and the run's error is that sentence.
- **The retention process** now ends a confirmed change that was left running for more than an hour as failed, "Outcome unknown: ... Check the record before asking for it again.", with a tool-call row. Such an action could not be confirmed again before either; it is now also removed after `approvalRetentionDays` like any other.
- The assistant's own tables can never be opened to the assistant, by a row or by code.
### Keeping the assistant read-only, or opening single tables
In the application's configuration class, not in the database:
```java
config.withDefaultTableAccess(DataAccessMode.READ) // the assistant proposes no change anywhere ...
.withTableAccess("orders", DataAccessMode.READ_WRITE) // ... except where the code says so
.withTableAccess("payroll", DataAccessMode.NONE) // never shown to the assistant, whoever asks
.withDefaultProcessAccess(DataAccessMode.NONE); // no process, when the process tools arrive
```
A pin replaces the default for its object. An administrator's row can lower a pin or the default and can never raise either: with `withDefaultTableAccess(READ)` a `READ_WRITE` row does nothing.
### Java API, for a host or another QBit that called these directly
- `SignedInPerson.ofSession(qInstance, session)` returns a person whose `session()` is the session the authentication module resolved, not the object passed in. Code that put permissions, security keys or values on its own session object and expected the assistant to see them must put them where the authentication module does (its customizer). The time zone value is carried over.
- `UiApiToolbox.Context.forRead(sessionUUID, timezone, gateway, policy, credentials, turnId)` is gone. `runtime.getToolCalls().readContext(person, timeout)` makes the context of one of the assistant's own reads, for a signed-in person, and throws `QException` when the person has no assistant access or the kill switch is on.
- `StarterPrompts.forUser(person, policy, configured, historyOffered)` is gone; `forUser(context, userId, configured, historyOffered)` takes a context from `readContext`. `new StarterPrompts(toolbox, clock)` no longer takes a gateway.
- `AssistantStore.expirePendingAction(id)` is `expirePendingAction(userId, id, now)` and returns whether it expired the action; `decidePendingAction` is final.
- `UiApiToolbox.definitions(policy, boolean)` is `definitions(policy, Set<String> writableTables)`; `UiApiToolbox.anyWritableTable()` is gone (`DataAccessService.anyWritableTable()` still says whether any table's data access allows writes, for nobody in particular). `new BulkLoadToolFamily(bulkLoadTools)` takes one argument.
- `AssistantRetentionStep.Result` has an eighth component, `pendingActionsFailed`; `purge(runtime, now)` is the whole run, `purge(config, store, now)` everything but ending the actions left running.
- `processes.DataAccessDefaultsProcess` is gone.
- The test-jar of `qbit-assistant` holds the `testing` package only. A project that used `RuntimeInternals`, `ToolCallServiceInternals` or `Toolboxes` from it (none of the provider modules did) keeps its own copy in its own test sources, inside the assistant's package it reaches into; they were never an API.
- A host's own `ToolFamily` and its tools compile unchanged. A host's tool that proposes a change is offered as before; it decides its own offer in `definition(ToolOffer)`.
### An existing test's expectation that changed
`ChatToolSetTest` compares the chat's tool list with a file written before the tool registry existed. No definition changed and no line of the file changed. What changed is who is offered the write tools: the four "everything" scenarios now ask as a person who may change tables (with no settings row; they used to ask as a person who may only read, with `READ_WRITE` rows), and the scenario of a person who may not save an analysis lost its `save_analysis` line.
## One decision per pending action, a closed tool surface, cards with before and after (Unreleased, UI API extension assistant 1.9.0)
**Apply the "Pending actions" block at the end of `docs/schema-postgres.sql` before deploying.** It is guarded and safe to re-run. It adds:
- `assistant_pending_action.action_kind VARCHAR(64)`, `before_json TEXT`, `record_version VARCHAR(64)` (all nullable);
- two indexes on `assistant_pending_action`: `(user_id, status)` and `(create_date)`;
- the table `assistant_pending_action_claim` (`id`, `pending_action_id` NOT NULL with a cascading foreign key to `assistant_pending_action`, `user_id`, `decision`, `create_date`), with **`UNIQUE (pending_action_id)`** and an index on `user_id`.
The unique key is what makes a confirmation run once across servers. A host that maps table or column names with a table customizer maps the new table and columns the same way; a host with its own migration tool copies the block into it. Without the block, proposing and confirming fail (the columns and the table are written on every proposal and decision).
What a host may notice:
- A proposal made before the upgrade and confirmed after it: an update or a delete is refused as changed (it kept no version of the record) and the user asks again; a create runs. Proposals live 15 minutes by default.
- `update_record` and `delete_record` now read the record (`GET /table/{tableName}/{primaryKey}`, as the user) when they propose and again when the user confirms. A record the user cannot read is refused at proposal time with that operation's answer.
- Confirming an update or a delete of a record that changed in the meantime fails with "This record changed after the change was proposed. Nothing was changed."
- `assistantRetention` now deletes decided and lapsed pending actions older than `approvalRetentionDays` (default 365). The setting was documented and never read; check its value before the first run after the upgrade.
- New operation `GET /qqq/v1/assistant/actions`; `AssistantActionV1` has new optional fields (`actionKind`, `conversationId`, `createdAt`, `decidedAt`, `tableLabel`, `recordHref`, `changes`, `before`). A released panel keeps working: `toolName` and the existing fields are unchanged.
- The input kept in `assistant_tool_call.input_json` is now also redacted with `customRedactionPatterns`.
- A request that carries several credentials (a session cookie and an `Authorization` header, say) has only the one its session was resolved from forwarded to the UI API.
Java API, for a host or another QBit that called these directly:
- `SignedInPerson.ofSession(session)` is now `SignedInPerson.ofSession(qInstance, session)`. It asks the authentication module to resolve the session's own id and throws `IllegalArgumentException` when that is not the user the session names.
- `new AssistantApi(runtime, session)` still exists and still serves the user's own conversations. It starts a turn, confirms or cancels an action, and explains access only after the same check, and answers 403 when the check fails.
- Gone from the public surface: `AssistantRuntime.getToolRegistry()`, `getUiApiGateway()` and `ownToolFamilies()`; `ToolCallService.getRegistry()`; `ToolCallService.execute` and `log` (use `executeAndLog`); the public constructors of `ToolCallRequest`, `ConfirmedToolCall` and `UiApiToolbox.Context`, which are now final classes with the same accessors (a context for one of the assistant's own reads is `UiApiToolbox.Context.forRead(...)`). `new ToolCallService(runtime, registry, store)` takes the gateway as a fourth argument.
- `AccessExplainer.explain(...)` has an overload without the gateway argument; `RagService` and `StarterPrompts` are handed the gateway by the runtime.
- `UiApiToolbox.ProposedWrite` has three more components (`kind`, `before`, `recordVersion`); the seven-argument constructor still exists. `AssistantRetentionStep.Result` has a seventh component, `pendingActionsDeleted`.
- A host's own `ToolFamily` and its tools compile unchanged (they read a request through its accessors and never constructed one).
## Every write waits for confirmation; the tool-call service owns the policy and the identity (Unreleased)
No schema change and nothing to apply to the database. What a host may notice:
- **Writes require confirmation** on the `assistant_policy` row no longer does
anything: every create, update or delete the assistant proposes waits for the
user's confirmation card. The column `writes_require_confirmation` stays in
place and is not read. A host that had switched it off gets the card again.
- `AssistantQBitConfig.withToolFamily` throws when it is called after the QBit
was produced (it was ignored). Register tool families before producing it.
- Java API, for a host that called these directly: `AssistantApi.withCredentials`
is gone (`new AssistantApi(runtime, session)` is unchanged; a route uses
`new AssistantApi(runtime, SignedInPerson.ofRequest(context))`);
`AssistantRuntime.getToolbox()`, `getKnowledgeTool()` and `getBulkLoadTools()`
are gone; `PolicyService.ToolPolicy` has three components
(`allowedTools`, `maxRowsPerCall`, `redactPii`); `ToolCallScope.clientChannel()`
is gone. A host's own `ToolFamily` and its tools compile unchanged.
## Conversation management (Unreleased, UI API extension assistant 1.6.0)
No schema change. `assistant_conversation` is now usable by every user holding
`assistant.hasAccess` (not only `assistant.admin`), together with three new
record actions (Archive, Restore and Delete conversations). Rows stay locked to
their owner. A user's table queries only return their own rows if their session
carries their owner key: call `AssistantSecurity.applySessionKeys` from the
authentication customizer for **every** user, not only for admins. Without it,
non-admin users see an empty table.
The panel can link to the table using `conversationsTable` from `GET /config`.
That field is present only when the user may query the table.
## Starter prompts follow the user's access (Unreleased)
`GET /assistant/config` no longer returns the profile's starter prompts as written.
`profile.starterPrompts` (same field, still `string[]`) is now built per user:
- Up to three prompts are generated from the tables the signed-in user can read
through the assistant and that have rows (a count as the user), using the table
labels, a date field when present, and "What changed in ... this week?" when the
change-history tool is offered. The pick is stable for the user for the day and
rechecked on every request, including table, row and field access. Generation
uses bounded UI API calls and no model calls. Any failure yields no generated prompts.
- A configured starter prompt (Assistant Profile > Starter prompts, one per line) is
shown only when it declares the tables it needs and the user can read all of
them. Declare them with a leading tag: `[blog_post] Which posts are drafts?`, or
`[blog_post, page] ...` for several. `[none] What can you help with?` names no
table and is always shown. A line without a tag is no longer shown, because
nothing says which tables it mentions: add a tag to keep it.
- Shown prompts are the permitted configured ones first, then generated ones, three
in all (a profile with three or more permitted prompts shows only those).
No schema change and no configuration. The panel needs no change.
## Chat turns are not jobs (Unreleased)
Ordinary chat turns no longer create core tracking jobs, so they no longer appear
in the Jobs tray or the Jobs tables and no longer trigger a per-answer "finished"
notification. No schema change and no configuration is needed.
The `AssistantRunTracker` SPI and `JobTrackingRunTracker` are removed, along with
`AssistantQBitConfig.withRunTracker`, `setRunTracker` and `getRunTracker`. A host
that called any of them must delete the call: `withRunTracker(AssistantRunTracker.NOOP)`
is now the only behavior, and a custom tracker has no replacement hook. Turn
progress, Stop and recovery are available through the assistant's own routes
(`GET /turns/active`, the turn events stream and `POST /turns/{id}/cancel`).
Bulk loads the assistant launches are still durable jobs. Their confirmed tool
result now includes `jobHref` (the job's in-app link) and the confirm response's
`href` points at the job.
Scheduled task notifications now use core `QAlerts.notify`; they reach whatever
alerts sink the host installed (or core's logging sink when none is).
## Provider setup state (Unreleased)
Apply the **Provider setup state** block at the end of `docs/schema-postgres.sql`
**before** deploying: the provider table now reads `setup_state`, so the new jar
fails on `assistant_provider` without it. It adds a nullable `VARCHAR(16)` with
default `'ACTIVE'` and a check for `ACTIVE` / `SETTING_UP`; existing rows behave as
before. It is `IF NOT EXISTS` and safe to re-run.
To stop an unfinished provider from being routed to or health-checked, edit it in
Assistant > Assistant Provider (Provider section) and set **Setup state** to **Setting up** (SQL:
`UPDATE assistant_provider SET setup_state = 'SETTING_UP' WHERE id = <id>;`).
Enabled providers that are neither the default nor the only route for a configured
model now report DEGRADED, not DOWN, when unreachable. No configuration change.
## Fail soft (Unreleased, UI API extension assistant 1.5.0)
Apply the **Fail soft** block at the end of `docs/schema-postgres.sql` before
deploying. It creates `assistant_client_error` (errors the panel reports through
`POST /qqq/v1/assistant/client-errors`) with its indexes and `SET NULL` foreign keys
to the conversation, message and turn. It is `IF NOT EXISTS` and safe to re-run.
New configuration, all optional: `toolCallTimeoutSeconds` (default 60),
`clientErrorsPerUserPerMinute` (default 30) and `clientErrorWarnPerHour` (default
20, when the `assistant.client-errors` health check turns degraded). The new table
is in the Assistant app's nav for administrators.
## Transcript redaction (Unreleased)
Apply the transcript-redaction column widening block in `docs/schema-postgres.sql`
to existing databases. Stop reasons, request IDs, tool-call IDs, feedback/data-audit/
budget-override reasons and scheduled-run summaries/errors now use `TEXT`; token
expansion must not fail a write or truncate secured-data tokens. The corresponding
entity field limits are removed, including pending-action and tool-result summaries.
## Foreign keys (2026-10-14)
Recommended: apply the "Foreign keys" block at the end of
`docs/schema-postgres.sql` (after the index audit). It is guarded and safe to re-run.
- Adds the missing foreign keys across the conversation tables, usage, data audit,
budget accounting and profile skills, with the indexes they need.
- Detail rows of a conversation `CASCADE`; the message/turn cycle, usage and the data
audit use `SET NULL`, so usage keeps its cost rows when a conversation is deleted.
- Usage, budget accounting and the data audit hold their provider (`NO ACTION`):
disable a provider with history instead of deleting it.
- `assistant_model.provider_id` changes from `NO ACTION` to `CASCADE`
(constraint renamed `fk_assistant_model_provider`).
- Rows that point at a missing parent are first copied to `qqq_fk_quarantine`
(created if absent), then nulled, or deleted for the `NOT NULL` profile-skill
links. Check that table after the upgrade.
No code change.
## Index audit (2026-10-14)
Recommended, not required: apply the "Index audit" block at the end of
`docs/schema-postgres.sql`. It adds six indexes, every one `IF NOT EXISTS`:
- usage by turn (read on each scheduled run)
- the three retention date filters (turn events, tool calls, inactive conversations)
- the two `ON DELETE SET NULL` foreign keys on `assistant_scheduled_run`
No code change.
## Durable turns (Unreleased)
Apply the **Durable turns** block at the end of `docs/schema-postgres.sql` before
starting the new runtime. It adds nullable `heartbeat_at`, `lease_owner`,
`cancel_requested_at`, `phase`, `concurrency_mode` and the `(user_id, status)`
index. A partial unique index on terminal events also prevents two replicas from
inserting separate `message.end` frames for the same turn. Check for historical
duplicate terminal frames before applying it (resolve any duplicates through your
normal migration process). Fresh installs run the full schema, including ALTERs.
Existing rows with no heartbeat use `started_at` for lease expiry. During the
first upgrade from a runtime without heartbeats, drain old replicas' turns first:
a new replica cannot distinguish an old live turn from a dead owner after 45 s.
Defaults: heartbeat 5 s, lease 45 s, hard duration 300 s, force-finalization grace
20 s, shutdown grace 8 s. Configure these through the corresponding
`AssistantQBitConfig` withers. Heartbeat must be shorter than the lease. The JVM
shutdown hook is enabled by default; tests disable `registerShutdownHook` and
managed hosts may call `runtime.shutdown(Duration)` themselves. Give the process
termination budget enough time for the configured shutdown grace and persistence.
`POST /conversations/{id}/turns` accepts `concurrency: "supersede" | "concurrent"`
(case-insensitive). Default `SUPERSEDE` stops prior runs in that conversation;
`CONCURRENT` permits them. The default per-user cap is now 3; reaching it stops
the oldest run instead of returning the old in-progress 429. Per-minute and
budget 429 responses still apply. Superseded turns record `cancelReason=SUPERSEDED`.
Cancel now returns HTTP **200**, including for terminal turns, with the current
`status`. The OpenAPI fragment retains the old 202 response for mixed-version
rollouts. Active runs are discoverable through `GET /qqq/v1/assistant/turns/active`;
reattach through the existing events route. Consumers should recognize
`turn.phase` and `message.start.startedAt`. Starts during shutdown return
503 `SHUTTING_DOWN`; clients may retry against a ready replica.
`JobTrackingRunTracker` is now the default `AssistantRunTracker`, using core
`JobTracking` in QQQ **4.1.0-RC.8** to publish answers in the Jobs tray. The host
must provide the jobs tables (through qbit-jobs / core `DurableJobsMetaDataProvider`)
and run `DurableJobsRuntime`. Without that runtime, answers complete normally
and are simply not tracked.
Each job is owned by the assistant user, links to its `assistant_conversation`,
and reports thinking, tool and writing phases. Tool progress contains only the
tool label; terminal messages are fixed and never contain prompts or answers.
The tray's Stop cancels the assistant turn. All tracker callbacks remain isolated
from turn execution. Opt out with `withRunTracker(AssistantRunTracker.NOOP)`;
hosts may still supply their own `AssistantRunTracker`.
## Starter defaults, setup card and table help
The jar now ships `db/liquibase/qbit-assistant/assistant-defaults.changelog.sql`.
Include it from the host's master changelog **after** the changesets that create
the `assistant_*` tables:
```yaml
- include: { file: db/liquibase/qbit-assistant/assistant-defaults.changelog.sql }
```
It is insert-only and never changes a host that already configured the
assistant: the policy, the **$50/month instance budget** (warn 80%, downgrade
100%, stop 120%) and the Anthropic list prices are seeded only into empty tables,
and the `default` profile only when no default profile exists. Price rows take
precedence over `AssistantQBitConfig.withPricing`, which is why they are seeded
only when there are none. The four starter skills and a disabled example
scheduled task are always added (by name); on a host with its own default
profile the skills are not linked to it, and the Setup card says so. Requires
Liquibase 4.x or later (preconditions are `sql-check`s).
No DDL changes. New: the Assistant home's Setup card, the `assistant_data_access`
table action **Add rows for every table** (`assistantDataAccessAddDefaults`,
admin only) and bundle help on every assistant table (`help/qbit-assistant.yaml`).
## X1/Y1: channel wall and audit attribution
Requires QQQ 4.1.0-RC.6. Every assistant table, process, report and widget is
explicitly denied to channel `assistant`; startup rejects an exposed object.
Host customizations must preserve these denials. Administration uses `next-ui`.
Before enabling `AuditsMetaDataProvider.withClientChannelField(true)`, add the
nullable `audit.client_channel VARCHAR(100)` column using the host migration
conventions. kof22 needs a feature changeset under `changesets/features/` (for
example `2026-10-14-audit-client-channel.sql`); no host migration is applied here.
Existing audit rows remain null. The test fixture enables the field.
The existing `assistant_tool_call.auditHref` column now points to audit rows
filtered by record, table and client channel, including for deletes. It is empty
when the host has not enabled the audit channel field. Apply the X1/Y1 DDL in
`docs/schema-postgres.sql` to widen the existing `assistant_tool_call.audit_href`
column to TEXT: encoded filters with long table names or string record ids can
exceed the old 500-character limit. No new QBit column is required. `GET /assistant/access` accepts an optional owned `turnId` for attribution.
## Provider API extraction (#30)
**BREAKING:** every provider API type now lives in the exclusive package
`com.kingsrook.qbits.assistant.spi`. Update imports and recompile consumers.
Hosts still receive `qbit-assistant-provider-api` transitively from `qbit-assistant`.
No schema migration is needed.
| Old import | New import |
|---|---|
| `com.kingsrook.qbits.assistant.provider.X` (API types) | `com.kingsrook.qbits.assistant.spi.X` |
| `com.kingsrook.qbits.assistant.engine.ModelPricing` | `com.kingsrook.qbits.assistant.spi.ModelPricing` |
Internal classes such as `provider.RegistryModelProvider` and
`provider.fake.ScriptedModelProvider` stay in the runtime and are not API types.
External consumers such as qbit-marketing depend only on
`com.kingsrook.qbits:qbit-assistant-provider-api` for these contracts and allow
package `com.kingsrook.qbits.assistant.spi` in their guardrails. qbit-bom will add
this package to the G1 defaults.
External providers and applications using only provider/pricing types should
replace their `qbit-assistant` dependency with
`com.kingsrook.qbits:qbit-assistant-provider-api` at the same version. This removes
the assistant runtime and QQQ from that dependency path; only Jackson annotations
are required by the API. Anthropic, OpenAI, Bedrock and Vertex adapters use this
split; pgvector retains `qbit-assistant` for its runtime RAG interfaces.
Recompile custom provider types that override `validate(AssistantProvider)` and
change the signature to `validate(ProviderSettings)`. The settings carry the
configured kind, URL, API version, region, project, CA bundle name, timeouts and
`apiKeyEnvName`; the key and bearer-token supplier are not resolved for validation.
Providers using the default validator need only the import migration. Keep the runtime and
provider artifacts on the same version; old validator overrides must be rebuilt.
## P-g: scheduled tasks
Apply the **P-g** DDL in [docs/schema-postgres.sql](docs/schema-postgres.sql)
before deploying. The unique `(task_id, scheduled_for)` constraint is required
for multi-replica safety; retain run rows as schedule-slot claim history.
Existing conversations default to channel `panel`. Tasks start disabled.
Set `withScheduledTasksSchedulerName(...)` to an existing QQQ scheduler and
configure `withUiApiBaseUrl(...)` with this replica's own listener URL. The runner
polls every 60 seconds. QQQ must supply a scheduler session able to manage the
assistant tables; this QBit never creates that session. The configured backend
must enforce unique keys atomically (Postgres does).
Allow only designated credential references with
`withAllowedScheduledCredentialEnvNames(Set.of("ASSISTANT_SCHEDULED_TOKEN"))`.
Provide that variable through the host's secret manager. It must hold a token
accepted by the host's `manageSession` authentication provider for `runAsUserId`.
The runner fails closed on missing/disallowed credentials or an identity mismatch.
See [scheduled-task setup](README.md#scheduled-tasks) for kof22 provisioning.
QQQ RC.5 has no conditional UpdateInput. The runner uses a database-unique
schedule-slot insertion as its optimistic claim, then advances `nextRunAt`
before starting work. Crashes can skip a slot; they never retry it. No ArchUnit
exception or fabricated session is introduced. Core RC.6 enforces client-channel denials; scheduled requests carry
`assistant/scheduled/<taskId>`.
Notifications use the optional Alerts QBit notification SPI with a single-user
audience. Without it, delivery logs run id/status only. All output modes retain
the owner-locked audit row and conversation. HIPAA run summaries never copy the
answer; they contain only `see conversation`.
## P-f: knowledge retrieval
Apply the **P-f** assistant-table DDL in
[docs/schema-postgres.sql](docs/schema-postgres.sql) before deployment. It adds
`assistant_model.model_kind` (existing rows default to CHAT),
`embedding_dimensions`, and the admin-only `assistant_rag_source` table.
No sources are seeded. Existing chat selections remain unchanged; explicitly
EMBEDDING models are excluded from chat, classifier, fallback and downgrade
selection. Correct discovery's kind/dimensions if a model name is ambiguous.
For persistent vectors, add `qbit-assistant-rag-pgvector` at the same version as
the QBit, supply the host's JDBC DataSource/driver, and install pgvector in the
database. A DBA runs `CREATE EXTENSION IF NOT EXISTS vector`; the application
role needs schema creation rights for initial setup (or a DBA-created schema),
then normal DML rights. `ensureSchema()` checks for an existing extension first
and does not require extension installation privileges when it is already present.
The optional pgvector DDL is a **commented reference block**, separate from the
assistant-table migration. See [RAG setup](README.md#rag-setup).
Use a distinct validated schema for each QQQ instance. `vector(N)` fixes dimensions
for that store; changing dimensions requires a new schema and re-indexing.
Sources can use different models with that dimension; ranking is restricted to
sources using the query's model. If the weights behind an unchanged model id
change, clear and rebuild its sources. Editing source chunk/filter/model settings
or registry model identity/dimensions automatically clears affected vectors.
Every indexing batch produces EMBED usage and updates existing budget rollups.
PHI source content and PHI conversation queries require a HIPAA-eligible embedding
provider. No background runner or designated credentials are installed in P-f.
The default in-memory store is lost on restart and must be indexed again.
## P-e: profile routing and provider fallback
Apply the additive **P-e** ALTER TABLE statements at the end of
[docs/schema-postgres.sql](docs/schema-postgres.sql) before deploying. Existing
profiles default to OFF; optional model references and health columns start null.
No routing or fallback configuration is seeded. Existing turns retain unknown
routing/final-provider values; fallback_count defaults to zero.
Profiles now have a **Model routing** section. Select HEURISTIC or CLASSIFIER,
then optional simple/complex registry models and a classifier model. Empty route
slots retain provider/policy selection. CLASSIFIER with no usable classifier
uses HEURISTIC. OFF preserves prior provider selection. With no effective profile,
code-configured escalation supplies the complex model/effort when enabled.
Set a model's **Fallback model** to opt into a cross-provider chain. Saves reject
cycles, more than four edges, or disabled/missing targets. Provider health is
read-only; NETWORK/TIMEOUT opens a 60-second circuit, and successful calls close
it. Local endpoints with stale health get a bounded two-second probe. Automatic
fallback without an explicit link is confined to local models of the same adapter
family. PHI/HIPAA eligibility and budgets apply to every destination.
Classifier usage is recorded with purpose ROUTE and included in budgets. Reports
that filter usage purpose should include ROUTE. Turn screens show route decision,
source, fallback count and final provider; the home shows health and fallback totals.
All configuration remains assistant.admin-only, with AI exposure NONE.
## P-d: data-driven budgets
Requires QQQ 4.1.0-RC.5 for the core Alerts API and alert type metadata.
Create `assistant_budget`, `assistant_budget_override`, `assistant_budget_event`
and `assistant_budget_usage`
using the P-d DDL in [schema-postgres.sql](docs/schema-postgres.sql) before deploying.
The event unique key is required: it deduplicates each state per budget, scope and
UTC period across replicas. Do not remove it or truncate events during a period.
Budgets appear in Assistant navigation. Overrides and immutable events are child
lists. All four tables require `assistant.admin` and have AI exposure NONE.
Overrides record the actual session grantor and require a reason and expiry.
The standard host QQQ audit configuration applies to budget and override edits.
`assistant_budget_usage` is a private accounting projection populated from persisted
usage writes. It copies only counters, time, user, provider and model, with no owner
lock, so shared INSTANCE/PROVIDER/MODEL sums can run under the caller's own session.
The source usage table retains its owner locks. The projection is read-only, has no
nav entry and no AI exposure. **Pause usage writes during migration and run the DDL's
backfill before restarting**; otherwise shared totals would omit pre-upgrade usage.
Keep accounting rows through their budget periods even if detail rows are removed.
ROLE budgets give each permission holder a separate allowance, checked with their
current session. Override scopeValue, when set, narrows the allowance to one user id.
Existing policy/config budgets and provider monthly caps remain additional limits;
an override of a budget row does not waive those existing caps.
Provider and MODEL scope values are numeric provider/model registry row ids;
USER is a user id and ROLE is a permission name. INSTANCE has no scope value.
Periods reset at midnight UTC or the first of the month UTC. Token usage includes
input, output, cache-read and cache-write tokens; USD uses the recorded micro-dollar
cost, not today's model price. No default budget rows are seeded.
## P-c: classification, PHI routing and encrypted storage
Apply these additive schema changes before deploying P-c (adjust column mappings
for your backend). New conversations/turns record `PUBLIC`, `CONFIDENTIAL`, `PII`
or `PHI`; legacy null classifications read as PUBLIC. Backfill known historical
PHI conversations to PHI before allowing their history to be used with a provider.
```sql
ALTER TABLE assistant_conversation ADD COLUMN max_classification VARCHAR(16);
ALTER TABLE assistant_turn ADD COLUMN classification VARCHAR(16);
ALTER TABLE assistant_turn ADD COLUMN classification_tables VARCHAR(500);
ALTER TABLE assistant_conversation ALTER COLUMN title TYPE TEXT;
ALTER TABLE assistant_tool_call ALTER COLUMN result_summary TYPE TEXT;
ALTER TABLE assistant_pending_action ALTER COLUMN summary TYPE TEXT;
ALTER TABLE assistant_pending_action ALTER COLUMN result_summary TYPE TEXT;
CREATE TABLE assistant_data_audit
(
id SERIAL PRIMARY KEY,
user_id VARCHAR(255),
conversation_id INTEGER,
turn_id INTEGER,
provider_id INTEGER,
provider_name VARCHAR(200),
model VARCHAR(200),
hipaa_eligible BOOLEAN,
classification VARCHAR(16),
tables VARCHAR(500),
decision VARCHAR(16),
reason VARCHAR(1000),
create_date TIMESTAMP WITH TIME ZONE
);
CREATE INDEX assistant_data_audit_user_id ON assistant_data_audit (user_id);
CREATE INDEX assistant_data_audit_conversation_id ON assistant_data_audit (conversation_id);
CREATE INDEX assistant_data_audit_turn_id ON assistant_data_audit (turn_id);
```
`assistant_data_audit` is an admin read-only, owner-locked child list on a
conversation, outside navigation. It records each model call carrying PII/PHI and
each routing block, without record values. It expires with the conversation.
PHI conversations use `hipaaRetentionDays` (2190 by default), even outside HIPAA
mode; null uses normal retention. Existing tool-result/replay cleanup still uses
`toolResultRetentionDays`.
Before enabling HIPAA mode, mark the permitted provider rows `hipaaEligible = true`
and provision `conversationEncryptionKeyEnvName` with a base64-encoded 32-byte
key. Startup refuses missing or invalid keys. A code-only provider is not eligible.
The flag records the operator's eligibility decision; it does not establish a BAA.
New/updated message parts, titles, conversation context, tool payloads/redactions
and summaries, replay data, and pending-action payloads/summaries use
`enc:v1:<base64(iv|ciphertext|tag)>` (AES-256-GCM, random 12-byte IV, 16-byte tag).
Only `AssistantStore` decrypts; direct backend/admin record views show ciphertext.
String columns holding encrypted summaries/titles must be TEXT to allow expansion.
Legacy plaintext reads are supported; existing rows are not automatically rewritten.
For an existing deployment, migrate legacy payloads through AssistantStore under
the appropriate owner/admin session before treating the whole archive as encrypted.
Keep the key available for the lifetime of encrypted data and backups. Changing the
key requires a controlled decrypt/re-encrypt migration; P-c has no key rotation job.
Corrupt ciphertext, a wrong key, and encrypted data without a key fail closed.
## 0.1.0-RC.3 to 0.1.0-RC.4: provider settings, prices, per-call usage, app home
Three new tables. Create them before you deploy (see `docs/schema-postgres.sql`, which also seeds the Anthropic prices):
- `assistant_provider` (with the `api_key_status` column: it is filled on read and never written, but the backend selects it)
- `assistant_model_price`
- `assistant_usage`
Then, to keep answering exactly as before:
1. Register the provider type: `config.withProviderType(new AnthropicProviderType())`. Keep `withModelProvider(AnthropicModelProvider.fromEnvironment())` as the fallback.
2. Add one provider row (or seed it in your migration): type `anthropic`, model = your current main model, enabled, default, `apiKeyEnvName` = `ANTHROPIC_API_KEY` (or leave it empty for the default). No key value is ever written.
3. Run **Test connection** on it.
Until a row exists, the provider registered in code answers, as in RC.3. Other changes: the app label is **Assistant**; the nav no longer lists the retention process (it is a table action on conversations) or the audit tables (they are child lists). Grants on `assistant.admin` cover everything new.
## 0.1.0-RC.1 to 0.1.0-RC.3: snake_case names
The rename shipped in 0.1.0-RC.3. 0.1.0-RC.2 on Maven Central is an accidental republish of the RC.1 code (see CHANGELOG.md), so upgrading from RC.2 is the same as upgrading from RC.1.
RC.3 applies the QRun-IO naming rule (owner decision, 2026-10-12): *"table names should always be lowercase and use _, and qbits should always be prefixed with their name"*. The QQQ table names, the owner security key type and the possible-value source change. Permissions do not. **This is a metadata-only change**: no table, column or index in your database changes, and no Liquibase or DDL step is needed.
### What changed
| Kind | RC.1 (and RC.2) name | RC.3 name |
|---|---|---|
| Table | `assistantConversation` | `assistant_conversation` |
| Table | `assistantMessage` | `assistant_message` |
| Table | `assistantTurn` | `assistant_turn` |
| Table | `assistantTurnEvent` | `assistant_turn_event` |
| Table | `assistantToolCall` | `assistant_tool_call` |
| Table | `assistantFeedback` | `assistant_feedback` |
| Table | `assistantPolicy` | `assistant_policy` |
| Table | `assistantUsageDaily` | `assistant_usage_daily` |
| Possible-value source (conversations) | `assistantConversation` | `assistant_conversation` |
| Permissions | `assistant.hasAccess`, `assistant.admin` | unchanged (QQQ's standard `<base>.hasAccess` suffix; the `assistant` base already follows the rule) |
| Security key type | `assistantUserId` | `assistant_user_id` |
| All-access key | `assistantUserIdAllAccess` | `assistant_user_id_all_access` |
Unchanged: Java class names, field names (`userId`, `conversationId`, ...), the `assistantAdmin` app, the `assistantRetention` process, the `/qqq/v1/assistant` routes and the `assistant` supplemental instance meta-data.
The old names stay available as `@Deprecated` constants for one release, so migration code can refer to them: `<Entity>.LEGACY_TABLE_NAME`, `AssistantQBitConfig.LEGACY_DEFAULT_OWNER_KEY_TYPE` and `AssistantMetaDataBuilder.LEGACY_ALL_ACCESS_SUFFIX`. They will be removed in 0.2.0.
### Physical tables
`docs/schema-postgres.sql` already creates `assistant_*` tables. If your RDBMS backend mapped the old QQQ names to those tables (for example with `inferBackendName`), the new QQQ names now equal the physical names and nothing else is needed; the mapping becomes a no-op. If you created tables literally named `assistantConversation` and so on, either rename them:
```sql
ALTER TABLE "assistantConversation" RENAME TO assistant_conversation;
ALTER TABLE "assistantMessage" RENAME TO assistant_message;
ALTER TABLE "assistantTurn" RENAME TO assistant_turn;
ALTER TABLE "assistantTurnEvent" RENAME TO assistant_turn_event;
ALTER TABLE "assistantToolCall" RENAME TO assistant_tool_call;
ALTER TABLE "assistantFeedback" RENAME TO assistant_feedback;
ALTER TABLE "assistantPolicy" RENAME TO assistant_policy;
ALTER TABLE "assistantUsageDaily" RENAME TO assistant_usage_daily;
```
or keep them and set `RDBMSTableBackendDetails.tableName` to the old name in your table customizer.
### Stored references to update
QQQ stores table names, permission names and key names outside this qbit. Update them in the same deployment. The examples are for Postgres with QQQ's default snake_case backend names; adjust the table and column names to your schema.
1. **Security keys.** Permission grants (`assistant.hasAccess`, `assistant.admin`) need no change. If your authentication customizer grants the all-access key itself rather than calling `AssistantSecurity.applySessionKeys`, grant `assistant_user_id_all_access`. If you set `ownerSecurityKeyType` explicitly, the all-access key is now `<keyType>_all_access`.
2. **Saved views and saved reports** on the assistant tables:
```sql
UPDATE saved_view SET table_name = CASE table_name
WHEN 'assistantConversation' THEN 'assistant_conversation'
WHEN 'assistantMessage' THEN 'assistant_message'
WHEN 'assistantTurn' THEN 'assistant_turn'
WHEN 'assistantTurnEvent' THEN 'assistant_turn_event'
WHEN 'assistantToolCall' THEN 'assistant_tool_call'
WHEN 'assistantFeedback' THEN 'assistant_feedback'
WHEN 'assistantPolicy' THEN 'assistant_policy'
WHEN 'assistantUsageDaily' THEN 'assistant_usage_daily'
ELSE table_name END
WHERE table_name IN ('assistantConversation', 'assistantMessage', 'assistantTurn', 'assistantTurnEvent',
'assistantToolCall', 'assistantFeedback', 'assistantPolicy', 'assistantUsageDaily');
```
Run the same statement against `saved_report` (and `quick_saved_view` / `shared_saved_view` only if they store a table name in your schema).
3. **Audit table names.** QQQ audits reference tables through the `audit_table` lookup. Rename the rows so old and new audits stay under one table:
```sql
UPDATE audit_table SET name = CASE name
WHEN 'assistantConversation' THEN 'assistant_conversation'
WHEN 'assistantMessage' THEN 'assistant_message'
WHEN 'assistantTurn' THEN 'assistant_turn'
WHEN 'assistantTurnEvent' THEN 'assistant_turn_event'
WHEN 'assistantToolCall' THEN 'assistant_tool_call'
WHEN 'assistantFeedback' THEN 'assistant_feedback'
WHEN 'assistantPolicy' THEN 'assistant_policy'
WHEN 'assistantUsageDaily' THEN 'assistant_usage_daily'
ELSE name END
WHERE name IN ('assistantConversation', 'assistantMessage', 'assistantTurn', 'assistantTurnEvent',
'assistantToolCall', 'assistantFeedback', 'assistantPolicy', 'assistantUsageDaily');
```
If a new-name row was already created (the app ran RC.3 before this update), repoint `audit.audit_table_id` from the old row to the new one first, then delete the old row.
4. **Anything else that stores a table name**: frontend links or bookmarks to `/app/.../assistantConversation`, Quick Search index rows (`quick_search_index.table_name`; reindex instead of editing), ESB topics or reports you configured by table name.
### Order
Deploy RC.3 and run the statements above in the same maintenance window. Until the update runs, saved views and reports on the assistant tables do not open, and admins who get the all-access key from their own customizer only see their own rows.
## 0.1.0-RC.6: tools through the UI API
Database (see `docs/schema-postgres.sql`):
- new tables `assistant_data_access`, `assistant_pending_action`, `assistant_profile`, `assistant_skill` and `assistant_profile_skill`;
- new columns `assistant_tool_call.ui_api_operation`, `http_status`, `audit_href`, `pending_action_id`;
- new columns `assistant_policy.writes_require_confirmation`, `allowed_tools`, `max_rows_per_call`, `redact_pii`.
Runtime: the tools call this server's own `/qqq/v1` on `127.0.0.1` and the port the first assistant request arrived on. If the server is not reachable that way (a different bind address, a TLS-only listener), set `withUiApiBaseUrl(...)`. Grant `assistant.admin` holders the new tables through the qbit's existing admin permission (no new permission names).
Behavior: nothing is writable until an admin adds a READ_WRITE data access row; before that the assistant reads exactly what it did before, now through the UI API.
## P-a: provider and model registry
Before starting the new version, database-backed hosts add the provider columns
and model sub-table below. Memory-backed hosts need no migration. Existing rows
derive kind and location from the adapter and keep their model behavior until
models are registered. Keys remain environment-variable references.
```sql
ALTER TABLE assistant_provider
ADD COLUMN kind VARCHAR(30),
ADD COLUMN tls_ca_bundle VARCHAR(100),
ADD COLUMN location VARCHAR(10),
ADD COLUMN hipaa_eligible BOOLEAN DEFAULT FALSE,
ADD COLUMN region VARCHAR(50),
ADD COLUMN api_version VARCHAR(30),
ADD COLUMN connect_timeout_seconds INTEGER DEFAULT 10 CHECK (connect_timeout_seconds BETWEEN 1 AND 600),
ADD COLUMN request_timeout_seconds INTEGER DEFAULT 120 CHECK (request_timeout_seconds BETWEEN 1 AND 600),
ADD COLUMN last_discovery_at TIMESTAMP WITH TIME ZONE,
ADD COLUMN last_discovery_status VARCHAR(16),
ADD COLUMN last_discovery_error VARCHAR(500);
ALTER TABLE assistant_provider ALTER COLUMN model TYPE VARCHAR(200);
ALTER TABLE assistant_provider ALTER COLUMN last_test_model TYPE VARCHAR(200);
ALTER TABLE assistant_usage ALTER COLUMN model TYPE VARCHAR(200);
ALTER TABLE assistant_message ALTER COLUMN model TYPE VARCHAR(200);
ALTER TABLE assistant_turn ALTER COLUMN model TYPE VARCHAR(200);
CREATE TABLE assistant_model
(
id SERIAL PRIMARY KEY,
provider_id INTEGER NOT NULL REFERENCES assistant_provider(id),
model_id VARCHAR(200) NOT NULL,
display_name VARCHAR(200),
enabled BOOLEAN,
supports_tools BOOLEAN,
supports_vision BOOLEAN,
supports_streaming BOOLEAN,
supports_reasoning_effort BOOLEAN,
context_window_tokens INTEGER,
max_output_tokens INTEGER,
input_usd_per_m_tok NUMERIC(12, 4),
output_usd_per_m_tok NUMERIC(12, 4),
source VARCHAR(16),
discovered_at TIMESTAMP WITH TIME ZONE,
last_test_at TIMESTAMP WITH TIME ZONE,
last_test_status VARCHAR(16),
last_test_latency_ms INTEGER,
last_test_error VARCHAR(500),
create_date TIMESTAMP WITH TIME ZONE,
modify_date TIMESTAMP WITH TIME ZONE,
UNIQUE (provider_id, model_id)
);
```
Register the optional `qbit-assistant-provider-openai` module and
`OpenAiCompatibleProviderType` to offer its kinds. Azure and generic endpoints
require an explicit `allowedBaseUrls` entry; local defaults may be keyless.
Discover models preserves existing rows. Cloud catalogs larger than 20 are added
disabled; enable the models to offer before leaving the provider's model empty.
HIPAA eligibility records an administrator's fact; P-a does not implement PHI routing.
Custom CA bundles are server configuration, never database content. Register them
with `withTrustedCaBundle(name, pem)` or `withTrustedCaBundleFile(name, path)` at
startup, then set the HTTPS provider row's `tlsCaBundle` to that name. Empty
references retain JVM default trust. The bundle adds trust without replacing JVM
roots; hostname verification remains enabled. If the P-a columns were already
applied, add only `tls_ca_bundle VARCHAR(100)` to `assistant_provider`.
## P-b: AWS Bedrock and Google Vertex
Before starting this version, database-backed hosts that already applied P-a add:
```sql
ALTER TABLE assistant_provider ADD COLUMN cloud_project VARCHAR(100);
```
Fresh installations use `docs/schema-postgres.sql`; memory-backed hosts need no
migration. Existing providers leave the new column null. Vertex requires the GCP
project id in `cloudProject`; Bedrock and Vertex both require `region` and reject
`apiKeyEnvName`. They default to CLOUD; `hipaaEligible` remains a manual admin fact.
Add the optional `qbit-assistant-provider-bedrock` and/or
`qbit-assistant-provider-vertex` module at the QBit version and register
`BedrockProviderType` / `VertexProviderType`. Credentials come from the server's
AWS default chain / Google Application Default Credentials. No credentials are
stored in provider rows. Hosts using AWS IRSA or web identity supply
`software.amazon.awssdk:sts` on their own runtime classpath (matching the
transitive AWS SDK version); this QBit does not add it. See README Providers for
configuration, endpoint overrides and static discovery limitations.