Edit permissions
Folio’s permission model is intentionally tiny: each property carries an
x-editable-by array of fnmatch patterns (ADR-0007).
A direct write is allowed when the actor matches at least one pattern.
The shape
- name: company_name logicalType: string required: true x-editable-by: ["agent:human", "agent:ops:*"]When Sheet.upsert_records is called with actor="agent:ops:akiko",
Folio matches against each pattern. "agent:ops:akiko" matches
agent:ops:* (the * glob), so the write is allowed.
If a write fails to match any pattern, Folio raises
PermissionDeniedError (HTTP 403 in the Viewer) and writes nothing.
Patterns
x-editable-by accepts any fnmatch
pattern. The full set is:
| Pattern | Matches |
|---|---|
* | Anything |
agent:* | agent:human, agent:ops:akiko, agent:enrichment, … |
agent:human | exactly that string |
agent:ops:* | agent:ops:akiko, agent:ops:lin, … but not agent:human |
agent:human:? | agent:human:a, agent:human:b, … (one-character suffix) |
[ab] | a or b |
Folio does not invent its own DSL. The principle is “you already know fnmatch from POSIX shells, no surprise glyphs.”
Convention for actor strings
Folio doesn’t enforce a format, but the convention used throughout the docs
and examples/:
agent:human— generic interactive human (the Viewer’s default).agent:human:<id>— a specific human, e.g.agent:human:akiko.agent:<role>— a non-human, e.g.agent:enrichment,agent:research,agent:demo.
So a typical x-editable-by for a field humans can edit but bots can’t:
x-editable-by: ["agent:human", "agent:human:*"]Or, more permissively:
x-editable-by: ["agent:human*"](matches agent:human and any agent:human:<id>).
What permission does not affect
- Reads.
query,list_records,get_recordignore actor entirely. Anyone with the directory can read. - Materialize writes. Derivations write derived fields. The actor on the
materialize call is logged in provenance but is not matched against
x-editable-by— derivations are governed by the contract’sx-derivedandx-inputsinstead. - Deletes.
delete_recordsremoves whole rows; it does not checkx-editable-by(which is per-field).
If you need to gate reads or deletes, that’s an application-level concern, not something the spec covers.
Default behaviour
A property without x-editable-by (or with x-editable-by: []) is not
human-editable. Only a derivation may write it.
This is the right default: a field that no one is authorized to edit is either fully derived or fully read-only — both safer than “anyone can edit because we forgot to declare anything.”
Worked example
- name: id logicalType: string primaryKey: true required: true # primary key — never editable
- name: company_name logicalType: string required: true x-editable-by: ["agent:human"]
- name: industry_tag logicalType: string x-derived: true x-inputs: [company_name] # not editable: only the derivation writes it
- name: notes logicalType: string x-editable-by: ["*"] # anyone, for demo onlyA write tries upsert_records:
sheet = open_sheet("./customers", actor="agent:enrichment")sheet.upsert_records([{"id": "cust_001", "company_name": "X"}])# → PermissionDeniedError: agent:enrichment may not write company_namesheet = open_sheet("./customers", actor="agent:human")sheet.upsert_records([{"id": "cust_001", "company_name": "X"}])# → OK; appends a "human" provenance line for company_namePatterns in the wild
| Use case | Patterns |
|---|---|
| HR-only fields | ["agent:hr*", "agent:human:hr-*"] |
| Anything writeable by humans | ["agent:human*"] |
| Specific bots only | ["agent:enrichment", "agent:summary"] |
| Demo / sandbox | ["*"] |
| Read-only / derivation-only | [] (or omit the attribute) |
Combined with respect_human_override
x-editable-by decides who can write. respect_human_override (on
materialize) decides whether the derivation can overwrite a human edit.
A field with x-editable-by: ["agent:human"] can be edited by a human; the
next materialize call sees source: human on the latest provenance line
and skips that cell unless --force is passed.
This is the pattern Folio is designed for: agents do the bulk, humans
correct the long tail, and respect_human_override keeps the human’s word
final.