Compare commits

..
Author SHA1 Message Date
CI b4bda50ae2 Bump version to 0.1.0-RC.31 [skip ci] 2026-11-14 14:37:13 +00:00
james.maes a4b2b01612 Merge pull request 'release: merge develop into release/0.1 (one tool registry, tool-call service, signed-in writes, spending limit, process confirmation)' (#111) from rc/develop-into-release-0.1-registry into release/0.1
maven / build (push) Successful in 8m54s
build / maven (push) Successful in 8m54s
2026-11-14 14:28:28 +00:00
james.maesandClaude Fable 5.2 a51fd12799 release: merge develop into release/0.1 (one tool registry, tool-call service, signed-in writes, spending limit, process confirmation)
maven / build (push) Successful in 8m7s
build / maven (push) Successful in 8m7s
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-14 07:17:14 -06:00
CI 88aec4afec Bump version to 0.1.0-RC.30 [skip ci] 2026-11-11 17:22:56 +00:00
james.maes ceb31a71b3 Merge pull request 'release: merge develop into release/0.1 (the store as trusted code, a person's own budget)' (#103) from rc/develop-into-release-0.1-everyone into release/0.1
maven / build (push) Successful in 5m33s
build / maven (push) Successful in 5m33s
2026-11-11 17:17:32 +00:00
james.maesandClaude Fable 5.2 e3f68bf7ee Merge develop into release/0.1 for the next 0.1.0 RC (the store as trusted code, a person's own budget)
maven / build (push) Successful in 6m4s
build / maven (push) Successful in 6m5s
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 11:10:47 -06:00
CI e37602b71e Bump version to 0.1.0-RC.29 [skip ci] 2026-11-11 16:21:20 +00:00
james.maes cd1cdaa1c0 Merge pull request 'release: merge develop into release/0.1 (weekly budgets, refusal sentence, limit ceiling)' (#101) from rc/develop-into-release-0.1-weekly into release/0.1
maven / build (push) Successful in 5m34s
build / maven (push) Successful in 5m34s
2026-11-11 16:15:55 +00:00
james.maesandClaude Fable 5.2 1ae7bb9cfa Merge develop into release/0.1 for the next 0.1.0 RC (weekly budgets, refusal sentence, limit ceiling)
maven / build (push) Successful in 5m16s
build / maven (push) Successful in 5m16s
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 10:08:23 -06:00
CI 6d66deabc9 Bump version to 0.1.0-RC.28 [skip ci] 2026-11-11 14:03:10 +00:00
james.maes 08c8264f23 Merge pull request 'release: merge develop into release/0.1 (save_analysis, decided actions, ask before guessing)' (#99) from rc/develop-into-release-0.1-save into release/0.1
maven / build (push) Successful in 5m55s
build / maven (push) Successful in 5m55s
2026-11-11 13:57:25 +00:00
james.maesandClaude Fable 5.2 07b06f2070 Merge develop into release/0.1 for the next 0.1.0 RC (save_analysis, decided actions, ask before guessing)
maven / build (push) Successful in 5m13s
build / maven (push) Successful in 5m13s
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 07:51:36 -06:00
CI 61fca48424 Bump version to 0.1.0-RC.27 [skip ci] 2026-11-11 13:51:22 +00:00
james.maes d761bd4a7d Merge pull request 'release: merge develop into release/0.1 (analyze_records)' (#97) from rc/develop-into-release-0.1-analyze into release/0.1
maven / build (push) Successful in 6m16s
build / maven (push) Successful in 6m16s
2026-11-11 13:45:08 +00:00
james.maes f1634b0911 Merge develop into release/0.1 for the next 0.1.0 RC (analyze_records)
maven / build (push) Successful in 6m27s
build / maven (push) Successful in 6m27s
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>

# Conflicts:
#	CHANGELOG.md
#	pom.xml
2026-11-11 07:34:30 -06:00
CI feab4caf17 Bump version to 0.1.0-RC.26 [skip ci] 2026-11-09 15:59:36 +00:00
james.maes ba21b72188 Merge pull request 'fix(engine): a stalled database can no longer hang a turn on 'Thinking'' (#94) from fix/turn-watchdog-never-hangs into release/0.1
maven / build (push) Successful in 5m5s
build / maven (push) Successful in 5m5s
2026-11-09 15:54:38 +00:00
james.maesandClaude Sonnet 5.5 0a65702dab fix(engine): a stalled database can no longer hang a turn on 'Thinking'
maven / build (push) Successful in 5m3s
build / maven (push) Successful in 5m3s
The watchdog ran the deadline logic after two database reads and heartbeat() was synchronized on the monitor that finish() needs, so one blocked database call stopped the deadline, the force-finalize and the final message.end. The deadline now runs first with no database access, housekeeping runs on a bounded abandonable worker, finish() has a guard that ends the stream with an error event, and every phase logs at INFO.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 09:48:47 -06:00
CI 739ca6981f Bump version to 0.1.0-RC.25 [skip ci] 2026-11-07 17:28:54 +00:00
james.maes b51dd4dad1 Merge pull request 'release: merge develop into release/0.1 (Gitea-aligned RC)' (#92) from align/develop-into-release-0.1 into release/0.1
maven / build (push) Successful in 8m11s
build / maven (push) Successful in 8m11s
2026-11-07 17:20:44 +00:00
james.maesandClaude Opus 5.5 c3a03b8d84 Merge develop into release/0.1 (Gitea-aligned RC)
maven / build (push) Canceled after 37s
build / maven (push) Canceled after 43s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:19:04 -06:00
james.maes 2d25f0b769 Merge pull request 'fix(assistant): store the transcript before message.end so the panel shows the answer' (#85) from fix/transcript-stored-before-message-end into release/0.1
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 7s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (push) Successful in 29s
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (push) Successful in 5m57s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (push) Successful in 16s
QRun-IO/ci-policy: ci / java-docker-3 (push) Successful in 24m15s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 8s
2026-11-05 10:17:10 +00:00
james.maesandClaude Sonnet 5.5 c5125f9788 fix(assistant): store the transcript before message.end so the panel shows the answer
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 11s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (pull_request) Successful in 36s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (pull_request) Successful in 7m8s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (pull_request) Successful in 23s
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Successful in 12m50s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 10s
The panel reloads the conversation the moment the stream ends. The
redacted transcript (message parts, pending actions, stored events) was
flushed only after message.end was sent, so that reload read the "[]"
placeholders and showed the newest question and answer as empty bubbles
until the panel was closed and reopened. Persist and flush first, then
emit message.end.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 19:15:44 -06:00
james.maesandCodex 3de664e0fd ci: centralize release branch verification and workflows (#75)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 12:38:46 -05:00
CircleCI 2caa634044 Bump version to 0.1.0-RC.24 [skip ci] 2026-10-16 17:08:39 +00:00
james.maesandClaude Opus 5.5 c77e5b0123 Merge develop into release/0.1 for 0.1.0-RC.24 (#62 optional unreachable providers report DEGRADED)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:59:08 -05:00
CircleCI f46735b1c0 Bump version to 0.1.0-RC.23 [skip ci] 2026-10-16 16:04:25 +00:00
github-actions[bot] 86563c64d2 Merge develop into release/0.1 for the next 0.1.0 RC 2026-10-16 15:58:52 +00:00
CircleCI 82febe0f19 Bump version to 0.1.0-RC.22 [skip ci] 2026-10-16 02:27:26 +00:00
james.maesandClaude Opus 5.5 1ea23dfc49 Merge develop into release/0.1 for 0.1.0-RC.22 (#60 fail-soft assistant server)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:17:28 -05:00
CircleCI 9d31de46a6 Bump version to 0.1.0-RC.21 [skip ci] 2026-10-16 01:13:38 +00:00
james.maesandClaude Opus 5.5 24b0b3b14a Merge develop into release/0.1 for 0.1.0-RC.21 (#59 records render always carries rows and columns)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 20:04:06 -05:00
CircleCI 1c33e97664 Bump version to 0.1.0-RC.20 [skip ci] 2026-10-15 21:41:12 +00:00
james.maesandClaude Opus 5.5 fcaf2ded7e Merge develop into release/0.1 for 0.1.0-RC.20 (#56 UI API 1.6.0 extension, QQQ RC.15)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:30:07 -05:00
CircleCI 2cea8ffd44 Bump version to 0.1.0-RC.19 [skip ci] 2026-10-15 21:12:25 +00:00
james.maesandClaude Opus 5.5 6b98d5216b Merge develop into release/0.1 for 0.1.0-RC.19 (#57 shutdown hook holds the runtime weakly)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:04:27 -05:00
CircleCI 6adf390bf8 Bump version to 0.1.0-RC.18 [skip ci] 2026-10-15 03:36:35 +00:00
james.maesandClaude Opus 5.5 bc92c89665 Merge develop into release/0.1 for 0.1.0-RC.18 (#45 health checks)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:29:33 -05:00
CircleCI d43402e262 Bump version to 0.1.0-RC.17 [skip ci] 2026-10-15 02:40:56 +00:00
james.maesandClaude Opus 5.5 fe725ac8ff Merge develop into release/0.1 for 0.1.0-RC.17 (#54 assistant answers in the Jobs tray, QQQ RC.8)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:32:46 -05:00
CircleCI dfb05d8935 Bump version to 0.1.0-RC.16 [skip ci] 2026-10-15 02:20:24 +00:00
james.maesandClaude Opus 5.5 523fd0128a Merge develop into release/0.1 for 0.1.0-RC.16 (#53 help bundle)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 21:11:51 -05:00
CircleCI 47115fe359 Bump version to 0.1.0-RC.15 [skip ci] 2026-10-15 00:18:50 +00:00
james.maesandClaude Opus 5.5 f2d2b50459 Merge develop into release/0.1 for 0.1.0-RC.15 (#44 transcript redaction)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 19:10:35 -05:00
CircleCI 8c33db9fcf Bump version to 0.1.0-RC.14 [skip ci] 2026-10-14 23:13:46 +00:00
james.maesandClaude Opus 5.5 75a5dedf7f Merge develop into release/0.1 for 0.1.0-RC.14 (#46, #47 indexes and FKs)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:07:05 -05:00
CircleCI c1cceaf4c3 Bump version to 0.1.0-RC.13 [skip ci] 2026-10-14 22:27:30 +00:00
james.maesandClaude Opus 5.5 f8a114bbb8 Merge develop into release/0.1 for 0.1.0-RC.13 (#48 seeds and Setup card, #49 durable turns, #50 seed fixes, #52 rollback)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:20:45 -05:00
CircleCI 3a7ff1c659 Bump version to 0.1.0-RC.12 [skip ci] 2026-10-14 16:03:00 +00:00
james.maesandClaude Opus 5.5 ea81166454 Merge develop into release/0.1 for 0.1.0-RC.12 (#19 #39 #40 #42 #43)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:56:16 -05:00
CircleCI cfcc044055 Bump version to 0.1.0-RC.11 [skip ci] 2026-10-14 12:57:45 +00:00
james.maesandClaude Opus 5.5 620dee488d Merge develop into release/0.1 for 0.1.0-RC.11 (#36 scheduled tasks, #37 provider-api module)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:51:34 -05:00
CircleCI e23206f1e8 Bump version to 0.1.0-RC.10 [skip ci] 2026-10-14 12:12:23 +00:00
james.maesandClaude Opus 5.5 c6bae3bcf2 Merge develop into release/0.1 for 0.1.0-RC.10 (#38 bulk-load tools)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:09:32 -05:00
CircleCI 2d8459bb24 Bump version to 0.1.0-RC.9 [skip ci] 2026-10-14 01:42:57 +00:00
james.maesandClaude Opus 5.5 ac06a96787 Merge develop into release/0.1 for 0.1.0-RC.9 (presentation #32, RAG and contingent PVs #33, demo #34, P-d/P-e)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:39:02 -05:00
CircleCI d4a1f64480 Bump version to 0.1.0-RC.8 [skip ci] 2026-10-14 00:55:57 +00:00
james.maesandClaude Opus 5.5 c28e782075 Merge develop into release/0.1 for 0.1.0-RC.8 (core alerts #31, budgets #26, routing #27)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 19:52:20 -05:00
CircleCI 5c76a13245 Bump version to 0.1.0-RC.7 [skip ci] 2026-10-13 23:31:30 +00:00
james.maesandClaude Opus 5.5 8d1348888d Merge develop into release/0.1 for 0.1.0-RC.7
Provider/model registry and OpenAI-compatible adapter (#22), Bedrock and
Vertex (#23), data classification, PHI routing and HIPAA mode (#24).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:28:10 -05:00
CircleCI 47cc572c36 Bump version to 0.1.0-RC.6 [skip ci] 2026-10-13 19:51:20 +00:00
james.maesandClaude Opus 5.5 8099f64bf2 Merge develop into release/0.1 for 0.1.0-RC.6
Tools via /qqq/v1 with a loopback stand-in (#14); profiles, skills,
explain access, denials, ArchUnit and the denial matrix (#15); #16; the
demo pending-action card (#18).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:49:14 -05:00
CircleCI ac6ec75deb Bump version to 0.1.0-RC.5 [skip ci] 2026-10-13 18:44:09 +00:00
james.maesandClaude Opus 5.5 1f48a370e3 Merge develop into release/0.1 for 0.1.0-RC.5
Tools are no longer all sent as strict, so Anthropic no longer rejects
every turn with "compiled grammar is too large" (#13).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 13:40:36 -05:00
CircleCI d0ad2aa0fa Bump version to 0.1.0-RC.4 [skip ci] 2026-10-13 15:24:59 +00:00
james.maesandClaude Opus 5.5 16c8500f59 Merge develop into release/0.1 for 0.1.0-RC.4
App home, provider settings with Test connection, per-call usage and
model prices (#12).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:23:04 -05:00
CircleCI a8c5f586ee Bump version to 0.1.0-RC.3 [skip ci] 2026-10-12 21:22:35 +00:00
james.maesandClaude Opus 5.5 3dfa775b21 Merge develop into release/0.1 for 0.1.0-RC.3
Assistant Admin record screens (#6), deterministic RobustnessTest (#9)
and the GA-only tag filter (#8), on top of the snake_case rename (#3).
0.1.0-RC.2 on Central is an accidental republish of RC.1, so this
candidate is RC.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:20:10 -05:00
james.maesandClaude Opus 5.5 67f78fd660 ci: fix the GA tag filter YAML (no brackets in the flow sequence) [skip ci]
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:01:54 -05:00
james.maesandClaude Opus 5.5 ff67c6f7cc Set version to 0.1.0-RC.2 and publish only GA tags [skip ci]
0.1.0-RC.2 is already on Maven Central (an RC tag build republished the previous code), so the next release-branch build must publish the following RC. RC tags no longer trigger a publish (/^v[0-9]+\.[0-9]+\.[0-9]+$/ only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:56:59 -05:00
james.maesandClaude Opus 5.5 19551ac2af Merge develop into release/0.1 for 0.1.0-RC.2
Brings the snake_case assistant_ table and key names (#3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:18:00 -05:00
james.maesandClaude Opus 5.5 7263ac627d Bump version to 0.1.0-RC.1 [skip ci]
The orb published 0.1.0-RC.1 but could not push this commit (the
CircleCI deploy key is read only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:49:33 -05:00
11 changed files with 8 additions and 335 deletions

No files matched your search

-7
View File
@@ -1,12 +1,5 @@
# Changelog
## Unreleased - render_widget follows the assistant's data access; no widget is rendered until a host allows it
- **A widget is rendered only when the host allowed it by name.** `render_widget` asked the application to render any widget the person may see. A widget is code of the application and can show rows of any table, so it could show a model the rows of a table the assistant may not read (data access `NONE`, a table kept from AI by its metadata). The tool now applies the assistant's own rules before the application is asked: a widget that is not in `AssistantQBitConfig.withAllowedWidgetNames` is refused (the list is empty by default: no widget is renderable), and so is one in `withHiddenWidgetNames`. The README already said the list governs the tool; since the tool moved from the core tool kernel to the UI API it did not.
- **A host says which tables a widget reads, and data access then decides.** New: `AssistantQBitConfig.withWidgetTables(widgetName, tableNames)`. An allowed widget with tables is rendered only while the assistant may read every one of them, so an administrator's `NONE` row, a pin in code and a table kept from AI each close the widget too. The refusal is one answer for every reason and does not say whether the widget exists.
- The tool is still offered as before (its definition and the cached request prefix do not change); a call for a widget that is not allowed is a DENIED result with a log row. Not offering the tool at all while no widget is allowed is left for later: it changes the tool set of every request.
- Tests: `RenderWidgetAccessTest` (the default is closed and the application is not asked; allowed; hidden wins; a widget whose table is closed by a pin, read-only, or missing).
## Unreleased - The assistant runs a process the signed-in person may run; four review findings closed
Builds package A4 of the actions design (`docs/actions-and-mcp.md`, section 4), with four findings of an independent review, each described by the guarantee it gives. **One behaviour change needs a host's attention before deploying: a table's own bulk edit and bulk delete become runnable through the assistant, and any other process once the host lists it (see `UPGRADE.md`).**
-1
View File
@@ -78,7 +78,6 @@ Always use the wrapper: Maven 3.10 cannot read the parent POM. The committed def
- A missing policy refuses; it never means "every tool".
- Data tools run only through `UiApiToolbox` → `UiApiGateway` (spec'd `/qqq/v1` operations with the user's `sessionUUID`); never core data actions for user data. Only `navigate` and `search_help` use `AiToolKernel.execute`.
- Data access (`DataAccessService`) and the policy only ever narrow what the UI API allows. The mode of a table or process is declared in versioned code: the default (`withDefaultTableAccess` / `withDefaultProcessAccess`, READ_WRITE) or a pin (`withTableAccess` / `withProcessAccess`). An administrator's `assistant_data_access` row narrows it and never raises it. A table whose AI exposure is NONE stays NONE, and so does every `assistant_*` table and every process of the QBit (`DataAccessService.isOwnTable`: a pin in code that no row and no host pin lifts). Rows that cannot be read and were never read mean NONE for everything, never the default.
- `render_widget` renders only a widget the host allowed by name in code (`withAllowedWidgetNames`; none by default), not hidden, and only while the assistant may read every table the host said it reads (`withWidgetTables`): a widget is code that can show rows of any table. The check is in `UiApiToolbox.renderWidget`, before the UI API is asked. Never render a widget on the person's permission alone.
- READ_WRITE grants nothing: the write tools are offered to a person only when the UI API says that person may create, edit or delete records of some table (`UiApiToolFamily.writableTables`, asked as the person, once per offer, within `toolCallTimeoutSeconds`; no answer means no write tool). The person's permission for the change itself is decided by the UI API when the confirmed change runs, with its own message. Never materialise defaults as rows again: the retired "Add rows for every table" action is why `assistant-data-access-follow-user.changelog.sql` exists.
- A write runs only after the owner confirms its pending action, with the confirming request's session, once; no policy or caller can turn confirmation off (`assistant_policy.writesRequireConfirmation` is no longer read). `ToolCallService.confirm` loads the stored action for that person, claims it, runs it and writes its row; whatever fails after the claim ends in FAILED with a row, never in RUNNING.
- A pending action is decided once: confirming, cancelling and expiring all go through `AssistantStore.decidePendingAction` (final; `expirePendingAction` is a claim with the decision EXPIRED), which inserts the action's row into `assistant_pending_action_claim` (unique `pendingActionId`). Never decide an action with a status update alone, never expire one from a copy read earlier or a time a caller names (`ToolCallService.expiredIfDue` decides from the stored action and the runtime's clock), and never drop that unique key.
+1 -2
View File
@@ -286,8 +286,7 @@ The nav lists the home and the main tables: **Providers**, **Conversations**, **
| `retentionSchedulerName` | none | Runs `assistantRetention` daily on that scheduler. |
| `ownerSecurityKeyType` | `assistant_user_id` | Key type of the owner record locks. |
| `pricing` | Anthropic list prices, 2026-10 | USD per million tokens per model, used for cost and budgets. |
| `allowedWidgetNames` | none | Dashboard widgets `render_widget` may render. The tool refuses every widget that is not on this list, before the application is asked, so with the default (none) no widget is renderable. This list in code is the **only** way to allow a widget: QQQ widget meta-data has no supplemental hook, so there is no per-widget "assistant-safe" flag. A widget is code of the application and can show rows of any table, whatever the assistant's data access says of that table; allow only widgets that render the same data for every user who can see them, and say which tables each one reads with `withWidgetTables`. |
| `withWidgetTables(widget, tables)` | none | The tables an allowed widget reads. The widget is rendered through the assistant only while the assistant may read every one of them (data access not `NONE`: the code default, a pin, an administrator's row, a table kept from AI by its metadata). A widget allowed without tables is rendered whatever the data access says. |
| `allowedWidgetNames` | none | Dashboard widgets `render_widget` may render. The kernel denies every widget not on this list. This config list is the **only** way to allow a widget: QQQ widget meta-data has no supplemental hook, so there is no per-widget "assistant-safe" flag. Allow only widgets that render the same data for every user who can see them. |
| `hiddenWidgetNames` | none | Widgets that are never rendered, even when allowed. |
| `customRedactionPatterns` | none | Extra regular expressions redacted from tool results and from the input kept in the tool-call log. |
| `killSwitchPollMillis` | 2000 | How often a running turn re-checks the kill switch while it waits on the model. |
-16
View File
@@ -1,21 +1,5 @@
# Upgrading qbit-assistant
## render_widget renders only widgets the host allowed (Unreleased)
**After this upgrade the assistant renders no dashboard widget until the application names it in code.** Until now `render_widget` rendered any widget the signed-in person may see, including one that shows rows of a table the assistant may not read.
What a host changes, only if it wants the assistant to render widgets:
```java
new AssistantQBitConfig()
.withAllowedWidgetNames(Set.of("openOrdersByStatus")) // nothing else is renderable
.withWidgetTables("openOrdersByStatus", Set.of("order", "customer")); // the tables its renderer reads
```
- Name the tables each allowed widget reads. The widget is then closed whenever the assistant may not read one of them (a `NONE` row of an administrator, a pin, a table kept from AI). A widget allowed without tables is rendered whatever the data access says: do that only for a widget that shows nothing of a table.
- Allow only widgets that render the same data for every person who can see them.
- No schema change, no permission change, no settings row.
## 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.
+1 -1
View File
@@ -20,7 +20,7 @@
<!-- Multi-module build. The qqq-orb sets the revision here; the modules -->
<!-- inherit it, and the flatten plugin resolves it in the published POMs. -->
<properties>
<revision>0.4.0-SNAPSHOT</revision>
<revision>0.1.0-RC.31</revision>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -159,7 +159,6 @@ public class AssistantQBitConfig implements QBitConfig
private Set<String> hiddenWidgetNames = new LinkedHashSet<>();
private Set<String> allowedWidgetNames = new LinkedHashSet<>();
private Map<String, Set<String>> widgetTables = new LinkedHashMap<>();
////////////////////////////////////////////////////////////////////////////
// tools sent with strict: true. Empty by default: Anthropic compiles all //
@@ -1645,10 +1644,7 @@ public class AssistantQBitConfig implements QBitConfig
/*******************************************************************************
** Fluent setter for allowedWidgetNames: the dashboard widgets render_widget
** may render. A widget that is not named here is refused, whoever asks
** (the list is empty by default, so no widget is renderable until a host
** names one). Name the tables a widget reads with {@link #withWidgetTables}.
** Fluent setter for allowedWidgetNames
*******************************************************************************/
public AssistantQBitConfig withAllowedWidgetNames(Set<String> allowedWidgetNames)
{
@@ -1658,49 +1654,6 @@ public class AssistantQBitConfig implements QBitConfig
/*******************************************************************************
** Getter for widgetTables
*******************************************************************************/
public Map<String, Set<String>> getWidgetTables()
{
return (this.widgetTables);
}
/*******************************************************************************
** Setter for widgetTables
*******************************************************************************/
public void setWidgetTables(Map<String, Set<String>> widgetTables)
{
this.widgetTables = widgetTables;
}
/*******************************************************************************
** Declare the tables an allowed widget reads. The renderer of a widget is
** code of the application: the assistant cannot see which tables it reads,
** so the host says so here. The widget is then rendered through the
** assistant only while the assistant may read every one of those tables
** (their data access is not NONE: the code default, a pin, a row of an
** administrator, a table kept from AI by its metadata, the tables of the
** assistant itself). A widget allowed without tables is rendered whatever
** the data access says: allow one that way only when it shows nothing of
** a table.
*******************************************************************************/
public AssistantQBitConfig withWidgetTables(String widgetName, Set<String> tableNames)
{
if(this.widgetTables == null)
{
this.widgetTables = new LinkedHashMap<>();
}
this.widgetTables.put(widgetName, tableNames == null ? Set.of() : new LinkedHashSet<>(tableNames));
return (this);
}
/*******************************************************************************
** Getter for customRedactionPatterns
*******************************************************************************/
@@ -196,7 +196,6 @@ public class AssistantRuntime
this.toolbox = new UiApiToolbox(qInstance, dataAccessService, exposurePolicy, new AiRedactor(settings.getCustomRedactionPatterns()),
config.getMaxCellChars() == null ? AiToolKernelSettings.DEFAULT_MAX_CELL_CHARS : config.getMaxCellChars());
this.toolbox.withScheduledDeliveryCheck(config.getScheduledDeliveryCheck());
this.toolbox.withWidgetRules(() -> new UiApiToolbox.WidgetRules(config.getAllowedWidgetNames(), config.getHiddenWidgetNames(), config.getWidgetTables()));
this.starterPrompts = new StarterPrompts(toolbox, clock);
VectorStore vectors = config.getVectorStore() == null ? new InMemoryVectorStore() : config.getVectorStore();
this.ragService = new RagService(this, vectors, uiApiGateway);
@@ -78,7 +78,7 @@ public class UiApiToolFamily implements ToolFamily
Map.entry(UiApiToolbox.COUNT_RECORDS, EXPOSE_THEN_READ),
Map.entry(UiApiToolbox.GET_RECORD, READ_THEN_EXPOSE),
Map.entry(UiApiToolbox.SEARCH_RECORDS, EXPOSE_THEN_READ),
Map.entry(UiApiToolbox.RENDER_WIDGET, List.of(TOOL_SWITCH, SPECD_OPERATION, DATA_ACCESS, PERSON_PERMISSIONS)),
Map.entry(UiApiToolbox.RENDER_WIDGET, List.of(TOOL_SWITCH, SPECD_OPERATION, PERSON_PERMISSIONS)),
Map.entry(UiApiToolbox.RECORD_HISTORY, READ_THEN_EXPOSE),
Map.entry(UiApiToolbox.CREATE_RECORD, PROPOSED_WRITE),
Map.entry(UiApiToolbox.UPDATE_RECORD, PROPOSED_WRITE),
@@ -33,7 +33,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Supplier;
import com.kingsrook.qbits.assistant.persistence.TranscriptRedactor;
import com.kingsrook.qbits.assistant.access.DataAccessMode;
import com.kingsrook.qbits.assistant.access.DataAccessService;
@@ -166,8 +165,6 @@ public class UiApiToolbox
private final int maxCellChars;
private final AnalysisTools analysis;
private Supplier<WidgetRules> widgetRules = () -> WidgetRules.NONE;
/*******************************************************************************
@@ -880,40 +877,12 @@ public class UiApiToolbox
/*******************************************************************************
** Which widgets render_widget may render, as the host declared it in code.
**
** @param allowed the widgets a host allowed; a widget not named is refused
** @param hidden widgets a host took away again; one named here is refused
** also when it is allowed
** @param tables for an allowed widget, the tables it reads: it is rendered
** only while the assistant may read every one of them
*******************************************************************************/
public record WidgetRules(Set<String> allowed, Set<String> hidden, Map<String, Set<String>> tables)
{
public static final WidgetRules NONE = new WidgetRules(Set.of(), Set.of(), Map.of());
}
/*******************************************************************************
** The widget rules of the host, read on every call. Without them no widget
** is renderable.
*******************************************************************************/
public UiApiToolbox withWidgetRules(Supplier<WidgetRules> rules)
{
this.widgetRules = rules == null ? () -> WidgetRules.NONE : rules;
return (this);
}
/*******************************************************************************
** The host's check of scheduled delivery (what is missing for a schedule to
** run and be emailed; empty when nothing is). Null: only what the instance
** itself shows is checked.
*******************************************************************************/
public UiApiToolbox withScheduledDeliveryCheck(Supplier<List<String>> check)
public UiApiToolbox withScheduledDeliveryCheck(java.util.function.Supplier<List<String>> check)
{
analysis.setScheduledDeliveryCheck(check);
return (this);
@@ -1526,46 +1495,10 @@ public class UiApiToolbox
/*******************************************************************************
** POST /widget/{widgetName}.
**
** The renderer of a widget is code of the application, and what it shows
** can be rows of any table: the assistant cannot tell, and the UI API
** decides only whether the person may see the widget. So the rules of the
** assistant decide first, before the application is asked: a widget is
** rendered only when the host allowed it by name (none is, by default),
** did not hide it, and the assistant may read every table the host said
** it reads. Everything else is refused with one answer, which does not
** say whether the widget exists.
*******************************************************************************/
private void renderWidget(Context context, Outcome outcome, Map<String, Object> in) throws QException
{
String name = requiredString(in, "name");
WidgetRules rules = widgetRules.get() == null ? WidgetRules.NONE : widgetRules.get();
String check = null;
if(rules.allowed() == null || !rules.allowed().contains(name))
{
check = "widget:notAllowed";
}
else if(rules.hidden() != null && rules.hidden().contains(name))
{
check = "widget:hidden";
}
else
{
for(String tableName : rules.tables() == null || rules.tables().get(name) == null ? Set.<String>of() : rules.tables().get(name))
{
DataAccessService.Decision decision = dataAccess.forTable(tableName);
if(qInstance.getTable(tableName) == null || !decision.mode().allowsRead())
{
check = "dataAccess:table:" + decision.mode();
break;
}
}
}
if(check != null)
{
deny(outcome.result.withWidgetName(name), check, "The widget \"" + shortText(name) + "\" is not available to the assistant.");
return;
}
String name = requiredString(in, "name");
if(qInstance.getWidget(name) == null)
{
throw (new AiToolException(AiToolOutcome.NOT_FOUND, "No widget named " + name + "."));
@@ -1,186 +0,0 @@
/*
* Copyright 2026 Kingsrook, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.assistant.tools;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import com.kingsrook.qbits.assistant.access.DataAccessMode;
import com.kingsrook.qbits.assistant.testing.AssistantTestFixture;
import com.kingsrook.qbits.assistant.testing.BaseAssistantTest;
import com.kingsrook.qbits.assistant.testing.ToolCalls;
import com.kingsrook.qbits.assistant.tools.registry.ToolCallOutcome;
import com.kingsrook.qqq.backend.core.actions.ai.AiToolOutcome;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.RawHTML;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qbits.assistant.testing.AssistantTestFixture.EDITOR;
import static org.assertj.core.api.Assertions.assertThat;
/*******************************************************************************
** render_widget follows the assistant's own rules, not only the person's
** permission to see the widget. A widget is code of the application that
** can show rows of any table, so a table the assistant may not read could be
** read through one: the widget is rendered only when the host allowed it by
** name, and only while the assistant may read every table the host said it
** reads.
**
** The application has one widget, which anybody may see on a screen and
** which shows a row of the blog posts.
*******************************************************************************/
class RenderWidgetAccessTest extends BaseAssistantTest
{
private static final String WIDGET = "latestPosts";
private static final String ROW = "ROW_OF_A_TABLE_8c41";
private static final AtomicInteger RENDERED = new AtomicInteger();
/*******************************************************************************
**
*******************************************************************************/
@Override
protected AssistantTestFixture.InstanceCustomizer instanceCustomizer()
{
return (RenderWidgetAccessTest::customize);
}
/*******************************************************************************
**
*******************************************************************************/
private static void customize(QInstance qInstance)
{
qInstance.addWidget(new QWidgetMetaData().withName(WIDGET).withLabel("Latest Posts").withType(WidgetType.HTML.getType()).withCodeReference(new QCodeReference(Renderer.class))
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED)));
}
/*******************************************************************************
**
*******************************************************************************/
@BeforeEach
void reset()
{
RENDERED.set(0);
}
/*******************************************************************************
** No widget is renderable until the host names one: the default is closed.
** The refusal is the assistant's own, before the application is asked, and
** is the same for a widget that does not exist.
*******************************************************************************/
@Test
void noWidgetIsRenderedUntilTheHostAllowsIt() throws Exception
{
ToolCallOutcome refused = call(WIDGET);
assertThat(refused.result().getOutcome()).isEqualTo(AiToolOutcome.DENIED);
assertThat(refused.result().getPermissionCheck()).isEqualTo("widget:notAllowed");
assertThat(refused.result().getModelContent()).isEqualTo("The widget \"" + WIDGET + "\" is not available to the assistant.").doesNotContain(ROW);
assertThat(refused.result().getRender()).isNull();
assertThat(RENDERED.get()).as("the application was not asked to render it").isZero();
assertThat(call("noSuchWidget").result().getModelContent()).as("and it does not say whether a widget exists").isEqualTo("The widget \"noSuchWidget\" is not available to the assistant.");
config.withAllowedWidgetNames(new LinkedHashSet<>(List.of(WIDGET)));
ToolCallOutcome rendered = call(WIDGET);
assertThat(rendered.result().getOutcome()).as(rendered.result().getModelContent()).isEqualTo(AiToolOutcome.OK);
assertThat(rendered.result().getModelContent()).contains(ROW);
assertThat(RENDERED.get()).isEqualTo(1);
config.withHiddenWidgetNames(new LinkedHashSet<>(List.of(WIDGET)));
assertThat(call(WIDGET).result().getPermissionCheck()).as("a widget the host hid is refused also when it is allowed").isEqualTo("widget:hidden");
assertThat(RENDERED.get()).isEqualTo(1);
}
/*******************************************************************************
** A widget the host allowed and said reads a table is rendered only while
** the assistant may read that table: a pin in code, a table the host named
** that does not exist, and an administrator's row each close it, and the
** rows of the table do not reach the model through the widget.
*******************************************************************************/
@Test
void aWidgetIsNotRenderedWhenTheAssistantMayNotReadATableItReads() throws Exception
{
config.withAllowedWidgetNames(new LinkedHashSet<>(List.of(WIDGET))).withWidgetTables(WIDGET, Set.of("blog_post", "page"));
assertThat(call(WIDGET).result().getOutcome()).as("both tables may be read").isEqualTo(AiToolOutcome.OK);
assertThat(RENDERED.get()).isEqualTo(1);
config.withTableAccess("blog_post", DataAccessMode.NONE);
config.getRuntime().getDataAccessService().invalidate();
ToolCallOutcome closed = call(WIDGET);
assertThat(closed.result().getOutcome()).isEqualTo(AiToolOutcome.DENIED);
assertThat(closed.result().getPermissionCheck()).isEqualTo("dataAccess:table:NONE");
assertThat(closed.result().getModelContent()).isEqualTo("The widget \"" + WIDGET + "\" is not available to the assistant.").doesNotContain(ROW);
assertThat(RENDERED.get()).as("the application was not asked to render it").isEqualTo(1);
config.withTableAccess("blog_post", DataAccessMode.READ);
config.getRuntime().getDataAccessService().invalidate();
assertThat(call(WIDGET).result().getOutcome()).as("a table the assistant may only read is read").isEqualTo(AiToolOutcome.OK);
config.withWidgetTables(WIDGET, Set.of("page", "noSuchTable"));
assertThat(call(WIDGET).result().getOutcome()).as("a table that is not there is not readable").isEqualTo(AiToolOutcome.DENIED);
}
/*******************************************************************************
** One render_widget call of the fixture's editor, logged.
*******************************************************************************/
private ToolCallOutcome call(String widget) throws Exception
{
return (config.getRuntime().getToolCalls().executeAndLog(ToolCalls.call(config, EDITOR, UiApiToolbox.RENDER_WIDGET, Map.of("name", widget)), null, false));
}
/*******************************************************************************
** The widget: it shows a row of a table.
*******************************************************************************/
public static class Renderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
RENDERED.incrementAndGet();
return (new RenderWidgetOutput(new RawHTML("Latest Posts", "<p>" + ROW + "</p>")));
}
}
}
@@ -325,10 +325,9 @@ class ToolCallArchitectureTest
if(origin.equals(AssistantRuntime.class.getName()) || origin.startsWith(AssistantRuntime.class.getName() + "$"))
{
///////////////////////////////////////////////////////////////////
// the runtime builds the toolboxes and their families, and //
// hands the record tools the host's rules for widgets //
// the runtime builds the toolboxes and their families //
///////////////////////////////////////////////////////////////////
return ("<init>".equals(name) || "withScheduledDeliveryCheck".equals(name) || "withWidgetRules".equals(name));
return ("<init>".equals(name) || "withScheduledDeliveryCheck".equals(name));
}
if(owner.equals(UiApiToolbox.class.getName()))
{