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

@@ -1,5 +1,5 @@
--- ---
status: reviewed status: draft
last_updated: 2026-05-30 last_updated: 2026-05-30
--- ---
@@ -21,6 +21,7 @@ Storage has Phase 1-3 of the metagraph implementation complete: Metagraph Module
| [schema-evolution.md](schema-evolution.md) | Value.Diff/Cast/Patch for schema migration, version strategy | reviewed | | [schema-evolution.md](schema-evolution.md) | Value.Diff/Cast/Patch for schema migration, version strategy | reviewed |
| [encrypted-data.md](encrypted-data.md) | Crypto utility, encrypted node type, key management | reviewed | | [encrypted-data.md](encrypted-data.md) | Crypto utility, encrypted node type, key management | reviewed |
| [forward-look.md](forward-look.md) | Pointers, dbtype, ujsx IR (conceptual, post-v1) | draft | | [forward-look.md](forward-look.md) | Pointers, dbtype, ujsx IR (conceptual, post-v1) | draft |
| [acl.md](acl.md) | Access control graph: principal/agent framework, scoping, operations integration | draft |
### Design Decisions ### Design Decisions
@@ -59,6 +60,10 @@ Storage has Phase 1-3 of the metagraph implementation complete: Metagraph Module
| [031](decisions/031-moduletodbschema-for-updates.md) | moduleToDbSchema() for schema updates | Accepted | | [031](decisions/031-moduletodbschema-for-updates.md) | moduleToDbSchema() for schema updates | Accepted |
| [032](decisions/032-single-author-not-crdt.md) | Single-author model, not CRDT | Accepted | | [032](decisions/032-single-author-not-crdt.md) | Single-author model, not CRDT | Accepted |
| [033](decisions/033-json-path-queries-for-v1.md) | JSON path queries and hand-written CRUD for v1 | Accepted | | [033](decisions/033-json-path-queries-for-v1.md) | JSON path queries and hand-written CRUD for v1 | Accepted |
| [034](decisions/034-acl-as-metagraph.md) | ACL is a metagraph, not domain-specific tables | Accepted |
| [035](decisions/035-actors-become-acl-nodes.md) | Actors become ACL nodes, standalone table removed | Accepted |
| [036](decisions/036-principal-agent-as-delegation-edges.md) | Principal-agent as delegation edges with scope narrowing | Accepted |
| [037](decisions/037-setup-vs-runtime-separation.md) | Setup-time definitions seed graph types, runtime instances are separate | Accepted |
### Open Questions ### Open Questions

629
docs/architecture/acl.md Normal file
View File

@@ -0,0 +1,629 @@
---
status: draft
last_updated: 2026-05-30
---
# Access Control Graph
How access control is modeled as a metagraph, how it integrates with
`@alkdev/operations`' `Identity` and `AccessControl`, and how the
principal-agent framework maps to graph relationships.
## Purpose
Access control in the alk.dev platform needs to serve two distinct but
overlapping audiences:
1. **Setup-time** — A hub/spoke administrator defines which operations exist,
what their access requirements are, and which principal-agent relationships
are valid. This is the *schema* of authorization.
2. **Runtime** — When a session calls an operation, the system resolves whether
that specific call is permitted given the caller's identity, their scope
within the org, and any delegation chain involved. This is the *evaluation*
of authorization.
Both can be expressed as graphs. The setup-time definitions (operations, org
structures, trust relationships) become graph types — schema-level declarations
of what kinds of nodes and edges are valid. The runtime instances (who has
delegated to whom, which org has which scope over which resource) become graph
instances — concrete data that the authorization evaluator traverses.
This document defines:
- The **AclGraph Module** — a TypeBox `Type.Module` that defines the node and
edge types for access control
- The **ACL graph instance model** — how principal-agent relationships,
org-scoped permissions, and operation-level access control are represented as
graph instances
- The **relationship between setup-time and runtime** — how `OperationSpec`
definitions seed ACL graph types, and how `Identity` is evaluated at runtime
- The **principal-agent hierarchy** — how liability flows upward and permissions
flow downward through delegation edges
- The **scoping model** — how org membership and resource access are graph edges
## Design Decisions
| ADR | Decision | Status |
|-----|----------|--------|
| [034](decisions/034-acl-as-metagraph.md) | ACL is a metagraph, not domain-specific tables | Accepted |
| [035](decisions/035-actors-become-acl-nodes.md) | Actors become node types in ACL and org graphs, standalone table removed | Accepted |
| [036](decisions/036-principal-agent-as-delegation-edges.md) | Principal-agent relationships are `delegates` edges with scope narrowing | Accepted |
| [037](decisions/037-setup-vs-runtime-separation.md) | Setup-time definitions seed graph types; runtime instances are separate graphs | Accepted |
**Sub-decisions** (documented inline, not separate ADRs):
- **`identityType` discriminator over separate node types**: A single
`PrincipalNode` with an `identityType` field is used instead of separate
`AccountNode`, `OrgNode`, `ServiceNode`, `RoleNode` types. This avoids
node type proliferation and keeps the AclGraph Module compact. The type
discriminator drives evaluation branching rather than requiring separate
Module entries for each identity type.
- **AclGraph Config: directed, no multi, no self-loops**: Delegation and
scoping are inherently directional. Multi-edges between the same pair
(delegating different scope sets) carry no additional information — a
single edge with `narrowedScopes` attributes is sufficient. Self-loops
would represent self-delegation, which is semantically meaningless.
## Two Kinds of Authorization Scopes
A distinction that makes ACL tractable:
| Aspect | Setup-Time (Schema) | Runtime (Data) |
|--------|---------------------|----------------|
| What it is | Operation definitions and their `AccessControl` requirements | Who can do what, where, under whose authority |
| Source | `OperationSpec.accessControl` registered in the operations registry | ACL graph instances, org memberships, delegation chains |
| Stored as | Operation rows in the hub's `operations` table | Nodes and edges in ACL graph instances |
| Mutability | Changes when operations are registered/updated | Changes when org membership, delegation, or scoping changes |
| Evaluation | None — it's declarative | Traversed at call time to resolve permissions |
**Setup-time** answers: "What operations *could* require?" — it defines the
`AccessControl` schema per operation.
**Runtime** answers: "Does *this specific caller* have permission for *this
specific call*?" — it traverses the ACL graph instance.
The ACL graph type captures **runtime** authorization data. Setup-time
definitions live in `@alkdev/operations` and the hub's `operations` table. They
are related but distinct: setup-time defines the *rules*, runtime instances
store the *facts* about who follows those rules.
## The AclGraph Module
### Graph Type Configuration
The ACL graph type is **directed**, does not allow multi-edges, and does not
allow self-loops. Delegation and scoping are inherently directional (principal
→ agent, org → member). Self-delegation and multi-edges between the same pair
don't carry additional information — a single `delegates` edge with narrowed
scope attributes is sufficient.
```ts
Config: Type.Composite([
Metagraph.Import("Config"),
Type.Object({
type: Type.Literal("directed"),
multi: Type.Literal(false),
allowSelfLoops: Type.Literal(false),
}),
]),
```
### Node Types
| Node Type | Purpose | Key Attributes |
|-----------|---------|----------------|
| `PrincipalNode` | An identity that can delegate authority | `identityId`, `identityType`, `scopes`, `resources` |
| `ResourceNode` | A resource that can be scoped | `resourceType`, `resourceId` |
#### `PrincipalNode`
Represents an entity that holds authority and can delegate it. This is the
node-level equivalent of `@alkdev/operations`' `Identity` interface, extended
with the `identityType` concept needed for the principal-agent framework.
```ts
PrincipalNode: Type.Composite([
Metagraph.Import("BaseNode"),
Type.Object({
identityId: Type.String({ minLength: 1, maxLength: 255 }),
identityType: Type.Union([
Type.Literal("account"),
Type.Literal("service"),
Type.Literal("org"),
Type.Literal("role"),
]),
scopes: Type.Array(Type.String()),
resources: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String()))),
}),
]),
```
- `identityId` references the account, service account, org, or role by its
canonical identifier. This is **not** a foreign key — it's a logical reference
to an entity in another system (the hub's `accounts` or `organizations` table,
or a role name in the `roles` table). The ACL graph stores the shape; the hub
stores the authoritative identity records. (See ADR-034.)
- `identityType` determines what kind of entity this principal represents. This
matters for evaluation: an `org` principal aggregates its members' scopes, a
`role` principal is a named set of scopes, an `account` principal is a human
or service identity.
- `scopes` and `resources` map directly from `@alkdev/operations`' `Identity`
interface. They represent the **base authority** of this principal before
any delegation narrowing.
- `OrgNode` and `AccountNode` are **not separate node types**. The
`identityType` discriminator on `PrincipalNode` avoids proliferating node
types for entities that are structurally identical (they all hold scopes
and resources). When evaluation needs entity-specific behavior (e.g., org
account aggregation), the `identityType` drives branching, not the node
type.
#### `ResourceNode`
Represents a resource that permission scopes can reference. Resources are the
targets of `AccessControl.resourceType` and `AccessControl.resourceAction`
projects, spokes, workspaces, or any entity that an operation scopes to.
```ts
ResourceNode: Type.Composite([
Metagraph.Import("BaseNode"),
Type.Object({
resourceType: Type.String({ minLength: 1, maxLength: 255 }),
resourceId: Type.String({ minLength: 1, maxLength: 255 }),
}),
]),
```
- `resourceType` maps to `AccessControl.resourceType` (e.g., `"project"`,
`"spoke"`, `"workspace"`).
- `resourceId` is the canonical identifier for the specific resource instance.
Together, `resourceType:resourceId` forms the key for
`Identity.resources` lookups.
### Edge Types
| Edge Type | Source → Target | Purpose |
|-----------|-----------------|---------|
| `DelegatesEdge` | PrincipalNode → PrincipalNode | Principal delegates (a subset of) authority to agent |
| `ScopesEdge` | PrincipalNode → ResourceNode | Principal has specific access to a resource |
| `BelongsToEdge` | PrincipalNode → PrincipalNode (identityType org) | Account/service is a member of an org |
#### `DelegatesEdge`
The core of the principal-agent framework. When principal P delegates to agent
A, a `DelegatesEdge` is created from P → A with `narrowedScopes` attributes
that represent the subset of P's authority transferred to A.
```ts
DelegatesEdge: Type.Composite([
Metagraph.Import("BaseEdge"),
Type.Object({
type: Type.Literal("delegates"),
narrowedScopes: Type.Array(Type.String()),
narrowedResources: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String()))),
}),
]),
DelegatesEdgeConstraints: Type.Object({
edgeType: Type.Literal("delegates"),
allowedSourceTypes: Type.Array(Type.Literal("Principal")),
allowedTargetTypes: Type.Array(Type.Literal("Principal")),
}),
```
- `narrowedScopes` is the **subset** of the principal's scopes that are
delegated. It MUST be a subset — an agent cannot receive more authority than
the principal holds. Enforcement happens at edge creation time: the evaluator
validates that `narrowedScopes` ⊆ principal's effective scopes.
- `narrowedResources` is an optional narrowing of resource-scoped permissions.
If absent, the agent inherits all of the principal's resource scoping. If
present, it must be a subset of the principal's effective resources.
- The **no-escalation invariant**: a delegation chain must never grant an agent
higher privilege than any ancestor. This is enforced by the evaluator at
edge creation time and at call-time evaluation. See Principal-Agent Framework
below.
#### `ScopesEdge`
Links a principal to a resource they have specific access to. This is the
graph-level representation of `Identity.resources`.
```ts
ScopesEdge: Type.Composite([
Metagraph.Import("BaseEdge"),
Type.Object({
type: Type.Literal("scopes"),
actions: Type.Array(Type.String()),
}),
]),
ScopesEdgeConstraints: Type.Object({
edgeType: Type.Literal("scopes"),
allowedSourceTypes: Type.Array(Type.Literal("Principal")),
allowedTargetTypes: Type.Array(Type.Literal("Resource")),
}),
```
- `actions` maps to `AccessControl.resourceAction` values. For example,
`["read", "write"]` on a `ScopesEdge` from an account principal to a project
resource means that account has read+write access to that project.
- This edge type directly mirrors `Identity.resources` where the key format
`"type:id"` maps to `resourceType:resourceId` on the target `ResourceNode`,
and the value array maps to `actions`.
#### `BelongsToEdge`
Declares that an account (or service) is a member of an organization. This is
the graph-level representation of the hub's `organization_members` table.
```ts
BelongsToEdge: Type.Composite([
Metagraph.Import("BaseEdge"),
Type.Object({
type: Type.Literal("belongs_to"),
membershipLevel: Type.Union([
Type.Literal("owner"),
Type.Literal("admin"),
Type.Literal("member"),
]),
}),
]),
BelongsToEdgeConstraints: Type.Object({
edgeType: Type.Literal("belongs_to"),
allowedSourceTypes: Type.Array(Type.Literal("Principal")),
allowedTargetTypes: Type.Array(Type.Literal("Principal")),
sourceTypeConstraint: Type.Optional(Type.Literal("account")),
targetTypeConstraint: Type.Optional(Type.Literal("org")),
}),
```
- `membershipLevel` maps directly from the hub's `organization_members.membershipLevel`.
- The target must be a PrincipalNode with `identityType: "org"`.
- The source is typically a `PrincipalNode` with `identityType: "account"` or
`"service"`.
- `sourceTypeConstraint` and `targetTypeConstraint` are **optional metadata**
on the edge constraints. They are NOT enforced by the metagraph schema
(which validates attribute schemas, not `identityType` values). They serve
as documentation for evaluation logic and can be validated at the
application layer. This is consistent with ADR-020 (no nodeTypeId on
nodes): the metagraph validates structural constraints (which node types
can be source/target), not domain semantics (which identityType values are
valid for a given edge type).
### Complete AclGraph Module
```ts
export const AclGraph = Type.Module({
Config: /* directed, no multi, no self-loops */,
PrincipalNode: /* identityId, identityType, scopes, resources */,
ResourceNode: /* resourceType, resourceId */,
DelegatesEdge: /* delegates with narrowedScopes, narrowedResources */,
DelegatesEdgeConstraints: /* Principal → Principal */,
ScopesEdge: /* scopes with actions */,
ScopesEdgeConstraints: /* Principal → Resource */,
BelongsToEdge: /* belongs_to with membershipLevel */,
BelongsToEdgeConstraints: /* account/service → org */,
MembershipLevel: Type.Union([
Type.Literal("owner"),
Type.Literal("admin"),
Type.Literal("member"),
]),
IdentityType: Type.Union([
Type.Literal("account"),
Type.Literal("service"),
Type.Literal("org"),
Type.Literal("role"),
]),
});
```
## Principal-Agent Framework
### Core Principle
The principal-agent framework comes from legal theory: a **principal** delegates
authority to an **agent**, and the principal bears responsibility for the
agent's actions within the scope of delegation. Two rules follow:
1. **Liability flows upward**: If an agent makes a mistake, the principal who
delegated to them is responsible. This chain continues upward through
delegation levels.
2. **Permissions flow downward**: An agent's effective permissions are always
a subset of their principal's permissions. Delegation can only narrow scope,
never expand it.
### As a Directed Graph
In the ACL graph, delegation is a `DelegatesEdge` from principal → agent:
```
User (principal)
│ delegates [{ scopes: ["dev:*"], narrowedResources: { "project:alpha": ["read", "write"] } }]
Coordinator (agent of User)
│ delegates [{ scopes: ["dev.fs.read", "dev.fs.write"], narrowedResources: { "project:alpha": ["read"] } }]
Implementer (agent of Coordinator)
```
Each `DelegatesEdge` carries `narrowedScopes` — what the principal is passing
down. The no-escalation invariant means:
- If User has `scopes: ["admin", "dev:*"]`, the Coordinator can only receive a
subset: `narrowedScopes: ["dev:*"]`
- If the Coordinator has effective `scopes: ["dev:*"]`, the Implementer can
only receive `narrowedScopes: ["dev.fs.read", "dev.fs.write"]`
- At no point can the Implementer have higher privilege than User
### Evaluation Algorithm
Given a call with `Identity { id, scopes, resources }` and an operation's
`AccessControl { requiredScopes, requiredScopesAny, resourceType, resourceAction }`:
1. **Check scopes (AND)**: Every scope in `requiredScopes` must be in the
identity's effective scopes.
2. **Check scopes (OR)**: At least one scope in `requiredScopesAny` must be in
the effective scopes.
3. **Check resource**: If `resourceType` and `resourceAction` are set, the
identity's `resources["resourceType:resourceId"]` must include
`resourceAction`.
The **effective scopes** for an identity are computed by traversing the ACL
graph:
```
effectiveScopes(identity) =
identity.scopes
∩ (delegation chain intersection)
(org membership scopes)
```
For a chain of `DelegatesEdge`s from root principal to this identity, each
edge narrows the scope. The effective scopes are the intersection of all scope
sets along the chain from root to leaf.
For `resources`, the same narrowing applies: if a `DelegatesEdge` has
`narrowedResources`, those narrow the resource scoping. If absent, the chain
is unchanged (agent inherits principal's resource scoping).
**Scope string semantics** follow `@alkdev/operations`' keypal convention:
colon-separated hierarchical segments where `*` is a wildcard matching any
suffix. `"dev:*"` matches `"dev.read"`, `"dev.write"`, `"dev.fs.read"`, etc.
`"dev:read"` matches only `"dev:read"`, not `"dev:write"`. The subset check
`narrowedScopes ⊆ effectiveScopes` uses this wildcard matching, not simple
string equality. This is consistent with `@alkdev/operations`' `AccessControl`
enforcement (see `checkAccess` in `access.ts`).
**Graph traversal optimization**: The evaluator can cache effective scopes per
identity per graph. When a `DelegatesEdge` or `ScopesEdge` is created or
removed, only identities in the affected subtree need recalculation.
### Multi-Parent Aggregation
A `PrincipalNode` can have multiple incoming edges. The aggregation rules
differ by edge type:
**Multiple `DelegatesEdge` incoming edges** (multiple principals delegating
to the same agent): The agent's effective scopes are the **union** of
delegation chains. Each chain is independently intersected (narrowed), then
the results are unioned. This allows an agent to receive authority from
multiple independent principals without either principal's chain restricting
the other.
```
effectiveScopes(agent) = (chain_i: root_i.scopes ∩ ... ∩ edge_n.narrowedScopes)
```
This preserves the no-escalation invariant per chain: no single chain can
exceed its root principal's authority. The union across chains means the agent
accumulates authority from multiple independent delegations, each bounded by
its own chain.
**`DelegatesEdge` + `BelongsToEdge` incoming edges**: These are additive.
`DelegatesEdge` provides delegation-chained scopes; `BelongsToEdge` provides
org-level scopes. The identity's effective scopes are the **union** of all
delegation chain results plus org-derived scopes.
```
effectiveScopes(identity) =
( delegation_chains) ( org_memberships)
```
Each component is independently bounded: delegation chains by their root
principal's authority, org memberships by the `membershipLevel`-derived
scope mapping.
### Transitivity
Delegation edges are **transitive**: if A delegates to B, and B delegates to C,
then C's effective authority is:
```
effectiveScopes(C) = A.scopes ∩ B.narrowedScopes ∩ C.narrowedScopes
```
This is equivalent to: C's effective authority is whatever A gave B, further
narrowed by what B gave C. The graph traversal is a depth-first walk up the
delegation chain, intersecting at each step.
### Cycle Detection
The ACL graph must be a **directed acyclic graph (DAG)**. Cycles in delegation
create paradoxes (A delegates to B who delegates back to A) and make scope
computation non-terminating.
**Detection algorithm**: At edge creation time, perform a DFS from the target
node upward through all `DelegatesEdge`s. If the source node is reachable, the
edge would create a cycle and must be rejected.
**`allowSelfLoops: false`** in the AclGraph Config prevents self-delegation
edges (A → A), but does NOT prevent longer cycles (A → B → A). Cycle
detection is performed at the application layer during edge insertion. The
`BelongsToEdge` type cannot create cycles because its target must have
`identityType: "org"` and orgs cannot delegate (they are not agents), so only
`DelegatesEdge` cycles need to be checked.
The chain always terminates** because the ACL graph is a DAG enforced at
edge creation time.
### BelongsTo vs Delegates
`BelongsToEdge` and `DelegatesEdge` serve different purposes:
- `BelongsToEdge` declares **organizational membership**: "Account A is a
member of Org O with level 'admin'." It enables org-level scoping: when an
operation requires `resourceType: "project"` access, and the project belongs
to Org O, account A's membership in O can be checked.
- `DelegatesEdge` declares **authority transfer**: "Principal P delegates scopes
S to Agent A." It creates the principal-agent relationship for a specific
task or session.
A `BelongsToEdge` does NOT carry scope narrowing — org membership is not
delegation. The org's effective scopes for a member are determined by the
member's `membershipLevel`, which maps to org-level permissions (owner → full
control, admin → manage projects and members, member → access org resources).
Org-level permissions can also be modeled explicitly with `ScopesEdge`:
an org principal can have a `ScopesEdge` to a resource representing the org
itself, with `actions: ["admin"]` for owners, `["manage"]` for admins.
## Scoping Model
### Two-Level Scoping
The ACL system operates at two levels:
1. **Operation-level** (setup-time): `AccessControl` on `OperationSpec` defines
*what scopes and resource actions are required* to invoke an operation. This
is registered once when the operation is defined and changes infrequently.
This data lives in `@alkdev/operations` and the hub's `operations` table.
2. **Graph-level** (runtime): The ACL graph instance stores *who has what* and
*who delegates to whom*. This is queried at call time to resolve whether a
specific identity satisfies an operation's access control requirements.
### How They Connect
```
OperationSpec.accessControl ACL Graph Instance
┌─────────────────────┐ ┌─────────────────────────┐
│ requiredScopes: │ │ PrincipalNode │
│ ["dev:read"] │◄───────────│ identityId: "user-1" │
│ resourceType: │ evaluate │ scopes: ["dev:*"] │
│ "project" │ │ │
│ resourceAction: │ │ ScopesEdge │
│ "write" │ │ → ResourceNode │
│ │ │ (project:alpha) │
└─────────────────────┘ │ actions: ["read","write"]│
└─────────────────────────┘
```
The **evaluator** bridges the two:
1. Receives `AccessControl` from the operation definition
2. Looks up the caller's `Identity` (which may reference an ACL graph)
3. Traverses the ACL graph to compute effective scopes and resources
4. Applies `enforceAccess(accessControl, effectiveIdentity)`
### Org-Scoped Access
When a `BelongsToEdge` connects an account to an org, and the org has
`ScopesEdge` connections to resources, the account inherits org-level access
through its membership:
```
Account PrincipalNode ──belongs_to──→ Org PrincipalNode
scopes (actions: ["manage", "read"])
ResourceNode (project:alpha)
```
The evaluation rule: if Account belongs to Org with `membershipLevel: "admin"`,
and Org has `ScopesEdge` to Resource with `actions: ["manage", "read"]`, then
Account has the intersection of `[Org-scopes for membershipLevel admin]` and
`[actions on ScopesEdge]` for that resource.
This intersection ensures that org-level permissions don't automatically grant
admin access just because the org admin has `actions: ["manage"]` — the account's
own base scopes limit what they can exercise from org membership.
## What This Replaces
### The Disconnected `actors` Table
The `actors` table in `src/sqlite/tables/actors.ts` is replaced by
`PrincipalNode` in the ACL graph. The `ACTOR_TYPE` enum (`Human`, `Llm`,
`Agent`) maps to `identityType` values (`account`, `service`, `account` — LLMs
are accounts in the hub model per ADR-012). The standalone table has no
foreign key relationships and was explicitly deferred pending ACL design (OQ-03).
This does **not** mean the hub's `accounts` table is replaced. The hub's
`accounts` table remains the authoritative identity store with email, access
level, and Gitea linking. `PrincipalNode` in the ACL graph **references** the
account by `identityId` but does not duplicate its columns. The ACL graph
stores *authorization* data; the hub's identity tables store *authentication*
data.
### Hub's `organization_members` as a Source
The hub's `organization_members` table is the authoritative source for who
belongs to which org. When org membership changes, the hub updates both:
1. The `organization_members` row (fast lookup, FK constraints)
2. The `BelongsToEdge` in the ACL graph instance (graph traversal, evaluation)
This dual-write is necessary because the hub needs fast SQL lookups for
membership checks (e.g., "list all members of this org"), while the ACL graph
needs the edge for traversal-based evaluation (e.g., "compute effective scopes
for this account across all orgs").
### Hub's Permission Resolution
The hub's current permission resolution model (role requests ∩ account scopes ∩
spoke trust level) is preserved. The ACL graph adds a **fourth input**:
delegation chain intersection. The full resolution becomes:
```
Effective permissions = ACL Effective ∩ Role.requests ∩ Account.allowed ∩ SpokeType.capable
```
The "ACL Effective" is the delegation-traversal result from the ACL graph.
This ensures that even if a role requests `admin` access, and an account has an
API key with `admin` scope, if the ACL delegation chain only grants `dev:read`,
the effective scope is `dev:read`.
## Open Questions
- **OQ-20**: Should `DelegatesEdge` support temporary delegation with
expiration? (Referenced in [open-questions.md](open-questions.md))
- **OQ-21**: Should the ACL evaluator live in `@alkdev/storage` or in the hub?
(Referenced in [open-questions.md](open-questions.md))
- **OQ-22**: How are ACL graph instances created and managed? (Referenced in
[open-questions.md](open-questions.md))
- **OQ-23**: Should `BelongsToEdge` be derived (materialized from
`organization_members`) or primary (ACL graph is the source of truth)?
(Referenced in [open-questions.md](open-questions.md))
- **OQ-24**: How does `identityId` reference hub entities without creating a
package dependency? (Referenced in [open-questions.md](open-questions.md))
## References
- Metagraph Module system: [metagraph-module.md](metagraph-module.md)
- SQLite host and actors table: [sqlite-host.md](sqlite-host.md)
- Operations `Identity` and `AccessControl`: `/workspace/@alkdev/operations/src/types.ts`
- Hub identity tables: `/workspace/@alkdev/hub/docs/architecture/storage/identity.md`
- Hub agent-roles model: `/workspace/@alkdev/hub/docs/architecture/agent-roles.md`
- Hub roles table: `/workspace/@alkdev/hub/docs/architecture/storage/roles.md`
- Encrypted data and SecretGraph pattern: [encrypted-data.md](encrypted-data.md)

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`

View File

@@ -1,5 +1,5 @@
--- ---
status: reviewed status: draft
last_updated: 2026-05-30 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 | | Status | Count |
|--------|-------| |--------|-------|
| Open | 8 | | Open | 13 |
| Partially resolved | 1 | | Partially resolved | 1 |
| Resolved | 10 | | Resolved | 11 |
**Open questions requiring decisions:** **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-04** (repository layer host-specific vs host-agnostic) — start host-specific
- **OQ-07** (encryptRaw performance) — low priority, add if needed - **OQ-07** (encryptRaw performance) — low priority, add if needed
- **OQ-10** (Edit[] classification) — needs POC - **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-12** (schema evolution vs event-sourced replay) — post-v1 concern
- **OQ-13** (schema evolution events in event stream) — post-v1 - **OQ-13** (schema evolution events in event stream) — post-v1
- **OQ-19** (storage-operations bridge package location) — depends on long-term CRUD strategy - **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:** **Partially resolved:**
- **OQ-01** (flowgraph Module export) — storage can start without it - **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 Impact
| ADR | Resolves | | ADR | Resolves | Informs |
|-----|----------| |-----|----------|---------|
| ADR-003 | OQ-01 (partial — storage can start without flowgraph Module) | | ADR-003 | OQ-01 (partial — storage can start without flowgraph Module) | |
| ADR-015 | OQ-05 (constraint semantics) | | ADR-015 | OQ-05 (constraint semantics) | |
| ADR-018 | OQ-17 (v1 decision: dbtype integration deferred, JSON path for v1) | | 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-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-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 ## 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? ### OQ-03: Should actors be a node type or a standalone table?
- **Origin**: [overview.md](overview.md) - **Origin**: [overview.md](overview.md)
- **Status**: open - **Status**: resolved
- **Priority**: medium - **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. - **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-024, [encrypted-data.md](encrypted-data.md) - **Cross-references**: ADR-035, ADR-034, [acl.md](acl.md)
### OQ-04: Should the repository layer be host-specific or host-agnostic? ### 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 - **Status**: open
- **Priority**: medium - **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. - **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`

