Add ACL graph architecture spec with principal-agent framework

- New acl.md: AclGraph Module definition (PrincipalNode, ResourceNode,
  DelegatesEdge, ScopesEdge, MemberEdge), principal-agent hierarchy
  with no-escalation invariant, setup-time vs runtime separation,
  multi-parent aggregation rules, cycle detection, scope semantics
- ADR-034: ACL as metagraph (not domain-specific tables)
- ADR-035: Actors become PrincipalNode entries, standalone table removed
- ADR-036: Principal-agent as DelegatesEdge with scope narrowing
- ADR-037: Setup-time definitions seed graph types, runtime instances
  are separate graphs
- Resolve OQ-03 (actors table design) — actors become ACL nodes
- Add OQ-20 through OQ-25 (delegation expiration, evaluator location,
  graph instance lifecycle, BelongsToEdge derivation, identityId
  references, scope string semantics)
- Update README.md and overview.md to reflect new doc and ADRs
- Note: multi-tenancy / graph scoping problem (no ownerId/scopeId on
  graphs table, no identity tables at this level) still needs
  resolution — identity and org tables will likely need to be added
  at this level for referential integrity
This commit is contained in:
2026-05-31 07:11:59 +00:00
parent a2ee452a63
commit 6b5f32bad4
8 changed files with 884 additions and 18 deletions

View File

@@ -0,0 +1,47 @@
# ADR-034: ACL as a Metagraph
## Status
Accepted
## Context
The `@alkdev/storage` package provides a metagraph pattern: graph types define schemas, node types define data shapes, edge types define typed relationships, and graph instances hold concrete data. Three graph types exist already (Metagraph base, CallGraph, SecretGraph) following this pattern.
Access control needs to represent:
- Principal-agent delegation hierarchies (liability flows up, permissions flow down)
- Org membership and scoping
- Resource-level access rules
- The relationship between `@alkdev/operations`' `AccessControl` definitions and runtime identity resolution
Three approaches exist:
1. **Domain-specific tables** — Add `principals`, `agents`, `delegations`, `org_members`, `resource_scopes` tables with typed columns and foreign keys
2. **Metagraph** — Define an AclGraph Module with PrincipalNode, ResourceNode, DelegatesEdge, ScopesEdge, BelongsToEdge as node and edge types, stored in the existing metagraph tables
3. **Hybrid** — Use metagraph for delegation and scoping, but keep identity records in domain-specific tables
## Decision
ACL is modeled as a metagraph. The AclGraph Module defines `PrincipalNode`, `ResourceNode`, `DelegatesEdge`, `ScopesEdge`, and `BelongsToEdge` as TypeBox Module entries, stored in the existing `graph_types`, `node_types`, `edge_types`, `graphs`, `nodes`, and `edges` tables.
This follows ADR-002 (metagraph over domain-specific tables) and is consistent with CallGraph and SecretGraph.
## Consequences
**Positive:**
- Uniform storage and querying — ACL data uses the same tables, same bridge functions (`moduleToDbSchema`, `validateNode`, `validateEdge`), and same repository layer (once implemented) as all other graph types
- Natural fit for delegation traversal — graph algorithms (BFS/DFS up delegation chains) map directly to the metagraph edge model
- Extensibility — adding new edge types (e.g., `InheritsEdge` for role inheritance) is a Module change, not a schema migration
- Composability — ACL graphs can reference CallGraph or SecretGraph entities as `ResourceNode`s without cross-table foreign keys (per ADR-020: no nodeTypeId on nodes, and OQ-24: identityId as logical reference)
- Same `moduleToDbSchema` pipeline handles AclGraph type registration
**Negative:**
- JSON attribute queries for ACL checks may be slower than column-based lookups (mitigated by ADR-033: JSON path queries for v1, with future hybrid approach)
- No database-level referential integrity between ACL nodes and hub identity tables — `identityId` is a logical reference, not a FK. The hub's `accounts` and `organizations` tables are the authoritative identity stores.
- The evaluator (graph traversal for effective scope computation) is application-layer logic, not a database query. This is consistent with the metagraph pattern (ADR-020: type enforcement at application layer, not via FK).
## References
- [acl.md](../acl.md) — ACL graph architecture specification
- [metagraph-module.md](../metagraph-module.md) — Module system design
- [sqlite-host.md](../sqlite-host.md) — Existing table schema
- ADR-002: Metagraph over domain-specific tables
- ADR-020: No nodeTypeId on nodes

View File

@@ -0,0 +1,38 @@
# ADR-035: Actors Become ACL Nodes, Standalone Table Removed
## Status
Accepted
## Context
The `actors` table in `src/sqlite/tables/actors.ts` is a standalone identity table with columns `id`, `metadata`, `createdAt`, `updatedAt`, `name`, `type` (enum: human/llm/agent). It has no foreign key relationships to or from any metagraph table. OQ-03 explicitly deferred the decision on whether actors should be a node type or standalone table until ACL design.
The hub already has authoritative identity tables (`accounts`, `organizations`, `organization_members`, `api_keys`) that store authentication and membership data. Duplicating those columns in storage would create sync burden and potential inconsistency.
## Decision
The `actors` standalone table is removed. Identity entities (accounts, orgs, services, roles) are represented as `PrincipalNode` entries in ACL graph instances, with `identityId` as a logical reference to the hub's authoritative identity tables.
The `ACTOR_TYPE` enum is replaced by the `IdentityType` enum in the AclGraph Module (`account`, `service`, `org`, `role`). The mapping from the old `ACTOR_TYPE`:
- `Human``identityType: "account"`
- `Llm``identityType: "account"` (LLMs are accounts per ADR-012 in the hub)
- `Agent``identityType: "role"` (agents fill roles)
## Consequences
**Positive:**
- Eliminates the disconnected `actors` table that had no relations (resolves OQ-03)
- Identity data has a single source of truth: the hub's `accounts` and `organizations` tables for auth/membership, the ACL graph for authorization
- No sync burden between `actors` table and hub identity tables
- Principal-agent delegation is naturally expressed as graph edges, not self-joins on an actors table
**Negative:**
- Fast "find all actors of type X" queries require JSON path extraction on node attributes (`$.identityType`) rather than a simple `WHERE type = 'X'` column query. This is consistent with ADR-033 (JSON path for v1) and can be optimized with computed columns or indexes later.
- The identityId → hub entity reference is a logical reference, not a FK. Orphaned or dangling references are possible if a hub entity is deleted without updating the ACL graph. Mitigation: the hub's cascade logic should also clean up ACL graph instances.
## References
- [acl.md](../acl.md) — ACL graph architecture specification
- OQ-03: Should actors be a node type or a standalone table? (resolved by this ADR)
- ADR-002: Metagraph over domain-specific tables
- ADR-012 (hub): Agent vs Role vs Account terminology

View File

@@ -0,0 +1,44 @@
# ADR-036: Principal-Agent Relationships as Delegation Edges
## Status
Accepted
## Context
The principal-agent framework from legal theory maps directly to the hub's access control model:
- A principal delegates authority to an agent
- Liability flows upward (the principal is responsible for the agent's actions)
- Permissions flow downward (an agent's effective permissions are a subset of the principal's)
- No escalation: an agent cannot have higher privilege than any ancestor in the delegation chain
In the hub, this maps to: a coordinator (principal) spawning an implementation specialist (agent), or a human (principal) delegating scope to an LLM session (agent).
Three modeling approaches:
1. **Inheritance model** — Agents "inherit" permissions from principals, with overrides
2. **Intersection/delegation model** — Each delegation narrows scope; effective scope is the intersection of the entire chain
3. **Capability model** — Each principal directly grants a set of capabilities to each agent, no chain traversal needed
## Decision
Principal-agent relationships are modeled as `DelegatesEdge` edges in the ACL graph, using the **intersection/delegation model**. Each edge carries `narrowedScopes` (and optionally `narrowedResources`) that represent the subset of the principal's authority transferred to the agent.
Effective scope computation: starting from the root principal, traverse the delegation chain, intersecting `narrowedScopes` at each step. The result is always a subset of the root principal's scopes, guaranteeing the no-escalation invariant.
## Consequences
**Positive:**
- The no-escalation invariant is structurally guaranteed by the intersection algorithm — it's impossible for a leaf node to exceed a root node's authority
- Delegation chains are auditable — you can trace any agent's authority back to the root principal by following `DelegatesEdge`s
- The same graph structure supports multi-level delegation (user → coordinator → implementer → subagent) without special-case logic
- Compatible with the hub's existing `sessions.parentId` model — a session's parent is the principal's session, and the delegation edge in the ACL graph records what was delegated
- `narrowedScopes` on the edge provides explicit, auditable delegation records — you can answer "what did principal P delegate to agent A?" by reading one edge
**Negative:**
- Chain traversal at call time: computing effective scopes requires walking the delegation chain. For deep chains this could be slow. Mitigation: cache effective scopes per identity per graph, invalidate on edge mutation.
- The delegation chain must be acyclic. Cycle detection is enforced at edge creation time (consistent with `allowSelfLoops: false` in AclGraph Config).
## References
- [acl.md](../acl.md) — ACL graph architecture specification
- Hub agent-roles: `/workspace/@alkdev/hub/docs/architecture/agent-roles.md`
- Operations Identity: `/workspace/@alkdev/operations/src/types.ts`

View File

@@ -0,0 +1,44 @@
# ADR-037: Setup-Time Definitions Seed Graph Types, Runtime Instances Are Separate Graphs
## Status
Accepted
## Context
Access control has two phases:
1. **Setup-time**: A hub/spoke administrator registers operations with `AccessControl` definitions. These define *what* is required (scopes, resource types, actions). They are schemas for authorization.
2. **Runtime**: When a call is made, the system evaluates *whether* the caller satisfies the operation's access control by traversing the ACL graph instance.
When you set up a hub/spoke, you define which operations are available. Users/orgs then work within those boundaries. The ACL graph instance stores who has access to what, grounded in the operations' access control definitions.
Three approaches to relating setup-time definitions and runtime instances:
1. **Single graph** — Both operation definitions and runtime permissions are nodes in the same graph
2. **Separate graphs, shared type** — Operation definitions are in the `operations` table (setup-time), runtime permissions are in a separate ACL graph instance (runtime), but they share the same graph type definition
3. **Separate graphs, separate types** — Operation definitions and runtime permissions are entirely different graph types
## Decision
Setup-time definitions (`OperationSpec.accessControl`) seed the ACL graph type definition (what node and edge types are valid in an ACL graph), but runtime permission data (who delegates to whom, who has access to which resources) lives in separate ACL graph instances.
The AclGraph Module defines the *type system* for authorization (PrincipalNode, ResourceNode, DelegatesEdge, ScopesEdge, BelongsToEdge). The `OperationSpec.accessControl` definitions inform the *specific operations* that will be evaluated against this graph. But the `AccessControl` data itself lives in the hub's `operations` table, not in the ACL graph.
The ACL graph instance stores runtime facts: "Account A is a member of Org O", "Principal P delegates scopes S to Agent A", "Account A has read/write access to Project X".
## Consequences
**Positive:**
- Clean separation of concerns — operation definitions (what's available) and ACL instances (who has access) evolve independently
- The AclGraph Module can be defined in `@alkdev/storage` without importing `@alkdev/operations` — it mirrors `Identity` and `AccessControl` shapes but doesn't depend on them
- Multiple ACL graph instances are possible: one per org, one global, or one per spoke context. The type system is shared; the instances differ.
- Consistent with the existing pattern: CallGraph Module defines the type system; call graph instances hold specific call data. SecretGraph Module defines the type system; secret graph instances hold specific secret data.
**Negative:**
- Two sources of truth for "what requires access control": `operations.accessControl` (setup-time) and the ACL graph type definition (structural). These must be kept consistent by convention, not by FK.
- The evaluator must bridge both: given an operation's `AccessControl`, compute the caller's effective permissions from the ACL graph instance, then apply `enforceAccess`. This is application-layer logic, not stored in the database.
## References
- [acl.md](../acl.md) — ACL graph architecture specification
- ADR-034: ACL is a metagraph
- Operations AccessControl: `/workspace/@alkdev/operations/src/types.ts`
- Hub operations table: `/workspace/@alkdev/hub/docs/architecture/storage/spokes.md`