Scope
Every MCP session carries a read-scope set — the explicit list of node IDs the agent is allowed to read in this session. Reads of nodes outside the set return {error: scope_expansion_required, ...} until the user authorises expansion (or, for a client that declares the MCP elicitation capability, confirms a real dialog). See Scope Enforcement for the conceptual model, including session types and how the separate write-scope set is gated.
The scope set is normally seeded automatically when an MCP client opens the session with ?home_node_id=<id> in the URL. portuni_mirror materializes that URL into every mirror’s .mcp.json (Claude Code) and .vibe/config.toml (Mistral Vibe); it also writes .claude/settings.local.json, .codex/config.toml (sandbox config only — the Codex MCP connection lives in the user-scoped ~/.codex/config.toml), .cursor/rules, PORTUNI_SCOPE.md, and marker blocks in CLAUDE.md / AGENTS.md when those files already exist. The tools below cover the cases where auto-seed is absent, where reads need to reach beyond the seed, and where you want to audit what the agent has looked at.
portuni_session_init
Section titled “portuni_session_init”Manually initialise the read-scope set for this MCP session. Use only when auto-seed is absent — legacy client, ad-hoc connection, or a programmatic re-seed mid-session. Seeds the scope with the home node and its depth-1 neighbours. Idempotent — replaces any prior home node and re-seeds.
| Parameter | Type | Required | Description |
|---|---|---|---|
home_node_id |
string | no | Node ID (ULID) whose local mirror contains the cwd. Provide this OR home_node_name; omit both when no home node applies |
home_node_name |
string | no | Case-insensitive node name as an alternative to home_node_id |
Returns: { home_node_id, home_node_name?, home_node_type?, session_type, scope_size, seeded } — or { home_node_id: null, session_type, scope_size, note } when called with no home node (every subsequent read will require explicit expansion — except for interactive_chat, whose reads are permission-only regardless of the scope set; see Scope Enforcement). session_type is derived server-side from the connection’s auth path (interactive_task, interactive_chat, headless, or env) — it is never something this tool sets or accepts.
portuni_expand_scope
Section titled “portuni_expand_scope”Add one or more nodes to the current session’s read-scope set — and, with writable: true, to its write-scope set too. Required when a read or mutating tool returned {error: scope_expansion_required, ...} / {error: write_expansion_required, ...}. For read-only expansion, clients that declare the MCP elicitation capability get a real dialog tried first; this tool is the honor-system fallback (and the only path) for clients without that capability. Write-set expansion has no such fallback: writable: true always goes through a real elicitation dialog, whether triggered by this call directly or by a mutating tool call that hit write_expansion_required first — a client without the elicitation capability cannot grant write access via this tool (or any other path) at all. Such a client is told so up front: the mutating tool’s write_expansion_required payload carries elicitation_supported: false and a hint that does not recommend this call. The one write grant that needs no dialog is a node created by the same user’s connector session (interactive_chat) – see Scope Enforcement.
| Parameter | Type | Required | Description |
|---|---|---|---|
node_ids |
string[] | yes | Node IDs (ULIDs) to add. At least one |
reason |
string | yes | Why scope is being expanded. Be honest about the trigger: user-requested: <quoted prompt fragment> for prompt-derived expansions, user-confirmed-in-chat for chat confirmations |
triggered_by |
enum | no | user (default) for prompt-named or chat-confirmed expansions; agent for the agent’s own initiative (rare — most agent-initiated reaches go through elicitation) |
confirmed_hard_floor |
boolean | no | Default false. Set to true only when the user has explicitly confirmed reaching a hard-floor node (visibility=private owned by another user, or meta.scope_sensitive=true). Without this flag, hard-floor nodes are refused even when reason claims user confirmation. Ignored outright for headless sessions — they can never override a hard floor |
writable |
boolean | no | Default false. When true, each node must additionally clear a real elicitation confirmation dialog before it is granted write access — reason alone is never enough. Rejected outright (write_expansion_impossible) for headless sessions, whose write set cannot expand mid-run |
Returns: { added, added_via, writable, unknown, refused_hard_floor, refused_write, scope_size, projected, not_projected, hint? } — added_via maps each accepted node ID to "edge" (reachable via a graph edge from the current scope set) or "disconnected" (found only via search/name, no edge path), classified server-side regardless of what reason claims; unknown lists requested IDs that don’t exist in the graph (or aren’t visible to the caller); refused_hard_floor lists { node_id, reason, permanent } for nodes that need confirmed_hard_floor=true (permanent: true for a headless session — no retry will succeed); refused_write lists { node_id, reason } for nodes requested with writable: true whose elicitation dialog was declined, or that could not be shown at all because the client lacks the elicitation capability; projected maps each accepted node ID with a local mirror on this device to its session-local hardlink projection directory (readable natively — this runs on the device even in central/agent-mode sessions, not just local ones); not_projected maps every other accepted node ID to why it wasn’t (seed_granted — it’s this session’s own home node, already readable at its real mirror; no_mirror — no local mirror for it on this device, read it with portuni_read_file instead; out_of_scope — the node is not in the session’s read scope; no_projection_root — no projection directory could be resolved; central — this session has no home node at all, e.g. a connector session); hint appears when there’s a clear next step.
Every expansion is audited and surfaced in portuni_session_log. See Scope Enforcement.
portuni_session_log
Section titled “portuni_session_log”Return the current read-scope set, session type, and ordered expansion history for this MCP session. Use to inspect what the agent has looked at — useful both for the human-in-the-loop and for retrospective review of an autonomous run.
No parameters.
Returns: { session_id, home_node_id, session_type, created_at, scope_size, scope, expansions } — scope is the ordered list of in-scope node IDs; expansions is the chronological log of every scope mutation with at, node_ids, reason, and triggered_by.
portuni_session_suspend
Section titled “portuni_session_suspend”Suspend this session: writes content to wip/sessions/<session-id>-handoff.md (a normal synced path, visible to the whole team on the routed remote), stores its hash and this session’s agent-conversation id, and marks the session suspended so it can be resumed later — respawned in the same mirror, continuing the underlying CLI’s own conversation when it still exists (claude --resume <id>), or starting fresh from the handoff otherwise. Requires a home node: interactive_chat sessions have no anchor to write into, and this tool errors for them.
Call this before the terminal closes — at the end of a task, or (for a RALPH-style autonomous loop) between iterations. Callable again on an already-suspended session to update the handoff with newer content; the stored hash changes, so a later resume can tell a human edited the handoff since suspend.
| Parameter | Type | Required | Description |
|---|---|---|---|
content |
string | yes | Handoff markdown: what was done, what’s next, anything the next session needs to pick up |
agent_session_id |
string | no | The underlying CLI’s own conversation id (e.g. Claude Code’s session UUID), so a later resume can offer to continue the same conversation. Omit if unknown |
Returns: { session_id, state, handoff_path, handoff_hash } — state is "suspended"; handoff_path is the synced path the content was written to (wip/sessions/<session-id>-handoff.md); handoff_hash is stored on the session record so a resume can detect a hand-edited handoff.
See also
Section titled “See also”- Scope Enforcement — the conceptual model: session types, edge-reachable vs. disconnected-jump expansion, hard-floor rules, protocol elicitation, the write-scope gate, audit trail
- Lifecycle States — orthogonal to scope, but referenced in node payloads the scope set surfaces