Skip to content

Replace safety tiers with capability toolsets - #128

Merged
tony merged 44 commits into
mainfrom
scope-overhaul
Aug 30, 2026
Merged

Replace safety tiers with capability toolsets#128
tony merged 44 commits into
mainfrom
scope-overhaul

Conversation

@tony

@tony tony commented Aug 29, 2026

Copy link
Copy Markdown
Member

Summary

  • Replace safety tiers with unordered inspect, manage, execute, and teardown toolsets that FastMCP visibility and server middleware both enforce.
  • Prevent hidden tmux format interpretation in caller-supplied names, titles, option names, and working directories.
  • Bound caller-controlled matching and restrict display_message to literals and plain variable references.
  • Use conservative MCP annotations for calls targeting programmable tmux servers, where aliases and hooks can extend the requested operation.
  • Remove write-capable batch wrappers, preserve prompt adapters under filtering, and make failed calls single-attempt.
  • Document the trust boundary between MCP call gating, tmux configuration, client consent, and OS confinement.

This implements the current capability-model work described in issue #127.

Changes by area

Capability surface

  • Toolsets: LIBTMUX_TOOLSETS selects any combination of inspect, manage, execute, and teardown; the default remains inspect,manage,execute.
  • Named controls: LIBTMUX_TOOLS enables individual tools, LIBTMUX_EXCLUDE_TOOLS takes precedence, and unknown names fail startup.
  • Safety-tier migration: LIBTMUX_SAFETY is rejected with the corresponding toolset configuration.
  • Batching: call_read_tools_batch accepts inspect tools only; the write-capable batch wrappers are removed so client rules see each write tool directly.
  • Wire enforcement: disabled tools disappear from discovery and are refused during invocation; unclassified tools fail closed.
  • Prompt adapters: generated list_prompts and get_prompt tools remain visible through inspect.

tmux correctness

  • Literal arguments: session names, window names, pane titles, option names, and working directories are escaped before tmux format expansion.
  • Working directories: spawn tools resolve start_directory before invoking tmux and reject missing paths instead of silently landing in $HOME.
  • Bounded search: search_panes uses a bounded regex engine with a matching-only deadline and a separate pattern-length limit.
  • Format reads: display_message accepts literal text and plain #{variable} references, including hyphenated option names, while rejecting modifiers, conditionals, and jobs.

Trust and lifecycle

  • Whole-call annotations: tmux-requesting tools advertise conservative standard hints because command aliases and after-* hooks can replace or extend an operation.
  • Direct-operation classification: project toolsets preserve the distinctions that standard annotations cannot express against an arbitrary tmux server.
  • Single attempts: failed inspection calls are not retried automatically after tmux may already have applied an alias or hook.
  • Process-local shutdown: lifespan cleanup clears Python caches without issuing tmux commands or deleting shared-prefix buffers.
  • Stored execution: option and environment setters belong to execute because tmux or a future shell can interpret some stored values later.

Documentation and development tooling

  • Trust model: the trust page distinguishes MCP visibility, tmux configuration, shared sockets, ambient execution, client consent, and OS confinement.
  • Migration guide: configuration and batch-tool mappings cover every retired public name.
  • Toolset badges: documentation declares the project vocabulary and renders each toolset with a copyable label, icon, and tone.
  • Published docs packages: gp-sphinx, sphinx-autodoc-api-style, and sphinx-autodoc-fastmcp are pinned to the released 0.1.0a38 packages from PyPI.
  • Client swapping: mcp_swap.py removes retired safety settings only when an explicit toolset replacement preserves the intended surface.
  • Docs deployment: CloudFront invalidation covers every page replaced by the full S3 synchronization.

