43 Commits
Author SHA1 Message Date
james.maesandClaude Fable 5.2 a9ebe64883 fix(process): only a reviewed process runs; a person confirms what they were shown; an independent review of the process tools closed
maven / build (push) Successful in 7m48s
build / maven (push) Successful in 7m48s
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
james.maesandClaude Fable 5.2 7b4a2b9353 feat(process): the assistant runs a process the signed-in person may run, confirmed on a card; fix: four review findings
maven / build (push) Failing after 12m50s
build / maven (push) Failing after 12m50s
Process tools (package A4 of docs/actions-and-mcp.md), in tools/process:
list_processes, describe_process, run_process and get_run_status.

- The list and the description come from the person's own metadata, asked
  as them: a process they may not run is never listed or described, and is
  refused by name in the words used for a process that does not exist.
- run_process only proposes. Inputs are checked against the fields the
  process's screens declare before a card exists (unknown input, wrong
  type, missing required; no value is made up). The card (pending action
  kind process_run) shows the process, every input, the records and the
  steps that stop for the person.
- The confirmed run is the person's: it runs through the UI API with the
  confirming request's session, and a run whose records are no longer the
  ones the card counted does not start.
- A process eligible to run as a durable job is created as one and never
  started by the assistant; any other runs through its own init and step
  operations until it ends or needs the person (a review, a required field
  with no value), where going on takes a new card.
- A confirmed run never holds its pending action RUNNING: the action ends
  when the run was started, and get_run_status reports where it stands.
- What is never runnable through the assistant is decided in one place
  (ProcessRunnability): denied to the assistant channel, the assistant's
  own, data access not READ_WRITE, hidden, offered by no screen, on a table
  the assistant may not change, or with a screen it cannot present.

Review findings, by the guarantee each gives:
- A host notices each process that becomes runnable through the assistant:
  RunnableProcessReview fails the host's build when the runnable list
  differs from its committed file.
- An administrator is warned before the data-access migration opens a
  table they left read-only: UPGRADE.md has a query with the migration's
  own predicate, tested against the changeset.
- No tool says confirmation can be turned off (the cached request prefix
  changes once).
- No model is called without a spending limit in force: the routing
  classifier asks for its own model. The administrator's Test connection
  is the one documented exemption.

Also: a scheduled task is offered run_process only with its write
approval; a test that a confirmed update into a record-security key the
person lacks is refused by the application and changes nothing; UI API
extension assistant 1.10.0 (additive).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 15:04:09 -06:00
james.maesandClaude Fable 5.2 cc5fe2a700 feat(access): writes follow the signed-in person; no answer without a spending limit; fix: a fourth review of the tool boundary
maven / build (push) Successful in 7m33s
build / maven (push) Successful in 7m33s
Package A2 of the actions design (docs/actions-and-mcp.md, sections 2 and 3),
with two owner decisions of 2026-11-12, and five findings of an independent
review of the tool boundary, each closed by the guarantee it gives.

Writes follow the person
- The data access of a table or process is declared in versioned code:
  AssistantQBitConfig.withDefaultTableAccess / withDefaultProcessAccess (both
  READ_WRITE) and a pin per object (withTableAccess, withProcessAccess).
  READ_WRITE grants nothing: the assistant may propose what the signed-in
  person may do themselves, and every change waits for their confirmation.
- An administrator's assistant_data_access row can only narrow (READ, NONE).
  It never raises a code default, a pin or a table kept from AI.
- Every assistant_* table and every process of the QBit is NONE by a pin in
  code that no row and no host pin lifts.
- The write tools are offered to a person who may create, edit or delete
  records of some table (asked of the UI API as that person, once per offer,
  within the tool-call time limit; no answer means no write tool). GET /config
  features.writes is that answer for the person.
- Settings that cannot be read, and were never read, allow nothing.
- The table action "Add rows for every table" is retired. The changeset
  assistant-data-access-follow-user removes the unedited READ rows it wrote,
  and no other row.

No answer without a spending limit
- A person for whom no spending limit is in force (BudgetService.limitInForce)
  is refused: the turn before anything is stored, every model call, every tool
  call, every embedding request, a scheduled turn. Restoring a limit restores
  service on the next request.

Review findings
- A pending action is never marked expired over its outcome: expiry is a
  decision made through the same claim as confirming and cancelling.
- No action stays RUNNING for ever: retention ends one left running for more
  than an hour as FAILED ("outcome unknown, check the record"), with a log row.
