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:
@@ -1,5 +1,5 @@
|
||||
---
|
||||
status: reviewed
|
||||
status: draft
|
||||
last_updated: 2026-05-30
|
||||
---
|
||||
|
||||
@@ -13,12 +13,11 @@ When a question is resolved, update its status to `resolved` and add a resolutio
|
||||
|
||||
| Status | Count |
|
||||
|--------|-------|
|
||||
| Open | 8 |
|
||||
| Open | 13 |
|
||||
| Partially resolved | 1 |
|
||||
| Resolved | 10 |
|
||||
| Resolved | 11 |
|
||||
|
||||
**Open questions requiring decisions:**
|
||||
- **OQ-03** (actors table design) — deferred to ACL design
|
||||
- **OQ-04** (repository layer host-specific vs host-agnostic) — start host-specific
|
||||
- **OQ-07** (encryptRaw performance) — low priority, add if needed
|
||||
- **OQ-10** (Edit[] classification) — needs POC
|
||||
@@ -26,6 +25,12 @@ When a question is resolved, update its status to `resolved` and add a resolutio
|
||||
- **OQ-12** (schema evolution vs event-sourced replay) — post-v1 concern
|
||||
- **OQ-13** (schema evolution events in event stream) — post-v1
|
||||
- **OQ-19** (storage-operations bridge package location) — depends on long-term CRUD strategy
|
||||
- **OQ-20** (delegation expiration) — ACL design
|
||||
- **OQ-21** (ACL evaluator location) — ACL design
|
||||
- **OQ-22** (ACL graph instance lifecycle) — ACL design
|
||||
- **OQ-23** (BelongsToEdge derivation) — ACL design
|
||||
- **OQ-24** (identityId reference mechanism) — ACL design
|
||||
- **OQ-25** (scope string semantics for subset validation) — ACL design
|
||||
|
||||
**Partially resolved:**
|
||||
- **OQ-01** (flowgraph Module export) — storage can start without it
|
||||
@@ -42,13 +47,17 @@ When a question is resolved, update its status to `resolved` and add a resolutio
|
||||
|
||||
## ADR Impact
|
||||
|
||||
| ADR | Resolves |
|
||||
|-----|----------|
|
||||
| ADR-003 | OQ-01 (partial — storage can start without flowgraph Module) |
|
||||
| ADR-015 | OQ-05 (constraint semantics) |
|
||||
| ADR-018 | OQ-17 (v1 decision: dbtype integration deferred, JSON path for v1) |
|
||||
| ADR-020 | OQ-02 (no nodeTypeId for now, can add later) |
|
||||
| ADR-033 | OQ-17 (JSON path queries for v1), OQ-18 (hand-written CRUD for v1) |
|
||||
| ADR | Resolves | Informs |
|
||||
|-----|----------|---------|
|
||||
| ADR-003 | OQ-01 (partial — storage can start without flowgraph Module) | |
|
||||
| ADR-015 | OQ-05 (constraint semantics) | |
|
||||
| ADR-018 | OQ-17 (v1 decision: dbtype integration deferred, JSON path for v1) | |
|
||||
| ADR-020 | OQ-02 (no nodeTypeId for now, can add later) | |
|
||||
| ADR-033 | OQ-17 (JSON path queries for v1), OQ-18 (hand-written CRUD for v1) | |
|
||||
| ADR-034 | OQ-03 (actors become ACL nodes) | OQ-21 (evaluator location), OQ-23 (BelongsToEdge derivation), OQ-24 (identityId references) |
|
||||
| ADR-035 | OQ-03 (standalone table removed) | |
|
||||
| ADR-036 | | OQ-20 (delegation expiration) |
|
||||
| ADR-037 | | OQ-21 (evaluator location), OQ-22 (graph instance lifecycle) |
|
||||
|
||||
## Theme 1: Package Boundaries and Dependencies
|
||||
|
||||
@@ -73,10 +82,10 @@ When a question is resolved, update its status to `resolved` and add a resolutio
|
||||
### OQ-03: Should actors be a node type or a standalone table?
|
||||
|
||||
- **Origin**: [overview.md](overview.md)
|
||||
- **Status**: open
|
||||
- **Status**: resolved
|
||||
- **Priority**: medium
|
||||
- **Notes**: Currently `actors` is a standalone table with no relations. If identity/authentication is a graph (ACL nodes based on `@alkdev/operations`' `Identity` interface), actors become node types. If identity needs special query patterns (auth lookups, session joins), standalone tables may be better. Decision deferred until ACL design.
|
||||
- **Cross-references**: ADR-024, [encrypted-data.md](encrypted-data.md)
|
||||
- **Resolution**: Actors become `PrincipalNode` entries in the ACL graph instance. The standalone `actors` table is removed. `ACTOR_TYPE` is replaced by the `IdentityType` enum in the AclGraph Module. See ADR-035.
|
||||
- **Cross-references**: ADR-035, ADR-034, [acl.md](acl.md)
|
||||
|
||||
### OQ-04: Should the repository layer be host-specific or host-agnostic?
|
||||
|
||||
@@ -204,4 +213,54 @@ When a question is resolved, update its status to `resolved` and add a resolutio
|
||||
- **Status**: open
|
||||
- **Priority**: medium
|
||||
- **Notes**: Four options: (1) hub-internal code, (2) dedicated `@alkdev/storage-operations` adapter, (3) `from-storage` adapter inside `@alkdev/operations`, (4) part of `@alkdev/dbtype`'s `from-dbtype` adapter. Option 1 is the most immediate (no new package). Option 2 is the cleanest separation. Option 3 creates an undesirable dependency direction (operations → storage). Option 4 is the long-term goal if dbtype is adopted. The choice depends on OQ-17/OQ-18 resolution: if hand-written CRUD, the bridge is trivial and can live in the hub; if auto-generated from dbtype, the bridge naturally lives with dbtype.
|
||||
- **Cross-references**: OQ-16, OQ-17, ADR-033
|
||||
- **Cross-references**: OQ-16, OQ-17, ADR-033
|
||||
|
||||
## Theme 8: Access Control
|
||||
|
||||
### OQ-20: Should `DelegatesEdge` support temporary delegation with expiration?
|
||||
|
||||
- **Origin**: [acl.md](acl.md)
|
||||
- **Status**: open
|
||||
- **Priority**: low
|
||||
- **Notes**: Currently, `DelegatesEdge` has `narrowedScopes` and `narrowedResources` but no `expiresAt`. If delegation should be time-limited (e.g., "delegate for this session only" or "delegate for 24 hours"), an expiration attribute is needed. Session-scoped delegation could be modeled by creating/removing edges per session, avoiding the need for an `expiresAt` attribute. Time-based expiration adds complexity to the evaluator (checking edge validity at call time) but may be useful for non-session contexts.
|
||||
- **Cross-references**: ADR-036
|
||||
|
||||
### OQ-21: Should the ACL evaluator live in `@alkdev/storage` or in the hub?
|
||||
|
||||
- **Origin**: [acl.md](acl.md)
|
||||
- **Status**: open
|
||||
- **Priority**: high
|
||||
- **Notes**: The ACL evaluator traverses delegation chains and computes effective scopes. Three options: (1) `@alkdev/storage` provides traversal primitives (walk edges, compute effective scopes for a principal given a graph instance) and the hub composes them with `@alkdev/operations`' `enforceAccess`. (2) The hub implements the evaluator from scratch, using storage's repository layer for graph queries. (3) A new `@alkdev/acl` package provides the evaluator, depending on both `@alkdev/storage` and `@alkdev/operations`. Option 1 keeps the dependency direction clean (storage doesn't depend on operations). Option 3 is the cleanest separation but adds a package. The choice depends on whether the evaluator is generic enough to be reusable across different hub implementations.
|
||||
- **Cross-references**: ADR-034, ADR-037
|
||||
|
||||
### OQ-22: How are ACL graph instances created and managed?
|
||||
|
||||
- **Origin**: [acl.md](acl.md)
|
||||
- **Status**: open
|
||||
- **Priority**: medium
|
||||
- **Notes**: Several options: (1) One global ACL graph instance per hub. Simple but means all orgs share a single graph — large graphs may have traversal performance implications. (2) One ACL graph instance per org. Isolated, each org's permissions are self-contained. Requires cross-org delegation to span graphs. (3) One ACL graph instance per "scoping context" (e.g., per spoke context). Most granular but most complex. The choice depends on whether delegation crosses org boundaries (if a user delegates to an agent in another org's context, graphs must be traversable across instances).
|
||||
- **Cross-references**: ADR-037
|
||||
|
||||
### OQ-23: Should `BelongsToEdge` be derived (materialized from `organization_members`) or primary (ACL graph is the source of truth)?
|
||||
|
||||
- **Origin**: [acl.md](acl.md)
|
||||
- **Status**: open
|
||||
- **Priority**: medium
|
||||
- **Notes**: The hub already has an `organization_members` table with `membershipLevel`. If `BelongsToEdge` is derived, the hub writes both `organization_members` rows and ACL graph edges when membership changes, keeping them in sync. If `BelongsToEdge` is primary, the ACL graph is the source of truth and the hub reads org membership from the graph. Derived is consistent with the hub's existing identity tables being authoritative. Primary means the ACL graph replaces org membership data, requiring graph queries for simple membership lookups. Lean toward derived — the hub's identity tables are authoritative for authentication, the ACL graph is authoritative for authorization.
|
||||
- **Cross-references**: ADR-034
|
||||
|
||||
### OQ-24: How does `identityId` reference hub entities without creating a package dependency?
|
||||
|
||||
- **Origin**: [acl.md](acl.md)
|
||||
- **Status**: open
|
||||
- **Priority**: medium
|
||||
- **Notes**: `PrincipalNode.identityId` references an account, organization, or role in the hub's database, but `@alkdev/storage` must not depend on `@alkdev/operations` or the hub. The `identityId` is a string, not a FK. This is consistent with ADR-020 (no nodeTypeId on nodes) — the metagraph pattern stores node attributes without assuming external referential integrity. Options: (1) Logical references (current design) — `identityId` is a string that the hub resolves. (2) Convention-based references — a URI scheme like `alk://account/user-1` or `alk://org/acme` that encodes the entity type and ID. (3) A shared types package that both storage and hub import. Option 1 is the simplest and consistent with the existing pattern. The burden of referential integrity falls on the consumer (the hub), not on storage.
|
||||
- **Cross-references**: ADR-020, ADR-034
|
||||
|
||||
### OQ-25: What are the scope string semantics for subset validation?
|
||||
|
||||
- **Origin**: [acl.md](acl.md)
|
||||
- **Status**: open
|
||||
- **Priority**: high
|
||||
- **Notes**: `narrowedScopes ⊆ effectiveScopes` is the no-escalation invariant, but the semantics of this subset check depend on how scope strings work. `@alkdev/operations` uses keypal's scope model (colon-separated hierarchical segments, `*` wildcard for suffix matching). `"dev:*"` matches `"dev.read"`, `"dev.write"`, `"dev.fs.read"`, etc. The ACL evaluator must use the same semantics or delegation validation will be inconsistent with runtime access checks. Option: import scope matching logic from `@alkdev/operations` or extract it to a shared utility. The ACL graph stores scopes as plain strings; matching is an evaluator concern, not a storage concern.
|
||||
- **Cross-references**: ADR-036, `/workspace/@alkdev/operations/src/access.ts`
|
||||
Reference in New Issue
Block a user