Toolsets that exist only for one kind of session now say so on their spec:
`platforms=frozenset({"desktop"})`. `TOOLSET_SESSION_PLATFORMS` is derived
from the specs that carry the key, `all` / `*` / no selection skip those
toolsets, and `toolsets.agent_tool_drops(agent)` removes their tools from any
agent whose own session platform is not listed. `agent_init._load_tools` and
`tools/mcp_tool_agent.py` (MCP refresh and prefix restore) call that one
helper. `tool_search._DIRECT_SURFACE_TOOLSETS` is derived from
`CLIENT_SURFACE_TOOLSETS` and `TOOLSET_SESSION_PLATFORMS`.
This replaces the profile-role gate: `profile_role_toolsets`, the role code in
`_with_session_toolsets` and the explicit-pin branch, the strip step in
`_select_tool_names`, and main's `setup` toolset (whose only tool moves out)
are deleted. `manage_catalog` moves into a new `catalog` toolset, which the
desktop client folds into every desktop chat like `project` and `desktop_ui`.
It is deferred behind tool_search by default (`config_defaults` defer list).
The shared-metrics schemas' toolset enum follows (`setup` out, `catalog` in).
The install card works outside the setup chat (#129650, with review fixes):
- Parallel installs: `_set_plugin_enabled` holds a per-home lock around the
read-then-admit section, so rows enabled at once no longer refuse each other.
- A catalog plugin already on disk and not enabled is enabled in place, so Try
again after a refused enable can finish.
- A failed catalog row counts as resolved (`contract.resolves`), so it no longer
holds the turn to the 300 s deadline. The snapshot carries `resolved`, and the
card reads it instead of a hand copy of the backend's rule.
- Each card draws its own operation by op id, because models reuse call ids.
- The row carries the approved non-secret choices (`approved`: force, enable,
ref) and the chosen profile, so Try again after the card settled repeats them.
- Install phases are ids (`InstallPhase`, wire `CatalogInstallPhase`) and the
installed-row notes are fields (`enabled`, `missing_env`, `server_errors`,
`already_installed`, `requires_hermes`); the card words them in English
strings (translations follow later). Failure reasons stay raw text, and
`detail` keeps the English notes for the model.
- Installs and search target the calling chat's own home, carried as a path;
the row's profile name is only a label, with no `'default'` fallback in
Python or TypeScript.
- `dashboard_install_plugin` is split into `_resolve_source`, `_place_tree` and
`_enable_placed`.
Desktop English strings for the card move to `en_catalog_install.ts` /
`types_catalog_install.ts`, which offsets the growth of `en.ts` and `types.ts`.
75 lines
3.2 KiB
Python
75 lines
3.2 KiB
Python
"""Connection contract: the states a target moves through, who may cause each move, and the
|
|
reasons an operation settles. ``operation.py`` enforces it; the desktop reads a generated copy.
|
|
|
|
A `str` enum so payloads serialise to the bare value and the TypeScript side sees literal unions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from enum import Enum
|
|
from typing import Dict, Optional, Tuple
|
|
|
|
|
|
class TargetState(str, Enum):
|
|
pending = "pending"
|
|
initiated = "initiated"
|
|
connected = "connected"
|
|
skipped = "skipped"
|
|
failed = "failed"
|
|
expired = "expired"
|
|
# Stamped by settle() on every unresolved target; never a transition target.
|
|
not_connected = "not_connected"
|
|
|
|
|
|
class Actor(str, Enum):
|
|
user = "user"
|
|
backend_watcher = "backend_watcher"
|
|
clock = "clock"
|
|
|
|
|
|
class SettleReason(str, Enum):
|
|
all_resolved = "all_resolved"
|
|
continue_ = "continue"
|
|
deadline = "deadline"
|
|
interrupt = "interrupt"
|
|
|
|
|
|
KINDS: Tuple[str, ...] = ("connector", "mcp", "plugin", "skill")
|
|
|
|
RESOLVED_STATES = frozenset({TargetState.connected, TargetState.skipped})
|
|
# A failed catalog row is done as well: the host already ran the install and the row carries the
|
|
# reason. Counting it as open held the model's turn until the deadline while nobody clicked. Try
|
|
# again still works while another row keeps the operation open, and the failed row keeps its state
|
|
# and reason when the operation settles.
|
|
_RESOLVED_BY_KIND = {kind: RESOLVED_STATES | {TargetState.failed} for kind in ("plugin", "skill")}
|
|
|
|
|
|
def resolves(kind: str, state: TargetState) -> bool:
|
|
return state in _RESOLVED_BY_KIND.get(kind, RESOLVED_STATES)
|
|
|
|
_S, _A = TargetState, Actor
|
|
|
|
# (kind, from) -> {to: the only actor allowed to cause it}. Every `connected` is witnessed by the
|
|
# backend: the gateway's account list for a managed target, the install / enable / OAuth worker for
|
|
# an MCP one. The card can only skip a target, or ask for a failed one to be run again.
|
|
TRANSITIONS: Dict[Tuple[str, TargetState], Dict[TargetState, Actor]] = {
|
|
("connector", _S.pending): {_S.initiated: _A.backend_watcher, _S.failed: _A.backend_watcher, _S.skipped: _A.user},
|
|
("connector", _S.initiated): {
|
|
_S.connected: _A.backend_watcher, _S.failed: _A.backend_watcher, _S.expired: _A.clock, _S.skipped: _A.user,
|
|
},
|
|
("connector", _S.failed): {_S.initiated: _A.user, _S.skipped: _A.user},
|
|
("connector", _S.expired): {_S.initiated: _A.user, _S.skipped: _A.user},
|
|
("mcp", _S.pending): {_S.initiated: _A.backend_watcher, _S.failed: _A.backend_watcher, _S.skipped: _A.user},
|
|
("mcp", _S.initiated): {_S.connected: _A.backend_watcher, _S.failed: _A.backend_watcher, _S.skipped: _A.user},
|
|
("mcp", _S.failed): {_S.initiated: _A.user, _S.skipped: _A.user},
|
|
}
|
|
# ``manage_catalog`` rows install like an MCP entry: approve starts the host's install, the install
|
|
# worker witnesses ``connected``, Try again re-runs a failed row.
|
|
for _kind in ("plugin", "skill"):
|
|
for (_k, _from), _edges in list(TRANSITIONS.items()):
|
|
if _k == "mcp":
|
|
TRANSITIONS[(_kind, _from)] = dict(_edges)
|
|
|
|
|
|
def allowed(kind: str, current: TargetState, to: TargetState) -> Optional[Actor]:
|
|
return TRANSITIONS.get((kind, current), {}).get(to)
|