Skip to content

Personal assistant governance: prompt library + tenant policy + runtime enforcement#417

Merged
MaxEriksson2000 merged 49 commits into
developfrom
feature/personal-chat-governance-plan
Jun 12, 2026
Merged

Personal assistant governance: prompt library + tenant policy + runtime enforcement#417
MaxEriksson2000 merged 49 commits into
developfrom
feature/personal-chat-governance-plan

Conversation

@MaxEriksson2000

@MaxEriksson2000 MaxEriksson2000 commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Tenant-level governance for the personal assistant (the default assistant in a user-owned personal space). A central admin can control which completion models, which MCP servers (and which of their tools), and which default system prompt the personal assistant may use — without configuring every user's default assistant individually.

The policy is modelled as data in tables that flows upward through pure, testable layers all the way to the UI, with a single pure resolver as the source of truth for "what is allowed". The same resolver drives both UI hints (read path) and runtime enforcement (write/ask path), so they can never drift apart.

A standalone architecture write-up lives at docs/plans/personal-chat-governance/architecture.html (Swedish discussion doc); review findings are tracked in docs/review-personal-chat-governance.md.

Scope of this PR

The governance feature is the headline, but this branch has accumulated alongside it and the diff (≈204 files) also lands the following. They are grouped here so reviewers know what to expect; the bulk of the description below still focuses on governance.

Area What it adds Where
Governance + prompt library (main feature) Tenant policy, pure resolver, 3-layer enforcement, admin UI governance_policy/, prompt_library/, admin/personal-assistant/
Prompt-library versioning Immutable version snapshots + current_version pointer per entry prompt_library_versions table, PromptLibraryVersion
Reasoning / chain-of-thought trace Persist + stream model reasoning and tool-call steps into a collapsible trace questions.reasoning column, ReasoningTrace/ReasoningToolStep, session/conversation persistence
AI Elements chat composition (folds in #488) Compound model-selector + prompt-input components, redesigned personal-chat composer lib/components/ai-elements/{model-selector,prompt-input}/, ChatComposer
shadcn primitives sidebar (26 files) + sheet (11 files) used by the admin/navigation layout lib/components/ui/{sidebar,sheet}/

What it does

The policy is per tenant and today targets a single scope: PERSONAL_DEFAULT_ASSISTANT. It has three independent dimensions, each opt-in:

Dimension Flag Meaning
Model restriction models_restriction_enabled Which completion models are selectable. Set as explicit models and/or whole providers (a whitelisted provider grants all of its current and future models).
MCP restriction mcp_restriction_enabled Which MCP servers are available, with a per-server "on by default" seed and an optional tool-level deny-set (switch individual tools off on an allowed server). Enabled + empty server list = explicit deny-all.
Prompt enforcement prompt_enforcement_enabled Forces a shared system prompt from the prompt library. The prompt text is never exposed to end users.

Key idea: all three *_restriction_enabled flags default to False. An auto-created "empty" policy produces no visible change — the feature is opt-in per dimension. This deliberately distinguishes "no restriction" from "deny all" (enabled + empty list).

For end users

  • Behaviour is unchanged until a policy is enabled.
  • Model policy active → only allowed models are visible; if exactly one is allowed, the switcher is hidden and shown as locked.
  • Prompt policy active → the user's saved prompt is visible but disabled and explicitly marked as governed by organization policy.
  • MCP policy active → only allowed MCP servers are selectable, denied tools are hidden, and stale disallowed selections are pruned from edit state.

Design principle: policy is data, not code

  • Policy lives in tables. scope is stored as a string, not a DB enum, with a composite unique key (tenant_id, scope). New scopes (shared assistants, workspace assistants) can be added as data later — no schema migration.
  • One pure resolver in the middle. policy_resolver.resolve() is side-effect-free: no DB calls, no awaits. It takes (assistant, scope, policy, tenant context) and returns an immutable EffectiveConfig. The same function powers read-path hints and ask-time enforcement.
  • Clean layers, clear ownership. domain/ owns business rules, application/ owns I/O orchestration, infrastructure/ owns SQL, presentation/ owns the HTTP contract. Dependencies point inward; the domain knows nothing about FastAPI or SQLAlchemy.
  • Defense in depth, not UI trust. The UI only hints (greys out disallowed models). Enforcement happens server-side at ask-time — stale entity state or direct API calls cannot bypass the policy.

Layer architecture

An admin update travels top-down (UI → API → service → repo → tables). A run travels bottom-up (tables → repo → resolver → enforcement in the chat flow). The resolver sits in the middle and is shared.

Data model

One main table + four M:N join tables, all tenant-isolated. Migrations: 202605211600_create_governance_policy.py, 202605221000_governance_policy_providers.py, 202606091000_governance_policy_mcp_defaults_and_tools.py.

erDiagram
  tenants ||--o| governance_policies : owns
  users ||--o{ governance_policies : updated_by
  prompt_library ||--o{ governance_policies : default_prompt
  governance_policies ||--o{ governance_policy_completion_models : allows
  completion_models ||--o{ governance_policy_completion_models : allowed_model
  governance_policies ||--o{ governance_policy_providers : allows
  model_providers ||--o{ governance_policy_providers : allowed_provider
  governance_policies ||--o{ governance_policy_mcp_servers : allows
  mcp_servers ||--o{ governance_policy_mcp_servers : allowed_server
  governance_policies ||--o{ governance_policy_disabled_mcp_tools : denies
  mcp_server_tools ||--o{ governance_policy_disabled_mcp_tools : denied_tool

  governance_policies {
    uuid id PK
    uuid tenant_id FK
    string scope "PolicyScope value"
    boolean models_restriction_enabled
    boolean mcp_restriction_enabled
    boolean prompt_enforcement_enabled
    uuid default_prompt_library_id FK "RESTRICT"
    uuid updated_by_user_id FK "SET NULL"
  }
  governance_policy_completion_models {
    uuid policy_id PK_FK
    uuid completion_model_id PK_FK
    boolean is_default
  }
  governance_policy_providers {
    uuid policy_id PK_FK
    uuid model_provider_id PK_FK
  }
  governance_policy_mcp_servers {
    uuid policy_id PK_FK
    uuid mcp_server_id PK_FK
    boolean is_default_enabled "seed only; user can toggle per chat"
  }
  governance_policy_disabled_mcp_tools {
    uuid policy_id PK_FK
    uuid mcp_tool_id PK_FK
  }
Loading

Constraints

  • UNIQUE (tenant_id, scope) — one policy per scope per tenant.
  • CHECK: NOT prompt_enforcement_enabled OR default_prompt_library_id IS NOT NULL — cannot enforce a prompt without pointing at one.
  • Partial unique index uniq_policy_default_model on (policy_id) WHERE is_default — at most one default model per policy.

Provider whitelist = subscription, not snapshot. The effective allowed model set is the union of explicitly listed models (_completion_models) and "all org-enabled models under any provider in _providers". Whitelisting a provider therefore automatically grants its future models — no recuration required.

MCP tools = allow-by-default, deny-set. A whitelisted server's tools are allowed unless explicitly listed in governance_policy_disabled_mcp_tools. Tools synced onto a server later are therefore allowed automatically; only the admin's explicit "off" choices persist.

Domain model

Plain Python dataclass, no DB/HTTP dependencies. Business rules live in the setters. backend/src/intric/governance_policy/domain/governance_policy.py

@dataclass
class GovernancePolicy:
    id: UUID | None
    tenant_id: UUID
    scope: PolicyScope                      # today only PERSONAL_DEFAULT_ASSISTANT

    models_restriction_enabled: bool = False
    mcp_restriction_enabled: bool = False
    prompt_enforcement_enabled: bool = False

    completion_models: list[PolicyCompletionModel] = field(default_factory=list)
    model_provider_ids: list[UUID]         = field(default_factory=list)
    mcp_server_ids: list[UUID]             = field(default_factory=list)
    default_prompt_library_id: UUID | None = None

    def set_models_restriction(self, *, enabled, models, provider_ids=None): ...
    def set_mcp_restriction(self, *, enabled, ids): ...
    def set_prompt_enforcement(self, *, enabled, prompt_library_id): ...

Setters validate invariants in the domain, regardless of caller: no duplicates, at most one default model, enabled model restriction requires at least one model or provider, enabled prompt enforcement requires a chosen prompt. Tenant/space membership (that the IDs actually belong to the tenant) is validated in the service layer, since it requires I/O.

The pure resolver — the heart of the design

Side-effect-free: (assistant, scope, policy, tenant context) → EffectiveConfig. No DB calls, no awaits, trivial to unit-test. backend/src/intric/governance_policy/domain/policy_resolver.py

def resolve(*, assistant, space_is_personal, policy,
            tenant_completion_models, tenant_mcp_servers,
            library_prompt_text) -> EffectiveConfig:

    # Short-circuit to "no enforcement" when the policy doesn't apply here:
    if not assistant.is_default or not space_is_personal or policy is None:
        return _EMPTY

    # MODELS: union of explicitly whitelisted + all models from a whitelisted provider
    # if exactly one available → lock it in the UI; track the admin-chosen default
    # MCP: filter the tenant's servers against the policy whitelist + tool deny-set
    # PROMPT: inject library text — but only if it actually exists (fail-safe)
    return EffectiveConfig(models_enforced=..., available_models=..., locked_model=...,
                           mcp_enforced=..., prompt_enforced=..., enforced_prompt_text=...)
  • Safe to call everywhere. For non-default assistants, non-personal spaces, or a missing policy it returns _EMPTY (all *_enforced = False) → "behave as before". No special cases at the call site.
  • EffectiveConfigService does the I/O. The application layer fetches policy + the tenant's models/MCP/prompt text and calls the pure resolver. The resolver stays pure; orchestration lives outside.

Service & API contract

Service Role
GovernancePolicyService Admin path (Permission.ADMIN). get_policy() auto-creates an empty policy if none exists; update_policy(models_restriction=, mcp_restriction=, prompt_enforcement=) validates tenant membership, delegates to the domain setters, saves via repo.save().
EffectiveConfigService Run/read path. Bridge between repos and the pure resolver: fetch policy → fetch tenant models + MCP → fetch prompt text → resolve(...).
GET  /api/v1/admin/governance-policy/   → GovernancePolicyPublic   (403 if not admin)
PUT  /api/v1/admin/governance-policy/   → GovernancePolicyPublic   (400 / 403)

# Partial request — only the dimensions you want to change are sent:
GovernancePolicyUpdate {
  models_restriction?:  { enabled, models[{completion_model_id, is_default}], provider_ids[] }
  mcp_restriction?:     { enabled, server_ids[], disabled_tool_ids[] }
  prompt_enforcement?:  { enabled, prompt_library_id }
}

An Assembler translates domain ↔ DTO. In EffectiveConfigPublic (which rides along with the assistant to the UI) the prompt is exposed only as the boolean prompt_lockednever the text itself.

The prompt library has its own tenant-scoped admin CRUD at /api/v1/admin/prompt-library/, kept separate from the immutable prompts history table; deleting a prompt referenced by a policy returns a friendly 409 instead of a raw FK violation.

Prompt-library versioning

Each prompt-library entry keeps an append-only history: every edit writes a row to prompt_library_versions (immutable name/description/text/created_by snapshot) and bumps prompt_library.current_version. This gives an audit trail of what a governed prompt said over time, separate from the live editable entry. Migrations: 202605211500_create_prompt_library.py, 202605221100_prompt_library_versions.py.

Frontend

Route /admin/personal-assistant/configuration (+ /admin/personal-assistant/prompts). Thin page, "fat" draft class.

  • Parallel loader (+page.ts): Promise.all over governancePolicy.get(), models, MCP settings, promptLibrary.list(), model providers; event.depends("admin:governance-policy").
  • PolicyDraft (policyDraft.svelte.ts) owns all interactive state with Svelte 5 runes: $state (per-dimension toggles + selections), $derived (dirty, canSave, effectiveModelIds = explicit ∪ provider, defaultModelId, readable summaries), and save() (builds confirmations for destructive changes → update()invalidate() → reseeds baseline).
  • Sections: ModelRestrictionSection, McpRestrictionSection, PromptEnforcementSection, plus a sticky PolicySaveBar and PolicyConfirmDialog.
  • Mirrored, not duplicated, logic. The frontend computes effectiveModelIds locally for instant UX, but that is only a mirror — truth and enforcement live in the backend resolver. The UI can never "allow" anything on its own.
  • All human-facing text goes through paraglide m.governance_* (en + sv).

Enforcement — defense in depth (3 layers)

The resolver is shared across all three; UI filtering alone is not enough.

flowchart LR
  Read["1 · Read path (UI hint)<br/>get_assistant_with_effective_config()"] -->|EffectiveConfigPublic, prompt_locked only| UI["Frontend: grey out / locked / 'prompt enforced' badge"]
  Write["2 · Update validation<br/>_ensure_governance_policy_allows_update()"] -->|disallowed model/MCP| Reject["400 BadRequest"]
  Ask["3 · Ask-time (the guarantee)<br/>AssistantService.ask"] -->|model/MCP/prompt override| LLM["Effective LLM request"]
  Read -. never leaks .-> Secret["admin prompt text"]
  Ask --> Secret
Loading
  1. Read path (UI hint) — exposes EffectiveConfigPublic so the UI can grey out disallowed models, show the locked state, and a "prompt enforced" badge. prompt_locked: bool — text never leaks.
  2. Update validation_ensure_governance_policy_allows_update() blocks saving an assistant with a disallowed model/MCP. Grandfathering: only newly added MCP servers must pass the policy; existing ones can be re-saved.
  3. Ask-time (the core) — in assistant_service.ask(), model / MCP / prompt are overridden at run time. This is the only layer that guarantees compliance against stale state and direct API callers.
effective_config = await self._resolve_effective_config(space=space, assistant=assistant_to_ask)
if effective_config is not None:
    if effective_config.models_enforced:
        # stale assignment? → fall back to policy default, then first allowed.
        if current_model is None or current_model.id not in allowed_ids:
            fallback = effective_config.policy_default_model or first_allowed
            if fallback is None:
                raise BadRequestException("...no allowed models — contact admin")
            completion_model_override = fallback
    if effective_config.mcp_enforced:
        mcp_servers_override = [s for s in assistant.mcp_servers if s.id in allowed_mcp_ids]
    if effective_config.prompt_enforced and effective_config.enforced_prompt_text:
        prompt_override = effective_config.enforced_prompt_text

Fail-safe: an empty model whitelist + enabled restriction makes the assistant refuse to answer (clear error, ask admin to act) rather than silently falling back to a disallowed model. The _handle_response() and returned AssistantResponse use the effective model, not stale assistant metadata.

Decisions worth discussing

These are deliberate trade-offs and open questions (the architecture doc frames them for review):

  • Scope as string, not DB enum — future scopes as data vs. YAGNI. Which scopes do we actually foresee?
  • Provider whitelist = subscription to future models — convenient, but a new model becomes available without an admin decision. Do we want that automation, or opt-in even under a whitelisted provider?
  • MCP tools allow-by-default (deny-set) — newly synced tools are auto-allowed on a whitelisted server. Same subscription trade-off as providers: convenient vs. silent.
  • One pure resolver shared by read + write paths — they can never drift, at the cost of keeping the resolver strictly side-effect-free.
  • Grandfathering on tightening — existing assistants can re-save with already-assigned (now-disallowed) MCP servers; only new additions are validated (ask-time still overrides at run time). Right call, or should tightening force cleanup?
  • Prompt text never exposed — UI only gets prompt_locked: bool. Is a boolean enough, or should admins/users see that a prompt applies (name) without content?
  • Empty whitelist = "deny all" vs. fail-safe refusal — is "refuse with an error" the right UX, or should the UI make that state unreachable?

File overview

backend/src/intric/governance_policy/
├── domain/        governance_policy.py · governance_policy_repo.py · policy_resolver.py
├── application/   governance_policy_service.py · effective_config_service.py
├── infrastructure/governance_policy_repo_impl.py
└── presentation/  governance_policy_router.py · governance_policy_models.py · governance_policy_assembler.py

backend/src/intric/prompt_library/        # same clean layering, admin CRUD + versions
backend/src/intric/database/tables/governance_policy_table.py · prompt_library_table.py
backend/alembic/versions/202605211500_create_prompt_library.py
                         202605211600_create_governance_policy.py
                         202605221000_governance_policy_providers.py
                         202605221100_prompt_library_versions.py
                         202606091000_governance_policy_mcp_defaults_and_tools.py
                         1d60c8c457d3_add_reasoning_column_to_questions.py
                         202606111200_merge_governance_reasoning_and_develop.py   # single head
backend/src/intric/assistants/assistant_service.py   # 3 enforcement points
backend/src/intric/assistants/api/assistant_models.py # EffectiveConfigPublic
backend/src/intric/main/container/container.py        # DI wiring

# Bundled alongside governance (see "Scope of this PR"):
backend/src/intric/{questions,sessions,conversations}/    # reasoning persistence
frontend/apps/web/src/lib/features/chat/components/conversation/Reasoning*.svelte
frontend/apps/web/src/lib/components/ai-elements/{model-selector,prompt-input}/
frontend/apps/web/src/lib/components/ui/{sidebar,sheet}/

frontend/apps/web/src/routes/(app)/admin/personal-assistant/{configuration,prompts}/
frontend/packages/intric-js/src/endpoints/{governance-policy,prompt-library}.js

Validation

  • Backend unit tests pass, incl. tests/unittests/governance_policy, tests/unittests/prompt_library, tests/unittests/assistants/test_governance_policy_runtime.py.
  • Backend integration tests: governance_policy + prompt_library route suites pass (admin/non-admin, CRUD, validation, ask-time runtime).
  • Frontend: bun run check0 errors, 0 warnings; prettier --check + eslint clean (incl. intric/no-hardcoded-text); i18n en/sv key parity.
  • Single Alembic head (202606111200) — chain re-pointed onto develop's head after merge.

Branch hygiene fixed in this PR

After merging the latest develop:

  • Alembic multiple heads — the governance/prompt-library/reasoning migrations branched from older bases while develop advanced; the chain is re-pointed and merged onto a single head so DB setup (and every integration test) works again.
  • prompt-library integration 401s — token fixtures now depend on patch_auth_service_jwt, matching the governance fixtures.
  • i18n — develop now enforces intric/no-hardcoded-text on all .svelte; remaining Swedish strings in the governance UI moved into messages/{en,sv}.json and called via m.*.

Out of scope

  • data_retention_days governance (needs a separate path in DataRetentionService).
  • Per-user policy overrides.
  • Prompt-library draft/publish workflow (version history exists; an explicit draft-then-publish flow does not).
  • Prompt templating variables ({{user.name}}).
  • A dedicated audit/event trail for policy changes beyond the normal updated metadata.

Adds a separate prompt_library table for editable prompt templates that
can later be shared across personal chat assistants. Kept distinct from
the existing prompts table because that one is an immutable history log
(each update inserts a new row); mixing both lifecycles would corrupt
audit + insights flows.

Includes:
- alembic migration with tenant FK and unique (tenant_id, name) constraint
- prompt_library package (domain entity + repo protocol + sqlalchemy impl
  + tenant-scoped admin service + presentation router with full CRUD)
- admin-only routes under /api/v1/admin/prompt-library/
- unit tests for entity invariants and service auth/duplicate handling
- integration test scaffolding for HTTP round-trip

created_by_user_id is ON DELETE RESTRICT so we never lose authorship
when a user leaves. Subsequent phases will add the cross-service guard
that prevents deleting a prompt referenced by a personal chat policy.
Adds the PersonalChatPolicy entity that backs admin configuration for
default (personal-chat) assistants. Stores three independent restriction
flags so admins can enable / disable model, MCP, and prompt enforcement
separately — and explicit per-dimension flags let us distinguish
"no restriction" from "deny-all" (empty whitelist).

Includes:
- alembic migration with personal_chat_policies + two m2m tables
  (completion models + mcp servers)
- DB-level CHECK constraint: prompt_enforcement_enabled implies a prompt
  is selected (mirrors the service invariant, belt-and-suspenders)
- partial unique index ensuring at most one model is flagged is_default
- domain entity with invariants on set_models_restriction (>=1 model
  when enabled, unique IDs, single default), set_mcp_restriction
  (empty list allowed = deny-all), and set_prompt_enforcement
- application service with get-or-create semantics (auto-created policy
  has all *_restriction_enabled=False so user-facing behaviour is
  unchanged until admin acts)
- tenant-scope validation: completion models, MCP servers, and prompt
  library entries are all verified against the caller's tenant before
  being persisted
- GET / PUT endpoints under /api/v1/admin/personal-chat-policy/
- cross-service guard in PromptLibraryService.delete: if a policy
  references the prompt, raise a friendly 409 with context instead of
  letting the FK ON DELETE RESTRICT surface as a 500
- unit tests for entity invariants

No assistants are affected yet — runtime enforcement lands in the
following commits.
Adds the central decision logic that translates a tenant policy into the
effective configuration for a given assistant, and surfaces it in the
public API so the frontend can render lock state.

Resolver design:
- policy_resolver.resolve() is a pure function in the domain layer:
  no awaits, no DB access. Same inputs always yield the same output,
  making it trivially testable.
- Returns an EffectiveConfig dataclass with separate flags for model,
  MCP, and prompt enforcement plus the filtered lists.
- Short-circuits to "all disabled" for non-default assistants and when
  no policy exists, so any caller can invoke it safely.
- Filters policy whitelist entries against the tenant's actual model /
  MCP catalogue, so stale policy associations don't crash the chat.

EffectiveConfigService (application layer) composes the resolver inputs
from live repos/services. Kept separate from the resolver so the pure
function stays pure.

Wire-up:
- AssistantPublic.effective_config exposes EffectiveConfigPublic to
  the frontend (locked_model, available_models, available_mcp_servers,
  prompt_locked). Note that the admin prompt text is never leaked —
  only the locked boolean.
- assistant_router.get_assistant resolves and attaches effective_config
  when the assistant is_default.
- space_assembler accepts default_assistant_effective_config and
  space_router populates it from the same service. This is the path
  the chat UI reads — space.default_assistant.effective_config — so
  the lock state surfaces without an extra round trip.

Subsequent commits add the matching write-path guards and ask-time
runtime enforcement.
… ask

Wires policy enforcement into the write paths (PUT assistant + the
separate single-MCP-add endpoint) and at ask-time. UI filtering alone is
not enough — stale entity state or direct API callers could otherwise
bypass the policy.

Backend changes:
- Assistant.ask gains optional completion_model_override, mcp_servers_override
  and prompt_override parameters. When set, they take precedence over the
  values stored on the entity. When None, fall back to the entity's own
  values — so non-default assistants are entirely unaffected.
- AssistantService accepts effective_config_service (optional) and, on
  ask, resolves the effective config for default assistants. Stale model
  assignment falls back to the policy default and then the first allowed
  model; refuses only when the whitelist is empty.
- assistant_router.update_assistant rejects (400) attempts to set a
  completion_model_id or mcp_server_ids outside the policy whitelist on
  a default assistant.
- assistant_router.add_mcp_to_assistant gets the same MCP guard so the
  single-server endpoint isn't a back door past the bulk one.
- Container: reorder mcp_server_settings_service / personal_chat_policy_service
  / effective_config_service to appear before assistant_service so the
  DI chain resolves top-to-bottom without forward refs.

Net effect: when policy enforcement is active, the chat receives exactly
what the policy allows — even if assistant.completion_model or
assistant.mcp_servers in the DB has gone stale relative to the latest
policy state, and even if a caller skips the UI entirely.
…end)

Adds the admin-facing configuration surface plus the user-facing lock
indicator that the backend phases enable.

SDK:
- intric.promptLibrary.{list,get,create,update,delete}
- intric.personalChatPolicy.{get,update}
- Regenerated schema.d.ts to pick up the new paths and the
  effective_config field on AssistantPublic.

Admin UI (/admin/personal-chat):
- Tabbed layout — Konfiguration | Promptbibliotek
- /prompts: list / create / edit / delete promptbibliotek-entries.
  Delete shows a 409 explanation if the prompt is referenced by the
  policy.
- /configuration: three cards (models / MCP / prompt) each gated by a
  master switch. Save is blocked until the dimension is internally
  valid (>=1 model, prompt chosen, etc). Confirmation dialog fires for
  the load-bearing transitions: collapsing to a single model, switching
  to deny-all MCP, enabling prompt enforcement.
- New "Personlig chatt" entry in the admin sidebar.

Chat lock display:
- DefaultAssistantModelSwitcher reads
  $currentSpace.default_assistant.effective_config. When models_enforced
  and a single locked_model is set, hides the dropdown entirely and
  shows the locked model name with a "(låst)" hint and an "låst av
  administratör" tooltip. When the policy allows multiple models, the
  dropdown shows only the whitelisted ones (intersected with the
  space's available models so we never offer something the tenant
  doesn't have).
Comment thread backend/src/intric/spaces/api/space_assembler.py Fixed
Comment thread backend/src/intric/governance_policy/domain/policy_resolver.py Fixed
Comment thread backend/src/intric/prompt_library/domain/prompt_library_repo.py Dismissed
Comment thread backend/src/intric/prompt_library/domain/prompt_library_repo.py Dismissed
Comment thread backend/src/intric/assistants/assistant_service.py Fixed
Align governance code with the user-facing product name
(m.personal_assistant() / "Personlig assistent" / README "Personal Assistant
Interface") so admins and end users see the same name for the same thing.

Backend: rename personal_chat_policy module, PersonalChatPolicy* classes,
DB tables (personal_chat_policies → personal_assistant_policies plus three
join tables), alembic migrations (in-place — branch is local only), tests.
Frontend: /admin/personal-chat → /admin/personal-assistant route,
personal-chat-policy.js endpoint, personalChatPolicy SDK key, all Swedish
"Personlig chatt" / "personliga chatten" labels → "Personlig assistent" /
"personliga assistenten". schema.d.ts regenerated.

Also addresses two pre-existing issues surfaced during verification:
* test_policy_resolver _mk_model fixture was missing the provider_id
  attribute the resolver expects after the providers join-table was added.
* configuration/+page.svelte used raw Map/Set inside $derived (must be
  SvelteMap/SvelteSet for fine-grained reactivity), and +layout.svelte
  passed a non-RouteId string to <a href> through resolve(); tabs now use
  literal RouteId strings with `as const` so the type-narrowed resolve()
  check at the call site holds.
Comment thread backend/src/intric/spaces/api/space_assembler.py Fixed
Comment thread backend/src/intric/governance_policy/domain/policy_resolver.py Fixed
Comment thread backend/src/intric/assistants/assistant_service.py Fixed
…hat-governance-plan

# Conflicts:
#	frontend/apps/web/src/lib/features/api-keys/ExtendExpirationDialog.svelte
#	frontend/apps/web/src/routes/(app)/spaces/[spaceId]/assistants/[assistantId]/edit/+page.svelte
#	frontend/packages/intric-js/src/types/schema.d.ts
…ce_policy and enforce policy on personal assistants

Renames the personal_assistant_policy package, table, migrations, services and
tests to governance_policy. Wires the effective-config resolution into
AssistantService so updates to the default personal assistant are validated
against the governance policy: disallowed completion models and MCP servers are
rejected, and resolution is skipped when the policy does not apply (non-default
assistant, non-personal space, or no service configured).
Adds the configuration admin surface for the renamed governance policy. The
page's interactive state lives in policyDraft.svelte.ts (selections, dirty
tracking, validation, summaries, confirm-before-apply and save), leaving
+page.svelte as thin wiring that binds the sections to the draft. The UI is
split into a shared PolicySection shell plus Model / Mcp / Prompt sections and
the save bar / confirm dialog, built on shadcn primitives. Renames the intric-js
endpoint to governance-policy and regenerates the API types.
Comment thread backend/src/intric/spaces/api/space_assembler.py Dismissed
Comment thread backend/src/intric/main/container/container.py Dismissed
Comment thread backend/src/intric/governance_policy/domain/policy_resolver.py Dismissed
Comment thread backend/src/intric/assistants/api/assistant_assembler.py Dismissed
Comment thread backend/src/intric/assistants/assistant_service.py Dismissed
Comment thread backend/src/intric/governance_policy/domain/policy_resolver.py Dismissed
Comment thread backend/src/intric/governance_policy/domain/policy_resolver.py Dismissed
Comment thread backend/src/intric/governance_policy/domain/governance_policy_repo.py Dismissed
Comment thread backend/src/intric/governance_policy/domain/governance_policy_repo.py Dismissed
Comment thread backend/src/intric/governance_policy/domain/governance_policy_repo.py Dismissed
…cepted

The prompt_library integration token fixtures created JWTs without the
test-settings JWT patch, so the auth dependency rejected them with 401
instead of exercising the routes. Mirror the governance_policy fixtures
by depending on patch_auth_service_jwt.
After merging develop, the prompt_library/governance migrations branched
from 202605061100 while develop had advanced past it, producing two
alembic heads and breaking DB setup for every integration test. Re-point
the chain base to develop's current head (20260501_backfill_model_costs)
so alembic resolves a single head.
Develop now enforces intric/no-hardcoded-text on all web .svelte files.
Move the Swedish strings introduced by the governance UI (prompt library
form, locked-model switcher, assistant edit policy hints) into en/sv
message catalogs and call them via m.*, reusing existing keys where they
already exist (cancel, name, prompt, governance_saving).
@MaxEriksson2000 MaxEriksson2000 changed the title Personal chat governance: prompt library + tenant policy + runtime enforcement Personal assistant governance: prompt library + tenant policy + runtime enforcement May 28, 2026
@MaxEriksson2000 MaxEriksson2000 marked this pull request as ready for review May 28, 2026 06:28
…k and preflight

Add a single pure resolver (select_effective_completion_model) and route both ask-time enforcement (assistant_service.ask) and read-time preflight (conversation_service) through it, so the projected and the actual model can no longer diverge. Preflight is now governance-aware. Also carries the per-message MCP opt-out (disabled_mcp_server_ids) through the conversation request and enforces the MCP policy whitelist at ask time. Adds unit tests for the resolver and updates preflight tests.
…tive_config

Add _assistant_response (assistant_router) and _space_response (space_router) helpers so GET/update assistant and the space endpoints can't return a personal default assistant without its governance effective_config — previously the update endpoint omitted it and the chat UI silently dropped model/MCP filtering. Also extract the ~300-line audit change-tracking out of update_assistant into _build_assistant_update_changes.
…er-message MCP toggle

Replace DefaultAssistantModelSwitcher with a shadcn-based composer toolbar: inline model selector (ChatModelSelect, filtered by governance effective_config), an MCP server toggle popover (ChatMcpServers) that sends disabled_mcp_server_ids per message, and refreshed input controls. The preflight estimate now also re-runs on a model switch.
Expose a single active-model source on ChatService (activeModelName plus the existing contextLimit) that prefers a single assistant's own global model over the latest message's model (the latter only applies to group chats); ContextUsageBar reads it instead of recomputing. Compare chat partners by id so switching the personal-assistant model no longer wipes the open conversation, and add one sync point in the chat page so the partner always tracks the canonical default assistant.
…tion UI

Add localized strings for the redesigned chat composer and context surfaces, and update the MCP server selection, personal-assistant policy draft, and assistant edit UI accordingly.
Satisfies the route-metadata pre-push guardrail, which requires a description on non-GET route decorators. The endpoint previously lacked one.
…hat-governance-plan

# Conflicts:
#	backend/src/intric/assistants/api/assistant_router.py
#	backend/src/intric/assistants/assistant_service.py
#	frontend/apps/web/src/routes/(app)/spaces/[spaceId]/chat/+page.svelte
#	frontend/packages/intric-js/src/types/schema.d.ts
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

🧹 Dead-code & unused-dependency report

Advisory — never gates the PR. Whole-repo scan, so some findings may be false positives (dynamic dispatch, framework hooks, runtime-resolved imports). Triage before removing.

No dead code or unused dependencies detected.

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

📊 Patch coverage

Share of this PR's new/changed lines exercised by tests. Report-only — never gates the PR.

Area Changed Uncovered Coverage
Frontend 2875 2645 8%
Backend 1126 256 77%
Uncovered lines — Frontend (100 files)
  • …/prompts/new/+page.ts — 7–8, 10–12
  • …/ai-elements/prompt-input/prompt-input.svelte — 1, 4–5, 14–15, 20, 23–27, 29, 31, 33–34, 36–38, 41
  • …/ui/sidebar/sidebar-menu-action.svelte — 1–2, 6–7, 9, 12, 18–22, 25–27, 29, 32, 34
  • …/components/conversation/ChatMcpServers.svelte — 12–19, 37, 39–41, 43, 45–48, 50–53, 55–60, 62–65, 67, 71–78, 81–82, 85, 88–89, 96, 99, 101–104, 107–110, 113–114, 116, 119–123, 125, 129–133, 139, 141–142, 145–148, 150–151, 154–156
  • …/personal-assistant/configuration/+page.ts — 7–19
  • …/ui/sidebar/sidebar-menu-button.svelte — 2, 4–10, 12–15, 18–20, 28–31, 34, 36–37, 41–43, 46, 56, 58–64, 66, 68–69, 71, 73, 80, 82–85, 88, 91, 94
  • …/mcp/components/SelectMCPServers.svelte — 8, 45–46, 62–64, 66–68, 70, 92–95, 97–101, 103–104, 106–110, 235
  • …/components/conversation/Message.svelte — 65–67, 69
  • …/ui/sheet/sheet.svelte — 1–2, 4–5, 7
  • …/ai-elements/model-selector/model-selector-list.svelte — 1, 3–4, 9, 11
  • …/ui/sheet/sheet-footer.svelte — 1–2, 5–6, 9, 11, 13–14, 16–17
  • …/ai-elements/model-selector/context.ts — 1, 7, 9–11, 13–19
  • …/personal-assistant/configuration/+page.svelte — 7–15, 20, 24–27, 29–30, 33–49, 51–61, 63–68, 75, 77–83, 85
  • …/personal-assistant/configuration/ModelRestrictionSection.svelte — 7–14, 48–49, 63, 65, 67–68, 70, 72–73, 76–78, 80, 83–85, 88–89, 92–96, 99–105, 109, 111–113, 115–121, 123, 129, 131, 133–137, 140–152, 156–158, 160–161, 165–172, 175–176, 187–190, 193–195, 199–200
  • …/(app)/admin/AdminMenu.svelte — 17, 20–22, 24, 29, 31–35, 41–43, 45, 47–48, 51–55, 59–63, 65–67, 71–75, 79–84, 88, 90–99, 101–103
  • …/prompt-library/components/PromptLibraryForm.svelte — 7–13, 28–30, 32–34, 36–38, 40–44, 46, 48–52, 54–59, 61–63, 65–72, 74–78, 81–85, 87, 89, 91, 93–94, 98–100, 102–103, 107–109, 111, 113–115, 119–120, 123–125, 127–128
  • …/ui/sidebar/sidebar-inset.svelte — 1–2, 5–6, 9, 11, 13–14, 16–17, 20
  • …/personal-assistant/configuration/PolicySaveBar.svelte — 7–10, 22, 25, 28, 30–31, 33–34, 36–37, 39–40, 43–46
  • …/ai-elements/model-selector/index.ts — 1–10
  • …/admin/prompt-library/+page.ts — 7–12
  • …/prompt-library/new/+page.svelte — 7–13, 15, 17–22, 24–25, 30, 33–37
  • …/ai-elements/prompt-input/prompt-input-button.svelte — 1–3, 5–8, 11, 13, 15, 17–21
  • …/components/conversation/ConversationInput.svelte — 2, 5–6, 11–13, 18–19, 62, 151–152, 216, 245–255, 257–260, 268–282, 285, 287–288, 294–296, 312–315, 322, 329, 335, 360, 365, 378–380, 387–391, 395–399, 401–402, 409–410, 413
  • …/admin/prompt-library/+page.svelte — 7–17, 21–24, 26–31, 33, 36–38, 40–59, 61–62, 65–70, 73–76, 79–80, 83–84, 86–87, 89–91, 95–97, 101–102, 105–106, 111–113, 116–124, 127–132, 135, 138–139, 141–142, 144–145, 147–148, 151–153, 155, 169–176, 178–182, 185–186, 188–190, 192–193
  • …/ui/sidebar/sidebar-menu-sub-item.svelte — 1–2, 5–6, 9, 11, 13–14, 17–18
  • …/ai-elements/model-selector/model-selector-group.svelte — 1, 3, 8, 10
  • …/ui/sidebar/sidebar-provider.svelte — 1–3, 5, 11, 13–16, 20, 26–30, 33–34, 36, 38, 40–41, 43–45, 48–49
  • …/ui/sidebar/index.ts — 1–24
  • …/features/chat/ChatService.svelte.ts — 7, 33–38, 99, 101, 109–110, 126–128, 130–131, 133–135, 137–139, 151–159, 166, 305–313, 323, 325–327, 353–361, 452–459, 462, 475–476, 506, 559–561, 563–566, 569–576, 666, 669, 671–673, 675, 677–680, 682, 684–686, 893–894, 896–906, 910–914, 916, 918–922, 924
  • …/[assistantId]/edit/+page.svelte — 65–87, 335–347, 375–382, 387, 391, 488–507, 570–594
  • …/ui/sidebar/sidebar-menu-item.svelte — 1–2, 5–6, 9, 11, 13–14, 17–18
  • …/components/conversation/MessageAnswer.svelte — 4, 28, 48–49, 70–81, 86–103, 105, 191–193, 197, 199–200
  • …/ui/sheet/sheet-close.svelte — 1–2, 4–5, 7
  • …/personal-assistant/configuration/PromptEnforcementSection.svelte — 7–15, 31–33, 38, 40, 42–43, 45, 47–48, 51–53, 55, 58–64, 66, 68, 73–74, 76–80, 83–87, 94–97, 102–103
  • …/[spaceId]/chat/+page.svelte — 65–78, 133
  • …/personal-assistant/prompts/+page.ts — 7–8, 10–12
  • …/components/switcher/ChatModelSelect.svelte — 13–20, 22–25, 29–30, 34–39, 41–44, 46–50, 53–59, 61–63, 65–71, 76–92, 94–95, 99–101, 103–104, 106, 108–109, 111–113, 116–119, 121, 123–133, 135–136
  • …/ui/sheet/sheet-description.svelte — 1–3, 5–6, 8, 10, 12–13, 15–16
  • …/ai-elements/model-selector/model-selector-empty.svelte — 1, 3, 8, 10
  • …/components/conversation/ConversationView.svelte — 8, 123
  • …/personal-assistant/configuration/policyDraft.svelte.ts — 151–152, 154–156, 158, 161–162, 170–171, 173, 177–178, 207–214, 225–226, 228–231, 276, 292–293, 298–301, 306–316, 343, 346–348, 356–359, 361–362, 365–371, 374–377, 382–385, 388–389, 392–394, 397–398, 401–408, 417–421, 423–424, 428–429, 431–432, 443–446, 448, 450–454, 456–459, 462, 474–476, 485, 492–493
  • …/ui/sidebar/sidebar-footer.svelte — 1, 3, 5–6, 9, 11, 13–14, 17–18
  • …/layout/Navigation/NavigationLink.svelte — 18–23, 29
  • …/ui/sidebar/constants.ts — 1–6
  • …/ui/sidebar/sidebar-header.svelte — 1, 3, 5–6, 9, 11, 13–14, 17–18
  • …/lib/hooks/is-mobile.svelte.ts — 1, 3, 5–9
  • …/admin/personal-assistant/+layout.ts — 7–8, 10–17
  • …/ui/sidebar/sidebar-group-action.svelte — 1–2, 6–7, 11, 16–18, 21–23, 25, 28, 30
  • …/ui/sidebar/sidebar-trigger.svelte — 1–4, 6, 8–9, 12, 17–18, 20–21, 26, 28–32, 34
  • …/ai-elements/prompt-input/prompt-input-tools.svelte — 1, 4, 8–9, 11
  • …/(app)/admin/+layout.svelte — 11–12, 14, 17, 29, 31–33, 36–37, 41–42, 46, 48–49, 54–55, 59
  • …/ui/sidebar/sidebar-group-content.svelte — 1–2, 5–6, 9, 11, 13–14, 17–18
  • …/ai-elements/model-selector/model-selector-logo.svelte — 7, 9–10, 13–18, 20–25, 28–34, 37–38, 44–45, 47–48, 50, 53, 56, 58
  • …/ai-elements/model-selector/model-selector-trigger.svelte — 1, 3–5, 13–14, 16, 18–19, 22, 25
  • …/ai-elements/model-selector/model-selector-name.svelte — 1, 3, 8, 10
  • …/ui/sidebar/sidebar-menu-badge.svelte — 1–2, 5–6, 9, 11, 13–14, 17–18, 21
  • …/prompt-library/[id]/+page.svelte — 1–235
  • …/ui/sidebar/sidebar-menu-sub.svelte — 1–2, 5–6, 9, 11, 13–14, 17–18, 21
  • …/ai-elements/prompt-input/prompt-input-body.svelte — 1, 4, 8–9, 11
  • …/ui/sheet/sheet-title.svelte — 1–3, 5–6, 8, 10, 12–13, 15–16
  • …/components/mentions/MentionButton.svelte — 2, 4–5, 12, 15, 19–20, 22, 24–25
  • …/prompt-library/[id]/+page.ts — 1–18
  • …/ui/sheet/sheet-header.svelte — 1, 3, 5–6, 9, 11, 13–14, 16–17
  • …/personal-assistant/configuration/PolicySection.svelte — 14, 16, 31, 33, 38–39, 45–47, 50, 54, 59
  • …/ui/sidebar/sidebar-menu-skeleton.svelte — 1–3, 6–7, 9, 11, 17–18, 20–21, 24–25, 27–28, 30, 33
  • …/prompts/[id]/+page.ts — 1–12
  • …/ai-elements/prompt-input/prompt-input-submit.svelte — 1–6, 8, 10–12, 14–15, 20–24, 26, 29, 34–37, 39
  • …/attachments/components/AttachmentUploadIconButton.svelte — 8–9, 14, 33–36, 38
  • …/components/conversation/ChatComposer.svelte — 11–14, 18, 20–23, 25
  • …/components/conversation/ReasoningToolStep.svelte — 8–10, 14, 18, 26–27, 31, 33–36, 38–41, 43–46, 48–51, 53–56, 58, 60, 62–63, 65–69, 71–75, 77, 81–85, 87, 90, 93–95, 100–104, 106–107, 109–110
  • …/ui/sidebar/sidebar-content.svelte — 1, 3, 5–6, 9, 11, 13–14, 17–18, 21
  • …/personal-assistant/configuration/McpRestrictionSection.svelte — 7–13, 45–46, 58, 60–63, 65–68, 70, 72–73, 75, 77–78, 81–83, 85, 88–92, 95–96, 98–108, 111–116, 118–119, 122–125, 127–130, 132–133, 136, 140–141, 144–150, 156–157, 160, 162, 165–166, 168–169, 172–175, 177, 180, 183–186, 188, 192–198, 200–201, 203–204, 206, 208, 212–215, 220–221, 229–231, 236–237
  • …/ui/sheet/index.ts — 1–10
  • …/ui/sidebar/sidebar-input.svelte — 1, 3–4, 6–8, 10, 12, 14–16, 19–20
  • …/ui/sidebar/sidebar.svelte — 1–3, 5–6, 8–12, 15, 22–23, 25–28, 31–32, 36–39, 43–44, 47–48, 51–53, 56, 62–63, 65–68, 72, 74–80, 83, 85–89, 91–93, 96, 98
  • …/ui/sheet/sheet-content.svelte — 5–6, 8–12, 15–16, 18–19, 22, 29, 31–34, 36–38, 41, 44–48
  • …/ai-elements/model-selector/model-selector-input.svelte — 1–2, 7, 9
  • …/ui/sidebar/context.svelte.ts — 1–3, 23–29, 31–35, 39–41, 44–49, 51–53, 55–58, 60, 68–70, 77–79
  • …/ai-elements/prompt-input/index.ts — 1–6
  • …/ui/sheet/sheet-overlay.svelte — 1–3, 5–6, 8, 10, 12–13, 15–16, 19
  • …/ui/sidebar/sidebar-separator.svelte — 1–3, 6–7, 9, 11, 13–14, 17–18
  • …/admin/personal-assistant/+page.ts — 7–8, 10–12
  • …/components/conversation/ReasoningTrace.svelte — 12–16, 25–28, 35, 40, 44–45, 49–58, 60–61, 64–65, 67–71, 74–75, 78–79, 86–87, 90–91, 93, 96–97, 101–109, 112, 115, 120–125, 128–134
  • …/personal-assistant/configuration/PolicyConfirmDialog.svelte — 7–10, 18–19, 21–25, 27–31, 34–36, 39–41, 43–44
  • …/ui/sidebar/sidebar-menu-sub-button.svelte — 1–2, 6–7, 11–13, 20–22, 25–29, 31, 34, 36
  • …/ui/sidebar/sidebar-group-label.svelte — 1–2, 6–7, 11, 16–18, 21–23, 25, 28, 30
  • …/ui/sidebar/sidebar-group.svelte — 1, 3, 5–6, 9, 11, 13–14, 17–18
  • …/components/conversation/ContextUsageBar.svelte — 121
  • …/ai-elements/prompt-input/prompt-input-footer.svelte — 1, 4, 8–9, 11, 13–14
  • …/admin/audit-logs/audit-action-labels.ts — 116–131
  • …/admin/personal-assistant/+layout.svelte — 7–11, 15, 20, 22–26, 29, 31–36, 39–46, 48–50, 52–53, 56–57, 64
  • …/ui/sheet/sheet-portal.svelte — 1–2, 4–5, 7
  • …/layout/Navigation/NavigationMenu.svelte — 9, 14
  • …/ai-elements/model-selector/model-selector.svelte — 1, 3–4, 8, 10–11, 13, 15
  • …/ui/sidebar/sidebar-rail.svelte — 1–2, 4, 6–7, 10, 13–14, 17–18, 22–23, 25–31, 34
  • …/ui/sidebar/sidebar-menu.svelte — 1–2, 5–6, 9, 11, 13–14, 17–18
  • …/ai-elements/prompt-input/context.ts — 1, 11, 13–15, 17–23
  • …/ai-elements/model-selector/model-selector-item.svelte — 1, 3–5, 16, 18–19, 21, 24–25, 27–29
  • …/ui/sheet/sheet-trigger.svelte — 1–2, 4–5, 7
  • …/ai-elements/model-selector/model-selector-content.svelte — 1, 3–5, 13–14, 16–17
Uncovered lines — Backend (19 files)
  • …/assistants/api/assistant_protocol.py — 175
  • …/prompt_library/presentation/prompt_library_router.py — 47, 93–94, 119, 130, 155–158, 189
  • …/governance_policy/application/effective_config_service.py — 16–17, 20–21, 24, 27–29, 79, 99, 110, 114
  • …/intric/assistants/assistant.py — 360
  • …/governance_policy/presentation/governance_policy_router.py — 33, 39, 41, 44, 52–55, 59–62, 66–67, 72–73, 84–89, 93–94, 98–99, 108, 120, 137–138, 151–152, 165–167, 177–181, 195
  • …/spaces/api/space_assembler.py — 52
  • …/governance_policy/domain/policy_resolver.py — 19–21, 24
  • …/spaces/api/space_router.py — 61, 83–84, 165, 1546, 1563
  • …/assistants/api/assistant_assembler.py — 40, 89
  • …/governance_policy/infrastructure/governance_policy_repo_impl.py — 38, 44, 51, 86–88, 102–103, 128, 130, 141, 144, 149–150, 163, 168–169, 182, 187–188, 197, 202–203, 212, 215–216, 226–228
  • …/intric/questions/questions_repo.py — 180
  • …/prompt_library/application/prompt_library_service.py — 26, 62, 74, 76, 134–135, 149
  • …/infrastructure/adapters/tenant_model_adapter.py — 1134–1137, 1187
  • …/completion_models/infrastructure/completion_service.py — 186–187
  • …/prompt_library/infrastructure/prompt_library_repo_impl.py — 70, 83, 91–93, 107–109, 143, 145, 158, 198
  • …/assistants/api/assistant_router.py — 64–65, 569, 584–585, 588, 591–593, 595, 597, 601–602, 604–606, 608–610, 612–614, 616–618, 620–622, 624–626, 630–632, 634–636, 640–642, 645–646, 648–649, 651–653, 656–658, 660, 680, 727
  • …/governance_policy/application/governance_policy_service.py — 23, 26, 29, 32, 65–66, 95–101, 106–109, 118, 126–130, 137, 140–143, 151, 154–159, 162, 165, 171–173, 182–185
  • …/governance_policy/domain/governance_policy.py — 93
  • …/intric/assistants/assistant_service.py — 87, 90, 94, 303, 675, 799–800, 802, 804, 807, 811, 1092–1093, 1127, 1136–1140, 1145, 1301, 1311, 1498, 1519

Replace the stacked MCP tool-call cards in the assistant message with a
collapsible chain-of-thought trace: tool calls render as interleaved
timeline steps that auto-expand while the assistant works and collapse
once the answer streams. Pending tool approvals stay as prominent cards
(approval logic unchanged) so a blocking decision is never hidden.

Adds ReasoningTrace + ReasoningToolStep; MessageAnswer splits tool calls
into traced vs pending. No backend change — uses the tool-call data the
stream already provides.
Throwaway /dev routes (excluded from i18n lint via eslint ignore): the
original design prototype at /dev/chat-demo and a live preview of the
production ReasoningTrace at /dev/reasoning-preview.
Forward the model's reasoning/thinking text (Anthropic extended thinking
surfaced by LiteLLM as reasoning_content) to the frontend over SSE as a new
"reasoning" event, and render it live in ReasoningTrace: the header shows
"Thinking…" while the model reasons and "Thought for Ns" once done, with the
thinking text in the collapsible body.

Backend: new ResponseType.REASONING + Completion.reasoning_content; capture
reasoning deltas in the streaming loop and pass them through every chunk filter
(_handle_tool_call, the assistant response stream) to emit SSEReasoning.
Frontend: onReasoning accumulates a runtime reasoning field on the message;
Message.svelte hides the standalone thinking badge once reasoning streams.

Reasoning streams live only — not persisted, so it is absent after a
conversation reload, by design.
… deny-set

- Rename the MCP dimension from restrict- to allow-semantics: the toggle
  now reads "Allow MCP servers in the personal assistant" and an enabled
  grant requires at least one server (deny-all = toggle off). Help and
  summary texts no longer claim a non-existent fallback to normal access.
- Replace checkboxes with switches across the governance page (providers,
  models, MCP servers) to match the rest of the settings UI.
- Add per-server "on by default" flag: allowed servers can start switched
  off in users' chat; the chat input seeds its MCP toggles from
  effective_config.default_disabled_mcp_server_ids per conversation.
- Add per-server tool disclosure with switches and bulk on/off. Disabled
  tools are stored as a deny-set (governance_policy_disabled_mcp_tools)
  so newly synced tools stay allowed; the resolver narrows the tool list
  on shallow copies, which enforces the policy in both the chat UI
  serialization and the MCP proxy tool registry.
- Migration adds is_default_enabled to governance_policy_mcp_servers and
  the disabled-tools join table; API models switch server_ids to
  servers[{mcp_server_id, is_default_enabled}] + disabled_tool_ids.
…ss all read paths

get_assistant() carved out the personal default assistant (gated by
PERSONAL_CHAT via can_read_default_assistant, not ASSISTANTS), but
get_assistant_with_effective_config() and get_effective_completion_model()
checked only can_read_assistants(). Both back GET /assistants/{id}/, the update
response, and the preflight model resolution, so:

- a PERSONAL_CHAT-only user got 403 on their own default assistant (and a
  desynced UI after a model-picker update that actually persisted), and
- preflight resolved a model for any assistant id with no authorization,
  leaking model_name/context_window across spaces and tenants.

Extract _authorize_read_assistant() with the carve-out and apply it on all
three read paths.
update_space (PATCH /spaces/{id}/) returned assembler.from_space_to_model(space)
directly, so a patched personal space serialized its default assistant with
effective_config=None. The frontend overwrites currentSpace with this response,
which silently drops governance filtering (all models/MCP servers shown) until
the next full space GET. Route it through the shared _space_response() helper.
_policy_changes compared completion_models as order-sensitive lists while every
other dimension was sorted, so saving the same model set in a different order
logged a spurious GOVERNANCE_POLICY_UPDATED event with identical old/new.
Normalize model entries by id like the other dimensions.
resolve_for fetched the full completion-model and MCP-server catalogs on every
resolution whenever a policy row existed, even with all restriction flags off —
and a row is auto-created the first time an admin opens the config page. Since
the resolver only reads those catalogs behind the enabled flags, gate each fetch
on its flag and run the independent fetches concurrently. Removes two full-table
scans per ask/preflight/space-read/assistant-read across the tenant.
create_entry/update_entry pre-checked the name with exists_by_name but had no
handling for the uq_prompt_library_tenant_name violation, so two concurrent
creates of the same name surfaced the second as a 500. Catch IntegrityError on
that constraint and raise the same 400 the pre-check does; re-raise unrelated
integrity errors unchanged.
…ssistant heads

The branch had two alembic heads after merging develop (202606091000 and
a1d4c7e90f23). Add an empty merge revision instead of editing an already-committed
migration's down_revision in place, which would leave alembic_version inconsistent
on any DB that ran the prior single-parent version.
…ing, guard model select

- partnerRuntimeSignature now hashes the MCP and prompt dimensions of
  effective_config (mcp_enforced, available/default-disabled server ids,
  prompt_locked), so an MCP/prompt-only policy edit replaces the partner instead
  of leaving the composer showing stale servers.
- Reasoning deltas stream through the same rAF buffer as answer text instead of
  mutating reactive state per token (avoids re-render storms on thinking-heavy
  models).
- ConversationInput gates the model selector on the SpacesManager context, so
  mounting the default assistant without it (e.g. a dashboard deep link) no
  longer throws in ChatModelSelect.
…den orphaned grants

Wire the policy draft to the per-server MCP shape (servers + per-server chat
default + tool deny-set) and extract disabledToolIdsForSelectedServers as a pure,
tested helper. Update the integration assertions to the new mcp_restriction shape
(servers/disabled_tool_ids; an enabled grant with no servers is now a 400).

Harden against orphaned grants: a server that was allowed in the policy but later
disabled for the tenant is filtered out of the selectable set, so seed, dirty
baseline and the save payload all intersect with currently-selectable servers.
Previously such a grant was invisible in the UI yet still sent on every save,
bricking saves with a 400 and inflating the summary count.
Record the branch review findings (auth, effective_config contract, MCP policy,
perf, migration hygiene, cleanup) with verification status and a resolution log.
…ector

- Add required `description` to prompt-library POST/PUT/DELETE and governance
  PUT mutation routes so the route-metadata guardrail passes; regenerate
  schema.d.ts to match the new OpenAPI operation descriptions.
- Replace raw `bg-black/10` with the semantic `bg-overlay-default` token in
  sheet-overlay and drop inert `dark:` variants in radio-group-item and the
  prompt-enforcement alert to satisfy intric/no-raw-color.
- Re-add `name="ask"` to the chat submit/stop buttons that the redesign
  dropped, fixing the chat E2E selector timeout.
…trace

Tool-call argument JSON was accumulated silently server-side, so a turn
with many parallel MCP calls left the stream (and UI) frozen for tens of
seconds before all steps appeared at once. The adapter now emits a
TOOL_CALL event with result_status=pending as soon as a call's name and
id arrive in the delta, so steps tick in one by one while arguments are
still being generated.

- Merge pending entries by tool_call_id in persistence and in the
  approval flow (backend + frontend) instead of blind appends, and fill
  in arguments once a later chunk carries them
- Map pending to a new "Preparing" step status; show approved calls as
  running for the whole streaming turn since tool calls can interleave
  with answer text across MCP rounds
- Fold duplicated server/tool name resolution into one helper
- Section the reasoning trace into Reasoning/Tools with self-contained
  tool step cards and an additive tool-count badge
Reasoning/thinking text was streamed live over SSE and discarded when the
turn finished, so reloading a conversation lost the trace while tool calls
remained visible. Store it in a nullable reasoning column on questions,
accumulated in the streaming loop alongside the answer (never mixed into
it), and expose it on the Message model so the existing ReasoningTrace
renders it from history. Aborted turns keep their partial reasoning via
the background-save path. Historical traces fall back to the plain
'Reasoning' label since no live timing exists on reload.
…hat-governance-plan

# Conflicts:
#	backend/src/intric/assistants/api/assistant_assembler.py
#	backend/src/intric/assistants/api/assistant_models.py
#	backend/src/intric/assistants/api/assistant_router.py
#	backend/src/intric/assistants/assistant_service.py
#	backend/src/intric/database/tables/__init__.py
#	backend/src/intric/main/container/container.py
#	backend/tests/integration/audit/test_audit_config_service.py
#	backend/tests/unit/test_audit_category_mappings.py
#	backend/tests/unit/test_audit_config_service.py
#	frontend/apps/web/messages/en.json
#	frontend/apps/web/messages/sv.json
#	frontend/apps/web/src/routes/(app)/admin/AdminMenu.svelte
#	frontend/apps/web/src/routes/(app)/spaces/[spaceId]/assistants/[assistantId]/edit/+page.svelte
#	frontend/packages/intric-js/src/types/schema.d.ts
…solve duplicate revision ids

The develop merge brought in help-assistants migrations that independently
reused revision ids 202605211100 and 202605211200, making down_revision
pointers ambiguous and breaking 'alembic upgrade head'.

Re-point the governance chain linearly on top of the help-assistants chain
with fresh ids (prompt_library 202605211500, governance_policy 202605211600),
update governance_policy_providers' down_revision accordingly, and drop the
now-redundant develop+help-assistants merge migration. Single head: 1d60c8c457d3.
…ing head

The dev DB already had the governance chain and the reasoning column applied
(stamped at 1d60c8c457d3) but never ran the help-assistants migrations, which
arrived via the develop merge. Anchoring that chain below the applied head made
alembic treat it as done, so the help-assistants schema (users.is_system_user,
org_space_assistant_roles, help_assistant_* tables) was never created and the
app crashed on login.

Re-point governance back onto backfill_model_costs and chain the help-assistants
migrations on top of the reasoning head (1d60c8c457d3) so 'alembic upgrade head'
applies them forward. Single head: 202605211400.
…selector (#488)

Introduce an ai-elements component layer following Vercel AI Elements
naming and compound-component patterns, built on the existing shadcn
primitives:

- prompt-input family (Root/Body/Footer/Tools/Button/Submit) replaces
  the hand-rolled form markup in ConversationInput; submit/stop button
  derives from a shared status context
- model-selector family (Trigger/Content/Input/List/Group/Item/Logo/
  Name) built on Popover + Command replaces the flat Select in
  ChatModelSelect: searchable command palette grouped by model vendor
  with provider logos and a check on the selected model
- vendor grouping falls back org -> provider_name -> provider_type;
  locked-model and single-model states unchanged
The develop merge added required org_space_assistant_role_repo and
help_assistant_assignment_history_repo constructor args plus an
assert_not_helper_assistant guard in ask(). Pass the new repos and
default them to 'not a helper' so the runtime tests stop failing.
@MaxEriksson2000 MaxEriksson2000 merged commit e1e7d05 into develop Jun 12, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants