Compare commits

...
Author SHA1 Message Date
bincxzandCursor ff9fd3afeb fix(ai): keep host_open off sidebar Catty
Sidebar chats are scoped to already-open terminals/workspaces; exposing
host_open let Catty expand scope mid-turn and then fail to drive the new
session. Restrict vault.host.open to global/MCP/CLI and stop prompting
Catty to open hosts itself.

Fixes #2856

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 14:05:31 +08:00
5 changed files with 38 additions and 28 deletions

No files matched your search

+4 -1
View File
@@ -1,6 +1,6 @@
"use strict";
const { CAPABILITY_STATUS } = require("../constants.cjs");
const { AGENT_KINDS, CAPABILITY_STATUS } = require("../constants.cjs");
/** @type {import("../types.cjs").CapabilityDefinition[]} */
const VAULT_CAPABILITIES = [
@@ -49,6 +49,9 @@ const VAULT_CAPABILITIES = [
status: CAPABILITY_STATUS.IMPLEMENTED,
description:
"Open a vault host by creating a new terminal tab and starting the connection. Returns the new sessionId so you can run terminal/SFTP tools against it. Use vault_hosts_list first when you only know the label or hostname.",
// Sidebar Catty is scoped to already-open terminals/workspaces and must not
// expand that scope mid-turn. Keep host_open for MCP / CLI / global agent.
agentKinds: [AGENT_KINDS.GLOBAL],
policy: {
write: true,
sensitiveRead: false,
@@ -39,7 +39,8 @@ test("listCattyToolSpecs includes vault host tools and SFTP transfer", () => {
const capabilityIds = listCattyToolSpecs().map((spec) => spec.capabilityId);
assert.ok(capabilityIds.includes("vault.host.get"));
assert.ok(capabilityIds.includes("vault.host.list"));
assert.ok(capabilityIds.includes("vault.host.open"));
// Sidebar Catty must not open hosts; that expands scope mid-turn.
assert.ok(!capabilityIds.includes("vault.host.open"));
assert.ok(capabilityIds.includes("vault.hosts.create"));
assert.ok(capabilityIds.includes("vault.host.update"));
assert.ok(capabilityIds.includes("vault.host.delete"));
@@ -50,6 +51,18 @@ test("listCattyToolSpecs includes vault host tools and SFTP transfer", () => {
assert.ok(capabilityIds.includes("sftp.upload"));
});
test("host_open stays on MCP and global agent, not sidebar Catty", () => {
const { AGENT_KINDS, listAgentToolSpecs } = require("./toolSurfaces.cjs");
const sidebarIds = listAgentToolSpecs(AGENT_KINDS.SIDEBAR).map((spec) => spec.capabilityId);
const globalIds = listAgentToolSpecs(AGENT_KINDS.GLOBAL).map((spec) => spec.capabilityId);
const mcpHostOpen = listMcpTools().find((tool) => tool.mcpTool === "host_open");
assert.ok(!sidebarIds.includes("vault.host.open"));
assert.ok(globalIds.includes("vault.host.open"));
assert.ok(mcpHostOpen);
assert.equal(mcpHostOpen.capabilityId, "vault.host.open");
});
test("listMcpTools includes vault host update and delete for external MCP clients", () => {
const tools = listMcpTools();
const create = tools.find((tool) => tool.mcpTool === "vault_hosts_create");
@@ -170,7 +183,7 @@ test("catty and mcp terminal tools share capability ids", () => {
assert.equal(mcp?.capabilityId, "terminal.execute");
});
test("implemented catalog tools with inputs are catty-eligible unless denylisted", () => {
test("implemented catalog tools with inputs are catty-eligible unless denylisted or agentKinds-restricted", () => {
const implemented = ALL_CAPABILITIES.filter((cap) => cap.status === CAPABILITY_STATUS.IMPLEMENTED);
for (const capability of implemented) {
const hasInputs = Object.prototype.hasOwnProperty.call(TOOL_INPUT_FIELDS, capability.id);
@@ -179,6 +192,11 @@ test("implemented catalog tools with inputs are catty-eligible unless denylisted
assert.equal(isCattyEligible(capability), false);
continue;
}
if (Array.isArray(capability.agentKinds) && capability.agentKinds.length > 0
&& !capability.agentKinds.includes("sidebar")) {
assert.equal(isCattyEligible(capability), false);
continue;
}
const hasRpc = Boolean(
capability.surfaces?.builtin?.rpcMethod
|| capability.surfaces?.public?.mcpTool,
@@ -28,3 +28,15 @@ test('system prompt prefers explicit script wait APIs', () => {
assert.match(prompt, /waitForRegex/);
assert.doesNotMatch(prompt, /sendLine`,\s*`waitFor`,\s*dialogs/);
});
test('system prompt does not tell Catty to call host_open', () => {
const prompt = buildSystemPrompt({
scopeType: 'workspace',
hosts: [],
permissionMode: 'confirm',
});
assert.doesNotMatch(prompt, /host_open/);
assert.match(prompt, /cannot open new terminal sessions yourself/i);
assert.match(prompt, /ask them to open/i);
});
+2 -2
View File
@@ -55,7 +55,7 @@ ${permissionRules}
**Vault → Hosts (SSH connections):** When the user asks to **add/create/import a host** (创建主机、添加主机、保存服务器连接凭据), use \`vault_hosts_create\` — NOT \`vault_notes_create\`. Extract \`hostname\`, \`username\`, \`password\` or local \`keyPath\`, \`port\`, \`group\`, \`tags\`, and \`label\` from the user's text; put long admin tables or remarks in the host's \`notes\` field (Host Details metadata). Call with \`dryRun: true\` first to preview, then write. Only use \`vault_hosts_import\` for known export formats (PuTTY, MobaXterm, CSV, SecureCRT, ssh_config). Use \`vault_hosts_list\` to check existing hosts and resolve \`hostId\` before \`vault_hosts_update\` or \`vault_hosts_delete\`.
**Open / connect a host:** If the user wants to operate on a saved host that is not already in your session scope, use \`vault_hosts_list\` (or \`host_get\`) to resolve \`hostId\`, then call \`host_open\` to open a terminal tab and start the connection. Use the returned \`sessionId\` with \`terminal_execute\` / SFTP tools. Connection may still be establishing when \`host_open\` returns — check \`get_environment\` or wait briefly if needed. Passphrase / keyboard-interactive prompts still require the user in the Netcatty UI.
**Open / connect a host:** You cannot open new terminal sessions yourself. Stay within the sessions listed under Available Sessions. If the user wants work on a saved host that is not already open in your scope, ask them to open that host (or add it to the current workspace) in the Netcatty UI, then continue once it appears in scope.
**Attached host files:** When the user asks to import attached host/server data, call \`list_attachments\` then \`read_attachment\`. If the attachment is a known export format, pass the exact text to \`vault_hosts_import\`. If the format is unknown or \`vault_hosts_import\` cannot detect it, do not search a terminal or remote filesystem; read the attached text, extract host fields yourself, and call \`vault_hosts_create\` with \`dryRun: true\` first. If a tool result is truncated or compressed and includes a \`tool_output_read\` handle, use \`tool_output_read\` to recover the needed original text before extracting fields.
@@ -77,7 +77,7 @@ ${permissionRules}
6. **Stay focused.** Keep responses concise and relevant to terminal and server operations. Avoid unrelated commentary.
7. **Respect connection status.** Only attempt operations on sessions that are currently connected. If a session is disconnected or the needed host is not open, use \`host_open\` (after resolving \`hostId\` via \`vault_hosts_list\`) rather than asking the user to click the host manually — unless auth interaction is required.
7. **Respect connection status.** Only attempt operations on sessions that are currently connected and listed in your scope. If a session is disconnected, ask the user to reconnect it in the Netcatty UI. If the needed host is not open in your scope, ask the user to open it (or join it into the current workspace) rather than inventing a workaround.
8. **Be careful with file operations.** When writing files via shell commands, prefer appending or targeted edits over full file overwrites when possible.
@@ -559,29 +559,6 @@
"bypassesChatCancel": true
}
},
{
"capabilityId": "vault.host.open",
"toolName": "host_open",
"rpcMethod": "vault/hosts/open",
"localExecution": false,
"description": "Open a vault host by creating a new terminal tab and starting the connection. Returns the new sessionId so you can run terminal/SFTP tools against it. Use vault_hosts_list first when you only know the label or hostname. Opens a terminal tab for a saved vault host (same as clicking the host in Netcatty). Connection may still be establishing when the tool returns — use get_environment or wait briefly before terminal_execute if needed. Call session_close with the returned sessionId when the task is finished. Auth prompts (passphrase / keyboard-interactive) still require the user in the Netcatty UI.",
"inputShape": {
"hostId": {
"type": "string",
"optional": false,
"description": "Vault host ID to open. Use vault_hosts_list / host_get to resolve id from label or hostname."
}
},
"policy": {
"write": true,
"sensitiveRead": false,
"longRunning": false,
"requiresChatSession": false,
"bypassesObserverBlock": false,
"bypassesApproval": false,
"bypassesChatCancel": false
}
},
{
"capabilityId": "vault.hosts.create",
"toolName": "vault_hosts_create",