View File

@@ -130,7 +130,7 @@ consumed by the hub and spokes, not by storage itself.
| ----------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------- | | ----------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------- |
| Repository/CRUD layer | High | ⚠️ Not yet implemented. Typed insert, find, update, delete functions for graphs, nodes, edges. No dependency on `@alkdev/operations` — consumer wires CRUD into registry. | | Repository/CRUD layer | High | ⚠️ Not yet implemented. Typed insert, find, update, delete functions for graphs, nodes, edges. No dependency on `@alkdev/operations` — consumer wires CRUD into registry. |
| PostgreSQL host | Medium | Same table shapes, `pgTable` + `jsonb` + `timestamp` + `pgEnum`. Stub only. | | PostgreSQL host | Medium | Same table shapes, `pgTable` + `jsonb` + `timestamp` + `pgEnum`. Stub only. |
| ACL graph type | Medium | Access control as a graph. Informed by `@alkdev/operations`' `Identity` and `AccessControl`. Depends on CRUD layer. | | ACL graph type | Medium | Access control as a metagraph. Principal/agent delegation, org scoping, resource access. See [acl.md](acl.md). |
| Task graph type | Low | Informed by `@alkdev/taskgraph`'s `TaskGraphNodeAttributes` and `DependencyEdge` schemas. | | Task graph type | Low | Informed by `@alkdev/taskgraph`'s `TaskGraphNodeAttributes` and `DependencyEdge` schemas. |
| Graphology bridge | Low | `moduleToGraphology()` and `fromGraphologyExport()` — Phase 4 of the metagraph implementation path. | | Graphology bridge | Low | `moduleToGraphology()` and `fromGraphologyExport()` — Phase 4 of the metagraph implementation path. |
@@ -259,7 +259,7 @@ storage node attributes and operations call events), they should either:
Open questions are tracked in [open-questions.md](open-questions.md). Key Open questions are tracked in [open-questions.md](open-questions.md). Key
questions affecting this package: questions affecting this package:
- **OQ-03**: Should actors be a node type or a standalone table? (open, deferred to ACL design) - **OQ-03**: Should actors be a node type or a standalone table? (resolved: actors become PrincipalNode in ACL graph — ADR-035)
- **OQ-04**: Should the repository layer be host-specific or host-agnostic? (open, start host-specific) - **OQ-04**: Should the repository layer be host-specific or host-agnostic? (open, start host-specific)
- **OQ-14**: Should encryption be per-attribute, per-node, or per-graph? (resolved: per-attribute) - **OQ-14**: Should encryption be per-attribute, per-node, or per-graph? (resolved: per-attribute)
- **OQ-15**: Should key management be in this package? (resolved: no, application provides key ring) - **OQ-15**: Should key management be in this package? (resolved: no, application provides key ring)