Design decisions

  • Toolsets are inventory, not permissions: filtering controls what this MCP server advertises and accepts; an enabled execution tool can still type an equivalent tmux or shell command.
  • The MCP owns non-surprise: literal or identifier inputs do not become hidden interpreter surfaces.
  • Clients own consent: tool names, schemas, annotations, and classifications provide approval context without pretending the model is an enforcement boundary.
  • The operator and OS own confinement: tmux runs processes with the authority of its environment; sockets separate tmux namespaces but do not restrict files, credentials, processes, or networks.
  • Only reads are batchable: one wrapper-level confirmation cannot preserve name-keyed approval for nested writes.
  • No payload denylist: shell and pane-input tools expose their actual authority rather than applying incomplete command filters.

Migration

Before After
LIBTMUX_SAFETY=readonly LIBTMUX_TOOLSETS=inspect
LIBTMUX_SAFETY=mutating LIBTMUX_TOOLSETS=inspect,manage,execute
LIBTMUX_SAFETY=destructive LIBTMUX_TOOLSETS=inspect,manage,execute,teardown
call_readonly_tools_batch call_read_tools_batch
call_mutating_tools_batch Call each write tool directly
call_destructive_tools_batch Call each teardown tool directly

Verification

$ uv run ruff check .
$ uv run ruff format . --check
$ uv run mypy .
$ uv run pytest -n 5 --reruns 0
$ just build-docs

Test plan

  • Exercise literal tmux-format arguments and display_message grammar against supported tmux behavior.
  • Prove pathological regex matching reaches its deadline while pane capture time remains outside the budget.
  • Verify every toolset subset, named inclusion, explicit exclusion, and empty surface through the production MCP wire.
  • Verify read batches can invoke classified nested reads without exposing those tools directly or bypassing explicit exclusions.
  • Verify retired configuration and tool names fail with migration guidance.
  • Exercise mcp_swap.py across every supported client configuration and available native client.
  • Build the documentation with live tool schemas, redirects, badges, and trust-page contracts.
  • Run the supported tmux compatibility matrix.
  • Demonstrate that each new regression gate fails when its protected behavior is deliberately reverted.

why: tmux expands `-c` as a format before using it as a working
directory (spawn.c), so `#(...)` in `start_directory` ran a shell
job, and a directory whose real name held a `#` silently landed the
pane in `$HOME` instead. All four spawn tools carried the sink while
advertising `destructiveHint: false` and `openWorldHint: false`.

what:
- Resolve `start_directory` to an existing directory before tmux
  sees it, then escape the result for tmux's format pass
- Refuse `#[`, which tmux hands to the style parser with no escaped
  form, rather than silently redirecting the pane
- Fail on a path that does not exist instead of falling back to the
  client's directory
- Cover `create_session`, `create_window`, `split_window` and
  `respawn_pane`, including directories named `#(id)` and `#{x}`
- State the parameter's contract where it previously restated its
  own name
@codecov-commenter

codecov-commenter commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.67391% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.91%. Comparing base (dd16e3c) to head (7150af9).

Files with missing lines Patch % Lines
src/libtmux_mcp/server.py 64.86% 7 Missing and 6 partials ⚠️
src/libtmux_mcp/middleware.py 87.87% 2 Missing and 2 partials ⚠️
src/libtmux_mcp/prompts/__init__.py 85.71% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #128      +/-   ##
==========================================
- Coverage   86.98%   86.91%   -0.08%     
==========================================
  Files          46       46              
  Lines        3834     3874      +40     
  Branches      577      583       +6     
==========================================
+ Hits         3335     3367      +32     
- Misses        350      355       +5     
- Partials      149      152       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tony tony changed the title Phase zero of the capability model: close the format sinks, bound the matcher, audit the hints Refactor scopes Aug 30, 2026
tony added 13 commits August 30, 2026 11:51
why: `search_panes` compiled a caller's pattern with the stdlib `re`,
which has no execution ceiling. A pattern of eleven characters against
an ordinary pane line hung the server indefinitely — `(a|a)+$` over 60
`a` characters never returned — and the tool is `readonly`, so the
hang was reachable at the most restrictive safety tier.

