Data groups: a provider's models read only their own group's data

Every connection is in a data group. Its models are handed, and can find,
only that group's memories, notes, skills, knowledge, reports and
personality -- by search and by id. A chat stays in the group it was
started in: switching its model, the endpoint fallback, the crowd, friends,
bases and the @ menu all stay inside it, and a chat whose model has moved
is refused rather than sent. A group may name its own embedder and image
reviewer. data.manage lets a person make personal groups, remap
connections for themselves and move their own records.

Also: a search no longer mixes two embedders of the same width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 16:07:55 +00:00
co-authored by Claude Opus 5.5
parent e65ea90fe6
commit 9970bb43c6
61 changed files with 3595 additions and 200 deletions
+104 -2
View File
@@ -57,6 +57,11 @@
<label class="tabs__tab" for="tab-memory">{{ icon("sparkle", "icon--sm") }} Memory</label>
{% endif %}
{% if several_groups or may_manage_groups %}
<input class="visually-hidden" type="radio" name="settings-tab" id="tab-data">
<label class="tabs__tab" for="tab-data">{{ icon("shield", "icon--sm") }} {{ t("Data") }}</label>
{% endif %}
<input class="visually-hidden" type="radio" name="settings-tab" id="tab-security">
<label class="tabs__tab" for="tab-security">{{ icon("key", "icon--sm") }} Security</label>
</div>
@@ -388,6 +393,9 @@
<input class="input" name="content" value="{{ memory.content }}"
maxlength="{{ memory_limit }}" style="flex: 1"
aria-label="{{ t('What this memory says') }}">
{% if several_groups %}
<span class="badge" title="{{ t('Data group') }}">{{ group_names.get(memory.data_group_id or 'default', '') }}</span>
{% endif %}
<button class="btn btn--sm" type="submit">{{ t("Save") }}</button>
<button class="btn btn--sm btn--danger" type="submit"
formaction="/api/library/memories/{{ memory.id }}/delete"
@@ -416,6 +424,14 @@
maxlength="{{ memory_limit }}"
aria-label="{{ t('Something worth remembering') }}"
placeholder="{{ t('Prefers metric units and a 24-hour clock.') }}">
{% if several_groups %}
<select class="select" name="data_group_id" aria-label="{{ t('Data group') }}"
style="flex: none">
{% for group in data_groups %}
<option value="{{ group.id }}">{{ group.name }}</option>
{% endfor %}
</select>
{% endif %}
<button class="btn btn--primary" type="submit">{{ t("Remember") }}</button>
</form>
<p class="field__hint">
@@ -439,7 +455,9 @@
{% for personality in personalities %}
<li class="model-list__item">
<div style="min-width: 0">
<strong>{{ personality.model_key }}</strong>
{% set key = split_key(personality.model_key) %}
<strong>{{ key[0] }}</strong>
{% if several_groups %}<span class="badge">{{ group_names.get(key[1], key[1]) }}</span>{% endif %}
{% if personality.author == "model" %}
<span class="badge badge--leaf">{{ t("its own words") }}</span>
{% endif %}
@@ -484,7 +502,9 @@
{% for impression in impressions %}
<li class="model-list__item">
<div style="min-width: 0">
<strong>{{ impression.model_key }}</strong>
{% set key = split_key(impression.model_key) %}
<strong>{{ key[0] }}</strong>
{% if several_groups %}<span class="badge">{{ group_names.get(key[1], key[1]) }}</span>{% endif %}
<div class="text-sm">{{ impression.content }}</div>
</div>
<form method="post"
@@ -504,6 +524,88 @@
</section>
{% endif %}
{# --- Data groups --- #}
{#
Which provider may read which of this person's data. Shown to anybody
once there is more than one group, because "which provider can read
my notes?" is a question anybody may ask; editable only with
`data.manage`, because the answer changes what a provider can see.
#}
{% if several_groups or may_manage_groups %}
<section class="tabs__panel" data-tab="tab-data">
<div class="card">
<h2 class="card__title">{{ t("Your data groups") }}</h2>
<p class="card__lede">{{ t("A model reads only the memories, notes, skills, knowledge, reports and personality of the data group its connection is in, and a chat stays in the group it was started in. Two providers in different groups never see each other's.") }}</p>
<ul class="model-list">
{% for group in data_groups %}
<li class="model-list__item">
<div style="min-width: 0">
<strong>{{ group.name }}</strong>
{% if group.personal %}<span class="badge">{{ t("yours") }}</span>{% endif %}
<div class="text-xs faint">
{% for label, n in group_counts[group.id].items() %}{{ n }} {{ group_labels.get(label, label) }}{% if not loop.last %} · {% endif %}{% endfor %}
</div>
</div>
{% if group.personal and may_manage_groups %}
<form method="post" action="/api/preferences/data-groups/{{ group.id }}/delete">
<button class="btn btn--sm btn--danger" type="submit"
data-confirm-button="{{ t('Delete the data group “%(name)s”?', name=group.name) }}"
aria-label="{{ t('Delete this') }}" title="{{ t('Delete this') }}">
{{ icon("trash", "icon--sm") }}
</button>
</form>
{% endif %}
</li>
{% endfor %}
</ul>
{% if may_manage_groups %}
<form method="post" action="/api/preferences/data-groups/new" class="row"
style="gap: var(--sp-2); margin-top: var(--sp-4)">
<input class="input" name="name" required maxlength="120" style="flex: 1"
aria-label="{{ t('New data group name') }}"
placeholder="{{ t('New data group name') }}">
<button class="btn" type="submit">{{ icon("plus", "icon--sm") }} {{ t("Create") }}</button>
</form>
<p class="field__hint">{{ t("A group of your own, for keeping one provider away from the rest of your data. Nobody else can see it.") }}</p>
{% endif %}
</div>
<div class="card">
<h2 class="card__title">{{ t("Which group each connection reads") }}</h2>
{% if may_manage_groups %}
<p class="card__lede">{{ t("Your choice applies to you alone. Following the instance keeps up with whatever the administrator sets. Moving a connection moves no data: from then on it reads the other group, and your chats on it that were started in the old group can no longer use it.") }}</p>
<form method="post" action="/api/preferences/data-groups" class="form-grid">
{% for row in group_connections %}
<div class="field">
<label class="field__label" for="dg-{{ row.connection.id }}">{{ row.connection.name }}</label>
<select class="select" id="dg-{{ row.connection.id }}" name="group__{{ row.connection.id }}">
<option value="">{{ t("Follow the instance (%(group)s)", group=group_names.get(row.instance, '')) }}</option>
{% for group in data_groups %}
<option value="{{ group.id }}" {{ 'selected' if row.chosen == group.id }}>{{ group.name }}</option>
{% endfor %}
</select>
<p class="field__hint">{{ t("Reads: %(group)s", group=group_names.get(row.in_force, '')) }}</p>
</div>
{% endfor %}
<div class="btn-row">
<button class="btn btn--primary" type="submit">{{ t("Save") }}</button>
</div>
</form>
{% else %}
<p class="card__lede">{{ t("Set by the administrator. Changing it for yourself needs the permission to manage your own data groups.") }}</p>
<ul class="model-list">
{% for row in group_connections %}
<li class="model-list__item">
<strong>{{ row.connection.name }}</strong>
<span class="badge">{{ group_names.get(row.in_force, '') }}</span>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</section>
{% endif %}
{# --- Security --- #}
<section class="tabs__panel" data-tab="tab-security">
<div class="card">