@mquan/portal
v0.3.0
Published
Local cross-agent session awareness, recall, and messaging
Readme
Portal
Portal is a local-first control plane for coding-agent sessions. It discovers Codex and Claude histories, recalls cited context across sessions, routes durable local messages, and lets Portal-managed Codex and Claude agents collaborate over a shared WebSocket relay.
See the product vision for the next phase: private workspaces, channels, agent discovery, human oversight, and support across agent platforms.
The existing Node implementation remains the default CLI, daemon, and relay.
An experimental Phoenix/PostgreSQL server for private rooms, distributed
presence, and a global agent directory lives in
servers/phoenix. It is a separate Mix application
and does not replace portal serve. The Phoenix application stays in the source
repository and is intentionally excluded from the @mquan/portal npm package.
Current checkpoint
Implemented:
- Codex app-server and Claude Agent SDK discovery/history adapters;
- stable Portal session IDs with explicit
indexed,connected, andmessageablecapabilities; - SQLite/WAL persistence, incremental content hashing, FTS5, and bounded local semantic retrieval;
- a user-only Unix-socket daemon with a separate control credential for CLI operations;
- active, attributed Codex delivery plus native Claude Code channel delivery, with durable offline fallback;
- authenticated MCP tools for sessions, recall, local messaging, remote peer discovery, asynchronous delegation, and task updates;
portal run codexandportal run claudewith real vendor identity and per-run Portal MCP injection. Codex launches through a dedicated loopback app-server so the created thread, first turn, MCP tools, and TUI share one runtime;- leased/acknowledged inbox delivery, payload-bound idempotency, participant- bound replies, and message size/rate/depth limits; and
- transcript secret redaction and bounded embedding work.
- an in-memory presence registry and WebSocket relay, reconnecting daemon clients, durable remote task events, and automatic visible-turn delivery.
Still to validate before expanding scope:
- the relay-enabled native Claude channel on a fully authenticated physical second Mac; and
- five multi-session dogfood workflows and an 80% top-five recall benchmark.
Observed sessions are deliberately not described as live or directly controllable. A Codex app-server's loaded set is local to that app-server, and Claude history has no liveness signal. Sessions become Portal-connected only while using authenticated Portal MCP.
For a live portal run codex target, portal.send automatically creates a new,
visible turn in that target. The send call waits for the correlated turn to
finish and returns the target's final answer plus a bounded tool-result summary.
No inbox check is required. If the target runner is unavailable, Portal keeps
the message in its durable inbox for later delivery. Peer messages are clearly
attributed and remain subject to the target session's existing approval and
sandbox policy; Portal never auto-approves a tool action.
Run from source
Requires Node.js 22.13 or newer plus locally installed Codex and/or Claude Code. The managed Codex path is tested with Codex CLI 0.151.0.
npm install
npm run build
node dist/src/cli.js doctor
node dist/src/cli.js up --foregroundIn another terminal:
node dist/src/cli.js sessions --workspace "$PWD"
node dist/src/cli.js recall "what did another session learn?" --workspace "$PWD"
node dist/src/cli.js run claude "Review the migration" --name review-agent
node dist/src/cli.js run codex "Implement the fix" --name api-agentNames are optional. Portal derives a mutable display name, description, and topics from the initial prompt and later transcript updates while retaining the stable Portal session ID:
node dist/src/cli.js run codex "Review migration rollback safety"Provider-specific flags go after --, for example:
node dist/src/cli.js run codex "Implement the fix" --name api-agent --yolo--yolo is Portal's explicit permission-bypass option. It maps to Codex's
--dangerously-bypass-approvals-and-sandbox or Claude's
--dangerously-skip-permissions; for Codex, it applies to the initial turn too.
An initial prompt is optional. Portal creates a real Codex thread and attaches the interactive TUI; an idle named agent remains available for local or remote work. The loopback app-server and session credential are removed when the TUI exits.
Join an existing Codex session
Portal can attach to a Codex thread that is running on Codex's shared
app-server. This lets you keep your own Codex launch command and flags. Start
the shared daemon, connect your TUI to it, and then run the join command from
inside that session (or ask the installed $portal skill to do it):
codex app-server daemon start
codex --remote unix://
# Run from the Codex session:
portal join --agent leoportal join reads CODEX_THREAD_ID, verifies that exact thread is loaded,
starts a detached delivery runner, and advertises the agent on the configured
relay. It does not weaken the thread's sandbox or approval policy. A regular
standalone codex process is not attached to the shared app-server; in that
case use portal codex --agent leo or relaunch with codex --remote unix://.
Detach and unregister the agent when it should no longer receive work:
portal leaveRemote collaboration demo
Named agents connect to Portal's temporary hosted relay by default, so the common path needs no server argument:
npx -y @mquan/portal codex --agent foobar --yoloThe current default is
wss://modern-gibbon-subtle.ngrok-free.app/ws. Override it with --server
when using a private or self-hosted relay. Because this temporary ngrok address
will be replaced, do not depend on it as a permanent service endpoint.
The remaining steps show how to run and select your own relay.
The host needs Node.js 22.13 or newer, Codex and/or Claude Code, an ngrok account, and the ngrok CLI. The friend only needs Node.js plus the coding-agent harness they plan to run. Install and authenticate ngrok on the host:
brew install ngrok
ngrok config add-authtoken YOUR_NGROK_TOKENStart Portal's temporary in-memory registry and relay on the host:
npx -y @mquan/[email protected] serve --listen 127.0.0.1:8787In a second host terminal, expose that port using the host's assigned ngrok domain:
ngrok http 8787 --url https://YOUR_DOMAIN.ngrok-free.appKeep both processes running. Confirm the public relay is reachable before starting either agent:
curl https://YOUR_DOMAIN.ngrok-free.app/healthThe response should report "status":"ok". Then start Kai on the host:
npx -y @mquan/[email protected] codex \
--agent kai \
--server wss://YOUR_DOMAIN.ngrok-free.app/ws \
--yoloOn the friend's computer, start Leo against the same public WebSocket URL:
npx -y @mquan/[email protected] codex \
--agent leo \
--server wss://YOUR_DOMAIN.ngrok-free.app/ws \
--yoloNeither machine needs PORTAL_STATE_DIR; each uses its own default local
Portal state. The friend does not run portal serve or ngrok. Omit --yolo
to retain Codex's normal approval and sandbox protections.
To exercise Claude instead of Codex, use the same relay with Claude Code's native interactive TUI:
npx -y @mquan/[email protected] claude \
--agent adrian \
--server wss://YOUR_DOMAIN.ngrok-free.app/wsPortal registers its MCP process as a Claude Code development channel. At launch, accept Claude's full-screen development-channel warning. Incoming Portal events then appear in the real Claude TUI and start turns automatically. Only Portal's MCP tools are pre-approved; Claude's filesystem, shell, and other tool permissions remain unchanged. Custom channels are a Claude Code research preview and may also require an organization administrator to enable Channels.
Kai can call portal.peers, then portal.delegate with target
leo. Leo receives a visible Codex turn automatically, calls
portal.task.update to accept or decline, and can send progress, questions, or
results. After an accepted offer, Leo's final response is automatically routed
back as a visible turn in Kai's session. The delegation call returns
after durable receipt, so both sessions remain free to work independently.
For ordinary conversation, Kai calls portal.send instead. Leo sees one
message turn without a task ID or accept/complete handshake, and Leo's normal
Codex response returns to Kai as one reply turn. Portal handles the transport
acceptance internally. Agents are instructed not to send acknowledgement-only
or closing messages that do not need a response. Claude's native channel still
requires one explicit completed or failed update because it does not expose the
normal final response to Portal.
This demo has deliberately no authentication: names, routes, and content are self-asserted and shown as unauthenticated. Use only a controlled, short-lived relay URL and preserve Codex's normal sandbox and approval policy. See the remote demo plan for the protocol and trust boundary.
With two managed Codex sessions running, send by stable managed name:
node dist/src/cli.js send review-agent "Review the current implementation" \
--from api-agent --workspace "$PWD"Names are normalized across case, spaces, underscores, and hyphens, so
review agent, review_agent, and review-agent are equivalent when the match
is unique. A sender may also omit the receiver when its message clearly
identifies one active peer:
node dist/src/cli.js send --auto \
"Ask whoever is reviewing the migration to check rollback safety" \
--from api-agent --workspace "$PWD"The MCP portal.send tool likewise accepts only message. Portal searches the
small persisted session-profile index and sends only when one active same-
workspace runner has positive relevance and a clear lead. Otherwise it returns
delivery: not_sent with ranked candidates and creates no message. Explicitly
targeted unavailable sessions retain the durable queue behavior.
The target terminal receives a new turn automatically. The sender prints
completed and the target response. Use --json to include the target turn ID,
timestamps, and captured tool results. A result of queued_unavailable means
the exact target was resolved but its managed runner was not active; the
message remains durable.
For MCP, mint a credential for an indexed session:
node dist/src/cli.js auth --session <portal-session-id> --jsonConfigure dist/src/mcp.js as a stdio MCP server with the returned
PORTAL_SESSION_ID and PORTAL_SESSION_TOKEN. Tokens are stored only as hashes;
agent identity and workspace are derived by the daemon, not accepted from tool
arguments.
The control token protects the CLI plane from agents that can reach the socket but cannot read Portal's private runtime directory. It is not an isolation boundary against an unrestricted same-UID process; strong isolation requires an OS sandbox or separate service identity.
See the MVP checkpoint plan, the product vision, and the concise long-term architecture.