what:
- Compile the caller's pattern with `regex`, which accepts a
  per-search wall-clock budget
- Share one 2 s deadline across every line of every captured pane, so
  the ceiling covers the call rather than each line
- Report exhaustion as a correctable error naming the cause
- Keep the tmux-side `#{C:...}` fast path untouched; it never runs a
  Python match

A literal scan of 20,000 lines costs ~3.5 ms, so 2 s is a ceiling a
real search never approaches.
why: MCP defines `destructiveHint: false` as a positive claim that a
tool performs only additive updates. Six tools that hand a caller's
payload to a program advertised it: `send_keys`, `send_keys_batch`,
`run_command`, `paste_text`, `paste_buffer`, and `pipe_pane`. Typed
input can overwrite a file or end a process, so the claim was false to
every connected client.

what:
- Advertise `destructiveHint: true` on the six input-delivering tools
- Move `load_buffer` off that preset: it allocates a fresh buffer and
  delivers nothing, so it is additive and closed-world, which is what
  the module docstring already said while the code did otherwise
- Assert the claim per tool against the registered surface, and drop
  the five tests that asserted the presets' contents instead — one of
  them pinned `destructiveHint: false` in place
- Require every tool to advertise all four hints, so a client never
  falls back on a protocol default
why: The four spawn tools advertised `openWorldHint: false`, but a new
pane runs a process with the user's full authority. `split_window` also
claimed `destructiveHint: false` while accepting a `shell` command that
replaces what the pane would otherwise have run.

what:
- Advertise `openWorldHint: true` on `create_session`, `create_window`,
  `split_window` and `respawn_pane`
- Move `split_window` and `respawn_pane` onto the payload-carrying
  hints; both take an authored command
- Add a preset for the two spawns that carry no command payload, so
  `create_session` and `create_window` keep their additive claim
- Fold the single-use mutating-destructive preset into the destructive
  one; the tier tag at the registration site already carried the
  distinction the name existed to document
- Retire the pane-scoped hint tests the surface-wide invariants now
  cover, including a docstring narrating an earlier preset refactor
why: MCP reserves `destructiveHint: false` for tools that perform only
additive updates. Renames, resizes, selections, moves, layouts, titles,
options, and environment writes all replace a value tmux already held,
and `swap_pane` and `delete_buffer` were additionally mis-preset — a
swap exchanges two panes, a delete removes a buffer.

what:
- Advertise `destructiveHint: true` on the replacement tools, and move
  `swap_pane`, `enter_copy_mode` and `delete_buffer` onto the removal
  hints
- Give `signal_channel` and `wait_for_channel` an additive preset: a
  `wait-for` channel latches, replacing nothing
- Rename the create preset to say what its one remaining user does,
  now that the session and window spawns have their own
- Assert the claim as a closed set: only five named tools may
  advertise additive-only updates, so a new tool cannot join them
  without saying so

The coarser hint is accepted, not worked around. A client that gates on
`destructiveHint` now prompts for a rename; the signal that separates a
rename from a shell command is the tool name and description.
why: The tools that return terminal content advertised
`openWorldHint: false`, which tells a client the result came from a
closed domain. A pane holds whatever was printed into it — an SSH
session, a package manager, another agent — so the text crossed a
trust boundary before this server read it. Being read-only says
nothing about where the bytes came from.

what:
- Advertise `openWorldHint: true` on `capture_pane`, `capture_since`,
  `snapshot_pane`, `search_panes`, `wait_for_text` and `show_buffer`
- Carry the same hint on `call_readonly_tools_batch`, which can invoke
  any of them under its own name
- Leave structural reads closed: listings, info, options, hooks and
  the tmux environment report tmux's own state
why: The hint table was hand-maintained, listed 29 of 56 tools, had no
`openWorldHint` column, and went stale the moment the annotations were
corrected. Nothing checked it. A paragraph also named a preset that no
longer exists.

