electric-sql / electric-sql/electric
RFC: Add principals as first-class entities
- Dominant language
- TypeScript
- Stars
- 10.4k
- Forks
- 375
- Avg merge
- 3d 1h
- Merged PRs (30d)
- 18
Description
## Summary
Add principals as a first-class entity type so that every action in the agents system traces to an owning identity. This provides the foundation for per-user agent sessions, auditing, observability, and (later) capability-based authorization.
Builds on #4305 which introduces asserted identity headers and `created_by` tagging for desktop dispatch.
## Motivation
Today the agents system has no concept of "who" — entities have `write_token` for mutation auth and a free-form `from` string on messages, but there's no structured identity that links agents to users, enables per-user observability, or supports multi-principal isolation.
Without principals you can't build basics like:
- An app showing all of a user's agent sessions
- Audit logs of which user triggered which agent actions
- Multi-tenant isolation where different users' agents are separated
- Per-user rate limiting or quota enforcement
### Three target use cases
**Personal system** — Single user, everything allowed, zero auth config. Principals exist for observability but impose no restrictions.
**Software factory** — Multiple engineers with individual accounts, automation agents, CI bots. Different principals see different things. Misbehaving agents are contained by the tools they're given.
**Customer success app** — External auth (Clerk, Better Auth, enterprise SSO) handles identity. Per-customer authorization expressed through what tools/functions the handler exposes based on the authenticated principal.
## Design
### Principals are entities
Addressed at `/principal/{type}:{id}` (e.g., `/principal/user:kyle`, `/principal/agent:ci-bot`). The `principal` entity type is built-in. The stream IS the principal — identity, inbox, state, and (eventually) capabilities are collections on the same stream.
**Principal types:**
- `user:` — human users
- `agent:` — automation agents owned by a user
- `service:` — system integrations (CI, webhooks)
- `system:` — framework-level principals (cron, default)
Type is part of the ID, follows SPIFFE/IAM convention.
### Identity via header
Inbound requests carry the principal in a header (e.g., `X-Electric-Principal: user:kyle`). The integrator's edge (reverse proxy, gateway, auth middleware) authenticates the user and sets the header. The agents server trusts the header.
**Dev mode:** Falls back to `dev:local` when the header is absent, so personal-system use cases work with zero config.
**Lazy materialization:** First sight of a principal creates its entity stream. No upfront registration required.
### Agents owned by principals
Every agent records its owning principal at spawn time (`created_by`). This enables:
- Querying all agents belonging to a user
- Cascading cleanup when a principal is removed
- Audit trail linking agent actions to the initiating user
### Authorization via handler & tools
For v1, authorization is encoded at the handler level — the integrator decides what tools and functions to expose to each agent based on the principal context. The handler can load auth rules from an external API (e.g., check a permissions service, query your database, call Clerk's API) to decide what to include in the agent's tool set.
```typescript
// Example: handler scopes tools based on principal
export default defineHandler({
async handle(ctx) {
const principal = ctx.principal; // { type: "user", id: "kyle" }
// Load permissions from your auth system
const perms = await myAuthAPI.getPermissions(principal.id);
// Only give the agent tools it's allowed to use
const tools = [];
if (perms.canReadDocs) tools.push(readDocsTool);
if (perms.canSendMessages) tools.push(sendMessageTool);
if (perms.isAdmin) tools.push(adminTool);
return runAgent({ tools, ...ctx });
}
});
```
This keeps authorization flexible and application-specific without the framework needing to own an auth model. The framework provides the principal identity; the integrator decides what that identity can do.
### Inter-principal messaging
`ctx.send("/principal/user:bob", msg)` writes to Bob's inbox collection. Same verb whether the target is a user, agent, or service. Authorization for cross-principal messaging is handler-level (the integrator decides whether to include `send` in the agent's tools).
## Future work (not in this RFC)
These are known future layers that this design should not preclude:
- **Public sharing links / signed URLs** — Serializable scoped authority for read-only (or scoped) access via URL. HMAC-signed, short-expiry. Needed fairly soon for sharing agent sessions, read-only dashboards, etc.
- **Capability expressions in entity streams** — A runtime-evaluated expression language for per-resource, per-principal authorization conditions (time-based, relationship-based, context-based). Stored in principal entity streams, materialized in Postgres for querying.
- **Named capability sets** — Conventional names like `viewer`/`editor`/`admin` defined as macros over capability expressions, providing an adoptable entry point that integrators can outgrow.
- **Monotonic capability decrease on delegation** — When an agent spawns a sub-agent, the child's authority is provably a subset of the parent's.
- **Cross-principal policy** — Framework-level rules for inter-principal communication (can agent owned by Alice message Bob?).
These can be layered on incrementally because principals + entity streams are already the substrate.
## Open questions
1. **Header name**: `X-Electric-Principal`? `X-Electric-User`? Align with #4305's `X-Electric-Asserted-*` headers?
2. **Principal entity handler**: What built-in collections does a principal stream have? (inbox, log, state, capabilities?)
3. **Trust boundary for self-hosted**: Managed Electric Cloud can enforce header trust via infrastructure. Self-hosted needs guidance — bind to localhost by default? Require explicit `trustHeaders` config?
4. **System principal bootstrap**: How do `system:cron`, `system:default` get created at startup?
5. **Principal GC**: When an upstream user is deleted, what happens to their principal stream, owned agents, and spawned entities?
## Context
This design emerged from a structured analysis of authorization approaches for the agents framework ([dialectic artifacts](https://github.com/electric-sql/electric/tree/signals/dialectic-darix-auth)). The key finding: ship principals + headers + tool-level constraints now, let capability machinery emerge from actual usage patterns rather than designing it in advance.
Contributor guide
Assessment
This issue has not been assessed yet.