diff --git a/policy-model.md b/policy-model.md new file mode 100644 index 0000000..945ef44 --- /dev/null +++ b/policy-model.md @@ -0,0 +1,196 @@ +# Non-Linear: Policy & Permissions Model + +## Overview + +Non-Linear uses a granular, policy-based permission system inspired by AWS S3/IAM. The system is built on fine-grained policies from day one. Roles are named bundles of policies — convenience layers, not hardcoded ceilings. + +## Core Concepts + +### Policy + +A policy is a single permission rule with three dimensions: + +``` +Policy = Actor + Action + Resource Scope + Effect +``` + +| Dimension | Description | Examples | +|-----------|-------------|---------| +| **Actor** | Who the policy applies to | User, agent, or role | +| **Action** | What operation is permitted/denied | `read_node`, `create_child`, `change_status` | +| **Resource Scope** | Where the policy applies in the graph | Global, subtree, single node | +| **Effect** | Allow or deny | `allow`, `deny` | + +### Actions (Enumerated) + +| Action | Description | +|--------|-------------| +| `read_node` | View a node's title, description, status, labels | +| `read_comments` | View comments on a node | +| `create_child` | Create a new child node | +| `edit_node` | Modify title, description | +| `change_status` | Change a node's status | +| `change_assignee` | Assign/unassign a user or agent | +| `add_label` | Add labels to a node | +| `remove_label` | Remove labels from a node | +| `add_comment` | Post a comment on a node | +| `create_link` | Create a lateral link (blocks, relates-to) | +| `remove_link` | Remove a lateral link | +| `reparent_node` | Move a node to a different parent | +| `delete_node` | Delete a node (and its subtree) | +| `manage_policies` | Create/edit/delete policies and roles | +| `manage_members` | Add/remove project members | + +### Resource Scopes + +| Scope | Meaning | Use Case | +|-------|---------|----------| +| **Global** | Entire project — all nodes | Default for most human roles | +| **Subtree** | A specific node and all its descendants | Agent confined to "Backend" component | +| **Single Node** | Exactly one node | Lock down a sensitive design doc | + +**Subtree scoping is the key feature.** Because the data model is a decomposition tree, resource scope maps naturally to graph position. "Backend API subtree" means "this node and everything decomposed beneath it." The graph *is* the namespace — no separate resource-naming scheme needed. + +## Roles + +A role is a named, reusable collection of policies. Roles are editable — the defaults are starting points, not limits. + +### Default Presets + +#### Owner + +Full control over the project. + +```yaml +role: owner +policies: + - action: "*" + scope: global + effect: allow +``` + +#### Member + +Standard team member. Can do everything except manage policies and delete nodes. + +```yaml +role: member +policies: + - action: [read_node, read_comments, create_child, edit_node, + change_status, change_assignee, add_label, remove_label, + add_comment, create_link, remove_link, reparent_node] + scope: global + effect: allow +``` + +#### Agent Reader + +Default agent role. Read-only plus comments and status changes. + +```yaml +role: agent-reader +policies: + - action: [read_node, read_comments, add_comment, change_status] + scope: global + effect: allow +``` + +### Custom Role Examples + +#### Backend Decomposer Agent + +An AI agent that can break down backend components into tasks, but only within the Backend subtree. + +```yaml +role: backend-decomposer +policies: + - action: read_node + scope: global + effect: allow + - action: [create_child, add_label, add_comment] + scope: subtree("Backend API") # scoped to node ID in practice + effect: allow +``` + +#### Triage Agent + +An agent that can reprioritize and reassign, but can't create or delete. + +```yaml +role: triage-agent +policies: + - action: [read_node, read_comments, add_comment, + change_status, change_assignee, add_label, remove_label] + scope: global + effect: allow +``` + +#### External Reviewer + +A stakeholder who can view and comment on high-level nodes only. + +```yaml +role: external-reviewer +policies: + - action: [read_node, read_comments, add_comment] + scope: subtree("root") # top two levels only (enforced by depth limit) + effect: allow +``` + +## Resolution Order + +When multiple policies apply to an actor+action+resource combination: + +1. **Direct policy on actor** takes precedence over role-inherited policies +2. **Deny always wins** over allow at the same level +3. **Narrower scope wins** over broader scope (single node > subtree > global) +4. **Default: deny** — if no policy explicitly allows an action, it is denied + +### Resolution Examples + +``` +Scenario: Agent "deploy-bot" has role "agent-reader" (global read + comment + status) + AND a direct policy: deny change_status on "Production Deploy" node + +Action: deploy-bot tries to change status on "Production Deploy" +Result: DENIED — direct deny on single node overrides role's global allow + +Action: deploy-bot tries to change status on "Backend API / Auth" +Result: ALLOWED — role's global allow applies, no deny in scope +``` + +``` +Scenario: Agent "decomposer" has role "backend-decomposer" + (global read, create_child on Backend subtree) + +Action: decomposer tries to create a child under "Frontend / Charts" +Result: DENIED — create_child only allowed in Backend subtree + +Action: decomposer tries to read "Frontend / Charts" +Result: ALLOWED — read_node is global scope +``` + +## Reparenting and Permission Changes + +When a node is moved to a different parent: + +1. All subtree-scoped policies are re-evaluated against the new position +2. Actors may gain or lose access depending on the new subtree +3. The system should warn the user: *"Moving this node will change who can access it"* +4. Specifically flag if any agent loses access to nodes it's currently assigned to + +## Lateral Links Crossing Permission Boundaries + +When a lateral link connects two nodes and the actor only has access to one: + +- **Link visible, target opaque.** The actor can see that a link exists and its type ("blocked by node X") but cannot read the target node's details. +- This enables agents to reason about blockers without leaking information across permission boundaries. + +## Implementation Notes for v0.1 + +- Build the full granular policy engine underneath +- Ship the three default presets (owner, member, agent-reader) +- Expose custom role creation through a settings panel (can be minimal UI) +- Store policies as structured data (JSON/YAML), not code +- Policy evaluation should be centralized — every API call goes through the policy engine +- Audit log: record every policy evaluation for debugging and compliance \ No newline at end of file