what:
- List every tool with all four hints, generated from the registered
  surface
- Assert the table against a freshly registered server, so a hint
  change that skips the docs fails the suite
- Say what `destructiveHint: false` and `openWorldHint: true` claim,
  and that hints are presentation, not enforcement
- Register into a fresh server rather than the production one, whose
  tier filter is fixed at import and would make the check depend on
  test ordering
why: The registration list named three of the four MCP hints, and the
suite now requires every tool to advertise all four.

what:
- List `openWorldHint` alongside the other three
why: `pipe_pane` already carried a correct tmux-format escaper, measured
against the expander's actual rule: a `#`-run followed by `[` is a style
sequence that tmux copies through verbatim, so doubling it corrupts the
value. Resolving `start_directory` added a second escaper under the same
name that doubled every `#` and refused `#[` because doubling broke it.
The refusal was a workaround for the wrong rule.

what:
- Move the run-aware escape into `_utils` as the one implementation,
  and have `pipe_pane` call it
- Leave `pipe_pane` only the half that is its own: `%` doubling for the
  `strftime` pass `format_expand_time` adds, which `-c` does not get
- Drop the `#[` refusal; a directory named `style#[x]` or `run##[x]`
  now reaches the pane intact
- Cover both forms in the round-trip matrix, which goes red under the
  naive rule
why: `start_directory` was one of eight arguments tmux runs through
`format_single`. The rest were missed. `create_session`'s `session_name`
and `window_name`, and `create_window`'s `window_name`, execute a
`#(...)` job — reproduced on tmux 3.7d at the default safety tier.
`rename_session`, `rename_window` and `set_pane_title` corrupt an
ordinary name instead: `w#Sx` became `wplain-namex` and a pane titled
`title #H here` became `title d here`.

what:
- Escape the caller's text on `create_session`, `create_window`,
  `rename_session`, `rename_window` and `set_pane_title`
- Escape the option name on `set_option` and `show_option` together;
  tmux expands it on both, so escaping one alone would stop a write
  and its read agreeing
- Cover every expanding argument in one file, named for the defect
  rather than for the spawn tools it started with
- Correct the fallback the resolver's docstring described: a cwd that
  cannot be entered falls back to `$HOME`, then `/`, not the client's
  directory
- Give `search_panes` the `Raises` section its deadline needs, since
  the docstring is what a calling model reads

Ruled out by measurement, not assumption: `-e` environment values and
`set-option`'s value expand only under `-F`, which is never passed.
why: The generated schema block picked up the new docstrings, but the
hand-written prose an agent reads first said nothing about either new
failure mode.

what:
- Add a gotcha covering the whole class: names are stored literally,
  option values are not
- Say on the four spawn pages that `start_directory` must exist
- Say on the search page that matching shares a two-second ceiling and
  which patterns exhaust it
why: The annotation invariants read the production server, whose tier
filter is fixed at `LIBTMUX_SAFETY` at import. Under
`LIBTMUX_SAFETY=readonly` 13 of them failed on missing keys rather than
on any annotation defect. The docs table gate had already solved this
three commits earlier; the fix was not carried across.

what:
- Register into a fresh server, as `test_topic_contracts` does
- Assert `idempotentHint` on the spawn tools, which nothing named: the
  retired per-tool test had covered `respawn_pane`, and the closed-set
  invariant checks only `destructiveHint`
why: `signal_channel` and `wait_for_channel` advertised additive,
idempotent behaviour. `tmux wait-for` is a consuming latch, measured on
3.7d: after `wait-for -S ch`, the first wait returns and the second
blocks, and signalling twice removes the channel so a later wait blocks
too. Repeating either call changes what a subsequent wait does, which is
neither additive nor idempotent.

what:
- Advertise both with the removal hints
- Narrow the closed additive set to the three tools that earn it, and
  drop the preset that now has no users
