diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f986a67..769901b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,5 +19,8 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: npm + - run: | + sudo apt-get update -qq + sudo apt-get install -y tmux - run: npm ci - run: npm run check diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 914c8af..ed26347 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,6 +9,6 @@ npm install npm run check ``` -Keep the safety model intact: workspace paths must remain fixed at server startup, child working directories must remain inside that boundary, Git calls must not use a shell or user-provided argument arrays, and new persistence paths must pass through redaction and sensitive-path filtering. +Keep the safety model intact: workspace paths must remain fixed at server startup, child working directories must remain inside that boundary, Git and tmux calls must not use a shell or user-provided argument arrays, and new persistence paths must pass through redaction and sensitive-path filtering. -For behavior changes, add an integration test using a temporary Git repository. For new MCP tools, explain why the behavior cannot fit one of the existing ten tools; a small and predictable tool surface is a product constraint. +For behavior changes, add an integration test using a temporary Git repository. Terminal changes also need an isolated tmux socket test; never attach tests to a user's default server. For new MCP tools, explain why the behavior cannot fit one of the existing tools; a small and predictable tool surface is a product constraint. diff --git a/LICENSES/tmux-bridge-mcp-MIT.txt b/LICENSES/tmux-bridge-mcp-MIT.txt new file mode 100644 index 0000000..26b3be4 --- /dev/null +++ b/LICENSES/tmux-bridge-mcp-MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Howard Peng and tmux-bridge-mcp contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 79ed5f6..14689bb 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,19 @@ **Let AI continue where another AI stopped.** -It tracks active sessions, task leases, advisory file ownership, decisions, failed attempts, Git state, and handoffs so Codex, Claude Code, and other agents can continue each other's work without rebuilding context from scratch. +It tracks active sessions, task leases, advisory file ownership, decisions, failed attempts, Git state, and handoffs so Codex, Claude Code, and other agents can continue each other's work without rebuilding context from scratch. Its built-in tmux transport also lets those agents read and message trusted panes without installing a second MCP server. > Not another chat memory. A local coordination layer for coding agents. -## What works in v0.3.1 +## What works in v0.4.0 - Local-first SQLite storage inside a repository or workspace - Fixed non-Git workspace boundaries with child-repository Git context -- MCP server with ten focused tools +- MCP server with ten coordination tools and five live terminal tools +- Built-in tmux transport with no `tmux-bridge-mcp` runtime dependency +- One-session tmux boundary with pane labels and self-message prevention +- Redacted, size-limited terminal reads and single-line message sends +- Persistent read-before-send guards and content-free terminal audit events - Agent sessions and heartbeats - Expiring task leases - Advisory file and glob claims with overlap warnings @@ -39,10 +43,19 @@ Agent B joins -> claims the continuation and starts from verified project state ``` -## Install from source +## Install AgentRelay is not published to npm yet. +Install the current GitHub repository as one self-contained CLI and MCP server: + +```bash +npm install -g github:arumwu/agentrelay +agentrelay --version +``` + +Or install from a source checkout: + ```bash git clone https://github.com/arumwu/agentrelay.git cd agentrelay @@ -51,7 +64,7 @@ npm run build npm link ``` -Node.js 22.13 or newer is required. +Node.js 22.13 or newer is required. The coordination and memory features work without tmux; the live terminal tools additionally require tmux 3.2 or newer. ## Quick start @@ -120,6 +133,31 @@ claude mcp add --scope local agentrelay -- \ AgentRelay advertises its lifecycle instructions through MCP initialization. For clients that do not consume server instructions, place the fallback rules from [`examples/AGENTS.md`](examples/AGENTS.md) in the repository's `AGENTS.md` or `CLAUDE.md`. +## Live tmux transport + +AgentRelay includes the terminal transport directly. Installing `@arumwu/agentrelay` or the GitHub package is enough; do not install or register a separate `tmux-bridge-mcp` server. + +Run the agents you want to coordinate in one tmux session. When AgentRelay starts inside tmux it automatically restricts terminal access to that current session: + +```bash +agentrelay terminal list +agentrelay terminal name %3 codex +agentrelay terminal read codex --lines 40 +agentrelay terminal send codex "Review src/auth.ts and report any regressions" +``` + +`terminal read` opens a 90-second, one-use guard for that actor and pane. After one send, read the pane again before sending another message. AgentRelay stores audit metadata such as the pane, byte count, and correlation ID, but never persists captured terminal output or message content. + +If AgentRelay starts outside tmux, explicitly allow one session: + +```bash +agentrelay terminal --tmux-session agent-work list +``` + +For MCP clients launched outside tmux, set `AGENTRELAY_TMUX_SESSION` in that server's environment. Set `AGENTRELAY_TMUX_SOCKET` only when using a non-default socket. Terminal targets outside the selected session are rejected. + +`terminal_send` types one literal, single-line message followed by Enter. A shell pane can interpret that text as a command, so only label and target panes you trust. AgentRelay intentionally exposes no arbitrary key or generic shell-execution tool. + ## Workspace mode A workspace may contain many independent Git repositories and non-Git projects: @@ -150,6 +188,11 @@ Task and scope claims remain workspace-wide. In the example above, claim `produc | `search_memory` | Search events, decisions, and handoffs with SQLite FTS5 | | `build_context` | Compile task-specific Git, coordination, and memory context | | `create_handoff` | Generate a Markdown handoff and optionally release leases | +| `terminal_list` | List panes only in the allowed tmux session | +| `terminal_read` | Read redacted, capped pane output and open a one-use send guard | +| `terminal_send` | Send one literal single-line message and Enter after a recent read | +| `terminal_name` | Assign an AgentRelay-specific pane label | +| `terminal_doctor` | Diagnose tmux binary, socket, session, and safety limits | ## Local data @@ -180,7 +223,11 @@ Decision conflicts are also advisory. AgentRelay flags related active decisions - The workspace root is resolved once when the server starts. - Agent working directories must exist inside that fixed workspace. - MCP tools cannot escape to another workspace path. -- AgentRelay does not expose arbitrary shell or test-command execution. +- AgentRelay does not expose a generic shell, arbitrary-key, or test-command tool. +- Terminal access is limited to the current or explicitly configured tmux session. +- Terminal sends require a recent read, reject self-targeting, and accept only one literal line. +- Terminal output is redacted and size-limited; captured output and message text are not persisted. +- Sending to a shell pane may still execute text, so terminal access is only for trusted panes. - Git inspection uses fixed argument arrays without a shell. - `.env`, private keys, and credential-like paths are filtered. - Common API keys, tokens, passwords, cookies, and bearer headers are redacted before storage. @@ -195,7 +242,7 @@ npm install npm run check ``` -The integration tests create temporary Git repositories and exercise task conflict, scope overlap, memory search, decision conflict, handoff generation, and a real in-memory MCP client/server handshake. +The integration tests create temporary Git repositories and exercise task conflict, scope overlap, memory search, decision conflict, handoff generation, and a real in-memory MCP client/server handshake. When tmux is installed, they also create an isolated socket and verify real pane listing, labeling, read-before-send enforcement, messaging, self-target rejection, and cross-session isolation. CI always installs tmux and runs this path. ## Roadmap @@ -209,4 +256,4 @@ The integration tests create temporary Git repositories and exercise task confli ## License -Apache License 2.0. See [LICENSE](LICENSE). +AgentRelay is licensed under Apache License 2.0. See [LICENSE](LICENSE). The built-in terminal transport includes work adapted from MIT-declared `tmux-bridge-mcp`; see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) and [LICENSES/tmux-bridge-mcp-MIT.txt](LICENSES/tmux-bridge-mcp-MIT.txt). diff --git a/SECURITY.md b/SECURITY.md index 8c59fc6..8911c95 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -14,23 +14,31 @@ Include the affected version, operating system, reproduction steps, security imp AgentRelay assumes coding agents and repository content may produce inaccurate or adversarial text. Stored memory is treated as context, not executable instructions. -The v0.1 boundary is intentionally narrow: +The boundary is intentionally narrow: - one fixed repository or workspace boundary per server process - local SQLite and Markdown storage -- no arbitrary shell command tool +- no generic shell, arbitrary-key, or test-command tool - fixed Git command argument arrays - workspace-relative scope paths only - path traversal and absolute-path rejection - sensitive filename filtering - best-effort secret redaction before persistence +- one allowed tmux session per server process +- one-use, actor-and-pane-specific read-before-send guards +- single-line terminal messages with byte limits and self-target rejection +- redacted and size-limited terminal reads +- terminal audit metadata that excludes captured output and message text Secret redaction is defense in depth, not a substitute for avoiding secret input. Users should not intentionally send credentials to AgentRelay. -## Out of scope for v0.1 +`terminal_send` types literal text followed by Enter. If the target is a shell pane, that text can execute as a command. Only expose AgentRelay to trusted MCP clients, use a dedicated tmux session, label intended agent panes, inspect the pane with `terminal_read`, and do not target unrelated shells or production consoles. + +## Out of scope - hostile local users with filesystem access to `.agentrelay/` - full-disk compromise - malicious changes to the AgentRelay executable or dependencies +- malicious or compromised processes already running inside the allowed tmux session - perfect detection of every possible credential format - enforcing scope claims against tools that bypass AgentRelay diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..0d4971c --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,23 @@ +# Third-party notices + +AgentRelay is licensed under Apache-2.0. The built-in tmux transport also adapts +the public behavior and selected implementation ideas of the following project. + +## tmux-bridge-mcp + +- Project: `tmux-bridge-mcp` +- Author and current contributor: Howard Peng (`howardpen9`) +- Upstream: https://github.com/howardpen9/tmux-bridge-mcp +- Reviewed revision: `eccf41bc1ab89a12d9c7540fcbf6d00e7161ff5c` +- Upstream package version: `0.3.0` +- Declared license: MIT in the upstream `package.json` + +The upstream repository did not contain a root `LICENSE` file at the reviewed +revision. AgentRelay therefore reproduces the standard MIT license text in +[`LICENSES/tmux-bridge-mcp-MIT.txt`](LICENSES/tmux-bridge-mcp-MIT.txt) and keeps +this provenance notice with source and package distributions. Copyright in the +upstream work remains with its author and contributors. + +AgentRelay's integration uses its own `terminal_*` interface and adds a single +tmux-session boundary, redaction and output limits, persistent read-before-send +guards, audit metadata, self-message prevention, and no arbitrary key tool. diff --git a/examples/AGENTS.md b/examples/AGENTS.md index 51a2e80..962603e 100644 --- a/examples/AGENTS.md +++ b/examples/AGENTS.md @@ -21,6 +21,16 @@ AgentRelay normally advertises this lifecycle through MCP server instructions. U - Record durable architecture choices with `record_decision`. - Never store secrets, credentials, raw `.env` values, or private keys in AgentRelay. +## Live terminal coordination + +1. Call `terminal_list` to inspect panes in the allowed tmux session. +2. Optionally call `terminal_name` once to assign a stable AgentRelay label. +3. Call `terminal_read` on the target before every `terminal_send`. +4. Send only to trusted agent panes. Do not target shells, production consoles, or your own pane. +5. Stop after sending; read again only when a new response is expected. Do not poll continuously. + +Terminal output is redacted and capped, but may still contain sensitive context that redaction does not recognize. Never ask another pane to print credentials or raw environment files. + ## Before ending or handing off 1. Record the latest test result and unresolved issues. diff --git a/package-lock.json b/package-lock.json index 7b4d7e4..d597d16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@arumwu/agentrelay", - "version": "0.3.1", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@arumwu/agentrelay", - "version": "0.3.1", + "version": "0.4.0", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", diff --git a/package.json b/package.json index c8b322e..bac3409 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@arumwu/agentrelay", - "version": "0.3.1", - "description": "One project. Multiple AI agents. Zero repeated work.", + "version": "0.4.0", + "description": "Local-first coordination, shared project memory, and live tmux transport for coding agents.", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -17,7 +17,10 @@ "files": [ "dist", "README.md", - "LICENSE" + "SECURITY.md", + "LICENSE", + "LICENSES", + "THIRD_PARTY_NOTICES.md" ], "scripts": { "build": "tsc -p tsconfig.json", @@ -25,6 +28,7 @@ "test": "vitest run", "test:watch": "vitest", "check": "npm run build && npm test", + "prepare": "npm run build", "prepack": "npm run check" }, "engines": { @@ -34,6 +38,8 @@ "mcp", "coding-agents", "multi-agent", + "tmux", + "terminal", "handoff", "project-memory" ], diff --git a/src/cli.ts b/src/cli.ts index 6cb371c..4a9a37f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,7 +2,9 @@ import { Command, Option } from "commander"; import { serveStdio } from "./mcp.js"; import { ProjectStore } from "./store.js"; +import { TerminalService, TerminalTransport } from "./terminal.js"; import type { AgentType, EventType, SearchResult } from "./types.js"; +import { AGENTRELAY_VERSION } from "./version.js"; function print(value: unknown): void { process.stdout.write(`${typeof value === "string" ? value : JSON.stringify(value, null, 2)}\n`); @@ -21,10 +23,37 @@ function withStore(repo: string, callback: (store: ProjectStore) => T): T { } } +async function withTerminal( + repo: string, + options: { tmuxSession?: string; tmuxSocket?: string }, + callback: (terminal: TerminalService) => Promise, +): Promise { + const store = new ProjectStore(repo); + const transport = new TerminalTransport({ + ...(options.tmuxSession ? { sessionName: options.tmuxSession } : {}), + ...(options.tmuxSocket ? { socketPath: options.tmuxSocket } : {}), + }); + try { + return await callback(new TerminalService(store, transport)); + } finally { + store.close(); + } +} + +function auditContext(options: { agentSession?: string; taskId?: string }): { + sessionId?: string; + taskId?: string; +} { + return { + ...(options.agentSession ? { sessionId: options.agentSession } : {}), + ...(options.taskId ? { taskId: options.taskId } : {}), + }; +} + const program = new Command() .name("agentrelay") - .description("Local-first coordination and shared project memory for coding agents.") - .version("0.3.1") + .description("Local-first coordination, shared project memory, and live tmux transport for coding agents.") + .version(AGENTRELAY_VERSION) .option("-r, --repo ", "Workspace or Git repository to coordinate", process.cwd()); program @@ -208,6 +237,88 @@ program }))); }); +const terminalCommand = program + .command("terminal") + .description("Coordinate trusted AI-agent panes through the built-in tmux transport") + .option("--tmux-session ", "Allow only this tmux session; defaults to the current session") + .option("--tmux-socket ", "Use an explicit tmux socket path"); + +function terminalOptions(): { tmuxSession?: string; tmuxSocket?: string } { + const options = terminalCommand.opts<{ tmuxSession?: string; tmuxSocket?: string }>(); + return { + ...(options.tmuxSession ? { tmuxSession: options.tmuxSession } : {}), + ...(options.tmuxSocket ? { tmuxSocket: options.tmuxSocket } : {}), + }; +} + +terminalCommand + .command("list") + .description("List panes in the allowed tmux session") + .action(async () => { + const repo = program.opts<{ repo: string }>().repo; + print(await withTerminal(repo, terminalOptions(), (terminal) => terminal.list())); + }); + +terminalCommand + .command("read") + .description("Read redacted pane output and open a short-lived send guard") + .argument("", "Pane id, tmux target, or AgentRelay pane label") + .option("--lines ", "Lines to read, from 1 to 200", "50") + .option("--agent-session ", "AgentRelay session for the audit event") + .option("--task-id ", "AgentRelay task for the audit event") + .action(async (target: string, options: { lines: string; agentSession?: string; taskId?: string }) => { + const repo = program.opts<{ repo: string }>().repo; + print(await withTerminal( + repo, + terminalOptions(), + (terminal) => terminal.read(target, Number(options.lines), auditContext(options)), + )); + }); + +terminalCommand + .command("send") + .description("Send one literal message and Enter after a recent terminal read") + .argument("", "Pane id, tmux target, or AgentRelay pane label") + .argument("", "Single-line message, up to 8192 UTF-8 bytes") + .option("--agent-session ", "AgentRelay session for the audit event") + .option("--task-id ", "AgentRelay task for the audit event") + .action(async (target: string, message: string, options: { agentSession?: string; taskId?: string }) => { + const repo = program.opts<{ repo: string }>().repo; + print(await withTerminal( + repo, + terminalOptions(), + (terminal) => terminal.send(target, message, auditContext(options)), + )); + }); + +terminalCommand + .command("name") + .description("Assign an AgentRelay-specific label to a pane") + .argument("", "Pane id or tmux target") + .argument("