diff --git a/docs/dev/adr/0001-capability-model.md b/docs/dev/adr/0001-capability-model.md new file mode 100644 index 00000000..9c5d9215 --- /dev/null +++ b/docs/dev/adr/0001-capability-model.md @@ -0,0 +1,376 @@ +(adr-capability-model)= + +# ADR 0001: Capability boundaries for tmux MCP implementations + +## Abstract + +This record defines the shared capability contract for tmux Model Context +Protocol (MCP) implementations. It specifies how implementations describe +available operations, how a tmux socket limits the structured tool namespace, +and what MCP annotations do and do not express. It does not prescribe an +implementation language, internal class design, or rollout sequence. + +## Status + +Proposed. Supersedes the former three-level capability model. + +## Context and problem + +A tmux MCP gives an agent a terminal. The implementation must describe that +authority without confusing tmux object selection, process execution, client +consent, and operating-system confinement. + +One ordered safety scale cannot describe those independent concerns. Running a +shell command and deleting a tmux window are different capabilities, not +different amounts of one capability. MCP also defines +[`readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`](https://github.com/modelcontextprotocol/python-sdk/blob/v1.29.1/src/mcp/types.py#L1247-L1294) +for client presentation. Reusing those annotations as severity labels would +change their protocol meaning. + +## Scope and non-goals + +This record applies to MCP servers that expose structured tmux operations. It +defines their shared capability vocabulary, tool-surface behavior, annotation +semantics, execution boundary, and disclosure obligations. + +This record does not define operating-system isolation, decide which shell +commands an operator permits, or make the model an enforcement boundary. It +does not track implementation progress or prescribe a programming language, +framework, storage format, or build system. + +## Terminology + +**tmux MCP implementation** +: An MCP server implementation that exposes structured tools for tmux. + +**server process** +: One running instance of a tmux MCP implementation. + +**structured tool** +: A typed MCP operation that targets tmux or a program running in a tmux pane. + +**selected socket** +: The single tmux server socket chosen by a server process at startup. + +**direct operation** +: The operation a structured tool requests, excluding behavior already + configured in tmux, such as command aliases and hooks. + +**workload process** +: A pane or host process whose behavior can be influenced by caller input. A + control-plane process used only to issue a tmux request is not a workload + process. + +**whole-call MCP annotation** +: A standard MCP annotation describing the observable tool call as a whole, + including configured tmux behavior activated by the direct operation. + +**structured tool surface** +: The fixed set of tools a server process advertises and accepts. + +**host command** +: Client-authored executable input handed directly to a process outside a tmux + pane. + +**minimal tmux configuration** +: Configuration supplied by the implementation that loads no user configuration, + plugin, hook, or status job. + +## Conformance + +In this record, uppercase **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and +**MAY** use the meanings defined by +[BCP 14](https://datatracker.ietf.org/doc/html/rfc8174). Lowercase forms have +their ordinary English meanings. + +A **conforming tmux MCP implementation** satisfies every applicable **MUST** and +**MUST NOT** requirement in this record. The Terminology, Conformance, +Architectural decisions, and Security and reliability considerations sections +are normative. Sections and paragraphs labeled informative do not define +conformance requirements. + +## Architectural decisions + +### CM-1: A tmux socket limits addressable tmux objects; it is not a security boundary + +A server process **MUST** select exactly one tmux socket before exposing +structured tools. The server process **MUST** retain that socket for its +lifetime. A structured tool **MUST NOT** accept a per-call socket selector. A +structured tool **MUST NOT** address tmux objects outside the selected socket. + +The default configuration **MUST** select a product-scoped dedicated socket. A +server process that creates the tmux server on that socket **MUST** use a minimal +tmux configuration. Selecting an inherited or user-configured socket **MUST** +require explicit operator configuration. A server process that finds an existing +server on the selected socket **MUST NOT** claim that server has minimal +configuration provenance. + +The selected socket limits which tmux sessions, windows, and panes structured +tools can name. It does not restrict filesystem access, process execution, +network access, credentials, or access through commands running in a pane. + +**Rationale, informative.** Socket pinning prevents accidental cross-server +object selection. It does not provide operating-system isolation and must not be +described as a sandbox. + +### CM-2: Independent properties describe each tool's direct capability + +Every structured tool **MUST** declare its direct process reach, direct tmux +effect, and output classes. An implementation **MUST NOT** collapse those +properties into an ordered safety level. + +Direct process reach **MUST** use one of these values: + +- `none`: starts no workload process and delivers no client-controlled input to + one. +- `configured-process`: starts a pane workload process without accepting a + command payload or client-controlled tmux-format input. +- `pane-input`: delivers client-controlled keys or text to a pane program. +- `pane-command`: runs a client-authored shell command in a pane. + +A conforming implementation **MUST** reserve `host-command` to describe a +prohibited public capability. + +Direct tmux effect **MUST** be a set containing one or more of `observe`, +`change`, and `delete`. Output classes **MUST** be a set drawn from +`tmux-metadata`, `terminal-content`, `process-environment`, and +`configured-command`. An implementation **MUST** separately record whether a +tool may expose secrets or return untrusted content. + +These properties describe the direct operation. Whole-call MCP annotations +separately account for execution or mutation added by configured aliases and +hooks. + +**Example, informative.** `rename_window` changes tmux state without accepting +executable input. `run_shell_command` also changes tmux state and accepts a +client-authored pane command. Their tmux effects overlap; their process reach +does not. + +### CM-3: One startup decision defines the advertised and callable tool surface + +Every structured tool **MUST** belong to exactly one unordered toolset: +`inspect`, `manage`, `execute`, or `teardown`. + +On the default dedicated socket with minimal tmux configuration, an +implementation **MUST** enable all four toolsets by default. On an inherited or +user-configured socket, an implementation **MUST** require explicit operator +selection before enabling `teardown`. It **MUST** apply the same requirement to +an existing server whose configuration provenance is unknown. + +At startup, an implementation **MUST** expand the selected toolsets. It **MUST** +then add named inclusions. It **MUST** then remove named exclusions. A named +exclusion **MUST** win over every inclusion path. + +An implementation **MUST** reject unknown toolset and tool names at startup. It +**MUST** freeze the effective structured tool surface for the server process's +lifetime. A tool outside that surface **MUST NOT** be advertised. A tool outside +that surface **MUST NOT** be callable by name. + +An aggregate tool **MAY** invoke a nested operation that is not separately +advertised. The aggregate tool **MUST** declare that nested operation as part of +its own authority. A named exclusion **MUST** remove an operation from every +aggregate tool's nested authority. + +The toolsets support inventory configuration, context reduction, model routing, +and documentation navigation. They do not restrict what an enabled pane-input +or pane-command tool can cause a pane program to do. + +**Rationale, informative.** Unordered toolsets permit combinations such as +`inspect,teardown`. Applying the same effective surface to discovery and +invocation prevents a hidden tool from remaining callable. + +### CM-4: MCP annotations describe an entire tool call + +An implementation **MUST** use standard MCP annotations only with their protocol +meanings. A whole-call MCP annotation **MUST** account for the direct operation +and configured tmux behavior that the operation activates. An implementation +**MUST NOT** use standard annotations as authorization decisions or project +severity labels. + +Direct process reach, direct tmux effect, and output classes **MUST** remain +separate from standard MCP annotations. Without evidence for the whole-call +claim, an implementation **MUST** set `readOnlyHint` to `false`. Without evidence +for the whole-call claim, an implementation **MUST** set `destructiveHint` to +`true`. Without evidence for the whole-call claim, an implementation **MUST** set +`idempotentHint` to `false`. Without evidence for the whole-call claim, an +implementation **MUST** set `openWorldHint` to `true`. + +An implementation **MUST** set `readOnlyHint` to `true` only when the whole call +cannot modify its environment. It **MUST** set `destructiveHint` to `false` only +when the whole call performs additive updates at most. It **MUST** set +`idempotentHint` to `true` only when repeating the call with the same arguments +has no additional effect. It **MUST** set `openWorldHint` to `false` only when the +whole call cannot interact with external entities. + +MCP annotations support client consent interfaces. They do not enforce tool +authorization, operating-system confinement, or command policy. + +### CM-5: One authoritative capability definition governs every public claim + +An implementation **MUST** maintain one authoritative, machine-readable +capability definition for its structured tools. Tool registration, the effective +structured tool surface, tool descriptions, documentation, and capability +reporting **MUST** agree with that definition. + +The capability definition **MUST** classify every caller-controlled input by the +interpreter boundary it reaches. An implementation **MUST** validate capability +claims against those input sinks rather than infer behavior from parameter +names. + +**Rationale, informative.** tmux expands formats in argument positions whose +names do not reveal the interpreter boundary. A shared manifest generated or +validated during CI is one implementation approach, not a required storage +format. + +### CM-6: Public tools do not execute client-authored host commands + +A conforming implementation **MUST NOT** expose a structured tool with +`host-command` process reach. It **MUST NOT** hand caller text directly to a +host-side shell. Client-authored shell commands **MAY** run through a +`pane-command` tool, where the process is represented by a tmux pane. + +This rule constrains the direct MCP surface. It does not prevent a pane command +from invoking tmux's +[`run-shell`](https://github.com/tmux/tmux/blob/3.2a/cmd-run-shell.c#L177-L181), +installing a +[`#()` status job](https://github.com/tmux/tmux/blob/3.2a/format.c#L392-L399), +or starting any process available to the tmux user's account. + +### CM-7: Capability disclosure is part of the product contract + +Every structured tool description **MUST** begin with a plain-language statement +of its direct process reach and tmux effect. An implementation **MUST** publish +its effective structured tool surface and selected socket. Installation and +trust documentation **MUST** state that execute tools run with the tmux user's +authority. + +Documentation **MUST** distinguish socket-scoped object selection from +operating-system confinement. Documentation **MUST** distinguish tool-surface +filtering from authorization. Documentation **MUST** describe whole-call MCP +annotations as consent metadata rather than enforcement. + +An implementation **MUST NOT** describe a dedicated socket, restricted tool +surface, payload filter, or MCP annotation as a sandbox or security boundary. + +## Consequences + +Conservative whole-call annotations may cause clients to prompt more often. +That cost preserves the protocol meaning of the annotations. + +Pinning one socket per server process requires separate configured MCP entries +to control separate tmux sockets. The selected socket reduces accidental object +selection without claiming exclusive ownership. + +Maintaining one authoritative capability definition adds review and validation +work. It also makes capability drift detectable across registration, +documentation, and runtime disclosure. + +Stable tool names become part of the client-consent surface. Renaming a public +tool therefore requires an explicit migration rather than an internal refactor. + +## Rejected alternatives + +**An ordered safety scale.** One rank cannot represent independent process +reach, tmux effect, and output sensitivity without hiding one of them. + +**Tool filtering as authorization.** An enabled pane-input or pane-command tool +can express operations omitted from the structured tool surface. Filtering is +still useful for inventory control and accident reduction. + +**Payload blocklists.** Shell and terminal input are composable. A filter that +recognizes selected strings cannot establish a command boundary and would invite +operators to rely on incomplete protection. + +**Per-call socket selection.** A caller-selected socket expands every tool's +object namespace and makes one server process represent several trust contexts. + +**Public host-command tools.** Host-side commands would bypass the observable +pane process and its completion boundary. + +**Generic mutating or destructive aggregates.** A wrapper hides the names on +which client consent policies depend and turns one approval into authority over +unrelated operations. + +## Security and reliability considerations + +### Ambient tmux behavior + +**Background, informative.** tmux +[expands command aliases](https://github.com/tmux/tmux/blob/3.2a/cmd-parse.y#L698-L715) +before dispatch and +[runs after-hooks](https://github.com/tmux/tmux/blob/3.2a/cmd-queue.c#L617-L627) +after many commands. A nominally observational direct operation can therefore +execute or mutate through existing configuration. Independent pane processes, +plugins, event hooks, and status jobs can also run without an MCP call. + +A conforming implementation **MUST NOT** claim that it prevents all subprocess +execution. A conforming implementation **MUST** describe ambient tmux behavior +separately from direct process reach. + +### Shared sockets + +**Boundary, informative.** Every process with access to the selected socket can +create or alter objects on it. A dedicated socket separates the structured +namespace from another tmux server; it does not establish exclusive ownership. + +An implementation **MUST NOT** claim ownership of every object on a shared +socket. A self-kill guard **MUST** be described as protection against direct +teardown tools only, not against equivalent pane commands or other clients. + +**Shutdown behavior, informative.** With the default `exit-empty` enabled and +`exit-unattached` disabled +([tmux option defaults](https://github.com/tmux/tmux/blob/3.2a/options-table.c#L256-L268)), +tmux waits for every session to be removed and for clients to disconnect before +exiting +([tmux exit logic](https://github.com/tmux/tmux/blob/3.2a/server.c#L268-L286)). +Removing one MCP instance's sessions does not stop a shared server while another +session remains. Socket-wide termination remains an operator action because it +may destroy sessions the caller does not own. + +### Untrusted and sensitive output + +**Background, informative.** Terminal output, environment values, configured +commands, and tmux metadata can contain secrets or untrusted instructions. + +An `inspect` tool **MUST NOT** be described as safe merely because its direct +operation is observational. An implementation **MUST** disclose when a tool may +return untrusted content. It **MUST** disclose when a tool may expose secrets. + +### Aggregate authority + +An aggregate tool **MUST** disclose the complete set of nested tools it can +invoke. Its effective authority **MUST NOT** exceed its advertised nested tool +set. A conforming implementation **MUST NOT** expose a generic mutating or +destructive aggregate. + +### Bounded operations + +An implementation that claims bounded pattern matching **MUST** bound both input +size and matching execution time. A capture or transport deadline **MUST NOT** be +misreported as a matching timeout. + +### Redaction and history suppression + +**Boundary, informative.** Audit redaction applies only to the audit record. +History suppression is best-effort shell hygiene. + +An implementation **MUST NOT** claim that either mechanism removes data from +client transcripts, pane scrollback, process arguments, shell history outside +its control, or operating-system observation surfaces. + +## When to reconsider + +Reconsider this decision if MCP gains enforceable authorization or nested-call +annotation semantics, if tmux exposes a public command mode that suppresses all +relevant ambient behavior, or if deployments require a stronger multi-user +boundary than one operating-system account and socket can provide. + +Reconsider the shared vocabulary if an implementation cannot express a required +capability without weakening an existing term. Add a new independent property +instead of stretching an old one into a severity rank. + +## References + +- [Issue #127: tool visibility is not operating-system confinement](https://github.com/tmux-python/libtmux-mcp/issues/127) +- [Source decision for ADR 0001](https://github.com/tmux-python/libtmux-mcp/issues/127#issuecomment-5463431049) +- [Detailed target-state capability model](https://github.com/tmux-python/libtmux-mcp/issues/127#issuecomment-5463166342) diff --git a/docs/dev/adr/index.md b/docs/dev/adr/index.md new file mode 100644 index 00000000..e02b0bc4 --- /dev/null +++ b/docs/dev/adr/index.md @@ -0,0 +1,12 @@ +(architecture-decisions)= + +# Architecture decisions + +Architecture decision records document product-wide choices whose trade-offs and +consequences outlive one implementation. + +```{toctree} +:maxdepth: 1 + +0001-capability-model +``` diff --git a/docs/project/index.md b/docs/project/index.md index 2b60711f..bd210e45 100644 --- a/docs/project/index.md +++ b/docs/project/index.md @@ -25,6 +25,12 @@ Ruff, mypy, NumPy docstrings, import conventions. Release checklist and version policy. ::: +:::{grid-item-card} Architecture decisions +:link: ../dev/adr/index +:link-type: doc +Long-lived design choices, their trade-offs, and their consequences. +::: + :::: ```{toctree} @@ -33,4 +39,5 @@ Release checklist and version policy. contributing code-style releasing +../dev/adr/index ``` diff --git a/tests/docs/test_topic_contracts.py b/tests/docs/test_topic_contracts.py index 955d3013..73c4eac1 100644 --- a/tests/docs/test_topic_contracts.py +++ b/tests/docs/test_topic_contracts.py @@ -334,6 +334,49 @@ def test_trust_docs_state_ambient_tmux_execution( assert "{ref}`trust`" in resources +def test_project_navigation_publishes_architecture_decisions( + docs_dir: pathlib.Path, +) -> None: + """The contributor landing page exposes the numbered ADR collection.""" + project = (docs_dir / "project" / "index.md").read_text(encoding="utf-8") + index = (docs_dir / "dev" / "adr" / "index.md").read_text(encoding="utf-8") + + assert ":::{grid-item-card} Architecture decisions" in project + assert ":link: ../dev/adr/index" in project + assert "\n../dev/adr/index\n" in project + assert "(architecture-decisions)=" in index + assert "# Architecture decisions" in index + assert "\n0001-capability-model\n" in index + + +def test_capability_adr_preserves_the_source_decision( + docs_dir: pathlib.Path, +) -> None: + """ADR 0001 retains the proposed capability model and its provenance.""" + text = (docs_dir / "dev" / "adr" / "0001-capability-model.md").read_text( + encoding="utf-8" + ) + + assert "(adr-capability-model)=" in text + assert text.startswith("(adr-capability-model)=\n\n# ADR 0001:") + assert "## Status\n\nProposed." in text + assert "issues/127#issuecomment-5463431049" in text + for number in range(1, 8): + assert f"### CM-{number}:" in text + for section in ( + "## Abstract", + "## Scope and non-goals", + "## Terminology", + "## Conformance", + "## Architectural decisions", + "## Consequences", + "## Rejected alternatives", + "## Security and reliability considerations", + "## When to reconsider", + ): + assert section in text + + def test_trust_toolset_labels_are_badged_and_copyable( docs_dir: pathlib.Path, ) -> None: