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
parent 757ab305ee
commit 9d7fb72bdb
34 changed files with 2405 additions and 390 deletions
+26 -11
View File
@@ -9,7 +9,7 @@ reasoning, tool calling with web search, custom HTTP tools and MCP servers,
agent chats that work on a machine over SSH, a knowledge library, notes, memory
and skills, speech in and out, image generation over ComfyUI, users and groups,
model administration, installable as an app, reports, messages, and scheduled
work that runs on its own. 2024 tests, `ruff` clean.
work that runs on its own. 2062 tests, `ruff` clean.
What remains before the first stable release is written out below, in phases,
under [The road to 1.0.0](#the-road-to-100).
@@ -521,15 +521,27 @@ seen working.
it finishes
### Phase 6 — permissions, quotas and sharing (`0.9.6`)
- [ ] **"What can this user actually do?"** answered on screen, from the
resolution that already computes it
- [ ] Membership edited from one side; a searchable, paginated user list
- [ ] Reading and writing split within a gate where the difference matters
- [ ] **Quotas on a group**, resolved by maximum — the union rule applied to
numbers — and enforced where the existing budgets are
- [ ] Deleting a group or a user forgets its grants, which it never did
- [ ] Sharing as its own action with a search box, a shared-with-me filter, and
reports shareable. Sharing stays read-only
- [x] **"What can this user actually do?"** answered on screen, and *where each
permission came from* — `explain()` is the resolution's working shown
rather than thrown away, which is the simulation the union rule exists to
make unnecessary
- [x] List plus detail for users and groups; membership edited from **one** side,
since a full-form POST from either used to overwrite the other's view
- [x] Reading and writing split for the three gates where the difference is a
real decision — checked on the tool's risk, after the gate, defaulting on
- [x] **Quotas on a group**, resolved by maximum with **zero meaning no limit
and winning outright**, and enforced at the five places 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
- [x] Usage recorded even for a reply that was stopped or failed, because an
endpoint charges either way and a quota a Stop button walks past is not one
- [x] **Deleting a group or a user forgets its grants, which it never did**
both halves for an account, since their rows cascade and the shares of
those rows have nothing to cascade from
- [x] Sharing as its own action with a search box — one grant per request, stored
the moment it is made rather than when the resource happens to be saved
- [x] A "Shared with me" filter in all four listings, reports shareable, and
`library.share` on by default. Sharing stays read-only
### Phase 7 — packaging and updating (`0.9.7`)
- [ ] **Docker**, with the data on a volume and a TLS proxy expected in front
@@ -625,7 +637,10 @@ Recorded because each looks like an oversight until you know the reason.
- **No JavaScript build step.** Browser libraries are hash-pinned and committed.
A self-hosted tool should work offline and not report page views to a CDN.
- **Permissions union, never deny.** With denies, "why can this user not do X"
- **Permissions union, never deny**, and quotas resolved by maximum for the same
reason -- with the corner that zero means *no limit* and therefore wins, or
"unlimited" would count for less than a large number. With denies, "why can
this user not do X"
cannot be answered without simulating every group.
- **System prompts replace, never stack.** Two layers that disagree give the
model contradictory instructions and nobody can tell which is losing.