You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: user-scope console chat and resolve chat credentials from the sender (#1982)
* fix: user-scope console chat and resolve chat credentials from the sender
On multi-user installs every user shared one per-project console chat, and
the orchestrator resolved per-user AI prefs + provider credentials from
conversation.user_id (the first creator) — so another user's turns executed
on the creator's API keys and model prefs.
- orchestrator: introduce executionUserId = sender ?? conversation.user_id
and use it at both resolution seams (prefs + provider env) and in the
profile-invalid log. Mirrors the workflow executor's run-starter
resolution. Creator stays as fallback, so solo installs are unchanged.
- console: listConversations now passes the existing non-enforcing
mine=true filter, giving each signed-in user their own lazily-created
per-project conversation (and thus their own AI session).
- tests: divergence coverage for sender-vs-creator in both the prefs and
credential-injection suites, plus an explicit creator-fallback case.
Closes#1976
* docs: sync CLAUDE.md prefs-seam description with sender-first resolution
The user_ai_prefs section still described the chat seam as keyed on
conversation.user_id; it now resolves sender-first with the creator as
fallback. Also records the accepted shared-thread consequences (per-turn
provider variance on Slack/Telegram threads; shared history rides the
sender's billed call).
* docs: sender-first prefs resolution in database reference (matches CLAUDE.md sync)
* review: thread gate/resume actor identity, trace identity fallbacks, harden tests
Addresses the multi-agent review on #1982 (I/S ids from that comment):
- I1: the two system-initiated dispatch sites no longer execute on the
conversation creator. tryAutoResumeAfterGate takes the gate actor's
identity (resolved via resolveWebUserId at the approve/reject routes —
both callers are HTTP routes with the request in scope, so threading
beats the suggested comment-as-intentional), and the run-resume route
dispatches as the resuming user. Solo installs pass undefined → creator
fallback, unchanged.
- I2: orchestrator warns (orchestrator.execution_identity_creator_fallback)
when a turn arrives without sender identity while per-user credentials
are active — makes a degraded auth resolution distinguishable from the
silent solo-install path.
- S1: three test hardenings — env-seam fallback argument pinned
('u-test'), combined both-seams-same-identity test (guards seam
divergence), profile-invalid log attribution asserted ('sender-2').
- S2: GET /api/conversations warns (api.mine_filter_identity_unresolved)
when mine=true narrowing was requested but no identity resolved on a
web-auth install; silent without web auth since the console always
sends mine=true on solo installs.
- S3: CLAUDE.md sweep — conversations entry (provenance vs execution),
user_provider_keys (acting user's env), user_ai_prefs table entry
(sender-first seam description).
Test adjustment: 'skips injection when feature is disabled' now uses a
persistent mockReturnValue — the install-level flag is read twice per
turn (I2 guard + env seam), so Once was incidental, not semantic.
---------
Co-authored-by: Archon Maintainer Bot <maintainer-implementer@archon.local>
Copy file name to clipboardExpand all lines: CLAUDE.md
+4-4Lines changed: 4 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -454,7 +454,7 @@ import type { DagNode, WorkflowDefinition } from '@/lib/api';
454
454
455
455
**18 Tables (all prefixed with `remote_agent_`):**
456
456
1.**`codebases`** - Repository metadata and commands (JSONB)
457
-
2.**`conversations`** - Track platform conversations with titles and soft-delete support; nullable `user_id` records first creator
457
+
2.**`conversations`** - Track platform conversations with titles and soft-delete support; nullable `user_id` records first creator (provenance + execution-identity **fallback** only — chat turns execute as the message sender, #1982)
458
458
3.**`sessions`** - Track AI SDK sessions with resume capability
11.**`workflow_node_sessions`** - Per-node provider session IDs persisted across workflow re-runs (opt-in via `persist_session`); keyed by `(workflow_name, node_id, scope_key, provider)`; `scope_key` is typically the conversation UUID
467
467
12.**`user_github_tokens`** - Per-user GitHub device-flow tokens encrypted at rest (AES-256-GCM); one row per Archon user (`UNIQUE(user_id)`), cascades on user deletion; numeric `github_user_id` anchors the commit no-reply email
468
-
13.**`user_provider_keys`** - Per-user AI-provider credentials encrypted at rest (AES-256-GCM, same `TOKEN_ENCRYPTION_KEY`); one row per `(user_id, provider)` (`UNIQUE(user_id, provider)`), cascades on user deletion; `kind` is `api_key` or `oauth`; resolved + injected into the user's runs/chat env at execution time. Gated on `TOKEN_ENCRYPTION_KEY`. Since #1955 the `provider` column holds **vendor-canonical credential ids** (`anthropic`, `openai`, `github-copilot`, plus the Pi backend vendors) — NOT agent ids; legacy `claude`/`codex`/`copilot` rows are renamed by an idempotent startup data fix (vendor row wins on conflict), and the connectable catalog is derived from provider registrations (`acceptedCredentials` via `credentials:` on `ProviderRegistration`), never hand-listed
469
-
14.**`user_ai_prefs`** - Per-user AI preferences (Phase 3): personal model `tiers`/`aliases` (JSON-as-TEXT) + `default_provider`. NON-encrypted (model names aren't secrets — mirrors `codebase_env_vars`, not the provider-key store); one row per user (`UNIQUE(user_id)`), cascades on user deletion. Folded into `buildAiProfile` as the highest-precedence layer at the userId-aware seams (workflow executor + chat orchestrator); needs a web/CLI identity but NO `TOKEN_ENCRYPTION_KEY`
468
+
13.**`user_provider_keys`** - Per-user AI-provider credentials encrypted at rest (AES-256-GCM, same `TOKEN_ENCRYPTION_KEY`); one row per `(user_id, provider)` (`UNIQUE(user_id, provider)`), cascades on user deletion; `kind` is `api_key` or `oauth`; resolved + injected into the **acting user's** (run starter / message sender) runs/chat env at execution time. Gated on `TOKEN_ENCRYPTION_KEY`. Since #1955 the `provider` column holds **vendor-canonical credential ids** (`anthropic`, `openai`, `github-copilot`, plus the Pi backend vendors) — NOT agent ids; legacy `claude`/`codex`/`copilot` rows are renamed by an idempotent startup data fix (vendor row wins on conflict), and the connectable catalog is derived from provider registrations (`acceptedCredentials` via `credentials:` on `ProviderRegistration`), never hand-listed
469
+
14.**`user_ai_prefs`** - Per-user AI preferences (Phase 3): personal model `tiers`/`aliases` (JSON-as-TEXT) + `default_provider`. NON-encrypted (model names aren't secrets — mirrors `codebase_env_vars`, not the provider-key store); one row per user (`UNIQUE(user_id)`), cascades on user deletion. Folded into `buildAiProfile` as the highest-precedence layer at the userId-aware seams (workflow executor: run starter; chat orchestrator: message **sender**-first, conversation creator only as fallback — #1982); needs a web/CLI identity but NO `TOKEN_ENCRYPTION_KEY`
470
470
15–18. **`remote_agent_auth_user` / `remote_agent_auth_session` / `remote_agent_auth_account` / `remote_agent_auth_verification`** - Better Auth tables for opt-in web login (**PostgreSQL only**; always created on Postgres via the idempotent schema apply, but populated only when web auth is enabled — `DATABASE_URL` + `BETTER_AUTH_SECRET`). Owned and shaped by Better Auth (text ids, camelCase columns); Archon never queries them directly — a session maps to the canonical `users` row via `user_identities('web', <betterAuthUserId>)`
471
471
472
472
**Key Patterns:**
@@ -930,7 +930,7 @@ Pattern: Use `classifyIsolationError()` (from `@archon/isolation`) to map git er
930
930
**Per-User AI Prefs (Phase 3; `requireWebUser` — identity only, NO `TOKEN_ENCRYPTION_KEY`):**
931
931
-`GET /api/auth/me/ai-prefs` - The current user's stored prefs (raw per-user layer, not merged with config); returns `{ tiers?, aliases?, defaultProvider? }`. 401 without identity — the console hides "Just me" on failure.
932
932
-`PATCH /api/auth/me/ai-prefs/tiers` / `…/aliases` / `…/default` - Per-key merge writes (`null` unsets); validate provider via `isRegisteredProvider`, effort via `isEffortValidForProvider`, alias names (`@` prefix, not a reserved tier keyword). All return the updated prefs.
933
-
- Stored in `remote_agent_user_ai_prefs` (non-encrypted); folded into `buildAiProfile` as the **highest-precedence** layer (global < repo < user) at the userId-aware seams — workflow executor (`deps.getUserAiPrefs`) and chat orchestrator (`conversation.user_id`). The per-user `defaultProvider` rebases tier defaults and the chat assistant. No identity → byte-for-byte config-only behavior (solo unchanged). A chat request for tier `large` that resolves via the fallback chain emits a one-line non-blocking nudge (`orchestrator.tier_fallback_nudge`).
933
+
- Stored in `remote_agent_user_ai_prefs` (non-encrypted); folded into `buildAiProfile` as the **highest-precedence** layer (global < repo < user) at the userId-aware seams — workflow executor (`deps.getUserAiPrefs`, resolved from the run starter) and chat orchestrator (sender-first: `executionUserId = context.userId ?? conversation.user_id` — the SENDER's prefs and credentials win; the conversation creator is only the fallback when no sender identity resolves, see #1982). The per-user `defaultProvider` rebases tier defaults and the chat assistant. No identity → byte-for-byte config-only behavior (solo unchanged). A chat request for tier `large` that resolves via the fallback chain emits a one-line non-blocking nudge (`orchestrator.tier_fallback_nudge`). Note: on genuinely shared threads (Slack/Telegram), per-sender prefs mean the provider can differ per turn within one thread (session transitions churn accordingly), and a sender's turn carries the shared thread history into a call billed to their credential — accepted semantics.
934
934
935
935
**Config (System; ungated — works on solo installs, NOT `requireWebUser`):**
936
936
-`GET /api/config` - Read-only safe config; returns `{ config, database }`. `config` includes `tiers` (configured small/medium/large presets), `tierDefaults` (built-in presets for the default provider, computed via `buildAiProfile` — lets the UI show what an unset tier resolves to), and `aliases` (configured `@custom` aliases, merged repo > global).
Copy file name to clipboardExpand all lines: packages/docs-web/src/content/docs/reference/database.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -140,7 +140,7 @@ The database has 18 tables, all prefixed with `remote_agent_`:
140
140
141
141
14.**`remote_agent_user_ai_prefs`** - Per-user AI preferences (personal model tiers, `@custom` aliases, default assistant)
142
142
- NON-encrypted (model names aren't secrets); one row per user (`UNIQUE(user_id)`), cascades on user deletion
143
-
-`tiers` / `aliases` are JSON-as-TEXT; folded into model resolution as the highest-precedence layer for runs/chats that user starts
143
+
-`tiers` / `aliases` are JSON-as-TEXT; folded into model resolution as the highest-precedence layer. Resolution follows the **acting user**: workflow runs use the run starter; chat turns use the message **sender** (the conversation creator's row is only a fallback when no sender identity resolves)
144
144
- Editable via the console "Just me" scope, `archon ai … --scope user`, or `/api/auth/me/ai-prefs*`
145
145
146
146
15–18. **`remote_agent_auth_user` / `remote_agent_auth_session` / `remote_agent_auth_account` / `remote_agent_auth_verification`** - Better Auth tables for opt-in web login
0 commit comments