Skip to content

Source Model — Rights, consent and access — Design Spec (#TBD)

Milestone: source-model Manual: TBD — a “Who may see this” section: recording rights and consent on a source or any part of it; community protocols and labels; restricting, redacting and truly removing material; and what exports and training sets do with restricted material.

Design-led (Testing Constitution). The creative director owns this intent; tests enforce it; code makes them pass. Status: DRAFT — raised by review, 2026-09-19. The maintainer has ruled that it belongs in this set and who may act; the rest is PROPOSED. A slice of the source model: read source-model.md first. Every behaviour below is tagged [GAP] with its issue. Nothing here is built. What exists today (VERIFIED on disk, fichero_server/security/authz.py): a person has one of three roles in a project: owner, editor or viewer; and grant-and-deny overrides on a target and everything under it already exist, enforced on every audited write (actions/registry.py) and in search. They stop at a document, not a word. The sharing and accounts specs have not been read in full for this slice, and must be before approval.

Routed fact, 2026-09-20 (#4917, read in review; in the worktree, not yet committed when this was written). The permission layer’s one ancestor walk now resolves an artifact, segment, pass, match, version, forwarding note or carry to its document, then walks the folders above it. So a grant or deny on a document or folder reaches everything the source model hangs on that document, and a lookup fault denies. The walk still does not go below a document: an override placed on a segment id matches that id alone, because the walk does not follow Segment.parent_segment_id. If the maintainer rules that rights become grants and denies on a segment, the build is one more step in that same walk (segment, parent segment, document), never a second check. This changes nothing about the block below.

BLOCKED (purge only). Purge waits on the audited-action layer’s split of each record into a chained part (who, what, when, ids, version numbers, a keyed fingerprint) and a content part outside the chain that a purge can blank, so the chain still checks out and the words are gone (ruled 2026-09-20; see “Rulings of 2026-09-20” in source-model.md). That split belongs to the audited-action layer, not to this set, and is not built yet. The other blocking question is answered: see Rulings.

Intent

Fichero is meant to serve under-resourced and Indigenous languages. Material in those languages often carries obligations: a community’s protocols about who may see, hear or share something; a speaker’s consent, which can be withdrawn; a family’s privacy; a repository’s conditions. The field’s name for the principle is CARE (collective benefit, authority to control, responsibility, ethics), alongside FAIR; its working tools include Traditional Knowledge and Biocultural labels.

Because the source model lets anything be addressed down to a word, a restriction must be able to apply down to a word too: one name in a diary; one passage of a recording; one photograph on a page. And because the model exports everything and builds training sets, restrictions have to hold before any of that exists, not be added after.

(Evidence note: CARE and the TK labels are named from general knowledge and are to be checked against their sources before approval.)

The design (proposed)

  • A rights record can be attached to a project, a source, or any segment. It says: who holds rights or authority; what was consented to, by whom, when; any conditions; any labels from an open list; who recorded this, and when. Traditional Knowledge and Biocultural labels belong to the communities that apply them and are administered by Local Contexts; Fichero carries them faithfully and does not invent them. A project can also keep labels of its own.
  • It passes downward, and only tightens. A rights record on a project covers everything in it; a record lower down may restrict further, never loosen what is above it.
  • Who may do what (ruled 2026-09-19). Owners and editors can set rights records, restrict and redact. Only the owner can purge. A viewer never sees restricted material. Ruled 2026-09-20: only the people a rights record names see restricted material; being an editor is not enough.
  • Restricted means those not allowed do not see a segment’s picture, readings, marks or statements. For them it is hidden, and the fact that something is hidden is shown (“one passage on this page is restricted”). It is never silently dropped.
  • A reference does not leak. A citable reference to a restricted segment opens to “restricted” for someone not allowed, never to its content.
  • Exports, training sets and the synced folder leave restricted material out by default and say how many segments they left out. Including it takes a deliberate, recorded act by someone allowed to restrict (an owner or an editor). The filter sits once, in the exporter’s one record stream, not in each writer (routed to export/exporter.md).
  • Sent to a model? Whether a segment may be sent to a cloud model, to a local model only, or to none, is part of its rights record. The engine refuses, and says why, when a workflow would break it.
  • Redaction is a verb of its own, different from delete: the segment stays, its content is covered for everyone not allowed, and the covering is visible as a covering (in the Source view, the Reader, exports and pictures of the page).
  • Removal on request. “Nothing is ever destroyed” cannot be the whole story when consent is withdrawn. A purge truly removes a segment’s content. Its reach is stated: the readings, pictures, marks and rights-holder’s words; everything worked out from them (search entries, vectors, word-level analysis); the files in the synced folder; and the quoted words inside any claim that rested on it. A claim itself is kept, with a plain note that its evidence was removed, so nothing points at nothing. A purge cannot reach what has already left the machine (an export, a shared training set), and Fichero keeps no list of what was exported (ruled 2026-09-19): following that up is the researcher’s own job, and the purge says so plainly. A purge is rarer and louder than delete; it cannot be undone; and it leaves a note that something was purged, by whom, when and why, without saying what it was.
  • All of these are audited actions, and work the same from the app, MCP and the command line.

Behaviors (every one is [GAP]: designed, not built; each cites its issue on milestone source-model, 322)

  • source.rights.one-check — [PARTIAL] (#4953) a rights record is enforced by the existing permission layer (a grant or deny on a segment id, inherited the way it already is); there is no second check. The record says what is meant and why (ruled 2026-10-04). Built, engine side (2026-09-28): authz._allowed ends in _rights_refuse, so a restricting record refuses every account it does not name – owner and editor included (ruled 2026-09-20) – for reads and writes, on its target and everything under it; two restrictions on the chain need both names; a withdrawn one restricts nothing; a library with no restricting record costs one query and behaves as before; a lookup error refuses. rights.set refuses a restriction that does not name the person setting it, out loud, rather than lock them out or silently add them (fichero-server/tests/unit/security/test_rights_enforcement.py, 11 tests, 8 failing on the code before). Not built: the app saying WHY something is refused (a restricted page reads as denied).
  • source.rights.record — [PARTIAL] (#4953) a rights and consent record can be attached to a project, a source or any segment, with labels from an open list. Built, engine side: RightsRecord (models/rights.py) with holders, consent (what, by whom, when), conditions (capped: it enters the audit chain), open labels, a restriction with the accounts it names, and model use. rights.set / rights.withdraw are audited and undoable (fichero-server/tests/unit/api/test_rights_records.py::TestARecordSaysWhatItSays::test_a_record_hangs_on_the_library_a_source_or_a_segment_with_open_labels, ::TestWithdrawingAndUndo::test_a_withdrawn_record_stops_applying_and_undo_brings_it_back). A restriction that names nobody is refused. Not built: any surface; enforcement (one-check).
  • source.rights.tighten-only — [OK] (→ #4953) a record passes downward; a lower level may restrict further and never loosen. effective_rights combines every live record from the library down: restricted if ANY record restricts, readers the INTERSECTION of each restricting record’s names, model use the STRICTEST stated. A record that would loosen model use is refused out loud, naming what is above it, rather than silently ignored (fichero-server/tests/unit/api/test_rights_records.py::TestItPassesDownAndOnlyTightens::test_a_lower_record_narrows_who_may_see_and_cannot_widen_it, ::TestItPassesDownAndOnlyTightens::test_an_unrestricted_record_below_does_not_lift_a_restriction_above, ::TestItPassesDownAndOnlyTightens::test_model_use_takes_the_strictest_and_a_looser_record_is_refused_out_loud).
  • source.rights.who-acts — [PARTIAL] (#4953) owners and editors set rights records, restrict and redact; only the owner can purge. Built: setting and restricting, through the existing write check (a record’s target is a write target, so a viewer is refused before the action runs: fichero-server/tests/unit/api/test_rights_records.py::TestWhoActs::test_a_viewer_cannot_set_a_record_and_an_editor_can). Not built: redact and purge.
  • source.rights.restricted-is-said — [GAP] (#4953) for anyone not allowed to see it (always a viewer, and any editor or owner the rights record does not name: ruled 2026-09-20), a restricted segment’s content is hidden, and the page says that something is hidden.
  • source.rights.citation-does-not-leak — [GAP] (#4953) a reference to a restricted segment opens to “restricted” for someone not allowed.
  • source.rights.exports-leave-out — [GAP] (#4953) exports, training sets and the synced folder leave restricted material out by default and report how much.
  • source.rights.model-use — [GAP] (#4953) a segment’s record says whether it may go to a cloud model, a local model, or none; the engine refuses a workflow that would break it, and says why.
  • source.rights.redact — [GAP] (#4953) redaction covers a segment’s content visibly, everywhere it would appear, without deleting it.
  • source.rights.purge-is-an-action — [GAP] (#4953) a purge is an action in the one audited registry, the same shape as the purge that exists for draft entities; not a separate route.
  • source.rights.purge — [GAP] (#4953) a purge removes a segment’s content, cannot be undone, and leaves a note that says who, when and why.
  • source.rights.purge-reaches-derivatives — [GAP] (#4953) a purge also removes search entries, vectors, pictures, synced-folder files and quoted evidence; a claim that rested on it is kept with a stated absence.

Test matrix

To be filled at approval.

Open questions

Most were ruled on 2026-09-19: see “Rulings of 2026-09-19” and “Still open” in source-model.md. The former blocking question (who enforces a rights record) is answered; see Rulings.

Rulings

  • 2026-10-04 (#4953): one check. The existing permission layer enforces; a rights record says what is meant and why, and is turned into grants and denies on segment ids, inherited the way they already are. There is no second permission system (source.rights.one-check).
  • 2026-10-04 (#5182, confirming the ruling of 2026-09-28): a page deny hides the claims drawn from it. Entity, claim and project reads filter out items drawn only from denied pages and count them as withheld; an entity or claim with no source page stays visible (source.rights.claims-with-only-denied-evidence-are-hidden).

Triaged from the backlog (2026-10-04)

  • source.rights.engine-wide-stores-follow-the-library — [GAP] (#5181) engine-wide stores that mention a library’s content (agent write audit, PyKEEN jobs and predictions, etc.) are filtered by that library’s permissions, and truly global settings are owner-only.
  • source.rights.claims-with-only-denied-evidence-are-hidden — [GAP] (#5182) a claim whose only evidence is a denied page is hidden, and one also supported by a readable page stays showing only the readable evidence, across entity, claim and project reads. Each read counts what it filtered out as withheld; an entity or claim with no source page at all stays visible. (Ruled 2026-09-28, restated 2026-10-04.)
  • source.rights.every-id-in-a-request-is-checked [BROKEN] (#4958): a read whose ids travel in the request body (resolving a location, a dataset query, search explain) is checked against every document it names, and an action that names two things (a pass and its source result, two matched segments) refuses unless both belong to the same document.