- Regenerate the safety table, which the docs gate caught
why: The spawn preset was inserted directly above the payload preset's
`#:` block, so Sphinx read the whole run as one docstring on the spawn
preset — opening with a rationale about payloads it does not take —
and the payload preset rendered with none.

what:
- Move the payload preset's comment down to sit above it
tony added 27 commits August 30, 2026 11:51
why: The match deadline starts after compilation, so a pattern's own
size was unbounded. The tool also read as if the ceiling covered the
call; capturing each pane is a tmux round-trip outside it, and the work
still grows with the panes in scope.

what:
- Cap the pattern length, which is what bounds compilation
- Say in the docstring what the deadline covers and what it does not
- Record why the shared budget is asserted by construction: the first
  pane to exhaust it aborts the call under a per-pane budget too, so no
  test separates them

A test asserting the shared budget was written and then removed: it
passed with the budget deliberately reset per pane, so it proved
nothing.
why: One escaper is not one contract. `pipe_pane` doubles `%` because
`pipe-pane` runs its argument through `strftime`; `-c` and the name
arguments do not, so the same doubling there would corrupt a literal
percent. Nothing held that line, and the function is not idempotent, so
a later upstream change could double-escape without a gate noticing.

what:
- Assert the hash escaper leaves `%` alone, doubles a run once, and
  compounds when applied twice
why: `readonly` / `mutating` / `destructive` read as a permission ladder
and was not one. It ranked unlike powers on one axis, so running a shell
command and deleting a window differed by degree rather than by kind,
and the tiers accumulated upward: the kill tools could not be enabled
without also enabling the typing tools. `readonly` was the worst of the
three — a capture returns whatever a pane holds, secrets included, so
the name promised a safety the tool never had.

what:
- Group tools into four unordered toolsets by what they do: `inspect`,
  `manage`, `execute`, `teardown`
- Put anything that hands input to a program or stores a value tmux
  later runs in `execute`, `set_option` and `set_environment` included
- Replace the tier gate with set membership, still fail-closed: a tool
  carrying no toolset is refused
- Delete `LIBTMUX_SAFETY` with a startup error naming its replacements;
  add `LIBTMUX_TOOLSETS`, `LIBTMUX_TOOLS`, `LIBTMUX_EXCLUDE_TOOLS`, and
  fail startup on an unknown name rather than falling back
- Default to `inspect,manage,execute`: this server still reaches
  whichever tmux server the environment points at, so deletion stays
  something an operator asks for by name
- Rename the annotation presets off tier words onto tmux effects, and
  `ReadonlyRetryMiddleware` onto the toolset it actually keys on

`LIBTMUX_TOOLSETS=inspect,teardown` is now a legal surface.
why: The tests and docs still taught the ladder the code no longer has.
A reader who found `readonly` in the glossary or the landing grid would
have learned the wrong model, and the docs contract asserted the old
table, so the vocabulary could not be half-removed.

what:
- Rewrite the safety page as a trust page: what the toolsets group,
  that `inspect` means "does not interpret your input as a command"
  rather than "safe", and that dropping a toolset is not containment
- Retire the mutating and destructive batch pages, and rename the read
  batch to match the tool
- Sweep the glossary, landing grid, configuration, architecture,
  troubleshooting, logging, prompting, gotchas and demo pages
- Point `LIBTMUX_SAFETY`'s entry at the three variables that replaced
  it, and the section badge map at the toolsets
- Rewrite the tests that encoded a ladder: membership instead of a
  ceiling, and a spawn refused by the read batch instead of a spawn
  batched through a wrapper that no longer exists
why: The sweep was done by reading, so it missed 39 files — the README
inventory, the contributor guide, thirty tool pages saying "Readonly."
under Side effects. A word this easy to reintroduce by habit needs a
gate, not a careful reviewer.

what:
- Add a test asserting no source or page names the retired tiers, with
  the four MCP hint fields excluded because those are the protocol's
  vocabulary and stay
