Artifacts get a real authorization model, the groundwork for review flow next.
Per-artifact permissions + sharing shipped shipped
Some people publish directly, others should only propose. That distinction now exists in the data, behind a single choke point: can(actor, action, visibility) gates every write.
- Roles viewer < commenter < editor < owner; actions read / comment / publish / approve / manage. A commenter creates content to be reviewed but cannot approve or publish; editors and owners do. This is deliberate: it makes "content someone has to review" expressible before reviews themselves exist.
- Sharing is a per-artifact role override. An owner adds a teammate by email at a role from the artifact header; the share lifts (or lowers) just that person on just that artifact, without touching their workspace role. Everyone unlisted falls back to the workspace default, stated plainly in the popover.
- Every mutating route (publish, restore, comment, react, edit/delete comment, webhook management) routes through
can(); denials return 403. The artifact response carries my_role so the UI shows only what you can do.
"Both": workspace baseline + per-artifact override decision
Effective role resolves per-artifact override, then workspace membership, then a visibility floor. Not pure-RBAC, not pure-ACL: the workspace role is your baseline, a share is the exception.
- Lazy provisioning: the first member of a workspace becomes its owner, everyone else joins at the configured default. A fresh secured instance has exactly one owner and needs no setup step.
- Open instance stays open: with no static token configured, an anonymous caller is trusted as owner, so zero-config self-host keeps working unchanged.
- Single workspace ("local") for now; existing artifacts already carried
org_id='local', so there was nothing to backfill. Multi-workspace is a later, additive workstream.
A clean textual rebase is not a correct one lesson
Four PRs merged to main mid-build (comments, dock init, slides, the agent loop). The rebase applied with one trivial import conflict, looked done, and was still broken: main had added three comment routes calling a writeOk helper this branch deleted. Rule now: after any rebase that touches shared files, typecheck and run the suite before trusting the merge, and audit that new mutating routes are actually gated (a missing gate is invisible to the compiler).