• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
c15r

c15r

sync

Agent collaboration layer https://sync.parc.land
Public
Like
1
sync
Home
Code
16
.claude
1
docs
11
frontend
12
mcp
5
reference
8
static
1
.gitignore
.vtignore
CLAUDE.md
README.md
auth.ts
cel.ts
deno.json
H
main.ts
schema.ts
timers.ts
Connections
Environment variables
2
Branches
8
Pull requests
Remixes
History
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
docs
/
agency-and-identity.md
Code
/
docs
/
agency-and-identity.md
Search
3/6/2026
Viewing readonly version of main branch: v246
View latest version
agency-and-identity.md

Agency and Identity: Users, Agents, and Rooms in sync v6

Design doc for the identity model that completes v6. Read against v6.md, the-substrate-thesis.md, what-becomes-true.md, and the Phase 3 MCP doc.

March 2026


I. The question this document answers

v6 established two axioms: register actions, register views. Everything else is derived. The constraint produces a specific property: progressive disclosure is implicit. Actions are what changes what you can see. Vocabulary construction is the only unilateral act. Everything else is collaborative.

The Phase 3 doc ("MCP client as agent") does good mechanical work: auto-join, vault-to-registry migration, identity plumbing. It says "an MCP client IS a v6 agent" as a design principle.

But it doesn't interrogate what that means against the substrate thesis. It treats the mapping as obvious when it is actually contested. This document names the contest, explores the design space, and proposes a model that honors both the substrate's philosophical commitments and the practical constraints of OAuth and current MCP products.

The central questions:

  1. What does it mean for agents to be manifested within room state — not just mechanically present but instantiated as room-internal entities?
  2. How does a user (who exists outside all rooms) relate to the agents they create, embody, or connect to rooms?
  3. How does this work through an OAuth flow that is, today, a one-time static grant — and how should it evolve?

II. What the substrate thesis says about identity

The substrate thesis makes a specific claim:

Software is a shared substrate of truth observed by self-activating components.

In this model, state is the substrate, surfaces are observers, actions are transitions, and agents and humans are equivalent participants. The interface is not a controller — it is a perceptual layer. Experiences emerge from observation of shared truth.

For identity, this claim has consequences:

An agent is its traces in the substrate. The agents table is mechanical presence: id, room_id, token_hash, grants, last_heartbeat, status, waiting_on. Everything semantic — what the agent is, what it wants, what it does, what it produces — lives in its own scope as state, projected through views it registers. Identity is self-authored. You are defined because you wrote.

The room is the execution environment, not the agent. "What Becomes True" describes the room as simultaneously memory and medium — Clark and Chalmers' extended mind made operational. The agent's context window, loaded with the room's state, views, and affordances, is not a representation of reality that the agent reasons about. It is the cognitive environment the agent reasons within. The substrate is the medium in which cognition happens.

Presence is meaningful. Liveness is implicit in participation. Every context read and action invocation updates last_heartbeat. An agent that stops reading stops being present. No explicit keepalive. The v6 rhythm — wait → perceive → reason → act → wait — is not a protocol. It is the shape of agency itself. An agent that doesn't follow this rhythm is not fully an agent.

Vocabulary registration is the first act of identity. An agent arriving in an empty room declares itself through the actions and views it registers. Its vocabulary is its thesis about what the room is for. Its objective view is its social contract with peers. Before registration, the agent is mechanically present but semantically absent.


III. Three modes of agency

The current system conflates different modes of agency. Naming them is the first step toward a coherent identity model.

Autonomous agents

The v6 archetype. A process running a wait loop. Condition-driven. Always present (while running). Self-activating in Nii's sense. Their agency is continuous — they don't "check in," they inhabit. The waiting_on field is a declarative statement of relevance: "wake me when something matters." Between wakes, the agent doesn't act but it is — present in the agents list, its heartbeat ticking, its views still resolving.

These are the agents the substrate essays describe: participants in a stigmergic medium, leaving traces, perceiving traces left by others.

Human-proxied agents

The MCP case. A human using Claude (or another LLM client) as an interface. Attention-driven. Intermittently present. Their agency is episodic — bursts of read-reason-act when the human engages. Between episodes, the agent is inert. The room holds their state but they are not perceiving it.

