Skip to content

docs(agents): design doc for agent-store ↔ entity bridge [#2847]#2959

Merged
viniciusdacal merged 1 commit intomainfrom
feat/agent-store-entity-bridge
Apr 22, 2026
Merged

docs(agents): design doc for agent-store ↔ entity bridge [#2847]#2959
viniciusdacal merged 1 commit intomainfrom
feat/agent-store-entity-bridge

Conversation

@viniciusdacal
Copy link
Copy Markdown
Contributor

Summary

Design doc for bridging AgentStore with Vertz entities so agent sessions and messages become queryable from app code with rules.* RLS applied — without changing the agent loop's atomic write path. Resolves Gap #4 of plans/open-agents-clone.md.

This PR is design-only. No implementation. Awaits human sign-off on the settled design at plans/agent-store-entity-bridge.md (Rev 6) before Task A/B/C implementation begins in a follow-up PR.

The design, in three sentences

  1. Developers spread agentSessionColumns / agentMessageColumns into their own d.table() calls, register via createDb({ models }), then call defineAgentEntities(db) to get { session, message } entities wired into createServer({ entities }). Tenant scoping auto-detected; access defaults are rules.where({ userId: rules.user.id }) + a before.create hook that injects userId/tenantId from the request context (ctx wins over input).
  2. Writes stay on the existing memoryStore / sqliteStore / d1Store implementations — atomic transaction() / batch() semantics preserved, D1 semantics unchanged, durable resume unaffected. AgentStore.appendMessages gains a session parameter (matches the existing appendMessagesAtomic shape) so stores can denormalize userId/tenantId onto agent_messages rows — enabling flat rules.where({ userId }) on Message without framework changes.
  3. Reads through the entity API (/api/agent-session auto-routes, ctx.entities.agentSession.list()) get full RLS. Reads inside the agent loop (store.loadSession) stay on the existing run.ts:222-252 ownership check. Honest scoping.

Public API changes

Additions (@vertz/agents/entities subpath):

  • agentSessionColumns, agentMessageColumns — plain ColumnRecord constants
  • agentSessionIndexes, agentMessageIndexes — index arrays
  • defineAgentEntities(db, opts?) — factory returning { session, message }

Breaking change (pre-v1, .claude/rules/policies.md permits):

  • AgentStore.appendMessages(sessionId, messages)appendMessages(sessionId, messages, session). All three in-repo store impls update. One call site in run.ts (~1 line). No known external consumers.
  • agent_messages table gains user_id / tenant_id columns. Every @vertz/agents user runs an additive ALTER TABLE agent_messages ADD COLUMN … migration on upgrade. Migration SQL provided.

Review process

Three full rounds of adversarial review (DX, Product, Technical) per .claude/rules/design-and-planning.md:

Rev DX Product Technical
1 Changes (6 blockers) Changes (5 blockers) Changes (4 blockers)
2 Changes (4 blockers) Changes (2 blockers) Changes (6 hallucinations + 3 design)
3 Approved (E2E fixes needed) Approved (polish only) Approved (E2E fixes needed)
5 Approved Approved Changes (3 E2E issues)
6 Clean (final targeted verify)

Architecture settled at Rev 3; Rev 4–6 were editorial passes fixing API-shape drift in the E2E acceptance test (verified against real file:line evidence).

Follow-up issues filed

Decision requested

One scope call flagged for explicit sign-off:

  • Non-adopter breaking change. Every @vertz/agents user must run a two-statement ALTER TABLE migration on upgrade, even if they never adopt entities. Rationale + alternative rejected in the design. If this scope is unacceptable, say so and I'll re-work the denormalization into an opt-in flag.

Test plan

  • Human reads plans/agent-store-entity-bridge.md end-to-end
  • Human signs off on the single scope decision (non-adopter breaking change)
  • Follow-up implementation PR tracks E2E test in apps/demo/e2e/agent-store-entity-bridge.test.ts passing against real createDb + sqliteStore + createCrudHandlers (not the stubbed snippet in the doc)

🤖 Generated with Claude Code

Design doc at plans/agent-store-entity-bridge.md for bridging AgentStore
with Vertz entities so agent sessions/messages become queryable with RLS
from app code. Resolves Gap #4 of plans/open-agents-clone.md.

Rev 6 after three rounds of adversarial review (DX, Product, Technical)
per .claude/rules/design-and-planning.md. All reviewers approved. Awaits
human final sign-off.

Key design decisions settled:
- Entities are a READ-VIEW over the store's tables. Writes stay on
  memoryStore/sqliteStore/d1Store; entity RLS applies to app-side reads.
- defineAgentEntities(db) factory + column packs (no extend API, no sugar
  helpers). Custom fields via normal d.table() spread.
- AgentStore.appendMessages gains session parameter (matches
  appendMessagesAtomic). Pre-v1 breaking change.
- agent_messages denormalizes userId/tenantId for flat rules.where().
  Non-adopter breaking change acknowledged explicitly.
- before.create hook injects ctx.userId/ctx.tenantId (ctx wins over input
  to prevent silent impersonation).
- Hook-bypass on agent-loop writes tracked as follow-up #2957.
- state/toolCalls → d.jsonb<T>() migration tracked as #2958.

Closes: none yet (design-only PR).
Refs: #2847, #2957, #2958, plans/open-agents-clone.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@viniciusdacal viniciusdacal merged commit b1344ba into main Apr 22, 2026
12 of 13 checks passed
viniciusdacal added a commit that referenced this pull request Apr 22, 2026
…/messages) [#2847] (#2966)

Agent sessions and messages become first-class Vertz entities, queryable
from app code with full `rules.*` enforcement (authenticated, tenant-scoped,
row-level `where`). Writes still flow through the existing sqliteStore /
d1Store / memoryStore implementations — atomic paths unchanged.

New `@vertz/agents/entities` subpath exports:
- `agentSessionColumns`, `agentMessageColumns`, `agentSessionIndexes`,
  `agentMessageIndexes` — spread into your own `d.table()`.
- `defineAgentEntities(db)` — looks up the registered tables via
  `db._internals.models` and returns `{ session, message }` entities
  pre-wired with user-scoped access rules and a `before.create` hook that
  injects `userId` / `tenantId` from the request context (ctx wins over
  input — prevents impersonation). Column-aware: only injects fields the
  extended table declares.

Breaking changes (pre-v1, per policies.md):
- `AgentStore.appendMessages(sessionId, messages)` gains a third
  `session: AgentSession` parameter, matching `appendMessagesAtomic`.
  All three in-repo store impls + run.ts + crash-harness updated.
- `agent_messages` table gains `user_id` / `tenant_id` columns
  (denormalized from the session row on every append; required for flat
  `rules.where({ userId: rules.user.id })` since access-enforcer evaluates
  conditions against the row directly). Fresh installs get the columns
  via the stores' updated DDL. Existing DBs run the one-shot migration in
  `packages/agents/migrations/001-add-rls-columns.sql`.

Tests: 254 passing. Integration test proves cross-tenant + cross-user RLS
isolation end-to-end via a shared DB file between sqliteStore writes and
entity reads. Direct store-level assertion confirms denormalization.
Three negative type tests for the factory options.

Docs: new mint-docs page `guides/agents/entity-bridge` covers setup,
default access, reading agent data (HTTP + ctx.entities + "do not use
db.*.list() with RLS"), schema extension, and the upgrade migration.

Design: `plans/agent-store-entity-bridge.md` (merged as #2959).
Follow-ups: #2957 (reject entity hook registration on factory entities),
#2958 (migrate state/toolCalls to `d.jsonb<T>()`).

Closes #2847.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant