Record types
Concrete schemas for the new record types. These are specific enough that someone could add them to the core and wire them through the mutation executor without re-litigating what they mean.
@arbe/core currently has three record types: agent, house, room. Each is a record with a typed content schema validated by zod. Permissions use rwx bitmasks resolved by parent_id chain-walking. Mutations go through a framework-agnostic executor with db adapter and stream hooks.
Resource
A resource is an external thing an agent can operate on. It has identity in the substrate so activation policies and audit trails can reference it by record ID rather than by opaque config blob.
type: 'resource'parent_id: house_idcontent: { name: string kind: 'discord-channel' | 'github-repo' | 'sandbox' | 'durable-stream' | 'linear-workspace' | 'pagerduty-service' | 'webhook-endpoint' | string external_id: string // the platform's own identifier config: Record<string, unknown> // kind-specific, non-secret configuration}The kind field is an open enum — new integrations add new kinds without schema changes. external_id is how we correlate back to the outside world (a Discord channel ID, a GitHub owner/repo, a Fly app name). config holds whatever the adapter needs that isn’t secret (webhook URL, default branch, channel name for display).
Resources are scoped to houses because that’s the permission boundary that makes sense — a house admin controls which external targets are available within their scope.
Resources do not carry secret references. A Discord channel does not own the Discord credential — many resources may share one credential, and one resource may need several. Credential binding is a separate concern (see capabilities).
Tool
Tools are already implied by the x permission bit but they’re not records yet. They should be.
type: 'tool'parent_id: null (global) or house_id (house-scoped)content: { name: string // 'send-message', 'create-pr', 'run-command' description?: string kind: 'builtin' | 'mcp' | 'http' | string schema?: Record<string, unknown> // input schema for validation/documentation config?: Record<string, unknown> // kind-specific (MCP server URL, endpoint pattern)}Global tools are platform capabilities. House-scoped tools are integrations specific to that house.
Activation policy
type: 'activation-policy'parent_id: agent_id (an agent owns its policies)content: { name?: string trigger: 'cron' | 'interval' | 'webhook' | 'stream-event' | 'github-event' | 'mention' | 'ambient' | string config: Record<string, unknown> // trigger-specific // cron: { expression: '0 * * * *' } // interval: { ms: 3600000 } // webhook: { path: '/hooks/my-agent' } // stream-event: { stream_id: '...', filter?: {...} } // mention: { scope_ids: ['...'] } // ambient: { scope_ids: ['...'], delay_ms: 5000 } resource_ids?: string[] // which resources this policy targets tool_ids?: string[] // which tools to use when activated enabled: boolean}Parent is the agent because policies define the agent’s activation behavior. resource_ids and tool_ids say what the policy cares about — the runtime still checks that the agent actually has permission on those tools and resources before executing.
This replaces the current trigger_mode field on AgentContent. The agent record itself should not carry activation configuration — that’s policy, not identity.
Run
When a run is shared (work dispatched remotely, involving shared resources, or involving multiple agents/humans):
type: 'run'parent_id: house_idcontent: { status: 'queued' | 'dispatching' | 'running' | 'waiting' | 'completed' | 'failed' | 'aborted' task_ref?: string // local task ID or shared task record agent_id: string sandbox_ref?: string // sandbox identity session_id?: string activation_id?: string // which policy triggered this started_at?: string finished_at?: string failure_phase?: 'dispatch' | 'execution' | 'observation' | 'result-collection'}Room (extended for sessions)
type: 'room'parent_id: run_id or house_idcontent: { name: string kind: 'conversation' | 'session' // distinguishes human rooms from execution rooms durable_stream_id?: string status?: 'active' | 'idle' | 'blocked' | 'errored' | 'completed' sandbox_ref?: string started_at?: string last_activity_at?: string}A query for active sessions is where type='room' and content->>'kind'='session' and content->>'status' in ('active','idle','blocked').
Artifact
type: 'artifact'parent_id: house_id or project_id (a durable scope, not a run)content: { kind: 'message' | 'file' | 'url' | 'commit' | 'screenshot' | 'log-ref' | string label?: string external_id?: string // remote message ID, PR URL, commit hash mime_type?: string byte_ref?: string // pointer to blob storage, git ref, or stream offset source_run_id?: string // which run produced this source_session_id?: string // which session produced this resource_id?: string // which resource this artifact relates to metadata?: Record<string, unknown>}Artifacts live under a durable scope because they outlive the sessions and runs that produced them. Provenance goes in content where it can be queried without polluting the ownership tree.
Memory
type: 'memory'parent_id: agent_id or house_idcontent: { kind: 'fact' | 'preference' | 'commitment' | 'reference' | string subject?: string // what this is about text: string // the actual memory content confidence?: number // 0-1, decays or strengthens over time source_ref?: string // run_id or session_id where this was learned supersedes?: string // ID of a memory record this replaces tags?: string[]}Agent-level memories are private to that agent. House-level memories are shared knowledge within the house. See memory for the design rationale.
Secret binding
type: 'secret-binding'parent_id: house_idcontent: { name: string // 'discord-bot-token', 'github-app-key' vault_key: string // stable key into the vault/secret store resource_ids?: string[] // which resources this credential serves agent_ids?: string[] // which agents can resolve this credential}A Discord bot token serves many Discord channel resources. A GitHub App key serves many repo resources. The binding record says which credential goes where. See capabilities for the full secrets model.
What changes in @arbe/core
Schema additions:
- Add record types: resource, tool, activation-policy, artifact, run, memory, secret-binding.
- Extend RoomContentSchema with optional
kind,status,sandbox_ref,started_at,last_activity_atfields. - Add content schemas for each new type.
- Add them to
contentSchemaMapandTypedRecord. - Add mutation builders: createResource, createTool, createActivationPolicy, createArtifact, createRun, createMemory, createSecretBinding.
- The mutation executor already handles arbitrary record types — new types mostly need content validation, not new executor logic.
- Remove
trigger_modeandambient_delay_msfrom AgentContentSchema — these move to activation-policy records.
Runtime additions:
resolveCapabilities(agentId, db)— query tool and resource records the agent hasxon, return the intersection.resolveActivationPolicies(agentId, db)— query policies for an agent, return with resolved resource and tool references.resolveSecrets(agentId, resourceIds, db)— query secret-binding records, return vault keys for runtime resolution.
Open questions
Should the tool schema be a full JSON Schema, or something lighter? Full JSON Schema is future-proof but heavyweight. A simpler shape might be enough to start.
Should constraint records exist from day one, or should the “capability = tool permission ∩ resource permission” model run until it breaks? Start without constraints. Add them when a real use case demands narrowing beyond what permissions express.