This is closer to how a player interacts with a game: you put it down, you pick it up, the world was there the whole time but your participation was paused.

The v6 rhythm breaks here. An autonomous agent waits on a condition — the substrate pushes context when it becomes true. A human-proxied agent polls on demand — the user says "show me the room" when they feel like it. The waiting_on field is never set because nobody is blocking. The heartbeat is spiky: active during a conversation, absent between conversations.

This is not a deficiency. It is a different mode. But the system should recognize it, not pretend it's the same as continuous presence.

Room-defined agents

The idea this document explores. The room itself declares that certain agent-shaped roles exist, with responsibilities, expected capabilities, and behavioral contracts. These aren't agents yet — they are agent slots. Vacancies in a cast. They become agents when something (a user, an autonomous process, or another agent) instantiates them.

Room-defined agents are manifested within room state. They exist as descriptions of what the room needs, independent of who or what fills them. When filled, the filling entity takes on the role's identity and responsibilities. When vacated, the role's state and history persist, available to the next occupant.


IV. What the current system provides

Core tables

The sync database has five core tables: rooms, agents, state, actions, views. State is the substrate — messages, audit, help, agent state are all scopes in one table.

The agents table is mechanical:

agents ( id TEXT NOT NULL, room_id TEXT NOT NULL, name TEXT NOT NULL, role TEXT DEFAULT 'agent', joined_at TEXT, meta TEXT DEFAULT '{}', last_heartbeat TEXT, status TEXT DEFAULT 'active', waiting_on TEXT, token_hash TEXT, grants TEXT DEFAULT '[]', last_seen_seq INTEGER DEFAULT 0, enabled_expr TEXT, PRIMARY KEY (id, room_id) )

Identity-bearing state lives in the agent's own scope:

state(room_id, scope="agent-1", key="objective", value="...")
state(room_id, scope="agent-1", key="status", value="...")

Projected through self-registered views:

views(id="agent-1.objective", room_id, scope="agent-1", expr='state["agent-1"]["objective"]')

MCP auth layer

The smcp_* tables support OAuth 2.1 + WebAuthn authentication:

  • smcp_users — user accounts (username + passkey credentials)
  • smcp_credentials — WebAuthn public keys
  • smcp_oauth_clients — registered OAuth clients (Claude.ai, Claude Code, etc.)
  • smcp_auth_codes, smcp_access_tokens, smcp_refresh_tokens — OAuth flow
  • smcp_vault — maps user → room → token (the current bridge)
  • smcp_sessions — browser sessions for consent/management UI

The vault as bridge

Currently, the vault stores raw tokens:

user christopher → room game-room → token room_abc123
user christopher → room work-room → token as_def456

An MCP tool call resolves: user → vault → token → room. The token is the identity. The user is the key holder. The agent (if any) is a side effect.

This is the "borrowing authority from a credential" problem the Phase 3 doc identified. The vault maps access, not identity. It answers "can this user reach this room?" but not "who is this user within this room?"


V. Agents are manifested within rooms

The theatrical metaphor

Is "Hamlet" an agent, or is "the actor playing Hamlet" the agent?

In sync, the answer is: Hamlet is a role manifested in the room's state. The actor is a user (or autonomous process) who embodies that role. The role persists across occupants. The occupant brings liveness.

This maps to three distinct entities:

  • Role — a description of needed agency, stored in room state
  • Agent — a mechanical presence in the agents table, with a scope
  • Driver — whatever is currently animating the agent (user, process, nothing)

The role is state. The agent is an instantiation of a role. The driver is a connection to an agent. All three are separable.

Roles as state (not mechanism)

The substrate thesis gives a clear answer here: roles don't need a new mechanism. They are a convention expressible in existing v6 primitives.

A room that needs a "researcher" and a "critic" declares this in state:

// scope: _shared, key: roles { "researcher": { "description": "Find and assess relevant sources", "bootstrap": ["submit_source", "assess_relevance"], "views": ["research_progress"], "filled_by": null }, "critic": { "description": "Challenge assumptions and identify weaknesses", "bootstrap": ["raise_objection", "request_evidence"], "views": ["objections_log"], "filled_by": null } }

This is vocabulary about vocabulary. The role definition is a meta-affordance: it describes what affordances should exist, not the affordances themselves. When something fills the role, it reads the definition, registers the expected actions and views, and begins working.

The pattern already exists embryonically. The standard library is a set of canonical action definitions. help({ key: "standard_library" }) returns ready-to-register templates. Room-defined roles are a more specific version: instead of a generic library, the room carries role-specific bootstrap instructions.

Filling a role

When an agent fills a role, it:

  1. Reads the role definition from _shared.roles
  2. Claims the role: writes filled_by: self (with if_version to prevent double-claim)
  3. Reads help({ key: "standard_library" }) for the bootstrap templates
  4. Registers the actions and views specified in the role definition
  5. Writes its objective to its own scope
  6. Begins the read → evaluate → act rhythm

The role and the agent are distinct entities in the substrate, linked by mutual state. Other agents can see both the role requirements and who is fulfilling them.

Vacating a role

When an agent vacates (disconnects, completes, times out):

  • filled_by is cleared (or set to a tombstone with departure timestamp)
  • The agent's scope state persists — objective, progress, traces remain
  • Registered actions and views persist (they are room state, not agent state)
  • A new occupant can embody the same agent, inheriting scope and history

This is the "agents are manifested within rooms" claim made concrete. The agent exists as state in the room regardless of whether anything is currently driving it. Embodiment is connecting a driver to an existing vehicle, not creating a new vehicle.

Standard library extension

Roles become a standard library pattern:

{ "id": "define_role", "description": "Declare a role this room needs filled", "params": { "role_id": { "type": "string" }, "description": { "type": "string" }, "bootstrap_actions": { "type": "array" }, "bootstrap_views": { "type": "array" } }, "writes": [{ "scope": "_shared", "key": "roles.${params.role_id}", "merge": { "description": "${params.description}", "bootstrap_actions": "${params.bootstrap_actions}", "bootstrap_views": "${params.bootstrap_views}", "filled_by": null, "defined_at": "${now}" } }] }
{ "id": "fill_role", "description": "Claim a role in this room", "params": { "role_id": { "type": "string" } }, "if": "has(state[\"_shared\"], \"roles.\" + params.role_id) && (state[\"_shared\"][\"roles.\" + params.role_id].filled_by == null || state[\"_shared\"][\"roles.\" + params.role_id].filled_by == self)", "writes": [{ "scope": "_shared", "key": "roles.${params.role_id}", "merge": { "filled_by": "${self}", "filled_at": "${now}" } }] }

No new platform feature. Roles are conventions in the substrate.


VI. Users are not agents

The meta-entity

A user (Christopher) is not an agent. A user is not a room. A user is a meta-entity — someone who exists outside all rooms and can:

  • Create rooms (and thus own them)
  • Instantiate agents within rooms (and thus participate)
  • Embody existing agents (and thus resume or take over)
  • Observe rooms without participating (view-level access)
  • Maintain relationships with multiple rooms simultaneously
  • Spawn autonomous agents and walk away

The user is an agent-factory. They create, configure, monitor, and sometimes embody agents. The agent is the room-internal entity. The user is the room-external entity that brings agents into being.

This is consistent with the substrate thesis. The thesis says: "You are the component. You are a participant organism inside it." But this applies to the agent, not the user. When Christopher's researcher agent is running inside a room — perceiving state, leaving traces — that agent is a participant organism. Christopher is the person who set it in motion and occasionally checks on it.

User-room relationships

The smcp_user_rooms table (from Phase 3) models this:

smcp_user_rooms ( user_id TEXT NOT NULL REFERENCES smcp_users(id), room_id TEXT NOT NULL, access TEXT NOT NULL DEFAULT 'participant', is_default INTEGER DEFAULT 0, label TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (user_id, room_id) )

Access levels: owner, collaborator, participant, observer.

But the Phase 3 doc doesn't model the user-agent relationship in any deep way. It assumes one user + one client = one agent per room. This is a reasonable default but an arbitrary restriction.

What users actually want to do

Five interaction patterns, each with different agency requirements:

Pattern 1: "Show me my rooms." Overview across all rooms. No agent involved. The user is above all rooms, not inside any of them. Pure smcp_user_rooms + context snapshots. Read-only. No presence footprint.

Pattern 2: "Let me work in this room." Focused engagement. Read context, send messages, invoke actions. The user becomes a human-proxied agent. Present while engaged, absent between episodes.

Pattern 3: "Set up this room for a task." World-building. Create a room, define roles, register vocabulary, seed state. The user is an architect — parental relationship, not participatory. May never enter the room as an agent.

Pattern 4: "Deploy an agent to this room." The user instructs their client to create an autonomous agent that runs independently. "Create a researcher agent in room X that checks for new papers every hour." The user wants to launch a process, not be a process.

Pattern 5: "Check on my agents." Meta-view across rooms. What are my agents doing? Are they stuck? Do they need intervention? Management, not participation.

Patterns 1, 3, and 5 don't require an agent in the room at all. Pattern 2 requires an agent but only while engaged. Pattern 4 creates an agent that outlives the interaction.


VII. Observation and embodiment are distinct

The principle

Phase 3 conflates "reading context" with "being present." When sync_read_context fires, it auto-joins the user as an agent, creates heartbeat entries, registers in the agents list. This is wrong.

Reading a room should not automatically create presence. A user glancing at a room shouldn't leave a christopher:claude-ai agent with a stale heartbeat confusing other agents about liveness. Observation is cheap. Presence is a commitment.

v6 says: presence is meaningful — an artifact of intentional engagement, not a side effect of authentication. An agent that stops reading stops being present. The inverse should also hold: an entity that hasn't committed to presence shouldn't appear present.

Two operations

Observe — read context without presence. Uses the user's room-level access (view token or owner access) to read state, views, agents, messages. No agent created. No heartbeat. No entry in the agents list. The user sees the room as an outsider looking in. This serves patterns 1, 3, and 5.

Embody — commit to presence as a specific agent. Either creates a new agent or takes over an existing one. Heartbeat starts. Agent appears in the agents list. context.self is set. Messages carry the agent's identity. The user is now inside the room. This serves patterns 2 and 4.

The distinction is mechanical:

ObserveEmbody
Agent createdNoYes (or existing)
HeartbeatNoYes
In agents listNoYes
context.selfnullagent ID
Can invoke actionsNoYes
Can register vocabularyNoYes
Can send messagesNoYes
Can read stateYes (scoped)Yes (full scope)

MCP tool mapping

sync_lobby         → observe all rooms (pattern 1, 5)
sync_read_context  → observe one room (pattern 1, 3, 5)
sync_embody        → commit to an agent in a room (pattern 2)
sync_invoke_action → act as embodied agent (pattern 2)
sync_spawn_agent   → create autonomous agent (pattern 4)
sync_disembody     → release agent, return to observation

The default is observation. Embodiment is explicit. This preserves v6's commitment that presence is meaningful.


VIII. Statelessness

The principle

sync is stateless. The MCP server holds no session state. There is no in-memory map of "this connection is currently embodied as agent X in room Y." All state lives in the database — in rooms, agents, state, views, and the smcp_* auth tables.

This is not a limitation. It is a design commitment. The substrate thesis says state is the substrate. If something matters, it is persisted. If it is not persisted, it does not matter. The MCP server is a stateless HTTP handler that resolves identity from the database on every request.

What this means for embodiment

"Current agent" is not MCP server session state. It is a persisted relationship:

smcp_user_sessions ( token_hash TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES smcp_users(id), client_name TEXT NOT NULL, room_id TEXT, -- currently focused room (null = lobby) agent_id TEXT, -- currently embodied agent (null = observing) scope TEXT NOT NULL, -- OAuth scope string (room grants) updated_at TEXT NOT NULL DEFAULT (datetime('now')), expires_at TEXT NOT NULL )

Every MCP tool call carries the OAuth access token. The server resolves:

access_token → token_hash → smcp_user_sessions → {user, client, room, agent, scope}

When the user calls sync_embody({ room: "game-room", agent: "researcher" }), the server:

  1. Validates the room is in the token's scope
  2. Validates the user has access to the room
  3. Validates the agent exists and is available (or creates it)
  4. Updates smcp_user_sessions.room_id and agent_id
  5. Touches the agent's heartbeat
  6. Returns confirmation with context.self

Subsequent tool calls resolve the embodied agent from the session row. No in-memory state. The session row IS the state.

When the user calls sync_disembody() or sync_lobby(), the server clears room_id and agent_id on the session row. The agent remains in the room (it's state, not a connection) but the user is no longer driving it.

Token refresh and session continuity

OAuth token refresh creates a new access token. The server must transfer the session state (room focus, embodied agent) from the old token to the new one.

This happens during the refresh flow:

  1. Client presents refresh token
  2. Server issues new access token
  3. Server creates new smcp_user_sessions row with same room_id, agent_id, scope
  4. Server deletes old session row

The user's embodiment survives token refresh without interruption. The session is identified by the access token, but the continuity is in the database row.


IX. The OAuth flow: scoping and progressive authorization

Current constraints

In the MCP OAuth flow as Claude.ai and Claude Code consume it:

  1. User clicks "connect" in client settings
  2. Browser opens → sync's /oauth/authorize page
  3. User authenticates (WebAuthn passkey)
  4. User consents to scopes
  5. Redirect back with auth code → token exchange
  6. Client holds access token + refresh token
  7. Every MCP tool call carries that token
  8. Token refreshes silently when expired

The scope is fixed at step 4. Whatever is granted is what the client has until the user goes to settings and re-authorizes. There is no mid-conversation "can I also have access to room X?" flow in current MCP products.

What the consent screen shows

The consent screen is the user's first interaction with the identity model. It should present not just "which rooms" but "how" — the mode of agency:

┌─────────────────────────────────────────────────┐
│  sync.parc.land — authorize Claude.ai           │
│                                                 │
│  christopher, grant access to:                  │
│                                                 │
│  ☑ game-room                       [owner]      │
│    Agents: researcher (idle 2h), game-master    │
│                                                 │
│  ☑ work-room                       [owner]      │
│                                                 │
│  □ shared-project                  [participant] │
│                                                 │
│  ☑ Can create new rooms                         │
│                                                 │
│  [Authorize]                                    │
└─────────────────────────────────────────────────┘

The scope string encodes room grants:

rooms:game-room rooms:work-room create_rooms

Agent selection (which agent to embody) is NOT part of the OAuth scope. It happens at the tool layer, within a room the user has already granted access to. This keeps the consent screen simple and the scope stable.

Scope as ceiling, tools as arbiter

The OAuth scope defines the ceiling of what the client can access. The tools govern the current focus within that ceiling:

  • Scope (OAuth, static per session): "I can access game-room and work-room"
  • Focus (tool layer, dynamic per interaction): "I am currently embodied as researcher in game-room"

This separation means:

  1. The consent screen stays simple — room selection, not agent selection
  2. Agent embodiment is fluid within a session — no re-auth to switch agents
  3. The scope can be broad without the client automatically touching everything

Progressive scope widening

Within the current OAuth constraints, scope widening is possible in two cases:

Case 1: New rooms created mid-session. When sync_create_room succeeds, the server can implicitly extend the session's effective scope to include the new room. Rationale: the user created the room through the authenticated client, so consent is implied. The smcp_user_sessions.scope field is updated:

UPDATE smcp_user_sessions SET scope = scope || ' rooms:new-room-id' WHERE token_hash = ?

The OAuth access token's encoded scope doesn't change (it can't — it's already issued), but the server's effective scope for this session widens. The server is the arbiter, not the token.

Case 2: Room shared with user mid-session. If another user invites Christopher to a room while his Claude session is active, the server can include the new room in his effective scope on the next tool call. Same mechanism: smcp_user_sessions.scope is the mutable, server-side scope that starts from the OAuth grant and can grow.

Progressive scope narrowing

The user should also be able to narrow scope mid-session:

sync_revoke_access({ room: "work-room" })

This removes the room from smcp_user_sessions.scope. The client can no longer access that room until re-authorized. This is useful for privacy ("I don't want this Claude conversation to see my work room anymore") and for safety ("I'm done with that room, lock it out").

The scope lifecycle

OAuth grant (static)
  → smcp_user_sessions.scope (mutable, server-side)
    → effective scope per tool call (scope + room creation + sharing - revocations)

The OAuth token proves identity and provides an initial scope. The server maintains the effective scope per session, which can grow or shrink based on user actions through the tools. No re-authorization required for changes that the user explicitly requests through tool calls.

When re-authorization IS required

Re-authorization (going back to client settings, re-running the OAuth flow) is needed when:

  • The user wants to add a room they didn't select at auth time and that wasn't created or shared during the session
  • The OAuth token scope is narrower than what the user now needs
  • The refresh token expires

The server should make this clear in tool responses:

{ "error": "room_not_in_scope", "room": "someone-elses-room", "message": "This room is not in your current session scope.", "options": [ "Ask the room owner to share it with you (will auto-widen scope)", "Re-authorize in client settings to add this room" ] }

X. The lobby pattern

Why a lobby

Given the constraint that OAuth scoping is coarse-grained, the fine-grained room and agent selection should happen at the tool layer. The "lobby" is the first interaction pattern — a meta-view of the user's rooms, agents, and available roles.

The lobby is not a special endpoint

Consistent with the statelessness principle, the lobby is not a stateful concept. It is the absence of embodiment. When smcp_user_sessions.agent_id is null, the user is "in the lobby" — they can observe any room in their scope but cannot act.

The sync_lobby tool returns:

{ "user": "christopher", "client": "claude-ai", "rooms": [ { "id": "game-room", "access": "owner", "label": "D&D Campaign", "agents": [ { "id": "researcher", "status": "idle", "last_heartbeat": "2h ago", "filled_by": null, "role_defined": true }, { "id": "game-master", "status": "active", "last_heartbeat": "5m ago", "filled_by": "bot-gm", "role_defined": true }, { "id": "christopher:claude-code", "status": "done", "role_defined": false } ], "state_summary": { "phase": "active", "turn": 7 } }, { "id": "work-room", "access": "owner", "label": null, "agents": [], "state_summary": {} } ], "can_create_rooms": true, "embodied": null }

The user sees their rooms, the agents in each, which roles are defined, which are vacant, which are active. They make an informed choice:

  • "Join game-room as the researcher" → sync_embody
  • "Just show me game-room's state" → sync_read_context (observe, no agent)
  • "Create a new room for the sprint" → sync_create_room

The lobby as affordance map

In substrate terms, the lobby IS a context read — but at the user level, not the room level. It is the user's affordance map across all their rooms. Just as /context presents a room's available actions, the lobby presents the user's available engagements.

This is the v6 principle of progressive disclosure applied to the meta-layer: the lobby shows you what you can do. Your choice of room and mode is the first act. Everything else follows.


XI. Embodiment mechanics

Embodying a new agent

sync_embody({ room: "game-room" })

Server:

  1. Validate room in scope
  2. Generate agent ID: christopher:claude-ai
  3. Call insertAgentDirect(room, { id, name: "christopher", role: "mcp-client", grants })
  4. Update smcp_user_sessions: set room_id, agent_id
  5. Return full context with self: "christopher:claude-ai"

Embodying an existing agent (own)

sync_embody({ room: "game-room", agent: "christopher:claude-code" })

Server:

  1. Validate room in scope
  2. Validate agent exists and was created by this user
  3. Rotate agent's token_hash to match this session
  4. Update smcp_user_sessions: set room_id, agent_id
  5. Touch heartbeat, set status to "active"
  6. Return full context with inherited scope, state, views

The user picks up where they (or their other client) left off.

Embodying a role-defined agent

sync_embody({ room: "game-room", role: "researcher" })

Server:

  1. Validate room in scope
  2. Read _shared.roles.researcher — check filled_by is null or is this user
  3. If role has an existing agent (previous occupant left state):
    • Take over that agent identity
    • Rotate token hash
  4. If role has no agent yet:
    • Create agent with id: "researcher" (the role IS the identity)
    • Set grants per user's access level
  5. Update smcp_user_sessions: set room_id, agent_id
  6. Write filled_by: self to the role state (with if_version)
  7. Return full context — agent inherits role's scope and any prior state

Disembodying

sync_disembody()

Server:

  1. Clear smcp_user_sessions.room_id and agent_id
  2. Do NOT delete the agent from the room
  3. Do NOT clear the agent's scope state
  4. Optionally set agent status to "idle" or "paused"
  5. If role-defined, optionally clear filled_by (or leave it — policy decision)
  6. Return lobby state

The agent persists in the room as state. It can be re-embodied later.


XII. Relation to Phase 3

This document extends, not replaces, Phase 3. The mechanical work in Phase 3 is correct:

  • ✓ smcp_user_rooms schema
  • ✓ insertAgentDirect primitive
  • ✓ findOrCreateAgent identity resolution
  • ✓ sync_wait going direct (no HTTP proxy)
  • ✓ Multi-client identity (username:client-name)

What this document adds:

  • The observation/embodiment distinction (Phase 3 conflates them)
  • The lobby pattern (Phase 3 assumes direct room access)
  • Room-defined roles as a state convention (Phase 3 doesn't address)
  • Progressive scope widening/narrowing via smcp_user_sessions
  • The statelessness commitment (no MCP server session memory — persisted rows only)
  • The three modes of agency as a design framework

Implementation sequence (additive to Phase 3)

Step 0: Phase 3 Steps 0–2 (export waitForCondition, smcp_user_rooms, CRUD helpers).

Step 1: smcp_user_sessions table. Replace the current approach of resolving tokens per-call with a persisted session row that tracks room focus and embodied agent.

Step 2: Observe vs. embody split. Modify sync_read_context to NOT auto-join. Add sync_embody and sync_disembody tools. sync_read_context uses view-level access when no agent is embodied.

Step 3: Lobby tool. sync_lobby returns rooms, agents, roles across all rooms in scope. Aggregates from smcp_user_rooms + room state queries.

Step 4: Role conventions in standard library. Add define_role, fill_role, vacate_role to the standard library help content. No platform changes — these are action templates.

Step 5: Progressive scope. Implement scope widening on sync_create_room and room sharing. Implement sync_revoke_access for narrowing. Scope changes update smcp_user_sessions.scope.

Step 6: Consent screen. Update /oauth/authorize to show rooms with agent status. Initial implementation: room checkboxes only. Agent/role information is displayed for context but selection happens at the tool layer.


XIII. What this does NOT do

  • Does not change the REST API. Non-MCP clients use tokens directly as before.
  • Does not force roles on any room. Roles are a convention, not a requirement.
  • Does not add user identity to the core agents table. The link is through smcp_user_rooms and smcp_user_sessions — the sync platform stays decoupled from the auth layer.
  • Does not implement autonomous agent spawning (pattern 4). That requires a separate execution environment — a process that runs the wait loop independently. The design supports it; the infrastructure doesn't exist yet.
  • Does not build room sharing UI. Schema supports it; the flow is future work.

Appendix A: MCP spec and dynamic scope evolution

Current state (March 2026)

The MCP specification (as consumed by Claude.ai and Claude Code) treats OAuth scope as a static grant. The authorization flow runs once at connection time. There is no mechanism for the server to request additional scope mid-session, and no mechanism for the client to offer it.

The MCP spec does include:

  • notifications/resources/updated — server can notify client that a resource has changed
  • notifications/tools/list_changed — server can notify client that available tools have changed

These are informational. They don't change the authorization scope. But they provide a channel through which the server can signal that scope changes would be useful.

The gap

The missing primitive is incremental authorization: the ability for a server to request additional scope during an active session, and for the client to prompt the user and grant it without a full re-authorization flow.

This is a solved problem in other OAuth ecosystems. Google's APIs support incremental auth — you start with basic profile scope, and individual services request additional scopes as needed. The user sees a targeted consent prompt ("X also wants access to your Calendar") rather than re-running the full OAuth flow.

How sync could use dynamic scope

If MCP clients supported incremental authorization, the flow would be:

  1. Initial auth: user identity + initial room selection
  2. User asks Claude about a room not in scope
  3. sync_read_context returns room_not_in_scope error
  4. Server sends scope-upgrade request via MCP: request_scope("rooms:new-room")
  5. Client prompts user: "sync.parc.land wants access to new-room. Allow?"
  6. User approves → client sends scope upgrade confirmation
  7. Server updates session scope → tool call succeeds
  8. Conversation continues without interruption

Similarly for narrowing:

  1. User says "forget about my work room"
  2. sync_revoke_access({ room: "work-room" }) succeeds server-side
  3. Server sends scope-downgrade notification via MCP
  4. Client acknowledges reduced scope

Proposed MCP extension (informing the spec)

sync's design could inform the MCP spec in two ways:

1. authorization/scope_request — server-initiated scope upgrade

{ "jsonrpc": "2.0", "method": "authorization/scope_request", "params": { "additional_scope": "rooms:new-room-id", "reason": "User requested access to room 'new-room'", "consent_url": "https://sync.parc.land/oauth/consent?scope=rooms:new-room-id", "required": false } }

The client can:

  • Auto-approve (if policy allows)
  • Prompt the user inline
  • Open the consent URL in a browser
  • Reject (if policy denies)

2. authorization/scope_reduced — server-initiated scope reduction

{ "jsonrpc": "2.0", "method": "authorization/scope_reduced", "params": { "removed_scope": "rooms:work-room", "reason": "User revoked access", "effective_scope": "rooms:game-room create_rooms" } }

Informational — the client updates its understanding of available scope.

3. authorization/scope_offer — client-initiated scope suggestion

{ "jsonrpc": "2.0", "method": "authorization/scope_offer", "params": { "available_scope": "rooms:shared-project", "context": "You were invited to 'shared-project' by alice" } }

The server can notify the client that new scope is available (e.g., a room was shared with the user). The client can present this to the user as an option.

Design for the future, build for today

The smcp_user_sessions.scope mechanism described in this document is designed to be forward-compatible with dynamic scope. Today, scope starts from the OAuth grant and can only widen/narrow through tool calls (server-side mutations). Tomorrow, when MCP clients support incremental auth, the same session scope field becomes the target of client-mediated scope changes.

The key invariant: the server is always the arbiter of effective scope. The OAuth token provides the initial grant. The session row tracks the current effective scope. Tool calls and (future) MCP scope requests mutate the session row. The server validates every tool call against the session's current scope.

This means sync doesn't need to wait for the MCP spec to evolve. The progressive scope model works today through tool-mediated widening/narrowing. When dynamic scope arrives in the spec, it becomes a better UX for the same underlying mechanism.


Appendix B: The substrate alignment check

Does this design honor the substrate thesis? A checklist:

"State is the substrate." ✓ — Roles are state. Agent identity is state. Session focus is persisted state. No in-memory server state.

"Surfaces are observers." ✓ — Role definitions, agent objectives, and vacancy status are all observable through views. The lobby is a meta-surface over room state.

"Actions are transitions." ✓ — fill_role, vacate_role, define_role are standard library actions. Embodiment writes to room state through actions.

"Agents and humans are equivalent participants." ✓ — An autonomous agent fills a role the same way a human-proxied agent does. The room cannot distinguish them. The substrate doesn't care who is driving.

"Progressive disclosure is implicit." ✓ — The lobby shows available rooms. Entering a room shows available roles. Filling a role reveals the role's bootstrap instructions. Each step discloses the next.

"Vocabulary construction is the only unilateral act." ✓ — Defining a role is vocabulary construction. Filling a role is claiming vocabulary. The room's purpose emerges through role definitions, not through configuration.

"No setup phase." ⚠ Partial — The consent screen is a setup phase. But it operates at the user level, not the room level. Within the room, the principle holds: the first agent arrives and declares itself. The OAuth flow is infrastructure, not room semantics.

"Presence is meaningful." ✓ — Observation does not create presence. Embodiment does. Disembodiment preserves state without claiming liveness. The distinction is structural.


Christopher · Edinburgh · March 2026

FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2026 Val Town, Inc.