Grants that outlive what they name, and a rule you can read

sharing.forget_principal has existed since shares did, documented as the thing
that stops a recycled id inheriting somebody's grant, and was called by nobody.
Deleting a group left every grant naming it; deleting an account left both the
grants to it and the grants of its own work -- that second half is the one
nothing else could catch, since their rows cascade and the shares of those rows
have nothing to cascade from. Both now run before the delete, while the rows are
still findable, and a deleted resource forgets its own.

library.share defaulted to False, which meant sharing shipped documented as done
and unreachable: the panel only renders for somebody holding it, so out of the
box nobody could share anything and nothing said why. It is on.

The panel itself was checkboxes inside the resource's *save form*, listing every
group and every account on the instance, unpaginated, on every detail page -- and
a tick only took effect if you also saved the resource. It is its own routes now:
search, one grant per POST, the panel re-rendered from what is stored. Anything
already shared stays listed whatever the search says, or removing a grant would
mean searching for the name it was given to.

Reports join the shareable set and memories still do not: a finished piece of
work is the thing somebody most wants to hand over, and a record about a person
is not content to pass round. reports.visible became sharing.visible_to, which is
the one line its own docstring predicted. Two things fell out: `owned` beside
`get`, because sharing grants reading and deleting is the owner's alone; and
reading somebody else's report no longer clears their unread dot.

Permissions gained the answer to "what can this person actually do?" --
explain() is resolve()'s working shown rather than thrown away, naming admin, the
baseline, or the groups that granted each one. That is the simulation the union
rule exists to make unnecessary, and until now the only way to get it was to open
every group and read the grids by eye. Users and groups are list-plus-detail, and
membership is edited from one side: it was on both, and a full-form POST from
either overwrote what the other had shown.

Read and write are split for notes, memory and skills -- checked on the tool's
declared risk, after the gate so it can only narrow, and defaulting on.

Quotas are the union rule applied to numbers, with the corner that makes it
interesting: zero means "no limit" and wins outright, or a group saying unlimited
would count for less than one saying a million. Absent means "no opinion".
_narrower folds a group's ceiling with the instance's and is deliberately not
min, for the same reason. Five axes, enforced where each is knowable -- before a
reply is built, before a second one starts, on an agent reply's clock, before a
minute of GPU, and beside the helper cap -- and usage is recorded even for a
reply that was stopped or errored, because an endpoint charges either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-06 16:48:14 +02:00
co-authored by Claude Opus 5
parent 20bb569b00
commit 1b8c9f948c
31 changed files with 2226 additions and 377 deletions
@@ -0,0 +1,141 @@
{% extends "admin/_layout.html" %}
{% from "_macros.html" import icon, model_avatar %}
{% set section = "groups" %}
{% block title %}{{ group.name }} - Groups - {{ brand.name }}{% endblock %}
{% block heading %}{{ group.name }}{% endblock %}
{% block admin_content %}
<p class="admin-lede">
<a href="/admin/groups">{{ icon("chevron-left", "icon--sm") }} All groups</a>
· A group only ever <em>adds</em>. Anything already in the baseline is shown
below as such, so a tick here that changes nothing looks like one.
</p>
{% if saved %}
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ saved }}</span></div>
{% endif %}
<form method="post" action="/admin/groups/{{ group.id }}" class="form-grid">
<section class="card">
<h2 class="card__title">Name</h2>
<div class="field">
<label class="field__label" for="name">Name</label>
<input class="input" id="name" name="name" value="{{ group.name }}" required>
</div>
<div class="field">
<label class="field__label" for="description">What it is for</label>
<input class="input" id="description" name="description"
value="{{ group.description }}" maxlength="1000">
</div>
</section>
<section class="card">
<h2 class="card__title">Permissions this group adds</h2>
{% for section_name, defs in permission_groups.items() %}
<div class="field">
<span class="field__label">{{ section_name }}</span>
{% for definition in defs %}
<label class="checkbox perm-row">
<input type="checkbox" name="permission" value="{{ definition.key }}"
{{ 'checked' if (group.permissions_json or {}).get(definition.key) }}>
<span>
<strong>{{ definition.label }}</strong>
{% if baseline[definition.key] %}
<span class="badge">already in the baseline</span>
{% endif %}
<span class="perm-row__desc">{{ definition.description }}</span>
</span>
</label>
{% endfor %}
</div>
{% endfor %}
</section>
{#
Quotas. Every one is zero-for-no-limit, and an *empty* box is different from
a zero: empty is "this group has no opinion" and contributes nothing to the
resolution, zero is "unlimited" and wins outright. Saying that here is the
only place somebody will read it.
#}
<section class="card">
<h2 class="card__title">Quotas</h2>
<p class="card__lede">
Resolved across a person's groups by <strong>maximum</strong> — the union
rule applied to numbers, so a second group can only grant more.
<strong>Leave a box empty</strong> for “no opinion”, and use
<strong>0</strong> for “no limit”, which beats any number another group
sets. Administrators are unlimited whatever is here.
</p>
<div class="field-row">
{% for key, label, description in limit_defs %}
<div class="field">
<label class="field__label" for="limit-{{ key }}">{{ label }}</label>
<input class="input" id="limit-{{ key }}" name="limit_{{ key }}"
type="number" min="0" step="1"
value="{{ limits.get(key, '') }}" placeholder="no opinion">
<p class="field__hint">{{ description }}</p>
</div>
{% endfor %}
</div>
</section>
{#
Membership lives here and only here. It used to be on the user page as well,
and a full-form POST from either side overwrote what the other had shown.
#}
<section class="card">
<h2 class="card__title">Members</h2>
<p class="card__lede">
The one place membership is edited. A user's own page links here rather
than offering a second control for the same value.
</p>
<div class="checkbox-row">
{% for person in users %}
<label class="checkbox">
<input type="checkbox" name="user_ids" value="{{ person.id }}"
{{ 'checked' if person in group.users }}>
<span>{{ person.name }} <span class="faint text-xs">{{ person.email }}</span></span>
</label>
{% endfor %}
</div>
</section>
<section class="card">
<h2 class="card__title">Models this group unlocks</h2>
<p class="card__lede">
A model marked public is available to everyone; one that is not is
available to the groups named here. Model access is separate from
permissions — one says what somebody may do, the other what with.
</p>
<div class="checkbox-row">
{% for model in models %}
<label class="checkbox">
<input type="checkbox" name="model_ids" value="{{ model.id }}"
{{ 'checked' if model in group.models }}>
<span>{{ model_avatar(model, "model-avatar model-avatar--sm") }} {{ model.label }}</span>
</label>
{% endfor %}
</div>
</section>
<div class="btn-row">
<button class="btn btn--primary" type="submit">Save group</button>
</div>
</form>
<section class="card">
<h2 class="card__title">Remove</h2>
<form method="post" action="/admin/groups/{{ group.id }}/delete"
data-confirm="Delete {{ group.name }}? Its members keep their accounts.">
<button class="btn btn--danger btn--sm" type="submit">
{{ icon("trash", "icon--sm") }} Delete this group
</button>
</form>
<p class="field__hint">
Members keep their accounts and lose whatever this group granted them. Every
share naming this group goes too — nothing cascades to those, so they are
deleted explicitly.
</p>
</section>
{% endblock %}