- A person is the session the authentication module resolved, not the object
  passed in: a hand-made permission gives nothing, a revoked one is gone.
- The assistant's own reads get their context from the tool-call service, for
  a person it checked; there is no public way to make a tool context from a
  session id, a gateway and a policy.
- The published test-jar holds the testing package only.
- AssistantStore's decision methods are final.

See CHANGELOG.md and UPGRADE.md (what a host must apply before deploying).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 13:11:20 -06:00
james.maesandClaude Fable 5.2 0dc325cd2c fix(tools): one decision per pending action, a tool surface closed at compile time; feat: pending actions with before and after
maven / build (push) Successful in 6m19s
build / maven (push) Successful in 6m19s
Closes the findings of a third independent review of the tool boundary and
builds the pending-action package of the actions design on top.

Guarantees:
- A pending action is confirmed or cancelled once, by exactly one request,
  on one server or several: the decision is an insert under a unique key
  (new table assistant_pending_action_claim). Whatever fails after the claim
  ends in FAILED or EXECUTED with a tool-call row, never in RUNNING.
- A tool cannot be run around the tool-call service from code outside the
  assistant's packages: execute, log and withdraw are package-private; a
  tool call request, a confirmed tool call and a tool context have no public
  constructor and are made only with the service's permit; the runtime has
  no public getter for the tool registry, the UI API gateway or its tool
  families. A host's own ToolFamily and AssistantTool compile unchanged.
- A signed-in person is one person: a session with no HTTP request is made a
  person only when the authentication module resolves the session's own id
  to the user the session names. The turn engine no longer makes a person.
- Only the credential a request's session was resolved from is forwarded to
  the UI API.
- A tool call that was cut short leaves no proposal anybody could confirm.
- The policy a tool call runs under is read fresh on every call.
- The hard budget is read on every call with one aggregate query per budget.
- The input kept in the tool-call log is redacted with the administrator's
  custom patterns too.

Pending actions (design package A3):
- update_record and delete_record read the record as the person when they
  propose; the card shows each field before and after (a delete: what the
  record holds), after the exposure rules of get_record.
- What was approved is what is written: the proposal keeps a version of the
  record (a digest, never values); a confirmed update or delete reads the
  record again and refuses when it changed, is gone or cannot be read.
- actionKind (record_create, record_update, record_delete, or the proposing
  tool's name), GET /assistant/actions (the person's own list), and
  approvalRetentionDays is now read by the retention process.
- UI API extension assistant 1.9.0 (additive).

Schema: three columns and two indexes on assistant_pending_action, and the
table assistant_pending_action_claim (docs/schema-postgres.sql, UPGRADE.md).

ChatToolSetTest and ToolCallChecksTest pass unchanged.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 11:35:43 -06:00
james.maesandClaude Fable 5.2 7c9176fcc8 fix(tools): the tool-call service owns the policy, the identity and the confirmation
maven / build (push) Successful in 6m51s
build / maven (push) Successful in 6m51s
An independent security review of engine.ToolCallService (after PR 106)
found what a caller could still hand it. The chat's turn engine is the
only caller today and builds everything correctly, so none of it was
reachable; it is closed before an MCP endpoint calls the service. The
rule: every tool call runs as the signed-in person with their current
permissions and budget, never as the assistant; a refusal happens before
any data action; every call that ran has a log row with redacted input.

Numbered as in the review:

 1. A caller cannot supply a policy. The service reads it
    (PolicyService.current(false).tools(), narrowed for a scheduled
    task's conversation) and builds the tool Context itself; a caller
    hands it a tools.registry.ToolCall (person, id, name, input).
    Every change always shows a confirmation card (owner, 2026-11-12):
    ToolPolicy.writesRequireConfirmation is removed and UiApiToolbox
    always proposes. The assistant_policy column stays and is no longer
    read; no migration.
 2. New SignedInPerson binds a session to the credentials forwarded to
    the UI API. No public constructor: ofRequest(Context), only for
    AssistantSpecs (ArchUnit), or ofSession(session). The context's
    gateway is always runtime.getUiApiGateway(). AssistantApi and
    TurnEngine lose withCredentials.
 3. The hard budget is read on every call, not once per scope.
    executeAndLog(call, conversationId, hipaaRoute) makes the scope.
 4. confirm(person, pendingActionId) replaces executeConfirmed,
    confirmationRefusal and logConfirmed: the service loads the stored
    action of that person, checks it, claims it once, runs it, marks it
    and writes the row. AssistantApi.confirmAction is a thin caller and
    the only one (ArchUnit).
 5. The toolboxes cannot be called around the service: their entry
    points are package-private, the runtime has no getter for a toolbox
    (each family is handed its own), and ArchUnit forbids calls into
    tools.. from outside it except through the registry. The assistant's
    own reads (starter prompts, knowledge re-read) use the new read-only
    UiApiToolbox.read.
 6. TurnEngine.executeTools writes each started call's row in a per-call
    finally: after an interruption of the engine, after a runtime
    exception while a result is processed or logged, and for a call cut
    short by its time limit, with the real outcome when the worker
    finished. A row that cannot be written for a call that changed data
    is logged at ERROR.
 7. Whatever fails after a confirmation's claim ends in FAILED with a
    row, never in RUNNING.
 8. The service checks that the scope's conversation and turn belong to
    the call's person and works out the row's client channel itself
    (ToolCallScope.clientChannel is gone). hipaaRoute stays the
    caller's; the javadoc says why.
 9. AssistantQBitConfig.withToolFamily throws once the QBit is produced.

Tests: ToolCallServiceTest (one per item, on the service directly),
ToolCallRowsTest (item 6; all three fail on develop), and
ToolCallArchitectureTest (items 2, 4, 5). ChatToolSetTest and
ToolCallChecksTest pass with unchanged expectations.
UiApiToolsSecurityTest's two tests that ran writes with confirmation off
now propose and confirm.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 10:37:13 -06:00
james.maesandCodex 3637ca3ad0 fix: integrate conversation management with current develop
Preserve current attachment, notification and starter prompt behavior. Encrypt user titles that resemble ciphertext unless they match the same stored record; verify all eight reactor modules.

Co-Authored-By: Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-19 09:52:41 -05:00
james.maesandCodex a353381d02 Merge develop into feature/starter-prompts-from-access
Co-Authored-By: Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-19 09:20:00 -05:00
james.maesandCodex b250585dde fix(assistant): recheck starter prompt access on every request
Cap combined prompts at three and remove cached authorization-sensitive suggestions. Preserve bounded deterministic generation and cover field, row, table, policy and history changes. Thirty focused tests passed.

Co-Authored-By: Codex <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-19 08:50:26 -05:00
james.maesandClaude Opus 5.5 589d8f5c1c docs: changelog and upgrade notes for conversation management
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:46:06 -05:00
james.maesandClaude Opus 5.5 e4f369a4e7 feat(config): build starter prompts per user from what they can access
GET /assistant/config returned the profile's starter prompts as static
text, so a user could be offered examples about tables they cannot see.
profile.starterPrompts (same field and shape) is now built per user:
up to three prompts generated from the tables the user can read through
the assistant that have rows (list_tables, count_records and
describe_table via the UI API with the user's own session), cached five
minutes, stable for the day, bounded and fail-soft. Configured prompts
stay only when tagged with tables the user can read.

Part of KofTwentyTwo/Website-Backend#303

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:34:35 -05:00
james.maesandClaude Opus 5.5 fc5026594d docs: changelog and upgrade notes for turns that are not jobs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 15:29:54 -05:00
james.maesandClaude Opus 5.5 cfad90eaee fix(assistant): optional unreachable providers degrade health; add provider setup state
An enabled provider that is not the default and not the only route for any
configured routing (enabled profile simple/complex model, budget downgrade,
knowledge source embedding, with no fallback on another usable provider) is
optional. When it is unreachable its provider and model checks now report
DEGRADED with a message naming where requests go, instead of turning the
whole "assistant" component DOWN (central health rolls up worst-of; WARNING
severity does not cap DOWN). The default provider and sole routes stay DOWN.
Failed provider probes carry reachable=false so the circuit breaker still
refuses the provider when its central outcome is only DEGRADED.

New assistant_provider.setup_state (Active / Setting up, empty = Active):
a provider that is Setting up is never routed to (default, profile models,
fallbacks, budget downgrades, embeddings) and gets no provider or model
health checks. Additive guarded DDL in docs/schema-postgres.sql; see
UPGRADE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:43:58 -05:00
james.maesandClaude Opus 5.5 38ac805fe7 feat(assistant): POST /assistant/client-errors with the assistant_client_error table and health check
UI API extension assistant 1.5.0 adds POST /qqq/v1/assistant/client-errors
(AssistantClientErrorReportV1 in, AssistantClientErrorResponseV1 out, 202).
Any signed-in user may report; scope and message are required (400
otherwise, also for a non-object body). Every field is bounded, message and
stacks are scrubbed (AiRedactor secrets, e-mails, digit runs over 6, mixed
tokens, query strings), the route keeps only its path, and conversation /
message / turn ids are kept only when they are the user's own. Over
clientErrorsPerUserPerMinute (default 30) the report is acknowledged with
accepted:false and dropped, never a 429.

Rows go to the new owner-locked assistant_client_error table (Assistant app
nav, record screens, bundle help, channel wall by pattern, SET NULL foreign
keys in the reference DDL) and are deleted by retention after
toolResultRetentionDays. The assistant.client-errors health check is
DEGRADED at clientErrorWarnPerHour (default 20) reports in the last hour, so
the Assistant health card shows it too. The deprecated AssistantErrorV1
description again says "since 1.3.0".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:38:12 -05:00
james.maesandClaude Opus 5.5 385b61019b docs: Jobs tray tracking (QQQ RC.8) in CHANGELOG and UPGRADE
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:20:46 -05:00
james.maesandClaude Opus 5.5 15938e07e1 Merge develop (durable turns, defaults, indexes, FKs) into feature/transcript-redaction
- Turn start keeps develop's supersede/concurrency, run tracker,
  register and forceFinalize, and attaches the TranscriptRedactor.
- The shared finalization flushes redaction after the final events and
  before the terminal turn update on every path (supersede, timeout,
  restart recovery, forceFinalize); the redactor always closes.
- Token-bearing columns widened to TEXT (H2-safe upgrade statements),
  entity length limits removed so tokens never truncate.
- Event reader race that could omit terminal events fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:59:03 -05:00
james.maesandClaude Opus 5.5 80d529e741 Merge feature/schema-indexes (with origin/develop) into feature/foreign-keys
Conflicts in CHANGELOG.md and qbit-assistant/pom.xml: kept develop's
durable-turns and starter-defaults entries and its H2/Liquibase test
dependencies alongside the Testcontainers/Postgres ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:55:19 -05:00
james.maesandClaude Opus 5.5 b50bc4a5c7 Merge origin/develop into feature/schema-indexes
Conflicts in UPGRADE.md and docs/schema-postgres.sql: kept develop's
durable-turns and starter-defaults notes and DDL; the index audit block
now follows the durable-turns block at the end of the schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:51:27 -05:00
james.maes 3d288ddfe9 Merge remote-tracking branch 'origin/develop' into feature/durable-turns
# Conflicts:
#	CHANGELOG.md
#	UPGRADE.md
2026-10-14 16:59:47 -05:00
james.maesandClaude Opus 5.5 ba8e7a6562 fix(defaults): portable preconditions; never change an existing host on deploy
Review of #48 found:
- the formatted-SQL table-exists precondition does not parse in Liquibase 5
  (and is unsupported in 4.19), failing the host's whole migration: use
  sql-checks only;
- the budget and prices were seeded beside a host's own budgets and over its
  configured pricing: seed the policy, budget and prices only into empty
  tables, the profile only when no default profile exists;
- the Setup card ignored registry and configured prices: price the default
  model exactly as calls are costed;
- "Add rows for every table" could read a stale data-access cache.
The changelog test now runs through Liquibase 4.33 (kof22's version):
fresh schema, re-run, an already configured host, and a missing table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:54:36 -05:00
james.maesandClaude Opus 5.5 c751fc055c docs: durable turns upgrade notes and changelog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:36:43 -05:00
james.maesandClaude Opus 5.5 a5c38d74a8 feat: starter defaults, setup card and table help so no config table starts empty
Owner requirement 2026-10-14: every assistant table should be populated and
visibly used, and an empty table should say how to set it up.

- db/liquibase/qbit-assistant/assistant-defaults.changelog.sql (host-included,
  insert-only, self-guarding): default policy, default profile, four starter
  skills linked to it, a $50/month instance budget (warn 80, downgrade 100,
  stop 120), current Anthropic prices, a disabled example scheduled task.
- Assistant home Setup card: each config table, its state and the next step.
- assistant_data_access action "Add rows for every table" writes the default
  access out as editable rows.
- help/qbit-assistant.yaml (help-bundle format) on the app and every table,
  saying what it is for and how rows get created (STAND-IN(core=#1198)).
- Tests: guardrail (every table used at runtime, seeded or explained, with
  empty-state help); changelog seeds a fresh H2 schema insert-only; seeded
  rows drive persona, skills in the model request, budget warning, policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:28:10 -05:00
james.maesandClaude Opus 5.5 fdf8d72367 feat(schema): add the missing foreign keys with orphan quarantine
Reference DDL gains a guarded, re-runnable foreign key block after the
index audit: the conversation tree CASCADEs (matching
AssistantStore.deleteConversation and retention), the message/turn cycle,
usage and the data audit are SET NULL so cost rows and the audit trail
survive, provider references from usage, budget accounting and the data
audit are NO ACTION, profile-skill links CASCADE, and
assistant_model.provider_id becomes CASCADE (fk_assistant_model_provider,
as on kof22). Orphans are recorded in qqq_fk_quarantine first. Every FK
column now leads an index.

SchemaForeignKeyPostgresTest applies the DDL on Postgres (Testcontainers)
and fails any declared relation without a foreign key, a wrong delete
rule, an unindexed FK column, or a broken conversation delete order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:23:25 -05:00
james.maesandClaude Opus 5.5 8aac6cf92d docs(schema): add the missing lookup, retention and FK indexes
The reference Postgres schema gains six IF NOT EXISTS indexes:
- assistant_usage (turn_id): ScheduledTurn.cost
- assistant_turn_event (create_date), assistant_tool_call (create_date) and
  assistant_conversation (modify_date): the retention filters
- assistant_scheduled_run (conversation_id) and (turn_id): the ON DELETE
  SET NULL foreign keys

UPGRADE.md notes them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:26:08 -05:00
james.maesandClaude Opus 5.5 8655ea21d4 refactor!: provider SPI in its own package com.kingsrook.qbits.assistant.spi
The qbit-assistant-provider-api artifact now holds exactly one package,
com.kingsrook.qbits.assistant.spi (23 types including ModelPricing), so
consumers such as qbit-marketing depend only on that artifact and the
qbit-bom G1 guardrails can allow it by package. BREAKING: imports change
from ...assistant.provider.X / ...assistant.engine.ModelPricing to
...assistant.spi.X (UPGRADE.md has the table). ArchUnit forbids split
packages between the API and the implementation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:51:34 -05:00
james.maes 64268e63d0 Merge remote-tracking branch 'origin/develop' into feature/x1-y1-channel-wall
# Conflicts:
#	CHANGELOG.md
#	UPGRADE.md
2026-10-14 08:19:55 -05:00
james.maesandClaude Opus 5.5 b0b80ead3d feat: the hard wall and audit attribution (X1/Y1)
X1:
- Every assistant_* table, process, report and widget (and every object
  the qbit produces) is denied to client channel assistant: the
  assistant_* pattern in ClientChannelsMetaData plus
  QPermissionRules.withDeniedClients on each object.
- Startup fails if any of them is not denied.
- Every UI API call carries assistant/<turnId> (scheduled:
  assistant/scheduled/<taskId>); confirm and explain-access carry the
  owning turn.
- Tests: assistant.admin through the assistant is denied (403, absent
  from list_tables and /metaData) on every configuration object, while the
  same admin through next-ui is allowed; the startup check fails on one
  undenied object; core's security-surface patterns are present.
Y1:
- assistant_tool_call.audit_href: an /app link to the written record's
  audit rows filtered by clientChannel, built with the shared filter-link
  builder.
- Tests: an assistant write audits the human actor with
  clientChannel assistant/<turnId>; the same write from the UI audits
  next-ui. Hosts enable the audit clientChannel field (documented).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:17:35 -05:00
james.maes 467c2199dc Merge remote-tracking branch 'origin/develop' into feature/provider-api-module
# Conflicts:
#	CHANGELOG.md
#	UPGRADE.md
2026-10-14 07:31:36 -05:00
james.maesandClaude Opus 5.5 de215f8ff9 refactor: qbit-assistant-provider-api module for the provider SPI and pricing (#30)
Architecture review QRun-IO/qqq#1150 F5: qbit-marketing compiled against
assistant internals.

- New artifact qbit-assistant-provider-api with the provider SPI and
  value types (same package names) and engine.ModelPricing; production
  dependency jackson-annotations only. An ArchUnit test keeps it free of
  assistant internals.
- AssistantProviderType's settings validation takes ProviderSettings
  instead of the assistant_provider entity (documented in UPGRADE.md).
- qbit-assistant and the provider modules depend on the API artifact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:49:45 -05:00
james.maesandClaude Opus 5.5 907d943f8d feat: scheduled assistant tasks as a designated user through the UI API (P-g)
- assistant_scheduled_task (Scheduled tasks): prompt, run-as user,
  credential env-var reference (allow-listed, never stored), profile,
  Quartz cron + time zone, output mode, write-mode request and per-task
  admin approval (cleared when the prompt, user, credential, profile or
  request changes), per-run cost cap; assistant_scheduled_run child list.
- Runner (60 s schedule): claims a slot via a unique (task, scheduledFor)
  run key, logs in exactly like a UI client (POST /qqq/v1/manageSession
  with accessToken), checks the session's user is the run-as user, then
  runs a normal turn on a new conversation (channel assistant/scheduled,
  X-QQQ-Client assistant/scheduled/<taskId>). No system or constructed
  sessions.
- PHI routing, budgets, routing/fallback apply unchanged; output as the
  conversation and run record, a notification through core alerts, or
  both. Record actions: Approve write mode, Revoke write mode, Run now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:34:46 -05:00
james.maes 1947688c34 Merge remote-tracking branch 'origin/develop' into feature/p6-rag 2026-10-13 19:51:42 -05:00
james.maesandClaude Opus 5.5 c487c87051 feat: RAG over instance content, permission-filtered through the UI API (P-f)
- Embeddings: EmbeddingProvider SPI, OpenAI-compatible /embeddings
  (batching, Azure path), assistant_model.model_kind CHAT/EMBEDDING and
  embedding_dimensions; chat selection ignores embedding models.
- VectorStore SPI: in-memory store, and the new qbit-assistant-rag-pgvector
  module (one schema per instance, HNSW cosine index, parameterized SQL).
  The index holds NO record content: table, key, field, chunk, hash and
  vector only.
- assistant_rag_source (Knowledge sources) with Index now / Clear index
  record actions that read through the UI API as the clicking admin,
  skip unchanged chunks and drop removed records.
- search_knowledge tool: vector search, then every hit re-read through the
  UI API with the asking user's session (403/404 hits dropped), AI field
  exposure and masking applied, /app citations; PHI rule for embeddings;
  classification raised by results.
- pgvector container test (pgvector/pgvector:pg16) passes with Docker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:51:40 -05:00
james.maesandClaude Opus 5.5 cb7d5d4d5a refactor: budget alerts through core QAlerts (QQQ 4.1.0-RC.5), alert types declared
Closes #28 (first half; the DataClassification and loopback gateway
stand-ins go with RC.6).

- Default QQQ is 4.1.0-RC.5.
- BudgetAlerts calls QAlerts.raise(AlertSignal...) directly instead of
  binding by reflection.
- The producer declares assistant.budget.warning, .downgraded and .stopped
  as QAlertTypeMetaData (assistant.admin, subject assistant_budget).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:30:01 -05:00
james.maesandClaude Opus 5.5 55804d9059 feat: simple/complex model routing and fallback chains across providers (P-e)
- Profiles gain routing (OFF, HEURISTIC, CLASSIFIER) with simple, complex
  and classifier models. The heuristic is a small documented rule set; the
  classifier is one 4-token call on the question text only (ROUTE usage,
  3 s timeout, heuristic on any failure, skipped for PHI on a
  non-eligible provider).
- assistant_model.fallback_model_id chains (cycle-checked, max 4 hops)
  across providers; retryable failures before any output fall back.
  Candidates that are disabled, down, over a STOP budget or fail the PHI
  rule are skipped; without a chain, LOCAL falls back only to LOCAL.
- Circuit breaker (provider health UP/DOWN, down_until) and a cached
  health probe for stale LOCAL providers.
- A mid-turn provider switch rebuilds history from neutral parts and drops
  adapter state (Anthropic thinking signatures).
- Turn route decision, source and fallback count; home shows health and
  fallbacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:09:26 -05:00
james.maesandClaude Opus 5.5 51fcfa6a33 feat: data-driven budgets with warn, downgrade, stop and admin overrides (P-d)
- assistant_budget (scope INSTANCE/USER/ROLE/PROVIDER/MODEL, DAY/MONTH,
  cost or tokens, warn %, DOWNGRADE or STOP at 100%, hard ceiling),
  assistant_budget_override (reason, grantor, expiry) and
  assistant_budget_event (deduplicated WARNED/DOWNGRADED/STOPPED).
- BudgetService checks before every model call; strictest outcome wins.
  Warnings raise a panel notice and an optional Alerts signal; downgrade
  prefers a cheaper LOCAL model and never crosses the PHI rule.
- assistant_budget_usage: an owner-lock-free accounting projection of
  usage counters so shared-scope sums run under the caller's own session.
- App home: budgets card with state and overrides, 30-day cost and token
  bars with user, model, provider and tool breakdowns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:43:45 -05:00
james.maesandClaude Opus 5.5 d15c7dce12 feat: data classification, PHI routing and HIPAA mode (P-c)
- DataClassification (PUBLIC < CONFIDENTIAL < PII < PHI): a qbit
  stand-in for core's (QRun-IO/qqq#1142) behind a pluggable
  ClassificationSource (config by default; core metadata or
  qbit-secured-data overrides later).
- Each turn tracks the highest classification its page context, history
  and tool results touched (turn.classification, conversation
  max_classification, never lowered).
- PhiRoutingGuard before EVERY model call: PHI only to hipaa_eligible
  provider rows; otherwise the turn ends PHI_BLOCKED with the reason and
  the tool results are never sent.
- HIPAA mode: no non-eligible providers (resolve, enable, code fallback),
  AES-256-GCM conversation storage (key by env-var reference, required),
  PHI retention period, no provider messages or stack traces in logs.
- assistant_data_audit: one row per model call that carried PII/PHI and
  per block (who, classification, tables, provider, model, decision).
- System rule 11: masked values stay masked; never ask to reveal them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:20:13 -05:00
james.maesandClaude Opus 5.5 c9ccd4c79b feat: AWS Bedrock and Google Vertex adapters (P-b)
- qbit-assistant-provider-bedrock: Claude on Bedrock through the
  anthropic-java Bedrock backend (AnthropicMapping reused), credentials
  from the server's AWS default chain, region required, no key reference.
  IRSA/web identity needs the host to add software.amazon.awssdk:sts.
- qbit-assistant-provider-vertex: Claude on Vertex through the
  anthropic-java Vertex backend, Gemini through Vertex's OpenAI-compatible
  endpoint with an ADC bearer token refreshed per request (never strict).
  Region and the new cloud_project column are required.
- AnthropicModelProvider.Builder.client(...) and a bearer-token supplier
  on ProviderSettings / the OpenAI adapter.
- Mocked contract tests: a Bedrock event-stream mock with SigV4 checks,
  Vertex rawPredict and OpenAI-compatible mocks, validation, and a golden
  tool round trip per module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:47:40 -05:00
james.maesandClaude Opus 5.5 8d869f65cd feat: provider and model registry, OpenAI-compatible adapter, discovery and health (P-a)
- assistant_provider gains kind, location, hipaa_eligible, region, api
  version, timeouts, a CA bundle reference and discovery status.
- New assistant_model table (child list on the provider): capabilities,
  prices per million tokens, enabled flag, last test.
- Provider SPI: ProviderSettings, kinds, discovery and health as default
  methods; create(apiKey, baseUrl) implementations keep working.
- New module qbit-assistant-provider-openai: OpenAI, Azure OpenAI,
  OpenRouter, Ollama, LM Studio, vLLM and llama.cpp over JDK HTTP, with
  streaming, tool calls, usage, timeouts, cancel, strict only where
  supported (and the existing fallback on refusal), keyless local
  endpoints and per-provider private CA trust.
- Record actions: Discover models (provider), Test connection (model);
  provider Test connection checks health first.
- Mock-server contract tests, a golden tool round trip, TLS tests and a
  config-driven live check (skipped unless configured).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:17:39 -05:00
james.maesandClaude Opus 5.5 0babaa7022 feat: profiles, skills, explain access and recent denials
Owner (2026-10-13): set a name, personality and extra skills for the
assistant, and the assistant stays limited to the UI API and to what the
signed-in user could do through it.

- assistant_profile (persona: display name, icon, bounded personality,
  greeting, starter prompts; default, app- or permission-scoped) and
  assistant_skill (admin Markdown instruction packs, read only when
  relevant, bounded) linked by assistant_profile_skill. Text only: it
  goes into the per-user system block and never reaches tool code.
- Explain access: GET /assistant/access and the assistantAccess home
  widget, per table and tool: allowed or why not, from GET /metaData with
  the user's own session plus data access and policy.
- Recent denials card on the Assistant home.
- Hard guarantee: ArchUnit forbids the tool / UI API / access packages
  from QQQ actions, the AI kernel and the profile/skill classes, and the
  store refuses any non-assistant_ table. A denial matrix proves query,
  get, insert, update and delete are denied identically via the UI API
  and via the assistant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:10:47 -05:00
james.maesandClaude Opus 5.5 01cc719c5b feat!: assistant tools go through the /qqq/v1 UI API with the user's session
Owner decisions (2026-10-13): tools via the UI API, configurable and
verifiable data access, and confirmation for writes.

- Every data tool runs a spec'd /qqq/v1 operation (UiApiGateway, a
  loopback request carrying the user's own sessionUUID cookie), so it
  passes the UI's authentication, permissions, record locks, validation,
  audit and error mapping. LoopbackUiApiGateway is a stand-in for core's
  UiApiInvoker (QRun-IO/qqq#1128). navigate and search_help (no records)
  stay on the core kernel.
- Data access (assistant_data_access): NONE / READ / READ_WRITE per table
  or process; sensitive tables NONE and cannot be raised; it only narrows.
- Writes (create/update/delete_record) propose a pending action
  (assistant_pending_action) the owner confirms; confirm runs it once
  through the UI API with the confirming request's session.
- Policy: writesRequireConfirmation, allowedTools, maxRowsPerCall,
  redactPii. Tool-call log: UI API operation, HTTP status, audit link,
  pending action; inputs stored with secrets redacted.
- UiApiToolsSecurityTest proves a user without a permission gets the same
  denial through the assistant as through the UI; golden and red-team
  evals run through a real application server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:56:18 -05:00
james.maesandClaude Opus 5.5 e54de89a49 feat: provider settings, per-call usage, model prices and the app home
- assistant_provider: type (registered AssistantProviderType; Anthropic),
  model, enabled/default, base URL override, cost cap, output and
  concurrency limits, last connection test. The key is never stored: the
  row names an allow-listed environment variable; apiKeyStatus is
  computed on read (last 4 at most) and dropped on save.
- Test connection: a record action (standard streamed ETL Review/Result)
  making one short live call; result saved on the row and as a TEST
  usage row.
- assistant_usage: one row per model call (turn steps and tests);
  assistant_model_price: editable USD/MTok used for every call's cost.
- The Assistant app home (health, providers, usage by day, costs vs caps,
  by user, top tools, recent failures); nav = home + main tables.
- AI exposure NONE on every assistant_* table.
- Tests, README, DESIGN, UPGRADE, CHANGELOG, reference DDL with the
  Anthropic price seed, demo server and screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:01:33 -05:00
james.maesandClaude Opus 5.5 e9a5980dec docs: name 0.1.0-RC.3 as the snake_case rename release
0.1.0-RC.2 on Maven Central is an accidental republish of RC.1, so the
rename shipped in 0.1.0-RC.3. Point UPGRADE.md, the @Deprecated(since)
annotations, their javadoc and the NamingTest comment at RC.3, and note
that upgrading from RC.2 is the same as upgrading from RC.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:05:33 -05:00
james.maesandClaude Opus 5.5 bf914dc76e fix: keep the assistant.hasAccess permission
QQQ core's standard permission suffix is hasAccess
(PrivatePermissionSubType.HAS_ACCESS) and the assistant base already
follows the naming rule, so the permission stays assistant.hasAccess,
matching the sql-workbench rename. Drops LEGACY_ACCESS_PERMISSION and
updates README, CHANGELOG, UPGRADE.md and NamingTest. Table, PVS and
key-type renames are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:12:00 -05:00
james.maesandClaude Opus 5.5 e5413d8ad0 feat!: snake_case table, permission and key names (assistant_ prefix)
Applies the QRun-IO naming rule (owner, 2026-10-12): table names are
lowercase snake_case and prefixed with the qbit's name. Metadata only;
docs/schema-postgres.sql already creates assistant_* tables.

- Tables: assistantConversation ... assistantUsageDaily become
  assistant_conversation, assistant_message, assistant_turn,
  assistant_turn_event, assistant_tool_call, assistant_feedback,
  assistant_policy, assistant_usage_daily. The conversation PVS follows.
- Permission assistant.hasAccess becomes assistant.has_access
  (assistant.admin unchanged).
- Owner key type assistantUserId becomes assistant_user_id; all-access
  key assistant_user_id_all_access.
- @Deprecated LEGACY_* constants keep the RC.1 names until 0.2.0.
- UPGRADE.md lists the stored references to update (permissions,
  saved views and reports, audit_table names) with sample SQL.
- NamingTest pins the new and legacy names.

Target: 0.1.0-RC.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:04:57 -05:00