# acl-on-sx: Access Control on Datalog rose-ash needs fine-grained, explainable, federation-aware access control. Subjects (users, groups, roles, services) × actions (read, edit, comment, moderate, federate) × resources (pages, posts, threads, peers). Decisions must come with a trace — not just permit/deny, but **why**. Datalog's bottom-up rule engine produces transparent permit/deny chains: the proof tree is the audit trail. Inheritance over groups + resource hierarchies is recursive Datalog in one rule. Federation extends naturally — fed-sx replicates ACL facts, peers reason over the union. End-state: a Datalog-on-SX layer specifically for ACL, with explanation API, audit log, and federation extension. Reuses `lib/datalog/` evaluator and term model where possible. ## Status (rolling) `bash lib/acl/conformance.sh` → **24/24** (Phase 1 complete) ## Ground rules - **Scope:** only touch `lib/acl/**` and `plans/acl-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, `lib/datalog/**`, or other `lib//`. You may **import** from `lib/datalog/` (its public API in `lib/datalog/datalog.sx`); do **not** copy or modify Datalog code. - **Shared-file issues** go under "Blockers" with a minimal repro; do not fix here. - **SX files:** use `sx-tree` MCP tools only. - **Architecture:** thin layer on top of `lib/datalog/`. Define schema, surface API, audit + federation hooks. The rule engine itself is Datalog's. - **Watch for shared patterns** going into `lib/guest/` — both acl-sx and mod-sx need rule-engine plumbing. If you find shared shape, flag it for extraction (don't extract yet — wait for mod-sx to start). - **Commits:** one feature per commit. Keep Progress log updated and tick boxes. ## Architecture sketch ``` ACL declarations (SX) User query │ │ ▼ ▼ lib/acl/schema.sx lib/acl/api.sx — subject sorts — (acl/permit? subj act res) — resource sorts — (acl/explain subj act res) — action sorts — (acl/audit subj act res :allowed?) — fact schema │ │ ▼ ▼ lib/acl/engine.sx lib/acl/facts.sx — builds Datalog query — actor(id, kind) — invokes lib/datalog/ — resource(id, kind) — extracts proof tree — member_of(actor, group) │ — child_of(res, parent) ▼ — grant(actor, act, res) lib/acl/audit.sx — deny (actor, act, res) — persistent decision log — query API ``` ## Phase 1 — Direct grants - [x] `lib/acl/schema.sx` — sorts: subject {user, group, role, service}, action, resource {page, post, thread, peer} - [x] `lib/acl/facts.sx` — `actor`, `resource`, `grant`, `deny` predicates as Datalog EDB - [x] `lib/acl/engine.sx` — `(permit? subj act res db)` reduces to Datalog query - [x] `lib/acl/api.sx` — public `(acl/permit? ...)` taking implicit current db - [x] `lib/acl/tests/direct.sx` — 15+ cases: direct grant, missing grant, explicit deny - [x] `lib/acl/scoreboard.{json,md}` baseline - [x] `lib/acl/conformance.sh` runs the suite ## Phase 2 — Inheritance - [ ] `member_of(actor, group)` chain — group grants apply to members (transitive) - [ ] `child_of(res, parent)` chain — parent grants apply to children (transitive) - [ ] role expansion — role contains list of (action, resource) tuples - [ ] deny-overrides — explicit deny wins over inherited allow - [ ] `lib/acl/tests/inherit.sx` — 25+ cases: nested groups, deep resource trees, conflict resolution, deny precedence - [ ] document the deny-overrides choice in plan ## Phase 3 — Explanation + audit - [ ] `(acl/explain subj act res)` → `{:allowed? T :proof }` - [ ] proof tree extracts from Datalog's derivation - [ ] `lib/acl/audit.sx` — append-only decision log (in-memory + serializer for disk) - [ ] `(acl/audit-tail n)` for recent decisions - [ ] `lib/acl/tests/explain.sx` — proof correctness, audit completeness ## Phase 4 — Federation - [ ] peer trust facts — `peer(addr, kind)`, `trust(peer, level)` - [ ] delegated grants — `delegate(peer, actor, action, resource)` - [ ] cross-instance permit chain — query asks local + queries trusted peers via fed-sx - [ ] revocation propagation — fact retraction across federation - [ ] `lib/acl/tests/fed.sx` — federated grant chains (mock fed-sx transport in tests) ## Progress log - **Phase 1 complete (24/24).** ACL is a thin layer over `lib/datalog/`: - `schema.sx` — sorts (subject/resource kinds, well-known actions) + EDB predicate arity table + `acl-fact-valid?` validator. Schema is data, since Datalog is untyped. - `facts.sx` — `acl-actor`/`acl-resource-fact`/`acl-grant`/`acl-deny` constructors returning Datalog fact tuples. - `engine.sx` — owns the ruleset `acl-phase1-rules` and reduces decisions to `dl-query`. `acl-build-db` = `dl-program-data facts rules`; `acl-permit?` = non-empty `(permit S A R)` query. - `api.sx` — `acl/load!` rebuilds an implicit current db; `acl/permit?` queries it. (Slash-symbols like `acl/permit?` parse fine as single tokens.) - **deny-overrides** encoded as `permit(S,A,R) :- grant(S,A,R), not deny(S,A,R)`. Stratifies cleanly because `deny` is EDB-only (no rule derives it). Verified: grant+deny on same (S,A,R) → denied. - Conformance: `conformance.conf` (datalog preloads + acl modules) + thin `conformance.sh` wrapper over `lib/guest/conformance.sh`. Scoreboard generated by the shared driver. - **Shared-plumbing note (for eventual `lib/guest/rules/`):** the `build-db = dl-program-data(facts, rules)` + `decide = non-empty ground query` shape is exactly what mod-sx (Prolog moderation) will also need. The reusable seam is engine.sx's two functions — facts→db and ground-query→bool — both pure pass-throughs to the rule engine. Not extracting yet (wait for mod-sx as second consumer per ground rules). - **Tooling note:** sx-tree path-based edit tools (`sx_replace_node`, `sx_read_subtree` with a path) ignored the path argument in this worktree (always resolved to index 0 / `[0,..]`). `sx_write_file`, `sx_validate`, `sx_find_all`, `sx_eval` all work; used full-file rewrites instead of path edits. ## Blockers (loop fills this in)