From 5b1f938ae6c82c16e1992ba7ad6ff247552d9e39 Mon Sep 17 00:00:00 2001 From: "Maksym Hryzodub [DREAM]" Date: Tue, 22 Sep 2026 15:49:11 +0300 Subject: [PATCH 01/13] =?UTF-8?q?docs(spec):=20agent=20tool=20parity=20?= =?UTF-8?q?=E2=80=94=20spec,=20plan,=20contracts=20and=20tasks=20(CLEAN-10?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5.1 --- .specify/feature.json | 2 +- .../checklists/requirements.md | 35 +++ .../contracts/agent-tools.openapi.yaml | 83 +++++++ .../contracts/tool-metadata.md | 83 +++++++ .../016-agent-tool-parity/contracts/tools.md | 208 ++++++++++++++++++ specs/016-agent-tool-parity/data-model.md | 124 +++++++++++ specs/016-agent-tool-parity/plan.md | 169 ++++++++++++++ specs/016-agent-tool-parity/quickstart.md | 86 ++++++++ specs/016-agent-tool-parity/research.md | 118 ++++++++++ specs/016-agent-tool-parity/spec.md | 204 +++++++++++++++++ specs/016-agent-tool-parity/tasks.md | 181 +++++++++++++++ 11 files changed, 1292 insertions(+), 1 deletion(-) create mode 100644 specs/016-agent-tool-parity/checklists/requirements.md create mode 100644 specs/016-agent-tool-parity/contracts/agent-tools.openapi.yaml create mode 100644 specs/016-agent-tool-parity/contracts/tool-metadata.md create mode 100644 specs/016-agent-tool-parity/contracts/tools.md create mode 100644 specs/016-agent-tool-parity/data-model.md create mode 100644 specs/016-agent-tool-parity/plan.md create mode 100644 specs/016-agent-tool-parity/quickstart.md create mode 100644 specs/016-agent-tool-parity/research.md create mode 100644 specs/016-agent-tool-parity/spec.md create mode 100644 specs/016-agent-tool-parity/tasks.md diff --git a/.specify/feature.json b/.specify/feature.json index 1ce695e1..97590af0 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "specs/015-chat-message-reliability" + "feature_directory": "specs/016-agent-tool-parity" } diff --git a/specs/016-agent-tool-parity/checklists/requirements.md b/specs/016-agent-tool-parity/checklists/requirements.md new file mode 100644 index 00000000..6c0ad29a --- /dev/null +++ b/specs/016-agent-tool-parity/checklists/requirements.md @@ -0,0 +1,35 @@ +# Specification Quality Checklist: Agent tool parity + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-09-22 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) — the audit names console sections and actions, not endpoints or classes +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain — 3 asked and settled 2026-09-22 (see Decisions in spec.md) +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded (in/out of scope in Overview; surfaces in Assumptions) +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- The audit table is derived from the console's actual sections and the agent's actual tool list as of 2026-09-22; it is the acceptance checklist for FR-001 and SC-001. +- All items pass. Ready for `/speckit-plan`. diff --git a/specs/016-agent-tool-parity/contracts/agent-tools.openapi.yaml b/specs/016-agent-tool-parity/contracts/agent-tools.openapi.yaml new file mode 100644 index 00000000..3fb98cbd --- /dev/null +++ b/specs/016-agent-tool-parity/contracts/agent-tools.openapi.yaml @@ -0,0 +1,83 @@ +openapi: 3.1.0 +info: + title: Ranch API — agent tool catalogue (CLEAN-109) + version: 1.0.0 +paths: + /agents/{id}/tools: + get: + operationId: getAgentTools + summary: >- + The tools this agent's runtime would receive from the built-in Ranch MCP + server, grouped by topic, with per-tool "present in the running pod" + flags, plus the agent's external MCP servers as opaque groups. Owner or + Admin only; agent tokens get their list from MCP tools/list instead. + tags: [Agents] + security: [{ bearer: [] }] + parameters: + - name: id + in: path + required: true + schema: { type: string } + responses: + '200': + description: The catalogue. + content: + application/json: + schema: { $ref: '#/components/schemas/AgentToolCatalogDto' } + '403': { description: Caller is not Owner/Admin, or is an agent token. } + '404': { description: Agent not found. } +components: + securitySchemes: + bearer: { type: http, scheme: bearer, bearerFormat: JWT } + schemas: + AgentToolCatalogDto: + type: object + required: [agentId, podStartedAt, listedAt, groups] + properties: + agentId: { type: string } + podStartedAt: + type: [string, 'null'] + format: date-time + description: When the current pod started; null when no pod runs. + listedAt: + type: [string, 'null'] + format: date-time + description: When the pod last called tools/list; null if it never did. + groups: + type: array + items: { $ref: '#/components/schemas/AgentToolGroupDto' } + AgentToolGroupDto: + type: object + required: [key, title, kind, afterRestart, tools] + properties: + key: + type: string + description: Topic key (e.g. mcp_servers) or mcp: for an external server. + title: { type: string } + kind: { type: string, enum: [builtin, external] } + description: + type: string + description: External servers only — the server row's description. Never its url or auth value. + afterRestart: + type: boolean + description: >- + builtin — at least one tool has inPod=false; external — the server row changed + after the pod started (same rule as GET /agents/{id}/mcp-status). + tools: + type: array + items: { $ref: '#/components/schemas/AgentToolEntryDto' } + description: Empty for external groups; their tools are served by the server itself. + AgentToolEntryDto: + type: object + required: [name, title, description, template, destructive, inPod] + properties: + name: { type: string, description: Technical MCP tool name. } + title: { type: string } + description: + type: string + description: The per-caller description, exactly what the runtime would be given. + template: { type: string, description: Starter prompt with «…» placeholders. } + destructive: { type: boolean } + inPod: + type: [boolean, 'null'] + description: null — no pod; false — the running pod did not list it; true — it did. diff --git a/specs/016-agent-tool-parity/contracts/tool-metadata.md b/specs/016-agent-tool-parity/contracts/tool-metadata.md new file mode 100644 index 00000000..03715f59 --- /dev/null +++ b/specs/016-agent-tool-parity/contracts/tool-metadata.md @@ -0,0 +1,83 @@ +# Contract: tool metadata, topics, gating, confirmation + +This is the contract every tool in `api/src/slices/**/*.tool.ts` follows from CLEAN-109 on. The registry enforces the shape at startup; the rule in `docs/agent-tools.md` explains it for people. + +## `@Tool` options + +```ts +@Tool({ + name: 'register_mcp_server', // snake_case verb_noun; never renamed once shipped + topic: ToolTopics.McpServers, // required + title: 'Register an MCP server', // required, ≤ 60 chars, sentence case, no trailing period + description: 'Register an external MCP server …', // what the model reads; ends with the confirm sentence when destructive + template: 'Register the MCP server at «url» named «name» with «bearer|none» auth', // required, ≤ 200 chars + destructive: false, // optional; true ⇒ confirm param required + parameters: z.object({ … }), +}) +``` + +Template conventions: imperative English sentence, placeholders as `«…»` with a short noun inside (`«agent name»`, `«url»`), never an id the person would not know (say `«agent name»`, the tool resolves names to ids or the model calls `list_*` first). One template per tool. A template is a starter, not a form (spec Decision 4). + +## Topics (`api/src/slices/mcp/decorators/topics.ts`) + +| key | title | order | what belongs | +|---|---|---|---| +| `agents` | Agents | 10 | lifecycle, config, admin flag, status/metrics/env/logs, MCP list, capacity | +| `agent_workspace` | Agent workspace | 20 | files, secrets, channels, share link | +| `templates` | Templates | 30 | templates, their files, skills/MCP bindings, install/export | +| `skills` | Skills | 40 | skills, import, search, redeploy | +| `llm` | LLM credentials | 50 | credentials, health, models, usage per credential | +| `mcp_servers` | MCP servers | 60 | registry of servers, enable/disable, OAuth | +| `knowledge` | Knowledge | 70 | bases, indexing, graph, sources, imports, `query_knowledge` | +| `settings` | Settings | 80 | platform settings | +| `peers` | Peers (A2A) | 90 | operator peer set, self-service set, `ask_agent` | +| `paddock` | Paddock | 100 | scenarios, evaluations | +| `users_keys` | Users & API keys | 110 | users, roles, API keys | +| `chats_usage` | Chats & usage | 120 | chats, transcripts, summaries, usage overview, `agent_usage` | +| `browser` | Browser & integrations | 130 | browser sessions, integration accounts | +| `attachments` | Attachments | 140 | `query_attachment` | +| `platform` | Platform | 150 | upgrade, rancher setup status | + +Empty topics are not returned by the catalogue endpoint. + +## Gating (who sees and may call) + +Three audiences, expressed with the shared helpers in `api/src/slices/mcp/tooling.ts` (moved from `agent/peer/toolSupport.ts`, which re-exports them): + +| audience | listing | call guard | helper | +|---|---|---|---| +| Operator (admin agent; token carries `Owner`) | `isListedForRequest → callerIsOperator(req)` | `requireOperator(req)` throws `ForbiddenException('… requires the Ranch operator role. Ask the operator to do it in the console.')` | `requireOperator` | +| Agent self-service (any runtime; `sub = agent:`) | `callerAgentId(req) !== null` (+ feature flag where one exists, e.g. peers self-service setting) | `requireAgent(req)` returns the agent id or throws | `requireAgent` | +| Everyone (any authenticated caller) | always | none | — | + +A tool class declares one audience for all its methods (split classes when a slice needs two, as `peerAdmin.tool.ts` / `peerSelf.tool.ts` do). `RancherTool.requireOwner` is replaced by `requireOperator` so refusals read the same everywhere. + +## Confirmation (destructive tools) + +```ts +parameters: z.object({ + id: z.string(), + confirm: z.boolean().describe('Set true only after the person confirmed in the chat.'), +}) +… +const refusal = confirmed(args, `delete agent «${agent.name}» and its workspace`); +if (refusal) return refusal; // err('This will delete agent «x» and its workspace. Ask the person to confirm, then call again with confirm: true.') +``` + +`destructive: true` on: delete/remove/revoke anything, replace-all secrets, set user role, run upgrade, stop agent (it interrupts work), regenerate share link (invalidates the old one). + +## Secrets + +Parameters may carry secrets. Results never do: strip `apiKey`, `authValue`, `password`, `secret`, `token` fields in the tool before `ok(...)`. Exception: `create_api_key` returns the key once (research R6). The cross-cutting spec `api/src/slices/mcp/tool-secrets.spec.ts` enforces this by sentinel. + +## Results + +Every tool returns `ToolResult` via `ok(value)` / `err(text)`. Errors name the next move ("Nothing was saved. …", "Call list_agents to find the id."). `HttpException`s thrown by domain services are turned into `isError` text by the MCP handler; tools that can hit coded refusals wrap with `withRefusalAdvice`. + +## Registration + +The tool class is a provider of its slice's module (`providers: [XService, XTool]`); the module imports whatever modules export the gateways the tool needs. Nothing else: `McpRegistryService` discovers every `@Tool` across all modules. + +## Tests + +`*.tool.spec.ts` beside the tool file. Minimum per tool: listed/not listed by audience; happy path maps arguments and returns `ok`; not-found returns advice; destructive without `confirm` refuses without calling the gateway. diff --git a/specs/016-agent-tool-parity/contracts/tools.md b/specs/016-agent-tool-parity/contracts/tools.md new file mode 100644 index 00000000..b00814ca --- /dev/null +++ b/specs/016-agent-tool-parity/contracts/tools.md @@ -0,0 +1,208 @@ +# Contract: the tool inventory + +The complete list of tools after CLEAN-109. **Existing** tools keep their name and behaviour and only gain metadata. **New** tools are the audit's Gap column made concrete. "Backing" names the domain service/gateway the tool calls — the same one the console's controller uses (spec FR-008). Audience: **O** operator only, **A** agent self-service (any runtime), **E** everyone. ⚠ = destructive (requires `confirm`). + +Templates are indicative; the implementer may improve wording but keeps the placeholder convention. + +## agents — Agents + +| name | status | aud | title | template | backing | +|---|---|---|---|---|---| +| list_agents | existing | O | List agents | List all agents and their status | IAgentGateway | +| get_agent | existing | O | Show an agent | Show the agent «name» | IAgentGateway | +| create_agent | existing | O | Create an agent | Create an agent «name» from template «template» using the LLM credential «credential» | AgentDeployService | +| update_agent | existing | O | Update an agent | Change the agent «name»: «what to change» | IAgentGateway + AgentDeployService | +| set_agent_admin | existing | O | Make an agent the Ranch admin | Make «name» the Ranch admin agent | IAgentGateway/AgentDeployService | +| restart_agent | existing | O | Restart an agent | Restart the agent «name» | AgentDeployService | +| stop_agent | new | O ⚠ | Stop an agent | Stop the agent «name» | AgentDeployService (as `POST :id/stop`) | +| start_agent | new | O | Start an agent | Start the agent «name» | AgentDeployService (as `POST :id/start`) | +| delete_agent | new | O ⚠ | Delete an agent | Delete the agent «name» and its workspace | AgentDeployService + IAgentGateway (`DELETE :id`, `wipeS3` option) | +| get_agent_status | new | O | Show live status and metrics | How is the agent «name» doing right now? | AgentStatusService + IPodGateway (`:id/metrics`, `status`) | +| get_agent_env | new | O | Show the pod environment preview | Show the environment the agent «name» runs with | as `GET :id/env` (values of secrets masked as the console masks them) | +| get_agent_logs | new | O | Read pod logs | Show the last «100» log lines of the agent «name» | LogController's gateway (`GET agents/:id/logs`, tail param) | +| list_agent_mcps | new | O | Show the agent's MCP servers | Which MCP servers does the agent «name» have, and does it need a restart? | AgentMcpResolver + detectMcpConfigDrift (no authValue in result) | +| get_cluster_capacity | new | O | Show cluster capacity | How much capacity is left for new agents? | as `GET agents/capacity` | + +## agent_workspace — Agent workspace + +| name | status | aud | title | template | backing | +|---|---|---|---|---|---| +| list_agent_files | existing | O | List workspace files | List the files of the agent «name» | IFileGateway | +| read_agent_file | existing | O | Read a workspace file | Show «path» from the agent «name» | IFileGateway | +| write_agent_file | existing | O | Write a workspace file | Write «path» in the agent «name» with: «content» | IFileGateway | +| delete_agent_file | new | O ⚠ | Delete a workspace file | Delete «path» from the agent «name» | IFileGateway (`DELETE files/content`) | +| sync_agent_files | new | O | Sync files from the pod | Sync the workspace files of the agent «name» from its pod | SyncGuardService + IFileGateway (`POST files/sync`) | +| export_agent_files | new | O | Export the workspace | Export the workspace of the agent «name» | returns the console download path (`GET files/export`) | +| list_agent_secrets | new | O | List secret names | Which secrets does the agent «name» have? | ISecretGateway (names only) | +| set_agent_secret | new | O | Set a secret | Set the secret «KEY» of the agent «name» to «value» | ISecretGateway (`PUT`) | +| delete_agent_secret | new | O ⚠ | Delete a secret | Delete the secret «KEY» of the agent «name» | ISecretGateway (`DELETE`) | +| replace_agent_secrets | new | O ⚠ | Replace all secrets | Replace all secrets of the agent «name» with: «KEY=value, …» | ISecretGateway (`POST replace`) | +| get_agent_channels | new | O | Show delivery channels | Which channels is the agent «name» connected to? | IAgentChannelGateway | +| set_agent_channels | new | O | Set delivery channels | Connect the agent «name» to «channel list» | IAgentChannelGateway (`PUT :id/channels`) | +| get_share_link | new | O | Show the share link | Does the agent «name» have a public share link? | ShareLinkService | +| create_share_link | new | O | Create a share link | Create a public share link for the agent «name» | ShareLinkService | +| regenerate_share_link | new | O ⚠ | Regenerate the share link | Regenerate the share link of the agent «name» | ShareLinkService | +| revoke_share_link | new | O ⚠ | Revoke the share link | Revoke the share link of the agent «name» | ShareLinkService | + +## templates — Templates + +| name | status | aud | title | template | backing | +|---|---|---|---|---|---| +| list_templates | existing | O | List templates | List all agent templates | ITemplateGateway | +| get_template | existing | O | Show a template | Show the template «name» | ITemplateGateway | +| update_template | existing | O | Update a template | Change the template «name»: «what to change» | ITemplateGateway | +| set_template_skills | existing | O | Set template skills | Give the template «name» the skills «skill list» | ITemplateGateway | +| list_template_files | existing | O | List template files | List the files of the template «name» | ITemplateFileGateway | +| read_template_file | existing | O | Read a template file | Show «path» of the template «name» | ITemplateFileGateway | +| write_template_file | existing | O | Write a template file | Write «path» in the template «name» with: «content» | ITemplateFileGateway | +| create_template | new | O | Create a template | Create a template «name» with image «image» described as «description» | ITemplateGateway | +| delete_template | new | O ⚠ | Delete a template | Delete the template «name» | ITemplateGateway | +| set_template_mcps | new | O | Attach MCP servers to a template | Attach the MCP servers «server list» to the template «name» | ITemplateGateway (`PUT :id/mcps`) | +| restart_template_agents | new | O ⚠ | Restart every agent of a template | Restart all agents of the template «name» | AgentDeployService (`POST restart-by-template/:templateId`) | +| preview_template_install_from_git | new | O | Preview a git template | What would installing the template from «git url» at «ref» bring? | TemplateInstallService | +| install_template_from_git | new | O | Install a template from git | Install the template from «git url» at «ref» | TemplateInstallService | +| export_template | new | O | Export a template | Export the template «name» | TemplateExportService → download path | + +Zip install and binary file upload stay console-only (spec Assumptions); `install_template_from_git` covers the same source without a browser. + +## skills — Skills + +| name | status | aud | title | template | backing | +|---|---|---|---|---|---| +| list_skills | existing | O | List skills | List all skills | ISkillGateway | +| update_skill | existing | O | Update a skill | Change the skill «name»: «what to change» | ISkillGateway | +| list_skill_agents | existing | O | Which agents use a skill | Which agents use the skill «name»? | ISkillGateway | +| redeploy_skill_agents | existing | O ⚠ | Redeploy agents of a skill | Redeploy every agent that uses the skill «name» | AgentDeployService | +| get_skill | new | O | Show a skill | Show the skill «name» | ISkillGateway | +| create_skill | new | O | Create a skill | Create a skill «title» that «what it does» | ISkillGateway | +| delete_skill | new | O ⚠ | Delete a skill | Delete the skill «name» | ISkillGateway | +| import_skill_from_url | new | O | Import a skill from GitHub | Import the skill at «github url» | SkillController import-url path (GithubSearch + gateway) | +| search_skills | new | O | Search public skills | Find public skills about «topic» | GithubSearch | +| import_skill | new | O | Import a found skill | Import the skill «name» from «repo» | as `POST skills/import` | + +## llm — LLM credentials + +| name | status | aud | title | template | backing | +|---|---|---|---|---|---| +| list_llms | existing | O | List LLM credentials | List the LLM credentials | ILlmGateway (keys stripped) | +| get_llm | new | O | Show a credential | Show the LLM credential «name» | ILlmGateway (key stripped) | +| create_llm | new | O | Create a credential | Create an LLM credential «name» for «provider» with key «key» | ILlmGateway | +| update_llm | new | O | Update a credential | Change the LLM credential «name»: «what to change» | ILlmGateway | +| delete_llm | new | O ⚠ | Delete a credential | Delete the LLM credential «name» | ILlmGateway | +| health_check_llm | new | O | Health-check a credential | Check that the LLM credential «name» works | ILlmHealthGateway | +| list_llm_models | new | O | List known models | Which models can I use with «provider»? | as `GET llms/models` (llm slice providers catalogue) | +| llm_usage | new | O | Usage of a credential | How much did the credential «name» cost in the last 30 days? | UsageController path (`GET llms/:id/usage`) | + +## mcp_servers — MCP servers + +| name | status | aud | title | template | backing | +|---|---|---|---|---|---| +| list_mcp_servers | new | O | List MCP servers | List the MCP servers registered in this Ranch | IMcpServerGateway (authValue stripped) | +| get_mcp_server | new | O | Show an MCP server | Show the MCP server «name» | IMcpServerGateway | +| register_mcp_server | new | O | Register an MCP server | Register the MCP server at «url» named «name» with «bearer|none» auth | IMcpServerGateway | +| update_mcp_server | new | O | Update or enable/disable a server | Disable the MCP server «name» | IMcpServerGateway (built-in rows: enabled/description only, as the controller enforces) | +| delete_mcp_server | new | O ⚠ | Delete an MCP server | Delete the MCP server «name» | IMcpServerGateway (built-ins refused with the controller's message) | +| start_mcp_oauth | new | O | Start OAuth for a server | Connect the MCP server «name» with OAuth | McpOauthService → returns the URL the person must open | + +## knowledge — Knowledge + +| name | status | aud | title | template | backing | +|---|---|---|---|---|---| +| query_knowledge | existing | A/E | Ask the knowledge bases | What does our knowledge say about «question»? | KnowledgeService | +| list_knowledges | new | O | List knowledge bases | List the knowledge bases | KnowledgeService | +| get_knowledge | new | O | Show a knowledge base | Show the knowledge base «name» | KnowledgeService | +| create_knowledge | new | O | Create a knowledge base | Create a knowledge base «name» described as «description» | KnowledgeService | +| update_knowledge | new | O | Update a knowledge base | Change the knowledge base «name»: «what to change» | KnowledgeService | +| delete_knowledge | new | O ⚠ | Delete a knowledge base | Delete the knowledge base «name» | KnowledgeService | +| index_knowledge | new | O | Start indexing | Index the knowledge base «name» | KnowledgeService | +| get_knowledge_overview | new | O | Source counts and size | How big is the knowledge base «name» and what is in it? | KnowledgeService (`:id/overview`) | +| list_knowledge_graph_labels | new | O | Entity labels | Which entity labels does the knowledge base «name» have? | ILightragClient (`:id/graph/labels`) | +| get_knowledge_status | new | O | Knowledge service status | Is the knowledge service ready? | IKnowledgeConfigGateway + ILightragClient (`status`) | +| list_knowledge_sources | new | O | List sources | List the sources of the knowledge base «name» | SourceService | +| add_knowledge_source | new | O | Add a URL or text source | Add «url or text» to the knowledge base «name» | SourceService (`POST`, kind url/text) | +| add_knowledge_sources_from_sitemap | new | O | Add URLs from a sitemap | Add every page of «sitemap url» to the knowledge base «name» | SourceService (`from-sitemap`) | +| reindex_knowledge_source | new | O | Retry one source | Reindex the source «name» in the knowledge base «base» | SourceService | +| extract_knowledge_source | new | O | Re-extract a scanned PDF | Re-run text extraction for «source» in «base» | SourceService | +| delete_knowledge_source | new | O ⚠ | Delete a source | Delete the source «name» from the knowledge base «base» | SourceService | +| list_knowledge_imports | new | O | Imports in progress | Are any imports running for the knowledge base «name»? | SourceService (`imports`) | + +File and archive uploads from a person's machine stay console-only; `add_knowledge_source` covers text the agent can produce and URLs. + +## settings — Settings + +| name | status | aud | title | template | backing | +|---|---|---|---|---|---| +| list_settings | existing (+dynamic description) | O | List settings | Show the settings in «group» | ISettingGateway + SETTING_CATALOG | +| upsert_setting | existing (+dynamic description) | O | Set a setting | Set «group».«name» to «value» | ISettingGateway | +| get_setting | new | O | Show one setting | What is «group».«name» set to? | ISettingGateway | +| delete_setting | new | O ⚠ | Delete a setting | Reset «group».«name» to its default | ISettingGateway | + +## peers — Peers (A2A) + +All 15 existing tools (`list_agent_peers`, `list_peer_candidates`, `preview_agent_card`, `list_agent_delegations`, `connect_agent_peer`, `import_external_agent`, `refresh_agent_peer`, `remove_agent_peer` — O; `list_my_peers`, `list_ranch_agents`, `preview_agent_card_by_address`, `connect_my_peer`, `import_my_peer_by_address`, `remove_my_peer` — A; `ask_agent` — A) gain metadata only. `remove_agent_peer` and `remove_my_peer` become ⚠. + +## paddock — Paddock + +Existing 12 (`list_paddock_scenarios`, `get_paddock_scenario`, `list_agent_paddock_scenarios`, `create_paddock_scenario`, `update_paddock_scenario`, `delete_paddock_scenario` ⚠, `run_paddock_evaluation`, `list_paddock_evaluations`, `get_paddock_evaluation`, `get_paddock_evaluation_report`, `abort_paddock_evaluation` ⚠, `rerun_paddock_evaluation`) gain metadata. New: + +| name | aud | title | template | backing | +|---|---|---|---|---| +| generate_paddock_scenarios | O | Generate scenarios from a description | Generate «3» paddock scenarios for the agent «name» about «topic» | IPaddockScenarioGeneratorGateway | +| get_paddock_evaluation_logs | O | Evaluation logs | Show the logs of evaluation «id» | PaddockEvaluationService | +| get_paddock_evaluation_scenario_result | O | One scenario's result | How did scenario «scenario» go in evaluation «id»? | PaddockEvaluationService | +| get_paddock_evaluation_trace | O | Evaluation trace | Show the trace of evaluation «id» | PaddockEvaluationService | + +## users_keys — Users & API keys + +| name | aud | title | template | backing | +|---|---|---|---|---| +| list_users | O | List users | List the users of this Ranch | IUserGateway (no password hashes) | +| get_user | O | Show a user | Show the user «email» | IUserGateway | +| create_user | O | Create a user | Create a user «name» with email «email» and password «password» | IUserGateway | +| update_user | O | Update a user | Change the user «email»: «what to change» | IUserGateway | +| set_user_role | O ⚠ | Set a user's role | Make «email» an «admin|owner|user» | IUserGateway (`PUT :id/role`, Owner only as the controller) | +| delete_user | O ⚠ | Remove a user | Remove the user «email» | IUserGateway | +| list_api_keys | O | List API keys | List the API keys | IApiKeyGateway (prefix/metadata only) | +| create_api_key | O | Create an API key | Create an API key named «name» | ApiKeyService (returns the key once — research R6) | +| revoke_api_key | O ⚠ | Revoke an API key | Revoke the API key «name» | IApiKeyGateway | + +## chats_usage — Chats & usage + +| name | status | aud | title | template | backing | +|---|---|---|---|---|---| +| agent_usage | existing | O | Usage of an agent | How much did the agent «name» cost in the last «30» days? | IUsageGateway | +| list_chats | new | O | List chats | List the recent chats «of agent name» | IChatGateway | +| get_chat | new | O | Show a chat | Show the chat «id» | IChatGateway | +| get_chat_messages | new | O | Read chat messages | Show the last «20» messages of chat «id» | TranscriptReaderService | +| sync_chats | new | O | Sync chats from runtimes | Sync chats «of agent name» from their runtimes | ChatSyncService | +| summarize_chat | new | O | Summarize a chat | Summarize the chat «id» | ChatInsightService | +| export_chat | new | O | Export a chat | Export the chat «id» | download path (`GET chats/:id/export`) | +| get_usage_overview | new | O | Usage across all agents | How much did all agents cost in the last 30 days? | IUsageGateway (`usage/overview`) | + +## browser — Browser & integrations + +Existing 6 `browser_session_*` (A, userId must match caller) gain metadata; `browser_session_close` and `browser_session_reset` become ⚠. New (same userId convention as the browser tools): + +| name | aud | title | template | backing | +|---|---|---|---|---| +| list_integration_catalogue | A | Available integrations | Which integrations can I connect? | IntegrationService | +| list_integration_accounts | A | My integration accounts | Which integration accounts do I have? | IntegrationService (secrets stripped) | +| create_integration_account | A | Connect an integration account | Connect my «service» account «label» | IntegrationService | +| request_integration_login | A | Ask for a login | Ask me to log in to «service» | IntegrationService (`accounts/:id/login`) | +| delete_integration_account | A ⚠ | Disconnect an account | Disconnect my «service» account | IntegrationService | + +## attachments — Attachments + +`query_attachment` (existing, E) gains metadata: title "Ask a question about an attached file", template "In the attached file, «question»". + +## platform — Platform + +| name | aud | title | template | backing | +|---|---|---|---|---| +| get_upgrade_status | O | Ranch upgrade status | Is a Ranch upgrade available? | UpgradeService | +| run_upgrade | O ⚠ | Upgrade Ranch | Upgrade Ranch to the latest version | UpgradeService | +| get_rancher_status | O | Rancher setup status | Is the Rancher setup complete? | RancherService | + +## Totals + +Existing tools annotated: 59. New tools: 93. Destructive (⚠): 30. Topics: 15 (attachments and platform included). diff --git a/specs/016-agent-tool-parity/data-model.md b/specs/016-agent-tool-parity/data-model.md new file mode 100644 index 00000000..b6e152fe --- /dev/null +++ b/specs/016-agent-tool-parity/data-model.md @@ -0,0 +1,124 @@ +# Data model: Agent tool parity (CLEAN-109) + +## 1. ToolMetadata (in-process, from the `@Tool` decorator) + +```ts +interface ToolMetadata { + name: string; // technical name, snake_case, unchanged for existing tools + description: string; // what the model reads; may be replaced per caller by describeForRequest + parameters: z.ZodTypeAny; + topic: ToolTopic; // one of ToolTopics (see contracts/tool-metadata.md) + title: string; // human title for the panel row, e.g. "Register an MCP server" + template: string; // starter prompt with «…» placeholders, e.g. "Register the MCP server at «url» named «name»" + destructive?: boolean; // true ⇒ parameters must include confirm: z.boolean() +} +``` + +Validation (at API bootstrap, `McpRegistryService`): +- `topic` ∈ `ToolTopics`; `title` non-empty ≤ 60 chars; `template` non-empty ≤ 200 chars and contains at least one `«…»` unless the tool takes no parameters. +- `destructive === true` ⇒ the JSON schema of `parameters` has a `confirm` property of type boolean. +- `name` unique across the registry (already implied by `findTool`). + +## 2. AgentToolListing (PostgreSQL, new) + +```prisma +// api/src/slices/agent/toolCatalog/toolCatalog.prisma +import { Agent } from "../agent/agent" + +// The tool names the API last served to this agent's runtime on tools/list — +// i.e. what the running pod believes it has. Written on every tools/list from +// an agent token (once per pod boot in practice); read by GET /agents/:id/tools +// to mark tools the pod does not yet know about (CLEAN-109). +model AgentToolListing { + agentId String @id + agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade) + toolNames Json // string[] + listedAt DateTime @default(now()) +} +``` + +Migration: `api/prisma/migrations/20260922120000_agent_tool_listing/migration.sql` — additive `CREATE TABLE` + FK, safe on existing databases. `Agent` gets the back-relation `toolListing AgentToolListing?`. + +Lifecycle: upsert on each agent-token `tools/list`; deleted with the agent (cascade). Never read by the runtime. + +## 3. Agent tool catalogue (API response, `GET /agents/:id/tools`) + +```ts +interface AgentToolCatalog { + agentId: string; + podStartedAt: string | null; // ISO; null when no pod runs + listedAt: string | null; // ISO of the snapshot; null when the pod never listed + groups: AgentToolGroup[]; // ordered by ToolTopics order, then external servers +} + +interface AgentToolGroup { + key: string; // topic key, or `mcp:` for an external server + title: string; // topic title or server name + kind: 'builtin' | 'external'; + description?: string; // external servers: the row's description; builtin: none + afterRestart: boolean; // builtin: any tool with inPod === false; external: server drift from detectMcpConfigDrift + tools: AgentToolEntry[]; // external groups: [] (tools are provided by the server itself) +} + +interface AgentToolEntry { + name: string; + title: string; + description: string; // per-caller description (dynamic when the tool provides one) + template: string; + destructive: boolean; + inPod: boolean | null; // null: no pod; false: pod lacks it; true: pod listed it +} +``` + +Derivation: +- `tools` = `ToolCatalogService.listFor(principal)` with `principal = { sub: 'agent:', roles: agent.isAdmin ? [Owner] : [Agent] }`. +- `inPod` = `podStartedAt === null ? null : (snapshot ? snapshot.toolNames.includes(name) : false)`. +- External groups = `AgentMcpResolver.resolveForAgent(agent)` minus built-in ids (`mcp-ranch`, `mcp-knowledge`, `mcp-documents`, `mcp-cleanslice`); `authValue` and `url` are **not** returned. +- Response is not cached; one DB read (snapshot) + resolver + pod list. + +Authorization: `@Roles(Owner, Admin)` — the same audience as `GET /agents/:id/mcp-status`. Agent tokens are refused (they get their list from `tools/list`). + +## 4. Admin store (`admin/slices/agent/toolCatalog/stores/toolCatalog.ts`) + +```ts +interface IAgentToolCatalog { /* mirror of §3, mapped by ToolCatalogMapper */ } + +state: { + catalogs: Record; // entity, keyed by agentId + ui: Record; // per-agent sheet state, session only +} +getters: byAgent(agentId) → IAgentToolCatalog | undefined +actions: + fetch(agentId): Promise // gateway → upsert → return the stored record + upsert(catalog): IAgentToolCatalog + setQuery(agentId, q) / toggleGroup(agentId, key) / setExpanded(agentId, keys) +``` + +Rules: components render `computed(() => store.byAgent(agentId))`; `useAsyncData` supplies only `pending` / `error` / `refresh`. After a restart, the sheet calls `fetch(agentId)` again when the agent's status returns to running (watch on `agentStore.byId(agentId)?.status`). + +## 5. Composer insertion (pure util) + +```ts +insertTemplate(draft: string, cursor: number, template: string): + { text: string; selectionStart: number; selectionEnd: number } +``` +- Inserts `template` at `cursor`; prefixes a space when `draft.slice(0, cursor)` is non-empty and does not end with whitespace; suffixes a space when the remainder does not start with whitespace. +- `selectionStart/End` bound the first `«…»` in the inserted text, or the end of the insertion when there is none. +- `hasPlaceholder(text)`: `/«[^»]*»/.test(text)`. + +## 6. Setting catalogue (constant) + +```ts +interface ISettingDefinition { + group: string; name: string; valueType: 'string' | 'json'; + description: string; restartRequired: boolean; +} +export const SETTING_CATALOG: ISettingDefinition[] // compiled from admin settings pages +``` +Used by `list_settings`/`upsert_setting` dynamic descriptions and `get_setting` validation messages. + +## State transitions worth naming + +- **Tool present → after restart**: API deploys with a new tool; pod still lists the old snapshot ⇒ `inPod=false`, `afterRestart=true` for its group. Operator restarts ⇒ new pod calls `tools/list` ⇒ snapshot overwritten ⇒ `inPod=true`. +- **Agent stopped**: `podStartedAt=null` ⇒ every `inPod=null`; the sheet shows the list without restart markers and the composer stays disabled as today. +- **Tool removed from the caller's view** (e.g. self-service switched off): it disappears from the catalogue and `tools/call` refuses it with the existing "not available to this caller" text. diff --git a/specs/016-agent-tool-parity/plan.md b/specs/016-agent-tool-parity/plan.md new file mode 100644 index 00000000..6ac40391 --- /dev/null +++ b/specs/016-agent-tool-parity/plan.md @@ -0,0 +1,169 @@ +# Implementation Plan: Agent tool parity + +**Branch**: `feat/CLEAN-109-agent-tool-parity` | **Date**: 2026-09-22 | **Spec**: [spec.md](./spec.md) + +**Input**: Feature specification from `specs/016-agent-tool-parity/spec.md` (all clarifications settled; see its Decisions section) + +**Tracker**: [CLEAN-109](https://dreamvention.atlassian.net/browse/CLEAN-109) + +## Summary + +Restore the "chat is the hands" model: every admin-console capability gets an agent tool, the chat gets a **Tools** shelf that shows the live, per-agent tool list grouped by topic and drops a starter prompt into the composer, and the repo gets a standing rule that a module ships agent tools alongside tests. + +Technical approach, in one paragraph. Tools stay what they are today: `@Tool`-decorated methods on NestJS providers, discovered by `McpRegistryService` and served to runtimes from the API's own MCP endpoint (`/mcp/mcp`, `JwtAuthGuard`), filtered per caller through `IConditionallyListedTool` / `IDynamicallyDescribedTool`. Three additions make the feature: (1) the `@Tool` metadata grows `topic`, `title`, `template` and `destructive`, and the registry refuses to boot a tool without them, so the rule is enforced by the API, not by review alone; (2) ~90 new tools land in per-slice `*.tool.ts` files that call the same domain services the controllers call, gated with the existing `callerIsOperator` / `callerAgentId` helpers and a required `confirm: true` on anything destructive; (3) a new read surface, `GET /agents/:id/tools`, runs the same per-caller listing the runtime would get, groups it by topic, marks each tool "present in the running pod" from a per-agent snapshot recorded whenever a pod calls `tools/list`, and appends the agent's external MCP servers as opaque groups. The admin console gets a `toolCatalog` slice (gateway → store → sheet) and a Tools button in the bridle composer that inserts a template at the cursor and selects the first «…». + +## Technical Context + +**Language/Version**: TypeScript 5 on Bun (API: NestJS 11 + Prisma 6 + PostgreSQL; admin: Nuxt 3 / Vue 3 / Pinia / Tailwind / shadcn-vue on reka-ui 2) + +**Primary Dependencies**: `@modelcontextprotocol/sdk` (served by the in-repo `api/src/slices/mcp`), `zod` + `zod-to-json-schema` (tool parameters), `@hey-api/openapi-ts` (admin SDK from `api/swagger-spec.json`), `reka-ui` (accordion, sheet), `lucide-vue-next` (icons) + +**Storage**: PostgreSQL via Prisma; one new table `AgentToolListing` (per-agent snapshot of the last `tools/list` served). Schema lives per slice (`api/src/slices/**/*.prisma`, merged by `prisma-import` into `api/prisma/schema.prisma`); migrations under `api/prisma/migrations/_/migration.sql`. + +**Testing**: API — jest, run directly (`cd api && NODE_OPTIONS=--experimental-vm-modules npx jest `; **never** `bun run test`, it re-runs `prisma generate` and kills a running dev API on Windows). Admin — `bun test slices` for pure utils (`*.spec.ts` next to the util), `npx nuxt typecheck` for types (note: `bun run typecheck` regenerates the SDK; revert generated files after). + +**Target Platform**: Linux containers on k3s (API + admin), agent pods in the `agents` namespace reading tools once at boot. + +**Project Type**: Web application — `api/` (NestJS, CleanSlice slices) + `admin/` (Nuxt, CleanSlice slices). `app/` untouched. + +**Performance Goals**: Tools panel opens with data in < 500 ms on a warm API (one request, one DB read for the snapshot, one resolver call); `tools/list` for a runtime stays a single pass over the registry (snapshot write is fire-and-forget). + +**Constraints**: No agent runtime image change. Admin is English-only. Client state per `docs/state.md` (entity lives once in its Pinia store; components render by id). Secrets never leave the API through a tool result. Existing tool names unchanged (Decision 3). No commits without `CLEAN-109`. + +**Scale/Scope**: 59 existing tools gain metadata; ~90 new tools across 17 slices; 1 new API endpoint + 1 table; 1 new admin slice + 1 composer change; 1 canonical doc + rule in `CLAUDE.md` + graft pointers + PR template. + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +`.specify/memory/constitution.md` is the unfilled template, so there are no ratified principles to gate on. The project's standing rules act as the gate instead: + +| Gate (source) | Status | How the plan satisfies it | +|---|---|---| +| Jira first, branch from `origin/main`, `CLEAN-` in every commit (`CLAUDE.md`) | PASS | CLEAN-109 in progress; branch `feat/CLEAN-109-agent-tool-parity` | +| OpenAPI: never hand-write DTO types the generator emits (`CLAUDE.md`) | PASS | new endpoint → `bun run generate:swagger` → `bun run build:api` in admin; gateway maps DTO → domain type | +| Client state: one entity, one store, render by id (`docs/state.md`) | PASS | `toolCatalog` store keyed by agent id; sheet renders `store.byAgent(id)`; `useAsyncData` only for pending/error | +| Admin English-only, `app` i18n via `en.json` (`docs/i18n.md`) | PASS | admin only; no `app` strings | +| Tools reuse domain services, never re-implement console rules (spec FR-008) | PASS | every tool constructor injects the same gateway/service the controller injects (see contracts/tools.md, "backing" column) | +| Tests for every new tool (spec FR-007) | PASS | one `*.tool.spec.ts` per new tool file, three paths each; secret-leak scan test | +| No secret in tool output (spec FR-004) | PASS with one documented exception | `create_api_key` returns the key once, exactly as the console does (research R6) | + +Post-design re-check (after Phase 1): no new violations. The one exception above is recorded in research.md and in the tool's description. + +## Project Structure + +### Documentation (this feature) + +```text +specs/016-agent-tool-parity/ +├── plan.md # This file +├── spec.md # Feature spec with audit table and Decisions +├── research.md # Phase 0: decisions R1–R12 +├── data-model.md # Phase 1: ToolMetadata, AgentToolListing, catalog DTO, admin store +├── quickstart.md # Phase 1: how to prove it works end-to-end +├── contracts/ +│ ├── tool-metadata.md # @Tool options, topics, startup validation, confirm convention +│ ├── agent-tools.openapi.yaml # GET /agents/{id}/tools +│ └── tools.md # the full tool inventory: topic · name · title · template · backing service +├── checklists/requirements.md +└── tasks.md # Phase 2 (/speckit-tasks) — not created here +``` + +### Source Code (repository root) + +```text +api/src/slices/ +├── mcp/ +│ ├── decorators/tool.decorator.ts # + topic, title, template, destructive (ToolOptions) +│ ├── decorators/topics.ts # NEW: ToolTopics const + titles + order +│ ├── services/mcp-registry.service.ts # + validateToolMetadata() at bootstrap (FR-005) +│ ├── services/handlers/mcp-tools.handler.ts # listing logic extracted to ToolCatalogService; snapshot hook +│ ├── services/tool-catalog.service.ts # NEW: listFor(principal) — the per-caller list, reused by handler + endpoint +│ ├── tooling.ts # NEW: ok/err/requireOperator/requireAgent/confirmed helpers (peer/toolSupport re-exports) +│ └── interfaces/tool-listing-recorder.interface.ts # NEW: optional IToolListingRecorder token +├── agent/toolCatalog/ # NEW slice +│ ├── toolCatalog.prisma # AgentToolListing +│ ├── toolCatalog.controller.ts # GET /agents/:id/tools +│ ├── toolCatalog.module.ts +│ ├── domain/toolCatalog.service.ts # groups + inPod flags + external servers +│ ├── domain/toolCatalog.types.ts +│ ├── data/toolListing.gateway.ts # Prisma upsert/read of the snapshot (implements IToolListingRecorder) +│ └── dto/agentToolCatalog.dto.ts +├── agent/agent/agentAdmin.tool.ts # NEW: stop/start/delete/status/env/logs/mcps/capacity +├── agent/file/file.tool.ts # NEW: delete/sync/export +├── agent/secret/secret.tool.ts # NEW +├── agent/agentChannel/agentChannel.tool.ts # NEW +├── agent/shareLink/shareLink.tool.ts # NEW +├── agent/template/templateAdmin.tool.ts # NEW: create/delete/set_mcps/restart agents +├── agent/templateInstall/templateInstall.tool.ts # NEW: git preview/install, export path +├── skill/skill.tool.ts # NEW: get/create/delete/import url/search/import +├── llm/llm.tool.ts # NEW +├── mcpServer/mcpServer.tool.ts # NEW +├── reins/knowledge/knowledgeAdmin.tool.ts # NEW (operator set; query_knowledge stays) +├── reins/source/source.tool.ts # NEW +├── setting/setting.tool.ts # NEW: get/delete + SETTING_CATALOG; rancher list/upsert gain dynamic description +├── user/user/user.tool.ts # NEW +├── user/apiKey/apiKey.tool.ts # NEW +├── chat/chat.tool.ts # NEW +├── usage/usage.tool.ts # NEW: overview, per-llm +├── log/log.tool.ts # NEW: get_agent_logs (or inside agentAdmin.tool.ts — tasks decide) +├── upgrade/upgrade.tool.ts # NEW +├── integration/integration.tool.ts # NEW +├── paddock/scenario/scenario.tool.ts # + generate +├── paddock/evaluation/evaluation.tool.ts # + logs/scenario result/trace +├── rancher/rancher.tool.ts # metadata only (topics/titles/templates), no behaviour change +├── agent/peer/*.tool.ts, bridle/attachment.tool.ts, browser/browser.tool.ts, reins/knowledge/knowledge.tool.ts # metadata only +└── **/*.tool.spec.ts # one per new tool file + registry validation spec + catalog spec + +admin/slices/ +├── agent/toolCatalog/ # NEW slice +│ ├── nuxt.config.ts, index.d.ts +│ ├── domain/toolCatalog.types.ts, toolCatalog.gateway.ts, toolCatalog.service.ts +│ ├── data/toolCatalog.gateway.ts, toolCatalog.mapper.ts +│ ├── stores/toolCatalog.ts # byAgent(id), fetch(id) upserts, filter/expanded state per agent +│ ├── utils/insertTemplate.ts (+ .spec.ts), filterCatalog.ts (+ .spec.ts) +│ └── components/toolCatalog/Sheet.vue, Group.vue, Row.vue, Empty.vue +├── bridle/components/bridle/Input.vue # + Tools button, template insertion, placeholder selection +└── setup/theme/components/ui/accordion/ # NEW primitive (shadcn-vue on reka-ui) + +docs/agent-tools.md # NEW canonical rule + how-to +CLAUDE.md # + "Agent tools" section +.claude/skills/graft/SKILL.md, .cursor/rules/graft.mdc # + fenced project pointer block +.github/PULL_REQUEST_TEMPLATE.md # NEW with the parity check line +``` + +**Structure Decision**: Web application with the existing two CleanSlice projects. New tools live in the slice that owns the capability (this is also what the new rule prescribes), the shared MCP plumbing stays in `api/src/slices/mcp`, and the catalogue read surface is its own small slice under `agent/` because it joins agents, pods, the MCP resolver and the registry. The admin gets one new slice and a surgical change to the composer. + +## Phase 0 — Research + +Complete: see [research.md](./research.md). All Technical Context unknowns are resolved; no NEEDS CLARIFICATION remains. + +## Phase 1 — Design + +Complete: [data-model.md](./data-model.md), [contracts/](./contracts/), [quickstart.md](./quickstart.md). + +Design highlights the tasks phase must keep: + +1. **Metadata before tools.** The `ToolOptions` extension and registry validation land first; every existing tool file is annotated in the same commit so the API boots. Only then do new tool files start. +2. **One helper module for gating.** `api/src/slices/mcp/tooling.ts` provides `ok`, `err`, `requireOperator(req)`, `requireAgent(req)`, `confirmed(args, what)`; `agent/peer/toolSupport.ts` re-exports the shared ones so nothing there changes behaviour. +3. **Listing logic is shared, not duplicated.** `ToolCatalogService.listFor(principal)` in the mcp slice is the single implementation of "which tools does this caller see, with what description"; `McpToolsHandler` calls it for `tools/list`, the endpoint calls it with a synthetic principal built the same way `issueAgentServiceToken` builds an agent token payload (`sub: agent:`, roles `Owner` for the admin agent, `Agent` otherwise). +4. **Snapshot is a side effect of `tools/list`.** When the caller is an agent runtime, the handler upserts `AgentToolListing { agentId, toolNames, listedAt }` through the optional `IToolListingRecorder` (resolved with `strict: false`, so the mcp slice stays independent). Fire-and-forget; a failed write only logs. +5. **`inPod` semantics** (data-model.md): no pod → `null`; pod and no snapshot → `false` (a pod that predates this feature needs a restart to get anything new); pod and snapshot → `toolNames.includes(name)`. +6. **Admin composer owns insertion.** The Tools button and the sheet mount inside `Input.vue` so the sheet's `pick(template)` can write `input.value` and set the textarea selection on the first «…» without prop drilling. Restart reuses `agentStore.restart(id)` (optimistic patch + rollback already there). +7. **Rule lives in three places, one canonical.** `docs/agent-tools.md` is canonical; `CLAUDE.md` states the rule and links it; graft skill and Cursor rule carry a fenced `` pointer block re-applied by a tiny script if `graft init` overwrites them (research R11). + +## Phase 2 — Tasks (preview, not generated here) + +`/speckit-tasks` should produce roughly this order, each topic a checkpoint comment on CLEAN-109: + +1. Metadata + validation + shared helpers + annotate all 59 existing tools; API boots; registry spec. +2. `AgentToolListing` + recorder hook + `ToolCatalogService` + `GET /agents/:id/tools` + swagger regen + tests. +3. Admin: accordion primitive, `toolCatalog` slice, composer button + insertion + "after restart" + utils specs; typecheck. +4. Tools by topic, one commit each with specs: agents · workspace · templates · skills · LLM · MCP servers · knowledge · sources · settings · users & keys · chats & usage · browser & integrations · paddock · platform. +5. Secret-leak scan test across all tools; quickstart run-through. +6. Docs: `docs/agent-tools.md`, `CLAUDE.md`, graft pointers, PR template; README one-liner. +7. Final: full jest run, admin typecheck, PR into `main`, link on CLEAN-109, In Review. + +## Complexity Tracking + +No constitution violations to justify. Two deliberate choices that add surface, with the simpler alternative and why it was rejected, are recorded in research.md: the per-agent snapshot table (R4, vs. reusing MCP-server drift) and the synthetic-principal listing (R3, vs. a second static catalogue). diff --git a/specs/016-agent-tool-parity/quickstart.md b/specs/016-agent-tool-parity/quickstart.md new file mode 100644 index 00000000..906fddc3 --- /dev/null +++ b/specs/016-agent-tool-parity/quickstart.md @@ -0,0 +1,86 @@ +# Quickstart: proving CLEAN-109 works + +Validation guide for the implemented feature. Implementation details are in tasks.md; this file only says how to run and what to expect. + +## Prerequisites + +- Local stack running: `make dev` (api :3000, admin :3002) with a k3d cluster, or the CLI `ranch dev`. +- An Owner login for the admin console and a Rancher admin agent deployed (Rancher page shows the chat). +- `.env.project` keys are not needed for validation. + +## 1. The API refuses to boot a tool without metadata (FR-005) + +```bash +cd api +NODE_OPTIONS=--experimental-vm-modules npx jest src/slices/mcp/services/mcp-registry.service.spec.ts +``` +Expected: the "validation" cases pass — a provider whose `@Tool` lacks `topic`, `title` or `template`, or is `destructive` without a `confirm` parameter, makes `onApplicationBootstrap` throw with the tool name in the message. + +## 2. Every tool file has tests; nothing leaks a secret (FR-004, FR-007) + +```bash +cd api +NODE_OPTIONS=--experimental-vm-modules npx jest "\.tool\.spec\.ts$" src/slices/mcp/tool-secrets.spec.ts +``` +Expected: green. The secrets spec calls each secret-taking tool with a sentinel and asserts the sentinel is absent from the result, except `create_api_key`. + +Never run `bun run test` in `api/` while a dev API is up — it re-runs `prisma generate` and kills the running process. + +## 3. The catalogue endpoint returns the live per-agent list (FR-009) + +```bash +# token: an Owner bearer from the admin login; AGENT: the Rancher admin agent id +curl -s -H "Authorization: Bearer $TOKEN" http://localhost:3000/agents/$AGENT/tools | jq '.groups[] | {key, kind, afterRestart, n: (.tools|length)}' +``` +Expected: builtin groups in topic order (agents, agent_workspace, templates, skills, llm, mcp_servers, knowledge, settings, peers, paddock, users_keys, chats_usage, browser, attachments, platform), each with `n > 0`; external MCP servers from the agent's template as `kind: external` with `n: 0`; no `url` or `authValue` anywhere in the body. + +Repeat with a non-admin agent id: operator-only groups are absent (only `knowledge`, `peers` self-service, `browser`, `attachments` remain). + +## 4. "After restart" is true, then clears (FR-014, SC-007) + +1. With the Rancher agent running, note `podStartedAt` and `listedAt` from step 3. +2. Deploy an API build that adds a tool (or, for a quick check, temporarily rename one tool's `name`); restart only the API. +3. `GET /agents/$AGENT/tools` → the new tool has `inPod: false` and its group `afterRestart: true`. +4. Restart the agent from the console (or `restart_agent`). After the pod reconnects, step 3 shows `inPod: true`, `listedAt` newer than `podStartedAt`. + +## 5. The Tools panel (FR-010 … FR-016) + +Open the admin console → Rancher page. Beside the paperclip there is a wrench button with a tooltip "Tools". + +- Click it: a sheet opens, accordions in topic order, each row shows title, technical name in small type, description. Empty topics are absent. +- Type `mcp` in the search: only MCP-related rows remain across topics, their topics expanded. Clear: previous expansion restored. +- Click "Register an MCP server": the sheet closes, the composer contains the template, the first «…» is selected, nothing was sent. Edit the text and send. +- With a draft already typed, pick a tool: the template is appended with a space, the draft is intact. +- Stop the agent: the panel still opens (no restart markers), the composer is disabled as before. +- After step 4.3: the affected group shows "after restart" with a restart button; clicking it runs the normal restart flow and the marker clears after the pod is back. +- Narrow the window to phone width: the sheet is full-width and scrollable; Tab/Enter reach every row. + +Same checks on an agent's Chat tab (`/agents/?tab=chat`). + +## 6. Parity through the chat alone (SC-002) + +In the Rancher chat, one prompt each, without opening another page: + +1. "Register the MCP server at https://example.com/mcp named Example with no auth and attach it to the researcher template" → MCP list shows it, template has it, researcher agents show "pending restart". +2. "Add https://docs.example.com/sitemap.xml to the knowledge base Docs and index it" → sources appear, indexing starts. +3. "Create an LLM credential Test-Anthropic for anthropic with key sk-test… and health-check it" → credential listed, key not in the reply, health result reported. +4. "Create a user Jane with email jane@example.com and make her an admin" → user listed with role admin, after the agent asked for confirmation on the role change. +5. "Create an API key named ci-deploy" → key shown once; "revoke it" → gone after confirmation. +6. "Delete the agent test-bot" → the agent asks to confirm first; "yes" → deleted. + +Then, as a non-admin agent (any agent chat in the console): "List the users" → refused with a message naming the operator/console. + +## 7. Admin typecheck and util tests + +```bash +cd admin +bun test slices +npx nuxt typecheck # not `bun run typecheck` — it regenerates the SDK; revert generated files if it did +``` +Expected: `insertTemplate` and `filterCatalog` specs green; typecheck clean. + +## 8. The rule is where people look (FR-018 … FR-020) + +- `CLAUDE.md` has an "Agent tools" section linking `docs/agent-tools.md`. +- `.claude/skills/graft/SKILL.md` and `.cursor/rules/graft.mdc` contain the `ranch:agent-tools` block; run `graft init --yes --no-global` and then `node scripts/ensure-agent-tools-rule.mjs` → the block is back. +- `.github/PULL_REQUEST_TEMPLATE.md` has the parity checklist line. diff --git a/specs/016-agent-tool-parity/research.md b/specs/016-agent-tool-parity/research.md new file mode 100644 index 00000000..3a7912c2 --- /dev/null +++ b/specs/016-agent-tool-parity/research.md @@ -0,0 +1,118 @@ +# Research: Agent tool parity (CLEAN-109) + +Date: 2026-09-22. Every item below resolves a question the plan depended on. Facts were read from the code on this branch (graft + source), not assumed. + +## R1 — Where tool metadata lives and how to make it mandatory + +**Decision**: Extend `ToolOptions` / `ToolMetadata` in `api/src/slices/mcp/decorators/tool.decorator.ts` with `topic: ToolTopic`, `title: string`, `template: string`, `destructive?: boolean`. Add `validateToolMetadata()` to `McpRegistryService.onApplicationBootstrap` (after `discoverTools`) that throws `Error('Tool "" is missing ')` for any tool without `topic`, `title` or `template`, and for any `destructive` tool whose Zod parameters lack a required boolean `confirm`. A thrown error at bootstrap fails the API start, which is the enforcement FR-005 asks for. + +**Rationale**: The decorator is the single entry point for every tool (59 today, all in the API), and the registry already walks them at boot. Failing fast at startup is cheaper and more reliable than a lint rule or a review checklist. + +**Alternatives considered**: ESLint rule on `@Tool({` calls (misses dynamic construction, needs a plugin); a test that snapshots the registry (would pass locally and fail only when someone remembers to run it). + +## R2 — Topics: how many and which + +**Decision**: A `ToolTopics` const in `api/src/slices/mcp/decorators/topics.ts` with 14 topics, each with a stable key, a display title and a sort order (see contracts/tool-metadata.md). Topics mirror console sections; small sections merge as the spec allows: *Users & API keys*, *Chats & usage*, *Browser & integrations*. Agents split into *Agents* (lifecycle, config, status) and *Agent workspace* (files, secrets, channels, share link) so neither accordion exceeds ~15 rows. + +**Rationale**: The accordion is only useful when a topic fits on one screen. Fourteen topics × ≤ 16 tools is scannable; one "Agents" topic with 30 rows is not. + +**Alternatives considered**: derive topic from the tool name prefix (breaks for `agent_usage`, `browser_session_*`, `query_attachment`); one topic per slice (too many, and slices are an implementation detail the panel should not show). + +## R3 — How the console gets "the list this agent would see" + +**Decision**: Extract the per-tool resolution loop from `McpToolsHandler.registerHandlers` (resolve provider → `isListedForRequest` → `describeForRequest` → static fallback) into `ToolCatalogService.listFor(principal)` in the mcp slice, where `principal` is the `IAuthTokenPayload`-shaped object that `JwtAuthGuard` puts on `httpRequest.user`. The handler wraps the real request; the new endpoint builds a synthetic one: `{ sub: 'agent:', roles: agent.isAdmin ? [Owner] : [Agent], email: '' }` — the same shape `issueAgentServiceToken` in `user/auth/domain/auth.service.ts` mints for runtimes (verify the exact role assignment there when implementing; `toolSupport.ts` documents "admin agents hold Owner, plain agents hold Agent"). + +**Rationale**: The listing hooks take an `express.Request` today and read only `.user`; a synthetic request with the same `user` yields exactly what the pod would see, including dynamic descriptions (e.g. `query_knowledge` listing bound bases). One implementation, two callers, no drift. + +**Alternatives considered**: a static catalogue in the admin (rejected in the spec's Decision 2); calling the MCP endpoint from the API to itself with a minted token (needless HTTP hop, session-id dance). + +## R4 — Knowing whether the running pod already has a tool + +**Decision**: New Prisma model `AgentToolListing { agentId String @id, toolNames Json, listedAt DateTime }` in a new `agent/toolCatalog` slice. `McpToolsHandler` after computing the list calls an optional `IToolListingRecorder.record(agentId, names)` (token resolved with `moduleRef.get(TOKEN, { strict: false })`, no-op when absent) when `callerAgentId(httpRequest)` is set. The catalogue endpoint computes `inPod` per tool: no pod → `null`; pod but no snapshot → `false`; else `toolNames.includes(name)`. A restart makes the pod list again, which overwrites the snapshot and clears the flags. + +**Rationale**: Precise per-tool truth with one small table and one write per pod boot. The existing MCP-server drift (`detectMcpConfigDrift`, compares server row `updatedAt` to pod start) cannot see a change inside the built-in server's tool set, since tools are code, not rows. Persisting to the DB (not memory) survives API restarts, which are exactly when new tools appear. + +**Alternatives considered**: bump the built-in Ranch server row's `updatedAt` on boot when a hash of the tool catalogue changed, so the existing drift banner fires (rejected: whole-group marking only, and it would flag every agent after every API deploy even when nothing they can use changed); in-memory map (lost on API restart, the common case). + +## R5 — Destructive tools and confirmation + +**Decision**: Every tool that deletes, revokes, replaces wholesale, changes a role, or runs an upgrade declares `destructive: true` and a `confirm: z.boolean()` parameter described as "Set true only after the person confirmed in the chat." A shared `confirmed(args, what)` helper in `mcp/tooling.ts` returns `err('This will . Ask the person to confirm, then call again with confirm: true.')` when `confirm !== true`. The tool description ends with the same sentence, so the model reads the rule before calling. + +**Rationale**: The API cannot see the chat; the argument is the only server-side proof that the model went through a confirmation step. The registry validation (R1) guarantees no destructive tool forgets the parameter. + +**Alternatives considered**: a chat-level confirmation UI (out of scope per spec); description-only (no server-side check; a model that skips the text deletes). + +## R6 — Secrets in and out + +**Decision**: Tools that accept a secret (`create_llm`, `update_llm`, `register_mcp_server`, `update_mcp_server`, `set_agent_secret`, `replace_agent_secrets`, `create_user` password, `create_integration_account` secret) take it as a parameter and return metadata only; listings return names/ids/metadata with secret fields stripped in the tool (never rely on the gateway to omit them). One exception, mirroring the console: `create_api_key` returns the plaintext key once in its result, because the key exists nowhere else afterwards; its description says so and tells the model to hand it to the person verbatim and not repeat it. A cross-cutting spec (`api/src/slices/mcp/tool-secrets.spec.ts`) calls every secret-taking tool with a sentinel string and asserts it does not appear in the result. + +**Rationale**: FR-004 with the one place where the product's own behaviour requires returning a secret. + +**Alternatives considered**: making `create_api_key` console-only (breaks parity for a common ask); returning a masked key (useless to the person). + +## R7 — Where the settings tool learns the keys + +**Decision**: A `SETTING_CATALOG` constant in `api/src/slices/setting/domain/settingCatalog.ts`: `{ group, name, valueType, description, restartRequired }[]` compiled from the admin settings pages (`admin/slices/setting/pages/settings/*.vue` and `components/setting/nav/Menu.vue`: organization, agents, auth, github, bridle, knowledge, rancher, mcp, storage, secrets). `list_settings` and `upsert_setting` implement `IDynamicallyDescribedTool` and append the catalogue (group · name · meaning) to their description; `get_setting` / `delete_setting` are added. Unknown group/name on upsert is still allowed (the console allows it), but the tool result names the nearest catalogued key. + +**Rationale**: FR-006. The catalogue is the smallest thing that makes "change the organisation name" work without the model guessing `organization.name`. + +**Alternatives considered**: reading the keys from the admin at runtime (wrong direction of dependency); a DB table of setting definitions (over-engineering for ~25 keys). + +## R8 — Admin UI primitives + +**Decision**: Use the theme's `Sheet` (side panel, works at phone width, focus-trapped, Esc closes) for the panel; add an `accordion` primitive under `admin/slices/setup/theme/components/ui/accordion/` generated the shadcn-vue way on `reka-ui` (already a dependency: `reka-ui ^2.9.6`); `Input` for search; `Tooltip` on the button; `Skeleton` while loading. Icon: `Wrench` from lucide. + +**Rationale**: The theme has sheet, tooltip, input, scroll-area and skeleton but no accordion or popover; reka-ui ships an accessible Accordion, so adding the shadcn wrapper is a 60-line file, not a dependency. + +**Alternatives considered**: `
` elements (no keyboard-consistent behaviour, no animation, inconsistent styling); dropdown-menu (not scrollable, no search). + +## R9 — Inserting the template at the cursor + +**Decision**: The Tools button and sheet live inside `admin/slices/bridle/components/bridle/Input.vue`, which already owns `input` (the draft) and `textareaRef`. `pick(template)`: read the native textarea (`textareaRef.value?.$el`), splice the template at `selectionStart` (prefix with a space if the draft is non-empty and does not end with whitespace), set `input.value`, then on `nextTick` focus and `setSelectionRange` on the first `«…»`. A pure `insertTemplate(draft, cursor, template)` util returns `{ text, selectionStart, selectionEnd }` and is unit-tested with `bun test`. Remaining placeholders are highlighted by a small `hasPlaceholder` computed that adds an outline class to the textarea (spec edge case). + +**Rationale**: No prop drilling through `Provider.vue`; the composer is the only component that should touch the draft. + +**Alternatives considered**: emitting `insert` from Provider down to Input (adds an event hop and a second owner of the draft); a contenteditable composer (out of scope). + +## R10 — Admin state and data flow + +**Decision**: New slice `admin/slices/agent/toolCatalog/` with the standard CleanSlice layout: `data/` gateway over the generated SDK (`AgentsService.getAgentTools` after regen) + mapper to `IAgentToolCatalog`; `domain/` types + gateway interface + service; `stores/toolCatalog.ts` holding `catalogs: Record`, `byAgent(id)`, `fetch(id)` (upserts), and UI state per agent (`query`, `expanded: string[]`) so reopening the sheet restores the accordion; `components/toolCatalog/Sheet.vue` renders `store.byAgent(agentId)` and uses `useAsyncData` for `pending`/`error`/`refresh` only. Restart goes through `useAgentStore().restart(id)`. + +**Rationale**: `docs/state.md` verbatim. The catalogue is an entity keyed by agent; two open chats must not share it (the bridle store learned this in CLEAN-102). + +**Alternatives considered**: fetching inside the sheet with `useFetch` and rendering `data` (forbidden by the state rules). + +## R11 — Where the rule lives so graft does not erase it + +**Decision**: `docs/agent-tools.md` is canonical (rule, definition of done, file layout, metadata, gating, confirm, secrets, tests, reference module = `agent/peer`). `CLAUDE.md` gets an "Agent tools" section (five lines + link). `.claude/skills/graft/SKILL.md` and `.cursor/rules/graft.mdc` get a fenced block: + +``` + +Project rule: a module is not done until the agent has tools for what it does — see docs/agent-tools.md. + +``` + +plus `scripts/ensure-agent-tools-rule.mjs` that re-inserts the block if missing (idempotent; run by `make init` / documented in the doc). `.github/PULL_REQUEST_TEMPLATE.md` is created with the checklist line "console capability added → agent tool added, with tests" (none exists today). + +**Rationale**: `graft init` rewrote the skill file on this very branch; a pointer that can be re-applied mechanically survives that. Keeping the rule short in graft files and long in `docs/` matches how the repo already treats `docs/state.md` and `docs/i18n.md`. + +**Alternatives considered**: editing only `CLAUDE.md` (the ticket asks for graft explicitly); a `graft` custom-section feature (none in `graft --help`). + +## R12 — Testing approach + +**Decision**: Follow `agent/peer/peerAdmin.tool.spec.ts`: construct the tool with `jest.Mocked` gateways, build requests with `{ user: { sub, roles } }`, assert (a) listed for operator / not for plain agent, (b) happy path calls the gateway with the mapped arguments and returns `ok(...)`, (c) not-found and refusal come back as `isError` text that names the next move, (d) destructive tools refuse without `confirm`. Registry validation gets its own spec with a fake provider missing each field. The catalogue service gets a spec for the `inPod` matrix and external-server grouping. Admin utils get `bun test` specs. Run jest directly, never `bun run test`. + +**Rationale**: The peer tools are the model the spec names; their tests are the most complete in the repo. + +**Alternatives considered**: e2e through the MCP endpoint (slower, and the handler already has its own spec). + +## Facts gathered (for tasks) + +- MCP endpoint: `McpModule.forRoot({ mcpEndpoint: 'mcp/mcp', guards: [JwtAuthGuard], streamableHttp: { statelessMode: false } })` in `api/src/app.module.ts`. +- Caller helpers: `callerAgentId`, `callerIsOperator`, `ok`, `err`, `withRefusalAdvice` in `api/src/slices/agent/peer/toolSupport.ts`; `RancherTool.requireOwner` duplicates the operator check with a different message. +- Built-in MCP servers and ids: `mcp-ranch`, `mcp-knowledge`, `mcp-documents`, `mcp-cleanslice` in `api/src/slices/mcpServer/domain/mcpServer.seeder.ts`; `AgentMcpResolver.resolveForAgent` returns template servers + built-ins; `GET /agents/:id/mcps` and `GET /agents/:id/mcp-status` already exist. +- Pod start time comes from `IPodGateway.list()` (`startedAt`), as used in `getMcpStatus`. +- Controllers' injected services (the "backing" column in contracts/tools.md) were read from each controller's constructor. +- Admin restart: `admin/slices/agent/agent/stores/agent.ts` → `restart(id)`; pending-restart state lives in the same store (`isPendingRestart`, `markPendingRestart`). +- Admin has no unit tests today (`bun test slices` finds none); `*.spec.ts` next to a util is the convention to start. +- Prisma: per-slice `*.prisma` merged into `api/prisma/schema.prisma`; migrations are hand-written SQL under `api/prisma/migrations/_/`. diff --git a/specs/016-agent-tool-parity/spec.md b/specs/016-agent-tool-parity/spec.md new file mode 100644 index 00000000..23521404 --- /dev/null +++ b/specs/016-agent-tool-parity/spec.md @@ -0,0 +1,204 @@ +# Feature Specification: Agent tool parity — the agent can do everything the console can + +**Feature Branch**: `feat/CLEAN-109-agent-tool-parity` + +**Created**: 2026-09-22 + +**Status**: Draft — clarifications settled 2026-09-22; ready for `/speckit-plan` + +**Tracker**: [CLEAN-109](https://dreamvention.atlassian.net/browse/CLEAN-109) — `[ADMIN][API]`, labels `admin`, `api` + +**Input**: User description: "Я хочу обсудить и закрыть фундаментальную проблему философии нашего ранчера. Изначальная задумка была в полной автономности настройки платформы через чат с агентом, который имеет всевозможные тулзы и через чат делает всё что угодно в скоупе ранча — создаёт/редактирует/добавляет/управляет. Мы отошли от правильного паттерна: то, что настраивается через админку, не видно для агента, у которого нет нужных инструментов. Искоренить: (1) изучить текущий спектр возможностей админки и то, что покрывают тулзы, дописать недостающие; (2) кнопка Tools в чате — аккордеоны по топикам (например a2a), внутри название и описание каждого инструмента, клик вставляет лёгкий промт-шаблон в чат, максимальный UX; (3) правило в CLAUDE.md и в graft: любой новый модуль помимо тестов должен покрываться тулзами для агента." + +## Overview + +Ranch was designed around one idea: **the console is a window, the chat is the hands.** An operator should be able to open the Rancher chat, say "create a researcher agent on the Claude credential with the docs knowledge base, give it the A2A peer «billing» and restart it", and have it happen. Every screen in the admin console is, in that model, a convenience over something the agent could also do. + +That model has drifted. Feature by feature, the console gained screens whose actions have no counterpart in the agent's tool list. Today the console can manage **19 kinds of things**; the agent has tools for **7 of them in full, 5 in part, and 7 not at all** (the audit below has the detail). An operator who asks the Rancher agent to "register the GitHub MCP server" or "add this URL to the docs knowledge base" gets "I can't do that from here" — not because the platform can't, but because nobody handed the agent the tool. + +This feature closes the gap and makes sure it stays closed. Three parts: + +1. **Parity.** Every capability the admin console exposes is reachable by the agent through a tool, with the same permission model the console applies. The audit in this document is the checklist; the work is done when every row reads "covered". +2. **A visible tool shelf.** A person in the chat should not have to guess what the agent can do. A **Tools** button beside the composer opens the agent's tool list grouped by topic; each tool shows its name and a plain-language description, and one click drops a ready-to-edit prompt into the composer. What the agent can do becomes something you can see and try, not something you discover by asking. +3. **A standing rule.** The project's working instructions (for people and for coding agents) state that a module is not finished until the agent has tools for what it does, the same way it is not finished without tests. The rule names where tools live, how they are grouped and described, and what the reviewer checks. + +**In scope**: the audit and the tools that close it, on the API; the Tools panel in the admin console's two chat surfaces (the Rancher page and an agent's Chat tab); a read surface the console uses to learn an agent's current tool list; the rule in `CLAUDE.md` and the graft guidance; tests for every new tool. + +**Out of scope, deliberately**: any change to the agent runtime image (tools are served by the API, as today); a Tools panel in the user console (`app`) or the public share page; tools for things the console itself cannot do; changing the permission model (a tool never lets a caller do what the console would refuse that same person); natural-language "macros" that chain several tools. + +## The audit: what the console can do vs. what the agent can do + +Read this table as the definition of "parity". "Console" is what an operator can do from the admin UI today. "Agent today" is what the agent has a tool for. "Gap" is what this feature adds. Names in the Gap column are indicative; the plan decides exact names, but every action listed must exist. + +| # | Area (console section) | Console can | Agent today | Gap to close | +|---|---|---|---|---| +| 1 | **Agents** | list, view, create, edit (name, credential, knowledge, resources), promote/demote admin, restart, stop, start, delete, view live status and metrics, view pod env preview, view pod logs, view its MCP servers and "pending restart" state, capacity | list, get, create, update, set admin, restart, usage | stop, start, delete, status/metrics, env preview, logs (tail), MCP list + drift state, capacity | +| 2 | **Agent → Secrets** | list keys, set, delete, replace all | — | list, set, delete, replace | +| 3 | **Agent → Channels** | view and set delivery channels | — | get, set | +| 4 | **Agent → Share link** | view, create, regenerate, revoke | — | get, create, regenerate, revoke | +| 5 | **Agent → Workspace files** | list, read, write, delete, sync from pod, export | list, read, write | delete, sync, export | +| 6 | **Agent → A2A peers** | card, peers, candidates, preview, connect, import external, refresh, remove, delegations | all of it (operator set + agent self-service set + `ask_agent`) | none — this is the model to copy | +| 7 | **Agent → Paddock** | scenarios per agent, run evaluation, report | covered (see 15, 16) | none | +| 8 | **Templates** | list, view, create, edit, delete, set skills, **set MCP servers**, files list/read/write/upload, install from zip, install from git, export | list, get, update, set skills, files list/read/write | create, delete, set MCP servers, upload file, install (zip/git, with preview), export | +| 9 | **Skills** | list, view, create, edit, delete, import from URL, search curated repos and import, which agents use a skill, redeploy them | list, update, agents-of-skill, redeploy | get, create, delete, import from URL, search + import | +| 10 | **LLM credentials** | list, view, create, edit, delete, health-check, models catalogue, usage per credential | list | get, create, update, delete, health-check, models, usage | +| 11 | **MCP servers** | list, view, register, edit / enable / disable, delete, start OAuth | — | list, get, register, update, enable/disable, delete (OAuth start: expose the URL the person must open) | +| 12 | **Knowledge bases** | list, view, create, edit, delete, index, overview, graph + labels, ask, service status | ask (`query_knowledge`, only bases bound to the caller) | list, get, create, update, delete, index, overview, graph labels, status | +| 13 | **Knowledge → Sources** | list, add file/url/text, add many files, from sitemap, from archive, reindex one, re-extract, delete, imports in progress, export | — | list, add url/text, from sitemap, reindex, re-extract, delete, imports (file/archive upload: accept a path the agent can already read, see Assumptions) | +| 14 | **Settings** (organization, agent defaults, authentication, GitHub, Bridle, Knowledge, Rancher, MCP, Storage, Secrets) | list group, view one, set, delete | list, upsert | get one, delete; and the tool must *tell* the agent which groups and keys exist (today it has to guess) | +| 15 | **Paddock → Scenarios** | list, view, create, **generate from description**, edit, delete | list, get, per-agent, create, update, delete | generate | +| 16 | **Paddock → Evaluations** | run, list, view, report, logs, per-scenario result, trace, abort, rerun | run, list, get, report, abort, rerun | logs, per-scenario result, trace | +| 17 | **Users** | list, view, create, edit, set role, delete | — | list, get, create, update, set role, delete | +| 18 | **API keys** | list, create, revoke | — | list, create, revoke | +| 19 | **Chats** | list, view, messages, sync from runtime, summarize, feedback, export | — | list, get, messages, sync, summarize, export | +| 20 | **Usage** | per agent, per credential, overview | per agent | overview, per credential | +| 21 | **Browser sessions / Integrations** | sessions: list, open, reset, status, VNC URL, close; integrations: catalogue, accounts, login, import cookies, secret, delete | sessions: all | integrations: catalogue, accounts list/create/login/delete | +| 22 | **Ranch upgrade** | status, run upgrade | — | status, run | +| 23 | **Chat attachments** | — (chat only) | `query_attachment` | none | +| 24 | **Setup wizard, login, sessions page** | one-time / per-person | — | none — not agent work | + +Two structural facts the plan must respect, because the audit found them and the panel depends on them: + +- **Every built-in tool is served from one place** (the Ranch MCP server the API hosts) and the list a caller sees is filtered per caller: some tools are only listed for operator-role callers, some only for agent runtimes, one only when the agent has knowledge bound. The Tools panel must show the list *this* agent sees, not the union. +- **A pod reads its tool list once at boot.** A tool added after the agent started is not in its hands until restart. The console already knows this for MCP servers ("pending restart"); the Tools panel must say it too, or people will click a tool the agent cannot yet call. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - The Rancher agent can do what the console can (Priority: P1) + +An operator opens the Rancher chat and asks for things they would otherwise click through: "register the MCP server at https://… with bearer auth and attach it to the researcher template", "add https://docs.example.com/sitemap.xml as sources of the docs knowledge base and index it", "create an API key named ci-deploy", "delete the test-bot agent". The agent does it, reports what it did, and the console shows the result on the next load. Where the console would ask for confirmation (deletes, revokes, role changes), the agent asks in the chat before acting. + +**Why this priority**: This is the philosophy the ticket restores. Without parity, the panel in Story 2 would only advertise how little the agent can do. + +**Independent Test**: For every row of the audit with a non-empty Gap, ask the Rancher agent to perform each listed action on a throwaway entity and verify in the console that it happened, then undo it through the agent. Ask a non-admin agent for an operator-only action and confirm it is refused with a message that names the console as the place to do it. + +**Acceptance Scenarios**: + +1. **Given** the audit table, **When** the feature ships, **Then** every action in the Gap column has a tool, and the agent's tool list for an operator-role caller includes each of them. +2. **Given** an operator asks the Rancher agent to register an MCP server and attach it to a template, **When** the agent finishes, **Then** the server is in the console's MCP list, attached to that template, and agents of that template show "pending restart". +3. **Given** an operator asks to delete an agent, revoke an API key, remove a user or change a role, **When** the agent has the tool, **Then** it states what will be removed and waits for a "yes" in the chat before calling the tool; the tool description carries that instruction so it holds for any runtime. +4. **Given** a caller without the operator role (a regular agent runtime), **When** it lists tools, **Then** operator-only tools are absent, and a direct call to one is refused with a message that explains who may do it. +5. **Given** an action the console performs with a secret (an LLM key, an integration secret, a bearer for an MCP server), **When** the agent performs it, **Then** the secret travels in the tool call and is never echoed back in the tool's result, the agent's reply, or the transcript. +6. **Given** every new tool, **When** the test suite runs, **Then** each tool has tests for the happy path, the refused-caller path, and the not-found path, in the style of the existing peer and knowledge tool tests. + +--- + +### User Story 2 - See what the agent can do, and try it in one click (Priority: P1) + +Beside the chat composer there is a **Tools** button. It opens a panel listing the agent's tools grouped by topic — *Agents, Templates, Skills, LLM credentials, MCP servers, Knowledge, Settings, Peers (A2A), Paddock, Users & keys, Chats & usage, Browser, Attachments* — as collapsible accordions. Expanding a topic shows each tool as a row: a short human title, the tool's technical name in smaller type, and a one-line description that says what it does in plain words. A search box filters across all topics as you type. Clicking a tool row inserts a **prompt template** into the composer — a short, natural sentence with obvious placeholders ("Restart the agent «…»", "Add the URL … to the knowledge base «…» and index it") — puts the cursor on the first placeholder, and closes the panel; the person edits and sends. Tools the running agent does not yet have (added since it booted) are shown greyed with "after restart" and a restart shortcut, so nobody sends a prompt the agent cannot act on. + +**Why this priority**: The ticket asks for "maximum UX": the value of parity is invisible if a person has to guess the vocabulary. The panel turns the tool list into a menu. + +**Independent Test**: Open the Rancher chat, press Tools, expand *MCP servers*, click "Register an MCP server"; confirm the composer holds the template with the cursor on the first placeholder and the panel is closed. Edit the template freely and send; confirm nothing constrained the edit. Type "knowledge" in the panel's search and confirm only knowledge tools remain. Attach a new MCP server to the agent's template without restarting, reopen Tools and confirm that group is marked "after restart" with a restart button that works. + +**Acceptance Scenarios**: + +1. **Given** the Rancher chat or an agent's Chat tab, **When** the person presses Tools, **Then** a panel opens listing exactly the tools that agent's runtime would receive (per-caller filtering applied), grouped by topic, each with title, technical name and description; topics with no tools are not shown. +2. **Given** the panel is open, **When** the person clicks a tool, **Then** its prompt template is inserted at the composer's cursor (appended with a space if the composer already has text), the first placeholder is selected, the panel closes, and nothing is sent. +3. **Given** the panel is open, **When** the person types in the search box, **Then** rows are filtered by title, technical name and description across all topics, with matching topics expanded; clearing the box restores the accordion state. +4. **Given** a tool was added after the agent's pod started, **When** the panel is opened, **Then** the tool (or its whole topic) is visibly marked "after restart", its click still inserts the template but the row explains the agent will not act until restarted, and a restart control is offered that reuses the existing restart flow. +5. **Given** the agent's tool list cannot be loaded (agent stopped, API error), **When** the panel is opened, **Then** it says so in one line and offers retry; the composer keeps working. +6. **Given** the panel is open on a narrow window, **When** it renders, **Then** it is usable at phone width (a sheet or drawer, not a hover popover), and keyboard users can open it, move between rows and pick one without a mouse. +7. **Given** the person has never used the panel, **When** they open the chat for the first time, **Then** the Tools button is discoverable next to the attach button, with a tooltip, and needs no onboarding. + +--- + +### User Story 3 - The rule that keeps parity (Priority: P2) + +A developer (or a coding agent) adds a new capability to the console. The project instructions tell them, in the same place that tells them to write tests and to create a Jira issue, that **the module is not done until the agent has tools for what it does**: where tool files live, that each tool declares a topic and a prompt template, how permissions are expressed, that every tool ships with tests, and that the PR reviewer checks it. The graft guidance carries the same rule, so a coding agent that starts from graft reads it before it reads the code. + +**Why this priority**: Without this, the audit will be out of date in a month. It is P2 only because it depends on the vocabulary Story 1 and 2 establish (topics, templates, gating). + +**Independent Test**: Read `CLAUDE.md` and the graft guidance as a newcomer: both state the rule, name the tool location and the required metadata, and point at one existing module as the reference example. Start a fresh coding-agent session in the repo and ask "I added a new console feature, what else must I ship?" — the answer names agent tools and tests without further prompting. + +**Acceptance Scenarios**: + +1. **Given** `CLAUDE.md`, **When** read, **Then** it has a short section stating the parity rule, the definition of done for a module (tests + tools + topic + template + permission check), the reference module, and the one-line check for reviewers. +2. **Given** the graft guidance the coding agents load, **When** read, **Then** it carries the same rule (or a pointer to the canonical doc), and it survives a `graft init` / `graft build` (see Assumptions for how). +3. **Given** a new module is added without tools, **When** a reviewer follows the checklist, **Then** the omission is caught before merge. + +--- + +### Edge Cases + +- **A tool the agent has but the console does not show** (e.g. `query_attachment`): the panel still lists it; parity is one-directional (console ⊆ agent), never a reason to hide a tool. +- **Two chats open for two agents**: each panel shows its own agent's list; switching agents does not leak the previous list. +- **The agent is the Rancher admin but the person is not an owner**: the panel shows what the *agent* can do; whether the person may ask for it is a chat-level question, not the panel's. Nothing in the panel reveals secret values (auth values of MCP servers, credential keys). +- **A tool call that the console would have confirmed** (delete, revoke, role change) arrives without a prior "yes" in the chat: the tool executes (the API cannot see the chat); the safeguard is the tool description's instruction plus the agent's reply. The plan may add a `confirm: true` argument to destructive tools so a runtime cannot call them by accident. +- **A file upload capability** (source file, template file upload, template zip install): the agent has no file picker. The tool accepts content it can already produce (text, a URL, a path inside its own workspace); binary upload from the person's machine stays a console action and the panel says so. +- **Prompt template placeholders**: shown as «…» in the composer; sending with a placeholder left in is allowed (the agent will ask), but the composer highlights the remaining placeholder. +- **A topic with one tool**: still an accordion, for consistency; no special case. +- **Runtime pods that never reload their tool list** and an operator who never restarts: the "after restart" marker stays until the pod restarts; it does not time out. +- **The tool list endpoint is called by a share-page visitor or a non-admin**: refused; the panel only exists in the admin console. + +## Requirements *(mandatory)* + +### Functional Requirements + +**Parity (API)** + +- **FR-001**: Every action listed in the Gap column of the audit MUST be available as an agent tool by the end of this feature; the audit table in this document is the acceptance checklist. +- **FR-002**: A tool MUST apply the same authorization the console applies to that action: operator-only actions are listed only for operator-role callers and refuse others with a message naming who may perform them; agent-self actions are listed only for agent runtimes. +- **FR-003**: Tools that remove or revoke (agents, templates, skills, credentials, MCP servers, knowledge bases, sources, users, API keys, share links, peers, files) MUST carry, in their description, the instruction to confirm with the person first, and MUST require an explicit confirmation argument so an accidental call cannot delete. +- **FR-004**: No tool result, error, or listing MUST contain a secret value (LLM keys, MCP bearer tokens, integration secrets, agent secret values); listings show names and metadata only, and set-operations acknowledge without echoing the value. +- **FR-005**: Each tool MUST declare a **topic** (the accordion it belongs to), a **human title**, a **description** written for a person reading the panel, and a **prompt template** with «…» placeholders; the system MUST reject registering a tool without these at startup, so the rule in Story 3 is enforced, not advised. +- **FR-006**: The tool for reading settings MUST tell the agent which groups and keys exist and what each means, so the agent does not have to guess names to change a setting. +- **FR-007**: Every new tool MUST have tests covering success, refused caller, and missing entity. +- **FR-008**: Tools MUST reuse the same domain services the console's endpoints use, so a change to a capability reaches both surfaces at once; a tool MUST NOT reimplement a console rule. + +**Tool shelf (console)** + +- **FR-009**: The admin console MUST be able to read, for a given agent, the tool list that agent's runtime would receive: per tool its name, title, description, topic, prompt template, and whether the running pod already has it. +- **FR-010**: Both admin chat surfaces (the Rancher page chat and the agent Chat tab) MUST show a **Tools** button beside the composer's attach button, with a tooltip. +- **FR-011**: The Tools panel MUST group tools by topic as collapsible accordions, show title, technical name and description per tool, hide empty topics, and remember which accordions were open within the session. +- **FR-012**: The panel MUST offer a search field that filters across all topics by title, technical name and description. +- **FR-013**: Clicking a tool MUST insert its prompt template into the composer at the cursor, select the first placeholder, close the panel, and not send. +- **FR-014**: Tools the running pod does not yet have MUST be visibly marked "after restart" with an explanation and a restart control that reuses the existing restart flow; the marker MUST clear once the pod restarts. +- **FR-015**: The panel MUST handle "list unavailable" (agent stopped, request failed) with a one-line message and a retry, without disabling the composer. +- **FR-016**: The panel MUST work at phone width and with a keyboard alone. +- **FR-017**: The panel MUST NOT display secret values or connection credentials of any kind. + +**Rule (repo guidance)** + +- **FR-018**: `CLAUDE.md` MUST state the parity rule as part of the definition of done, name where tools live, the required metadata (topic, title, description, template, authorization, tests), a reference module, and a reviewer check. +- **FR-019**: The graft guidance loaded by coding agents MUST carry the same rule or a pointer to the canonical document, in a form that survives `graft init` and `graft build`. +- **FR-020**: The PR template or review checklist used in this repo MUST include the line "console capability added → agent tool added, with tests". + +### Key Entities + +- **Tool**: one action the agent can perform; has a technical name, a title, a description, a topic, a prompt template, an authorization rule (operator / agent-self / everyone), and tests. Served to runtimes by the API. +- **Topic**: a named group of tools matching a console section (Agents, Templates, …); the accordion in the panel. +- **Prompt template**: a short natural-language sentence with «…» placeholders that a newcomer can send to reach this tool; belongs to exactly one tool. It is a starter, not a form: it only helps a person come up with an example, the person reshapes it freely, and the agent still decides which tool to call. +- **Agent tool list**: the set of tools a specific agent's runtime receives, after per-caller filtering, with a per-tool "present in running pod / after restart" flag. +- **Parity audit**: the table in this spec; every console capability mapped to its tool; the checklist reviewers and future work use. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 100% of console capabilities in the audit (rows 1–22) have a corresponding agent tool; the table reads "covered" in every Gap cell at the end of the feature. +- **SC-002**: An operator can complete each of these from the Rancher chat alone, without opening any other console page: register an MCP server and attach it to a template; add a URL source to a knowledge base and index it; create an LLM credential and health-check it; create a user and set their role; create and revoke an API key; delete a throwaway agent with confirmation. +- **SC-003**: From opening the chat, a person can find a tool by name or description and have its template in the composer in under 10 seconds and at most 3 interactions (open, search or expand, click). +- **SC-004**: Zero secret values appear in tool results, agent replies, or transcripts, verified by a test that scans every tool's output for the secret it was given. +- **SC-005**: Every new tool has tests; the API test suite passes; the console typecheck passes. +- **SC-006**: A newcomer reading `CLAUDE.md` or the graft guidance names "agent tools" as part of a module's definition of done without prompting. +- **SC-007**: The "after restart" marker is correct in both directions: present when a tool was added after the pod started, absent after restart. + +## Assumptions + +- **Surfaces**: the Tools panel ships in the admin console only (Rancher page chat and agent Chat tab). The user console (`app`) chat and the share page are not in scope; their agents keep their tools, only the shelf is absent. +- **Where the rule lives for graft**: graft rewrites its skill file and Cursor rule on `graft init`, so the canonical text lives in a project-owned document (`docs/agent-tools.md`), `CLAUDE.md` states the rule and links it, and the graft skill and Cursor rule get a short project-owned pointer block that is re-added if graft overwrites it. The plan may find graft supports a preserved section and use that instead. +- **File uploads**: tools that add binary files (source files, template zip, template file upload) accept a path inside the agent's own workspace or a URL, never a browser upload; the console remains the place for uploading from a person's machine, and the panel says so in the tool description. +- **Confirmation**: because the API cannot see the chat, "ask before deleting" is enforced by a required confirmation argument plus the description; the runtime's own judgement decides when to ask. No new chat-level confirmation UI is added. +- **Topics** mirror console sections one-to-one; the plan may merge small sections (Users + API keys, Chats + Usage) when a topic would hold fewer than three tools. +- **Prompt templates** are English, like the rest of the admin console (`admin/` is English-only per project rules). +- **Existing tools** keep their names; they gain topic, title and template metadata but no behavioural change, so the runtimes' current prompts still work. +- **Authorization vocabulary** already exists (operator-role gating, agent-self gating, per-caller listing); new tools reuse it rather than inventing another. +- **Restart awareness** reuses the existing "MCP configuration drift" detection; a tool added to the built-in server after pod start counts as drift of that server. + +## Decisions (clarifications settled 2026-09-22) + +1. **Parity extent for sensitive areas — full.** Users, API keys, LLM credential creation with keys, storage/secrets settings and every delete/revoke/role change get tools, listed only for operator-role callers. Destructive tools require an explicit `confirm: true` argument: the description tells the agent to call only after the person said yes in the chat; a call without it does nothing and returns what would be removed and a request to confirm. Secret values travel in, never out (FR-004). +2. **Tools panel source of truth — live per-agent list.** The console reads the list that this agent's runtime would receive: built-in tools after per-caller filtering, plus each external MCP server the agent is attached to as its own group, with the "after restart" flag per entry. A static catalogue is explicitly rejected. +3. **Legacy tool names — unchanged.** Only seven of the fifty-nine names deviate from the verb-noun scheme (the six `browser_session_*` tools and `agent_usage`); the panel shows a human title first and the technical name in small type, so the deviation is invisible to people. Existing tools gain topic, title, description and template metadata and keep their names; no aliases, no runtime restarts forced by this feature. +4. **Prompt templates are starters, not forms.** A template exists only to help a newcomer come up with an example message for that tool. It inserts a piece of text; the person reshapes it freely; nothing about it is binding for the person or the agent. diff --git a/specs/016-agent-tool-parity/tasks.md b/specs/016-agent-tool-parity/tasks.md new file mode 100644 index 00000000..93a32899 --- /dev/null +++ b/specs/016-agent-tool-parity/tasks.md @@ -0,0 +1,181 @@ +# Tasks: Agent tool parity (CLEAN-109) + +**Input**: Design documents from `specs/016-agent-tool-parity/` — plan.md, spec.md, research.md, data-model.md, contracts/{tool-metadata.md, agent-tools.openapi.yaml, tools.md}, quickstart.md + +**Tests**: Required by the spec (FR-007: every new tool has tests; FR-004: secret-leak scan). Test tasks are included and are part of each tool task, not optional. + +**Organization**: Foundational work first (metadata, validation, helpers), then the catalogue and Tools panel (US2) so every tool added afterwards is visible and testable from the chat, then the tools topic by topic (US1), then the rule (US3), then polish. This deviates from strict P1-first ordering on purpose (user request in the plan): US2 is small and turns US1 into something a person can verify by clicking. + +**Commands** (from plan.md): +- API tests: `cd api && NODE_OPTIONS=--experimental-vm-modules npx jest ` — never `bun run test` (kills a running dev API). +- Swagger + admin SDK: `cd api && bun run generate:swagger && cd ../admin && bun run build:api`. +- Admin: `cd admin && bun test slices` and `npx nuxt typecheck` (not `bun run typecheck`; revert regenerated SDK files if it touched them). +- Commit after each phase checkpoint with `CLEAN-109` in the subject; post a checkpoint comment on the Jira issue after phases 2, 3, each topic group of 4, 5 and 6. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: parallelizable (different files, no dependency on an unfinished task) +- **[Story]**: US1 parity tools · US2 Tools panel · US3 the rule + +## Path Conventions + +Web app: `api/src/slices//…` (NestJS, CleanSlice) and `admin/slices//…` (Nuxt, CleanSlice). Specs beside the file they test (`*.spec.ts`). + +--- + +## Phase 1: Setup + +**Purpose**: Nothing to scaffold; the branch and Jira issue exist. One check that the tree builds before touching it. + +- [ ] T001 Confirm the API boots and the current tool set is intact: `cd api && NODE_OPTIONS=--experimental-vm-modules npx jest src/slices/mcp src/slices/agent/peer src/slices/reins/knowledge/knowledge.tool.spec.ts` is green; note the 59 tool names from `contracts/tools.md` as the baseline + +--- + +## Phase 2: Foundational — metadata, validation, helpers, annotate existing tools + +**Purpose**: The `@Tool` contract every later task relies on. The API must boot at the end of this phase with all 59 existing tools annotated. + +**⚠️ CRITICAL**: No tool or panel work starts before T010 is green. + +- [ ] T002 Add `ToolTopics` const (15 keys with `title` and `order` per contracts/tool-metadata.md) and the `ToolTopic` type in `api/src/slices/mcp/decorators/topics.ts`; export from `api/src/slices/mcp/decorators/index.ts` +- [ ] T003 Extend `ToolOptions`/`ToolMetadata` with `topic`, `title`, `template`, `destructive?` in `api/src/slices/mcp/decorators/tool.decorator.ts` (parameters default stays `z.object({})`) +- [ ] T004 Add `validateToolMetadata()` to `api/src/slices/mcp/services/mcp-registry.service.ts`, called at the end of `onApplicationBootstrap`: throws with the tool name for a missing/invalid `topic`, empty `title` (>60 chars), empty `template` (>200 chars, must contain `«` unless the JSON schema has no properties), or `destructive` without a boolean `confirm` property in `zodToJsonSchema(parameters)` +- [ ] T005 Add validation cases to `api/src/slices/mcp/services/mcp-registry.service.spec.ts`: one fake provider per missing field, one destructive-without-confirm, one valid; assert throw messages name the tool +- [ ] T006 [P] Create `api/src/slices/mcp/tooling.ts` exporting `ToolResult`, `ok`, `err`, `callerAgentId`, `callerIsOperator`, `requireOperator(req)` (ForbiddenException: "… requires the Ranch operator role. Ask the operator to do it in the console."), `requireAgent(req)`, `confirmed(args, what)` (returns `err('This will . Ask the person to confirm, then call again with confirm: true.')` unless `args.confirm === true`), `stripSecrets(obj, keys)`; then make `api/src/slices/agent/peer/toolSupport.ts` re-export `ok/err/callerAgentId/callerIsOperator/ToolResult` from it (keep `withRefusalAdvice`, hints and peer helpers there) +- [ ] T007 [P] Spec for the helpers in `api/src/slices/mcp/tooling.spec.ts`: `confirmed` refuses without confirm, `requireOperator` accepts Owner and refuses Agent, `stripSecrets` removes nested keys +- [ ] T008 Annotate the 24 tools in `api/src/slices/rancher/rancher.tool.ts` with `topic/title/template` per contracts/tools.md (agents, agent_workspace, templates, skills, llm, settings, chats_usage), mark `redeploy_skill_agents` destructive with a `confirm` param + `confirmed()`, and replace `requireOwner` with `requireOperator` from `#/mcp/tooling` (keep behaviour; update `rancher.tool.spec.ts` if it exists, else add a minimal spec covering the operator refusal and the confirm refusal) +- [ ] T009 [P] Annotate the remaining existing tool files with `topic/title/template` (and `destructive` + `confirm` where contracts/tools.md marks ⚠): `api/src/slices/agent/peer/peerAdmin.tool.ts`, `peerSelf.tool.ts`, `askAgent.tool.ts` (peers); `api/src/slices/reins/knowledge/knowledge.tool.ts` (knowledge); `api/src/slices/bridle/attachment.tool.ts` (attachments); `api/src/slices/browser/browser.tool.ts` (browser; close/reset destructive); `api/src/slices/paddock/scenario/scenario.tool.ts` and `evaluation.tool.ts` (paddock; delete/abort destructive). Extend their specs for the new confirm refusals (`peerAdmin.tool.spec.ts`, `peerSelf.tool.spec.ts`; add minimal specs for browser and paddock tools if none exist) +- [ ] T010 Run `cd api && NODE_OPTIONS=--experimental-vm-modules npx jest src/slices/mcp src/slices/rancher src/slices/agent/peer src/slices/reins/knowledge src/slices/bridle/attachment.tool.spec.ts src/slices/browser src/slices/paddock` green, then boot the API once (`cd api && bun run start:dev` or the project's dev command) and confirm no validation error in the log; commit `feat(mcp): tool metadata, topics and startup validation (CLEAN-109)` + +**Checkpoint**: Jira comment — metadata contract in place, 59 tools annotated, API refuses unannotated tools. + +--- + +## Phase 3: User Story 2 — See what the agent can do, and try it in one click (Priority: P1) + +**Goal**: `GET /agents/:id/tools` returns the live per-agent catalogue with `inPod` flags; the admin chat has a Tools button, sheet with accordions, search, one-click template insertion, and "after restart" markers. + +**Independent Test**: quickstart.md §3, §4, §5 — the endpoint returns topic groups for the Rancher agent and fewer for a plain agent; the sheet opens, filters, inserts a template with the first «…» selected, and marks a tool added after pod start. + +### API — snapshot and catalogue + +- [ ] T011 [P] [US2] Create `api/src/slices/agent/toolCatalog/toolCatalog.prisma` with model `AgentToolListing { agentId @id, agent relation onDelete Cascade, toolNames Json, listedAt DateTime @default(now()) }`; add the back-relation `toolListing AgentToolListing?` to `api/src/slices/agent/agent/agent.prisma` +- [ ] T012 [P] [US2] Write `api/prisma/migrations/20260922120000_agent_tool_listing/migration.sql` (CREATE TABLE + FK + comment header like `20260914120000_agent_peer_delegation`); run `cd api && bunx prisma generate` (not `bun run test`) and confirm the client compiles +- [ ] T013 [P] [US2] Add `api/src/slices/mcp/interfaces/tool-listing-recorder.interface.ts`: `TOOL_LISTING_RECORDER` injection token and `IToolListingRecorder { record(agentId: string, toolNames: string[]): Promise }`; export from `interfaces/index.ts` +- [ ] T014 [US2] Create `api/src/slices/mcp/services/tool-catalog.service.ts` with `listFor(principal: IAuthTokenPayload): Promise` (`{ name, description, inputSchema, metadata }`) — move the per-tool resolve → `isListedForRequest` → `describeForRequest` → fallback loop out of `McpToolsHandler.registerHandlers` into it (build a request-like `{ user: principal }` for the hooks); register it as a provider in `api/src/slices/mcp/mcp.module.ts` +- [ ] T015 [US2] Refactor `api/src/slices/mcp/services/handlers/mcp-tools.handler.ts` to call `ToolCatalogService.listFor(httpRequest.user)` for `tools/list`, and after listing, when `callerAgentId(httpRequest)` is set, resolve `TOOL_LISTING_RECORDER` via `moduleRef.get(…, { strict: false })` in a try/catch and call `record(agentId, names)` without awaiting the result (log on failure); keep the `tools/call` path unchanged +- [ ] T016 [US2] Extend `api/src/slices/mcp/services/handlers/mcp-tools.handler.spec.ts`: listing still honours conditional/dynamic hooks; recorder is called with the agent id and names for an agent token and not for a person token; a throwing recorder does not break `tools/list`. Add `api/src/slices/mcp/services/tool-catalog.service.spec.ts` for `listFor` with operator vs agent principals +- [ ] T017 [US2] Create `api/src/slices/agent/toolCatalog/data/toolListing.gateway.ts` (Prisma upsert + `findByAgent`) implementing `IToolListingRecorder`, `domain/toolCatalog.types.ts` (mirror data-model.md §3), `domain/toolCatalog.service.ts` (`forAgent(agentId)`: agent → principal `{ sub: 'agent:', roles: agent.isAdmin ? [Owner] : [Agent], email: '' }` matching `issueAgentServiceToken` in `api/src/slices/user/auth/domain/auth.service.ts` → `ToolCatalogService.listFor` → group by `ToolTopics` order → `inPod` per data-model.md → external groups from `AgentMcpResolver.resolveForAgent` minus built-in ids with `afterRestart` from `detectMcpConfigDrift`; never include `url`/`authValue`) +- [ ] T018 [US2] Create `api/src/slices/agent/toolCatalog/dto/agentToolCatalog.dto.ts` (Swagger-decorated per contracts/agent-tools.openapi.yaml), `toolCatalog.controller.ts` (`GET agents/:id/tools`, `@Roles(Owner, Admin)`, 404 on unknown agent, 403 for `sub` starting with `agent:`), `toolCatalog.module.ts` (provides the gateway under `TOOL_LISTING_RECORDER` and exports it; imports AgentModule, McpServerModule, McpModule pieces as needed); register the module in `api/src/app.module.ts` +- [ ] T019 [US2] Spec `api/src/slices/agent/toolCatalog/domain/toolCatalog.service.spec.ts`: `inPod` matrix (no pod → null; pod + no snapshot → false; pod + snapshot → includes), topic ordering, empty topics dropped, external group has no url/authValue, admin vs plain agent principal; controller spec for 403 on agent token +- [ ] T020 [US2] Regenerate: `cd api && bun run generate:swagger && cd ../admin && bun run build:api`; confirm `AgentsService.getAgentTools` (or the generated name) exists in `admin/slices/setup/api/data/repositories/api/`; commit `feat(api): per-agent tool catalogue endpoint and listing snapshot (CLEAN-109)` + +### Admin — slice, primitive, composer + +- [ ] T021 [P] [US2] Add the accordion primitive from shadcn-vue on reka-ui in `admin/slices/setup/theme/components/ui/accordion/{Accordion,AccordionItem,AccordionTrigger,AccordionContent}.vue` + `index.ts`, styled like the existing `sheet/` files +- [ ] T022 [P] [US2] Create the slice skeleton `admin/slices/agent/toolCatalog/{nuxt.config.ts,index.d.ts}` following `admin/slices/agent/agent/` (auto-registered by `admin/registerSlices.ts`), plus `domain/toolCatalog.types.ts` (`IAgentToolCatalog`, `IAgentToolGroup`, `IAgentToolEntry`), `domain/toolCatalog.gateway.ts` (interface), `domain/toolCatalog.service.ts` +- [ ] T023 [US2] Create `admin/slices/agent/toolCatalog/data/toolCatalog.mapper.ts` (DTO → domain, defensive like `agent.mapper.ts`) and `data/toolCatalog.gateway.ts` extending `BaseGateway` and calling the generated SDK method (depends on T020, T022) +- [ ] T024 [US2] Create `admin/slices/agent/toolCatalog/stores/toolCatalog.ts` per data-model.md §4: `catalogs` by agent id, `byAgent`, `fetch` (upserts, returns the stored record), `upsert`, per-agent `ui { query, expanded }` with `setQuery`, `toggleGroup`, `setExpanded` +- [ ] T025 [P] [US2] Create pure utils `admin/slices/agent/toolCatalog/utils/insertTemplate.ts` (`insertTemplate`, `hasPlaceholder`, `firstPlaceholderRange`) and `utils/filterCatalog.ts` (`filterCatalog(catalog, query)` → groups with matching tools, match on title/name/description, case-insensitive) with specs `insertTemplate.spec.ts` and `filterCatalog.spec.ts` runnable by `bun test slices` +- [ ] T026 [US2] Create `admin/slices/agent/toolCatalog/components/toolCatalog/{Sheet.vue,Group.vue,Row.vue,Empty.vue}`: `Sheet` takes `agentId` + `open` v-model, emits `pick(template)`; renders `store.byAgent(agentId)` with `useAsyncData` only for pending/error/refresh; search `Input` bound to `store.ui[agentId].query`; accordions bound to `expanded` (all matching groups expanded while a query is set); `Row` shows title, `name` in `text-xs font-mono text-muted-foreground`, description, a `destructive` badge, and when `inPod === false` a muted "after restart" tag; `Group` header shows "after restart" with a Restart button calling `useAgentStore().restart(agentId)` (disabled while restarting) when `afterRestart`; external groups render name + description + "tools are provided by this server"; `Empty` covers no results / load error with Retry; keyboard: rows are `