- Exempt only what must name what it replaced: `CHANGES`, `MIGRATION`,
  the startup error refusing `LIBTMUX_SAFETY`, the test proving it
  fires, and the docs redirect
- Fix everything it found: README, AGENTS, the per-tool side-effect
  lines, the batch index, the glossary term, the badge demo, and the
  test names and docstrings that still described a ladder

The gate found more than the sweep did, which is the argument for it.
why: Two a17 entry contracts pinned the label the trust page replaced.

what:
- Assert the reference those entries now carry
why: A rename has to reach every instance, and an operator upgrading
needs the old-to-new map in one place rather than assembled from a
changelog entry.

what:
- Add `MIGRATION.md`: the three environment variables, the tool names,
  which toolset each tool is in, and a surface the ladder could not
  express
- Say why `set_option` and `set_environment` are in `execute`: tmux
  runs some stored values later
- Say what did not change, so nobody withdraws the MCP hints too
why: gp-sphinx no longer ships a default vocabulary, so a project that
declares none renders every tool without a toolset badge. That is the
right default there — a docs tool should not badge a project's tools
with words it never used — and it makes declaring one this project's
job.

The pin also moves to an exact rev. A branch ref resolves to whatever
that branch pointed at when the cache was warmed, which is not a thing
a lockfile should depend on.

what:
- Declare `fastmcp_toolsets` in `docs/conf.py`, in precedence order,
  with the tooltip and icon each badge carries
- Retitle the hand-written tool groups from Inspect/Act/Destroy to the
  four toolsets, splitting Act into `manage` and `execute`
- Pin `sphinx-autodoc-fastmcp` to a rev rather than a branch
why: The badges rendered transparent with inherited link-blue. The
stylesheet keyed its colours on the old tag names, so nothing matched
once the tags were renamed. gp-sphinx now ships tones and a project maps
its tags onto them.

what:
- Declare a tone per toolset: inspect green, manage blue, execute
  amber, teardown red
- Move the gp-sphinx pin to the commit carrying tones
why: MCP annotations describe the whole call. A target command
alias can replace argv, and an after-hook can extend it. Direct
operation hints promised more than an inherited tmux server can
guarantee.

what:
- Give each tmux-requesting tool conservative whole-call annotations
- Preserve direct semantics in its toolset classification
- Prove list_panes can activate a command alias and an after-hook
- Pin tmux wait-for signals as a consuming toggle
why: A tmux alias or hook may act before the command reports an error.
Retrying an inspect call could repeat that ambient effect.

what:
- Remove InspectRetryMiddleware and its retry policy
- Keep failed production-wire calls to one attempt
- Let the client or operator decide whether to retry
why: A process-wide prefix does not prove buffer ownership.
Shutdown cleanup could delete another instance's buffer and trigger
configured tmux behavior.

what:
- Remove automatic buffer deletion from lifespan shutdown
- Keep cache cleanup local to the MCP process
- Prove shutdown issues no tmux commands
why: Tool filtering must govern both discovery and invocation. Unknown
names and unclassified tools must fail closed instead of widening the
surface.

what:
- Validate named includes and exclusions at startup
- Make FastMCP visibility authoritative on the wire
- Keep middleware as a classified defense-in-depth check
- Retire misleading safety-tier wording
why: FastMCP generated list_prompts and get_prompt after visibility
classified the catalog. Enabled adapters then disappeared, and direct
calls returned Unknown tool.

what:
- Decorate generated prompt adapters after PromptsAsTools runs
- Classify them as inspect with pure, closed-world annotations
- Prove both adapters stay visible and callable on the production wire
why: tmux 3.6 can briefly report the relaunched shell before the child
crosses exec(2). An immediate pane_current_command assertion therefore
flaked while the behavior was correct.

