| title | Nanasa |
|---|---|
| description | Local coding-agent pool with tmux terminals, authenticated MCP messaging, and coordinator-visible status |
| author | Nanasa |
| ms.date | 2026-08-11 |
| ms.topic | overview |
Nanasa (නැනස) is a local-first orchestrator for running and observing multiple coding-agent terminals. The name means "wisdom" or "intellect" in Sinhala.
Nanasa is in early development. The current vertical slice manages groups, agents, tmux-backed runs, terminal access, and structured message and status records. Interfaces and configuration may change.
Every configured agent launches as its command directly in a tmux pane. Nanasa does not start model-specific subprocess protocols. Portal and MCP messages use the same durable terminal transport: bracketed paste followed by a separate Enter key. Use a harmless shell or Node.js fixture when evaluating the runtime without an agent account.
The Fastify daemon owns application state and terminal-provider processes. It stores projected agent runtime state, runs, messages, delivery outcomes, and domain events in SQLite. A private tmux server owns the durable agent panes. The daemon creates one deterministic linked tmux view session and supervises one loopback-only ttyd process for each active run.
The daemon publishes terminal status under /api/runs/:runId/terminal and
proxies each ready ttyd endpoint through a bounded same-origin /terminals
path. Upstream ports remain private. The React portal renders only ttyd iframes;
it does not implement a separate terminal renderer or terminal protocol.
During development, Vite proxies both /api and /terminals to the daemon. In
production, the daemon serves the built portal from apps/portal/dist with an
extensionless SPA fallback. API, event WebSocket, and terminal proxy routes keep
precedence over static content.
New agents receive a readable stable member ID in the form
<integration>.<adjective>-<surname>, for example pi.focused-hopper. Nanasa
generates the suffix with docker-names, normalizes it for use as an identifier,
and retries collisions within the group. Agent names remain independently
editable.
- Node.js 22 or later
- tmux
- ttyd 1.7.7, available on
PATHor configured throughNANASA_TTYD_PATH - An installed and authenticated agent CLI for each enabled integration
The development container installs the pinned ttyd binary for amd64 and arm64
and verifies its published checksum. The npm package does not bundle ttyd because
it is a native executable. nanasa start checks ttyd --version before starting
and explains how to configure a nonstandard executable path.
Each integration requires its command to be installed and authenticated as required by that CLI. Nanasa does not initiate interactive authentication or send a model prompt during startup.
Install Nanasa in the repository where you want to manage agents:
npm install --save-dev nanasa
npx nanasa init
npx nanasa setup
npx nanasa doctor
npx nanasa startnanasa init discovers the Git repository from the current directory and creates
.nanasa/config.yaml only when it is absent. It never overwrites configuration
or runtime state. Edit the generated agent commands for the CLIs available in
your environment, then commit .nanasa/config.yaml. Ignore .nanasa/state/ and
.nanasa/runtime/, and .nanasa/integrations/; they contain SQLite state, the
MCP signing secret, ttyd manifests, provider authentication state, generated
hooks and extensions, and persistent agent configuration for one checkout.
nanasa setup validates the configuration and creates private integration
homes without starting an agent or changing provider-global settings. nanasa doctor checks configured commands, ttyd, and integration directory ownership
and permissions. Authenticate a configured CLI inside its isolated home with:
npx nanasa auth copilotThe command launches the configured provider command and leaves its native login flow in control. Environment credentials supported by the provider are inherited without being copied into Nanasa files. An agent-scoped home requires the stable configured agent ID:
npx nanasa auth copilot --agent agent_exampleEach integration can select how provider configuration, authentication, and
session state are shared. Omitting agentConfigHome uses integration scope.
integrations:
copilot:
name: GitHub Copilot
kind: copilot
command: [copilot]
cwd: .
agentConfigHome: { scope: integration }
isolated-reviewer:
name: Isolated reviewer
kind: copilot
command: [copilot]
cwd: .
agentConfigHome: { scope: agent }
custom-home:
name: Custom home
kind: pi
command: [pi]
cwd: .
agentConfigHome:
scope: custom
path: homes/{integrationId}/{agentId}integration shares one home between agents using that integration. agent
uses a stable home for one configured agent across run restarts. custom paths
are relative to .nanasa/integrations and may use {integrationId} and
{agentId}. Absolute paths, traversal, unknown placeholders, and symlinked
integration directories are rejected.
Integrations, roles, groups, and agents are declared directly in
.nanasa/config.yaml. Agent map keys are stable IDs used by run history and
agent-scoped integration homes:
instructions:
- .nanasa/instructions/nanasa-mcp.md
- .nanasa/instructions/team.md
integrations:
copilot:
name: GitHub Copilot
kind: copilot
command: [copilot]
cwd: .
agentConfigHome: { scope: integration }
roles:
implementor:
name: Implementor
description: Implements assigned changes and validates the result
presentation:
icon: hammer
color: blue
instructions:
- .nanasa/instructions/implementor.md
permissionPolicy: inherit
reviewer:
name: Reviewer
description: Reviews changes without modifying files
presentation:
icon: shield-check
color: amber
instructions:
- .nanasa/instructions/reviewer.md
permissionPolicy: read-only
groups:
group_backend:
name: Backend
instructions:
- .nanasa/instructions/groups/backend.md
agents:
agent_reviewer:
memberId: copilot.reviewer
name: Reviewer
integrationId: copilot
roleId: reviewer
instructions: []
order: 0
messages:
retentionPerGroup: 1000Portal topology changes update this file atomically, then reconcile SQLite as a runtime projection. Run generations, delivery leases, messages, semantic status, event ordering, and idempotency remain transactional SQLite state.
Roles describe responsibility independently from integrations. An agent can
reference one role with roleId; omitting it leaves the agent unassigned.
Optional role presentation metadata gives the portal a consistent icon and
theme-safe color for the group tree, terminal tabs, and grid terminal titles.
Supported colors are amber, blue, cyan, rose, slate, teal, and
violet. Supported icons are briefcase-business,
clipboard-list, code, hammer, scan-search, shield-check, waypoints,
and wrench. An optional shortName (24 characters maximum) can replace a long
role name in compact terminal surfaces. Presentation metadata does not change
instructions or permission policy. The portal Role settings dialog updates these
presentation fields without restarting active agents.
Agent order is a zero-based group-local display position. The portal's
Move up and Move down commands atomically rewrite dense order values in YAML.
The resulting order is shared by the group tree, terminal tabs, and terminal
grid. Reordering does not restart active agents.
Nanasa composes the system-prompt suffix in this order: built-in MCP coordination guidance, top-level instructions, group instructions, effective role instructions, then agent instructions. Operator instruction layering is global, group, role, then agent after Nanasa's built-in coordination and assignment sections. References must be unique, repository-relative UTF-8 Markdown files. Symlinks, traversal, files larger than 64 KiB, and effective suffixes larger than 256 KiB are rejected.
Each launch composes a private system-prompt-suffix.md and manifest beneath
the agent integration directory. The suffix always starts with built-in Nanasa
MCP and incoming-message etiquette, then appends global, group, role, and agent
Markdown instructions in that order. Copilot uses a generated custom agent,
Claude appends the file to its default system prompt, Pi appends the file and
uses an extension for read-only enforcement, and OpenCode uses a generated
primary agent. Provider defaults, managed policy, repository instructions,
authentication, preferences, and unrelated configuration remain active. Change
a running agent's role only after stopping it; Nanasa rejects live role changes
instead of mutating an active prompt.
Running nanasa without a command is equivalent to nanasa start. The daemon
walks upward from the current directory to find .nanasa/config.yaml, stores
durable state beneath that repository, and serves the portal at
http://127.0.0.1:3210 by default.
The installed command accepts these options:
--host <host>overridesNANASA_HOST; MCP requires a loopback host--port <port>overridesNANASA_PORT--mcpenables authenticated MCP atNANASA_MCP_PATH(default/mcp)--ttyd-path <path>overridesNANASA_TTYD_PATH
The installed command also supports setup, doctor, and auth as described
above. These commands operate only beneath the repository's .nanasa
directory.
Package development uses pnpm 10. Install dependencies and build every workspace package:
pnpm install
pnpm buildStart the production daemon and built portal from the repository root:
pnpm startThe portal is available at http://127.0.0.1:3210 by default. Production mode
enables portal serving and resolves the built assets from
apps/portal/dist. The repository start script also enables authenticated MCP
for managed agents. Installed-package users retain explicit opt-in through
nanasa start --mcp.
Start the daemon watcher and Vite development server together:
pnpm devOpen http://127.0.0.1:5173. Vite proxies /api requests, domain-event
WebSockets, and /terminals ttyd HTTP and WebSocket traffic to
http://127.0.0.1:3210. Set VITE_DAEMON_URL to proxy to another daemon
origin.
The daemon accepts these environment variables:
NANASA_HOST, default127.0.0.1; it must remain loopback when MCP is enabledNANASA_PORT, default3210NANASA_REPO_ROOT, default discovered upward from the current directoryNANASA_DATA_PATH, default.nanasa/state/nanasa.sqliteNANASA_RUNTIME_PATH, default.nanasa/runtimeNANASA_TMUX_SERVER, defaultnanasaNANASA_TTYD_PATH, defaultttydNANASA_SERVE_PORTAL, default enabled only whenNODE_ENV=productionNANASA_PORTAL_PATH, set automatically by the installed commandNANASA_MCP_ENABLED, defaultfalseNANASA_MCP_PATH, default/mcpNANASA_MCP_URL, default derived from the daemon host, port, and MCP path; external advertised URLs must use HTTPSNANASA_MCP_OPERATOR_TOKEN, optional for local agent-only MCP and required for an external advertised URL; it must contain at least 32 characters
Run the complete non-browser validation suite:
pnpm typecheck
pnpm test
pnpm lint
pnpm format:check
pnpm build
pnpm smokepnpm smoke uses a safe Node.js line-echo fixture. It creates isolated data and
a unique tmux server, starts two runs in one group, and exercises ttyd index,
token, WebSocket input isolation, client disconnect, daemon restart, ttyd crash
recovery, owner-pane exit, and operator-stop cleanup.
The group tmux session and its panes own the running agent processes. Closing a browser terminal disconnects only ttyd's disposable tmux client. Gracefully stopping or restarting the daemon terminates supervised ttyd children but leaves active owner panes and their linked view sessions available for reconciliation.
Each active run has its own ttyd process and deterministic single-window view session. This prevents two runs in the same group from changing one another's current tmux window. ttyd permits one live browser client per run. Tabs therefore mount only the selected terminal, while grid mode mounts one client for each visible run. Close another tab, grid, or browser using the same run before reconnecting.
ttyd provides the browser terminal implementation and terminal WebSocket. The daemon allows only the endpoint index, token, and WebSocket paths, validates same-origin upgrades, strips credentials before forwarding, and never exposes the loopback upstream address.
Nanasa configures its private tmux server rather than loading ~/.tmux.conf.
It enables extended keys and clipboard signaling, advertises xterm extended-key
and clipboard features, and uses CSI-u on tmux 3.5 or newer. tmux 3.2 through
3.4 use the supported modifyOtherKeys format. Agent PTYs disable software flow
control so Ctrl+S and Ctrl+Q reach the active application instead of pausing
terminal output.
Use the browser or platform terminal shortcuts for clipboard operations, usually
Ctrl+Shift+C and Ctrl+Shift+V on Linux and Windows or Cmd+C and Cmd+V on
macOS. Plain Ctrl+C remains an interrupt sent to the agent. tmux copy mode and
applications may also use OSC 52 through ttyd; browser focus, clipboard
permissions, and secure-context policy still apply. Enabling clipboard signaling
allows terminal applications to replace clipboard contents, so only run trusted
agent commands.
Every agent command runs directly in its verified owner pane. Interrupt sends Ctrl+C to that pane. Message delivery loads text into the pane, enables bracketed paste, pastes the content, and sends Enter separately. A successful outcome means terminal injection completed; it does not claim that the CLI processed or completed the request. Delivery retries when a live ttyd browser writer owns the pane or when tmux is temporarily unavailable.
Terminal input is also the agent-to-agent message channel. The authenticated MCP tools submit through the same in-process message service as the portal REST API. The durable dispatcher then uses the guarded tmux paste-and-Enter path for every active recipient.
Before terminal injection, Nanasa prepends a trusted sender envelope derived
from persisted message identity. Agents receive input such as
[From: Reviewer | Member: pi.focused-hopper | Message: msg_123 | Conversation: conv_456 | Reply-To: msg_100 | Intent: request]; portal messages use From: Human. MCP callers cannot forge this envelope, and the stored message body
remains unchanged.
Nanasa tracks process lifecycle and semantic agent activity as separate
dimensions. Run status (starting, running, stopping, stopped, or
failed) describes the tmux-owned process. Agent status describes what the
harness reports: not_started, starting, working, waiting, idle,
suspected_stuck, stopped, or crashed.
Tmux remains the process authority. Nanasa verifies each pane's run ID and generation, records retained exit status or signal before recovery, and ignores failed tmux inspections rather than treating them as missing processes.
Nanasa provisions private lifecycle reporters for Claude Code hooks, GitHub Copilot CLI user hooks, a second Pi extension, and an in-process OpenCode plugin. The Copilot hook is installed under its repository-local Nanasa integration home. Reporters send normalized lifecycle names, correlation IDs, coarse errors, and wait labels. They do not send prompts, tool arguments, tool output, file paths, transcripts, reasoning, or provider headers. Reporter failure never blocks the native TUI; status degrades to process-only evidence.
Explicit permission, question, elicitation, and plan-approval requests produce
waiting. Silence cannot turn an outstanding request into
suspected_stuck. A working agent becomes suspected stuck only after its
semantic lease expires and two reconciliation probes find no progress. This is
a low-confidence inference, not a definitive harness state.
Settled events produce waiting with no attention requirement, not automatic
task success. Explicit questions and decisions also use waiting, with
input_required or decision_required attention. Agents can publish task
checkpoints with nanasa.report_progress, including stage, summary, next step,
blocker, and an optional final outcome. The portal displays semantic state,
phase, progress context, and attention independently of terminal run controls.
Reporter replay coverage is pinned to Claude Code 2.1.220, GitHub Copilot CLI
1.0.79, Pi 0.83.0 with pi-mcp-adapter 2.18.0, and OpenCode 1.18.15.
Message text is limited to 1,048,576 UTF-8 bytes across the portal, REST API, and MCP tools. Oversized requests return a helpful error. Place large content in a file inside the repository checkout shared by recipients, then send its repository-relative path. Nanasa does not automatically open paths supplied in messages, and a path on a remote MCP client's machine is not visible to agents until the content reaches the shared checkout.
SQLite retains the newest messages.retentionPerGroup messages for each group,
with a default of 1,000. Sequence numbers remain monotonic after retention or
history deletion. The portal loads the latest 20 messages, opens at the newest
message, and fetches older pages as the reader scrolls upward. Clearing history
from the portal deletes the group's stored messages and delivery outcomes for
all portal sessions.
Enable Streamable HTTP MCP at /mcp with nanasa start --mcp or
NANASA_MCP_ENABLED=true. The endpoint supports the MCP 2026-07-28 per-request
protocol and the legacy initialization handshake through the official MCP
TypeScript server and Node packages.
Nanasa exposes these tools:
nanasa.list_membersreturns active member IDs, aliases, effective roles, integration IDs, current run status, and which member is the authenticated callernanasa.list_agent_statusesreturns compact semantic and process status for every active member, optionally limited to agents needing attentionnanasa.get_agent_statusreturns one member's wait, progress, evidence, process exit details, and recent transitionsnanasa.report_progressrecords the authenticated agent's task checkpoint, next step, blocker, or final outcomenanasa.send_dmrequiresrecipientMemberIdand sends to one active membernanasa.send_multicastrequiresrecipientMemberIdswith at least two unique active membersnanasa.broadcast_groupsends to every active member and excludes the authenticated agent caller
The three send tools require text, limited to 1 MiB of UTF-8 content. Optional
fields are intent (inform, request, or response), contentType
(text/plain or text/markdown), conversationId, and replyTo. The defaults
are request and text/markdown. Operator calls must also provide groupId.
Agent calls derive the group from their credential and cannot select a different
group. Agent broadcasts always exclude the authenticated caller. Agent direct
and multicast calls reject any recipient list containing that caller.
Every tool uses terminal delivery. Agent tool arguments never choose the sender.
Nanasa signs a capability for the run's group, member, run ID, and generation,
then injects NANASA_MCP_URL and NANASA_MCP_TOKEN into the direct tmux CLI
environment. The signing key is stored at .nanasa/state/mcp-secret. Nanasa
requires the key to be a current-user-owned regular file with mode 0600 and
protects its directory with mode 0700. Stopping or replacing a run, changing
its desired state, or removing the agent revokes the capability during the
next request.
Agent commands receive three non-persisted environment variables when MCP is enabled:
NANASA_MCP_URLis the configured Streamable HTTP endpointNANASA_MCP_TOKENis a signed capability bound to the run, generation, member, and groupNANASA_STATUS_URLis the authenticated lifecycle reporter endpoint
Nanasa also registers its MCP endpoint with each supported CLI before launch.
Generated files live under .nanasa/integrations/, contain only an
environment-variable placeholder for the bearer token, and use private file
permissions. The generation capability remains only in the process
environment. Shared or agent-specific provider homes are reused after run
and daemon restarts.
Client integration follows each CLI's supported configuration contract:
- GitHub Copilot CLI receives a generated HTTP MCP config through
--additional-mcp-config;COPILOT_HOMEandCOPILOT_CACHE_HOMEpoint to its isolated Nanasa home, and--agentselects the generated role prompt - Claude Code uses an isolated
CLAUDE_CONFIG_DIRwith a generated user MCP entry and--append-system-prompt-file; direct Claude andmake claude-copilotlaunches use the same path - Pi uses
PI_CODING_AGENT_DIRand the pinnedpi-mcp-adapterextension, with Nanasa tools registered directly and the role prompt appended natively - OpenCode receives a generated remote MCP entry through
OPENCODE_CONFIGand isolated XDG config, data, state, and cache roots; a generated primary agent references the effective prompt file
Nanasa does not copy or link provider credentials into generated
configuration. Use nanasa auth to authenticate the native CLI in the selected
home, or provide a provider-supported credential through the inherited process
environment. Coding-agent sessions launched outside Nanasa continue using
their normal provider homes and do not load Nanasa-generated hooks or plugins.
Provider homes are persistent and not treated as generated scratch space.
Nanasa replaces only files or keys it owns: its named Copilot hook files,
mcpServers.nanasa for Claude and Pi, mcp.nanasa for OpenCode, and its named
status reporter assets. Provider onboarding, themes, authentication, sessions,
models, plugins, user hooks, other MCP servers, and unrelated settings are
preserved across repeated provisioning. Unsafe or malformed shared provider
configuration causes launch to fail instead of being overwritten.
Operator clients authenticate with Authorization: Bearer <token>. Configure
that token with NANASA_MCP_OPERATOR_TOKEN; it must contain at least 32
characters. The setting is optional for a loopback daemon that serves only
agent capabilities, but it is required for operator calls and whenever
NANASA_MCP_URL advertises an external host. Tokens are not accepted in query
strings. The endpoint validates Host and Origin before bearer authentication
and limits each principal to 30 requests per minute.
Nanasa must remain bound to a loopback host whenever MCP is enabled. For remote
MCP access, terminate TLS at a trusted reverse proxy and configure the proxy to
publish only the exact MCP path, /mcp by default. Do not proxy portal, REST,
event, or terminal routes. Set NANASA_MCP_URL to the external HTTPS URL,
including the configured path, and configure a strong
NANASA_MCP_OPERATOR_TOKEN. The proxy must preserve a Host value matching the
advertised URL and should restrict accepted origins. Never expose the Nanasa
listener directly to the network.
The Add agent form loads integrations and roles from .nanasa/config.yaml. New
integration keys appear without a portal code change. Creating an agent selects
its integration and optional role, then accepts agent-specific Markdown
instruction files. Instruction paths are entered one per line and must resolve
to repository-relative .md files. Existing agent rows expose one Agent
settings dialog for name, integration, role, and agent instructions.
Prompt-affecting integration, role, or instruction edits require the agent to be
stopped; names remain editable while agents run.
Group creation accepts shared group Markdown instruction files, and the selected group's Settings action edits both its name and those files. Group instructions apply to every member after the global suffix and before role-specific guidance. Changing them requires all agents in the group to be stopped.
Use Start all in the selected group header to start every active agent that is not already running. The result panel reports each member as started, already running, or failed. Repeated clicks while the operation is pending reuse one idempotent request.
Agent rows distinguish reconciling, restarting, recovered, and failed recovery states. Active recovery can be stopped but not started again. Retry is offered only when recovery cannot continue; a normally stopped agent retains the standard Start action.
The floating Messages overlay is a shared group-chat timeline backed by daemon messages. Portal submissions appear as Human; MCP messages use the authenticated agent's name. Agent-to-agent direct messages, multicasts, and broadcasts appear in the same oldest-to-newest timeline. Each message has an actor-initial badge and a collapsed delivery summary that expands to resolved recipients, retry information, statuses, and failure reasons. Agent messages show both alias and stable member ID; hovering an initials badge shows that ID. The newest message remains at the bottom, while a new-message control preserves position when older history is being read.
Browser storage records a per-repository, per-group read cursor. Rail and launcher badges count retained messages after that cursor, so read messages stay read across refreshes and another tab on the same browser. Selecting a group does not mark it read; opening its Messages overlay does. Retention and authoritative history deletion cannot recreate phantom unread counts. Clearing history deletes the group's messages and delivery outcomes from the daemon for every portal session.
The bottom-right Messages launcher opens independently of terminal tabs and grid layout. It remembers its open state and shows an unread badge while closed. Its compact bottom prompt opens a modal containing audience, recipients, intent descriptions, and the full message body. On narrow screens, Messages becomes an inset full-screen sheet and hides the launcher until closed from the header. Terminal grid mode renders up to three agent columns, stepping down to two and one at narrower widths. Terminal tabs, status bars, iframe titles, and accessible names show both the editable name and stable member ID. The agent-set revision remains an internal broadcast concurrency token and is not shown in the workspace header.
Browser terminals configure 10,000 lines of xterm scrollback and enable tmux mouse routing. PageUp and PageDown pass through ttyd and tmux to raw-mode coding agent TUIs. Wheel events reach TUIs that enable terminal mouse reporting; for ordinary shells, tmux can use the wheel for copy-mode scrollback. Full-screen alternate-screen TUIs own their visible history, so xterm cannot display normal shell scrollback while that mode is active.
The header theme selector supports light, dark, and system modes. Theme and
terminal tab or grid layout are stored under the versioned
nanasa.portal.preferences.v1 browser key and synchronize through storage
events. Invalid or unavailable browser storage falls back to system theme and
tab layout without blocking portal controls.
| Concept | Description |
|---|---|
| Integration | Executable CLI settings and provider configuration-home policy |
| Role | Reusable responsibility, instructions, permissions, and presentation metadata |
| Group | An operator-created pool with shared instructions and directly configured agents |
| Agent | A stable group-owned identity with an integration, role, name, and instructions |
| Run | One process generation with a tmux terminal binding |
| Message | Structured content and audience delivered through terminal injection |
- Tmux-backed groups, runs, terminal transport, and operational portal
- Direct terminal execution for configured coding-agent CLIs
- Authenticated MCP direct, multicast, and group messaging
- Group and member rename and removal operations
- Per-agent worktrees and artifact handoff
- Authentication, authorization, and remote runner isolation
- Delivery retries, dead letters, and cost controls
- ttyd 1.7.7 is the validated version; other system versions are not guaranteed
- Terminal delivery confirms guarded paste and Enter injection, not semantic model processing
- Remote MCP access requires operator-managed TLS termination and network access controls
Issues and pull requests are welcome. Open an issue before starting a large change while the architecture is still evolving.
See LICENSE.
