feat: scheduled assistant tasks as a designated user through the UI API (P-g) #36

Merged
KofTwentyTwo merged 3 commits from feature/p7-scheduled-tasks into develop 2026-10-14 12:18:37 +00:00
KofTwentyTwo commented 2026-10-14 01:37:40 +00:00 (Migrated from github.com)

P-g: scheduled assistant tasks, run as a designated user through the UI API

Spec: docs/DESIGN.md part P, item 5. This is based on develop (after #33 and #34).

Credentials: a designated user, never a system session

  • A task stores runAsUserId and the name of an allow-listed environment variable (withAllowedScheduledCredentialEnvNames) that holds that user's long-lived access token. The token is never stored or logged. credentialStatus is masked and stripped on save.
  • Login: a run logs in exactly as a UI client does: POST /qqq/v1/manageSession with accessToken, through the loopback base URL. The session UUID is resolved through the host auth module, the same lookup the UI request pipeline uses.
  • The session's user must equal runAsUserId before any model call.
  • No QSystemUserSession and no constructed QSession anywhere; ArchUnit is unchanged and green.

Tables

  • assistant_scheduled_task (in the nav as Scheduled tasks; admin):
    • the prompt and the run-as user;
    • the credential reference and the profile;
    • a Quartz cron with a time zone (DST-safe);
    • the output mode: RECORD, NOTIFICATION or BOTH;
    • a write-mode request with per-task admin approval;
    • a per-run cost cap.
  • Approval: it is cleared whenever the prompt, user, credential, profile or write-mode request changes, so an approved prompt cannot be swapped.
  • assistant_scheduled_run (child list, owner-locked): the status, the conversation, the cost and a bounded summary ("see conversation" in HIPAA mode).
  • Conversations gain scheduled_task_id and channel (panel or assistant/scheduled).

Runner (a 60 s QQQ schedule)

  • Claim: a due task is claimed through a database-unique (task, scheduledFor) run key (RC.5 has no conditional update), so two replicas cannot both run it. next_run_at advances before login.
  • The turn: each run is a normal turn on a new conversation owned by the run-as user. Every UI API call carries X-QQQ-Client: assistant/scheduled/<taskId>, and the tool-call log records the channel. Core enforcement of the channel arrives with the client-channel RC.
  • Rules that apply unchanged: PHI routing (P-c), budgets plus the per-run cap (P-d), and routing and fallback (P-e).
  • Write tools: offered only when the task requested write mode, an admin approved it, and the policy allows it. Write mode itself (#21) is not on develop yet, so until then the approval is only stored. Writes are never auto-confirmed by the scheduler.
  • Output: the conversation and the run row, and/or a notification through core alerts.
  • Record actions: Approve write mode, Revoke write mode, and Run now.

Tests

qbit-assistant has 206 tests; the full build is green in ./mvnw verify with the JaCoCo gates.

  • Runs: a due task runs once as the designated user; cron and time zone (including DST).
  • UI API: calls carry the scheduled header and use that user's session, and a table that user cannot read gets the same 403 as in the UI.
  • Credentials: a missing env var, a disallowed name, or a token belonging to another user all fail before any model call.
  • Write approval: approval rules, and approval is cleared on edit.
  • Claims: racing runners produce one run.
  • Output and caps: the output modes, the HIPAA-mode summary, and the cost cap and budgets.

kof22 (owner step, documented)

Provision an Authentik designated account with normal QQQ permissions, and an access token that the host auth accepts as accessToken, delivered through an ExternalSecret into an allow-listed env var. Nothing is provisioned in this PR.

🤖 Generated with Claude Code

## P-g: scheduled assistant tasks, run as a designated user through the UI API Spec: `docs/DESIGN.md` part P, item 5. This is based on `develop` (after #33 and #34). ### Credentials: a designated user, never a system session - A task stores `runAsUserId` and **the name** of an allow-listed environment variable (`withAllowedScheduledCredentialEnvNames`) that holds that user's long-lived access token. The token is never stored or logged. `credentialStatus` is masked and stripped on save. - **Login:** a run logs in exactly as a UI client does: `POST /qqq/v1/manageSession` with `accessToken`, through the loopback base URL. The session UUID is resolved through the host auth module, the same lookup the UI request pipeline uses. - The session's user must equal `runAsUserId` before any model call. - No `QSystemUserSession` and no constructed `QSession` anywhere; ArchUnit is unchanged and green. ### Tables - **`assistant_scheduled_task`** (in the nav as Scheduled tasks; admin): - the prompt and the run-as user; - the credential reference and the profile; - a Quartz cron with a time zone (DST-safe); - the output mode: RECORD, NOTIFICATION or BOTH; - a write-mode request **with per-task admin approval**; - a per-run cost cap. - **Approval:** it is cleared whenever the prompt, user, credential, profile or write-mode request changes, so an approved prompt cannot be swapped. - **`assistant_scheduled_run`** (child list, owner-locked): the status, the conversation, the cost and a bounded summary ("see conversation" in HIPAA mode). - Conversations gain `scheduled_task_id` and `channel` (`panel` or `assistant/scheduled`). ### Runner (a 60 s QQQ schedule) - **Claim:** a due task is claimed through a database-unique `(task, scheduledFor)` run key (RC.5 has no conditional update), so two replicas cannot both run it. `next_run_at` advances before login. - **The turn:** each run is a normal turn on a new conversation owned by the run-as user. Every UI API call carries `X-QQQ-Client: assistant/scheduled/<taskId>`, and the tool-call log records the channel. Core enforcement of the channel arrives with the client-channel RC. - **Rules that apply unchanged:** PHI routing (P-c), budgets plus the per-run cap (P-d), and routing and fallback (P-e). - **Write tools:** offered only when the task requested write mode, an admin approved it, and the policy allows it. Write mode itself (#21) is not on develop yet, so until then the approval is only stored. Writes are never auto-confirmed by the scheduler. - **Output:** the conversation and the run row, and/or a notification through core alerts. - **Record actions:** Approve write mode, Revoke write mode, and Run now. ### Tests qbit-assistant has 206 tests; the full build is green in `./mvnw verify` with the JaCoCo gates. - **Runs:** a due task runs once as the designated user; cron and time zone (including DST). - **UI API:** calls carry the scheduled header and use that user's session, and a table that user cannot read gets the same 403 as in the UI. - **Credentials:** a missing env var, a disallowed name, or a token belonging to another user all fail before any model call. - **Write approval:** approval rules, and approval is cleared on edit. - **Claims:** racing runners produce one run. - **Output and caps:** the output modes, the HIPAA-mode summary, and the cost cap and budgets. ### kof22 (owner step, documented) Provision an Authentik designated account with normal QQQ permissions, and an access token that the host auth accepts as `accessToken`, delivered through an ExternalSecret into an allow-listed env var. Nothing is provisioned in this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.