what:
- Poll the refreshed pane for at most one second
- Require the requested sleep process after the transient shell exits
why: Tool filtering was described more strongly than the
architecture allows. tmux aliases and hooks can extend a call.
Status jobs run without one, and a socket selects an endpoint
without confining its processes.

what:
- Assign non-surprise, consent, configuration, and confinement ownership
- Document aliases, hooks, resources, status jobs, and server provenance
- Rebuild the catalog around the four direct-semantics toolsets
- State visibility, stdio, prompt, buffer, retry, and channel contracts
- Pin catalog and topic claims with derived documentation tests
why: The earlier gate matched ordinary words and skipped
contributor guidance, skills, scripts, and project configuration.
Stale tier language could therefore survive a tree-wide rename.

what:
- Match retired identifiers, keys, headings, and exact taxonomy phrases
- Cover active guidance, skills, scripts, code, tests, and config
- Update remaining active guidance and fixtures to the toolset model
- Keep CHANGES and MIGRATION as the historical vocabulary homes
Render the classifications with their configured icon and tone. Keep
only these labels selectable so copied prose retains the names without
changing badge behavior elsewhere.
why: Pane capture latency could exhaust the regex deadline before matching began, contradicting the documented matching-only budget.

what:
- Carry the remaining aggregate budget only while matching captured lines
- Cover delayed capture and catastrophic backtracking behavior
why: tmux format variables include hyphenated option and user-option names on every supported tmux version.

what:
- Admit hyphens after the first variable-name character
- Cover built-in and user-option expansion while retaining modifier and job rejection
why: Enabling only call_read_tools_batch advertised the wrapper while FastMCP visibility hid every nested inspect tool. Explicit exclusions must remain authoritative.

what:
- Scope visibility and toolset enforcement to the validated nested read call
- Keep direct calls hidden and explicit exclusions dominant
- Cover the production profile with an isolated tmux namespace
why: Renaming the batch-tool page otherwise leaves existing public links at a deleted route after deployment.

what:
- Redirect the former call-readonly-tools-batch route to call-read-tools-batch
`fastmcp_toolsets` becomes a `capability` axis, keeping the four terms
with their tones, tooltips and icons unchanged.

Our tags already sit on one axis — the capability name implies the risk,
which is why teardown reads as more than a topic — so one axis says what
the vocabulary always meant. The rendered classes now name it:
`__axis-capability`, `__capability-inspect`.

`fastmcp_section_badge_map` keeps naming bare terms; with a single axis
declared, a bare term resolves against it.
The S3 sync replaces the whole site with `--delete`, but the
invalidation named only `/index.html`, `/objects.inv` and
`/searchindex.js`. Every other page kept serving CloudFront's cached
copy until its TTL expired, so a deploy landed in the bucket and stayed
invisible for the better part of an hour.

A wildcard covers the pages the sync actually replaced, and bills as one
path rather than one per page.
why: The toolset badge vocabulary is available in the published docs
packages, so the branch can use released artifacts.

what:
- Pin gp-sphinx, sphinx-autodoc-api-style, and
  sphinx-autodoc-fastmcp to 0.1.0a38
- Resolve all three packages from PyPI
why: use-local preserved LIBTMUX_SAFETY from a trunk registration, so
the branch rejected its own generated server entry. Dropping it
unconditionally could widen an old restricted surface.

what:
- Remove the retired key when LIBTMUX_TOOLSETS defines its replacement
- Reject swaps without a replacement and explicit attempts to add it
- Cover replacement and already-local paths
why: Users need one upgrade-time account of the new tool inventory,
stricter input contracts, and tmux trust boundary.

what:
- Document safety-tier migration and batch changes
- Record format, matching, annotation, retry, and cleanup behavior
- Explain trust documentation and mcp_swap migration
@tony tony changed the title Refactor scopes Replace safety tiers with capability toolsets Aug 30, 2026
@tony
tony merged commit fd4c6d7 into main Aug 30, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants