fix(guidance): keep aggregator routes for hidden channels and check search links

- A chat channel that an aggregator can serve is no longer replaced by
  the unavailable result, so its routes stay offered.
- Test that every page connections_search links is a mounted UI route,
  and cover the partial-name and ranked own-server instructions.
- Drop the unreachable unavailable branch and the added code comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 5.5 committed 2026-11-09 02:25:08 -06:00
1 parent c5402bd33a
commit c77024412d
5 files changed
+17 -15

No files matched your search

@@ -232,15 +232,17 @@ describeEmbeddedPostgres("connectionIntentService", () => {
const service = connectionIntentService(db);
try {
await settings.updateExperimental({ enableChatConnectors: false });
// A longer request, as an agent words it, still finds the hidden channel.
expect((await service.search(claims, "iMessage chat messaging with a contact")).results.find(item => item.service === "imessage-photon")?.state).toBe("unavailable");
const off = await service.search(claims, "iMessage");
const imessage = off.results.find(item => item.service === "imessage-photon");
expect(imessage).toMatchObject({ state: "unavailable", methods: [] });
expect(imessage?.reason).toMatch(/\[Experimental settings\]\(\/[A-Z0-9]+\/company\/settings\/instance\/experimental\)/);
expect(off.instruction).toContain("Explain the reason");
// Channels that also offer tools keep their usual result; only their chat methods stay hidden.
expect((await service.search(claims, "slack")).results.some(item => item.state === "unavailable" && item.methods.length === 0)).toBe(false);
expect((await service.search(claims, "discord")).results.some(item => item.service === "discord" && item.state === "unavailable")).toBe(false);
const partial = await service.search(claims, "imess");
expect(partial.results.map(item => item.service)).toEqual(["imessage-photon"]);
expect(partial.instruction).toContain("Explain the reason");
await settings.updateExperimental({ enableChatConnectors: true });
const on = (await service.search(claims, "iMessage")).results.find(item => item.service === "imessage-photon");
expect(on?.state).not.toBe("unavailable");
@@ -251,6 +253,9 @@ describeEmbeddedPostgres("connectionIntentService", () => {
});
it("points a service the catalog does not list at the secure own-server setup page", async () => {
const ranked = await connectionIntentService(db).search(claims, "Synthetic notes service connect my account with an API key");
expect(ranked.results.length).toBeGreaterThan(1);
expect(ranked.instruction).toMatch(/^These are ranked connection matches[\s\S]*\[Connect your own MCP server\]\(\/[A-Z0-9]+\/apps\/byo\)/);
const result = await connectionIntentService(db).search(claims, "Synthetic notes service");
expect(result.instruction).toMatch(/\[Connect your own MCP server\]\(\/[A-Z0-9]+\/apps\/byo\)/);
expect(result.instruction).toContain("Do not create the connection yourself, ask for the key in chat");
@@ -6,8 +6,6 @@ import { runnerApiCatalog, searchRunnerApi } from "../services/native-runtime/ru
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
// Agents asked for a step only a person can take used to search the API until their turn ran out,
// because no catalog entry they read said who acts or where. Each entry below names both.
it.each([
["connect my assistant mcp setup", "GET /api/mcp/setup"],
["assistant connection consent approve access", "POST /api/mcp/requests/{id}/consent"],
@@ -52,8 +50,6 @@ it("says on the interaction listing that only the addressee resolves a connectio
expect(list?.description).toContain("Only that person can connect or decline it");
});
// The catalog text is the only place these page paths live on the server, so a renamed UI route
// would leave agents linking a page that no longer exists. Every page named must be mounted.
it("names only pages that the board UI mounts", () => {
const appRoutes = readFileSync(path.join(repoRoot, "ui/src/App.tsx"), "utf8");
const pages = new Set(
@@ -68,3 +64,12 @@ it("names only pages that the board UI mounts", () => {
for (const page of pages) expect(appRoutes, page).toContain(`<Route path="${mounted(page)}" element=`);
expect(readFileSync(path.join(repoRoot, "ui/src/pages/apps/app-tabs.ts"), "utf8")).toContain("\"permissions\"");
});
it("links only mounted pages from connections_search", () => {
const appRoutes = readFileSync(path.join(repoRoot, "ui/src/App.tsx"), "utf8");
const search = readFileSync(path.join(repoRoot, "server/src/services/connection-intents.ts"), "utf8");
for (const page of ["apps/byo", "company/settings/instance/experimental"]) {
expect(search, page).toContain(`}/${page})`);
expect(appRoutes, page).toContain(`<Route path="${page}" element=`);
}
});
-1
View File
@@ -205,7 +205,6 @@ export function chatChannelRoutes(db: Db, options: ChatChannelRouteOptions) {
async (req, res) => {
const companyId = req.params.companyId as string;
await assertConnectionManager(req, companyId);
// Setup pages are hidden while the provider's experiment is off; the API must not create the endpoint either.
assertChatProviderEnabledForCreate(req.body.provider, await instanceSettingsService(db).getExperimental());
if (req.body.provider === "slack" && !req.body.slackSetupMethod) {
req.body.slackSetupMethod = service.slackRegistration.managerGrants.defaultMethod();
-3
View File
@@ -11353,7 +11353,6 @@ registerCurrentRoute({
body: connectionRequestInputSchema,
});
/** Agents read this in API search; without it they look for a way to resolve another person's card. */
const ADDRESSED_CONNECTION_REQUEST = "Person-only: a connection request card is addressed to one person, its addresseeUserId. "
+ "Only that person, signed in to their own browser, can open its setup, connect their account or decline it. The server refuses every other member, board user and agent, so nobody can finish it for them. "
+ "When someone else offers to finish it, tell them the addressee must connect their own account from the card on its task, linked as [<identifier>](/<prefix>/issues/<identifier>). "
@@ -11722,7 +11721,6 @@ registerCurrentRoute({
},
});
/** Agents read this in API search; without it they cannot find where a quarantined action is reviewed. */
const QUARANTINED_ACTION_REVIEW = "Catalog entries with status quarantined are new actions that a catalog refresh found and holds for a person's review. "
+ "No agent can call a quarantined action, through this connection or any other transport, until a person who manages the connection allows it on the app's Permissions page, "
+ "[Permissions](/<prefix>/apps/<connectionId>/permissions), with this conversation's company prefix and the connection's id. Allowing it is that person's decision, not an agent's. "
@@ -12945,7 +12943,6 @@ registerCurrentRoute({
400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict },
});
/** Agents read this in API search; without it they hunt for the page where a person connects an assistant. */
const ASSISTANT_CONNECTION_SETUP_PERSON_ONLY = "Person-only: a person connects their own assistant, such as Claude, ChatGPT or another MCP client, in their browser on "
+ "[Assistant connections](/<prefix>/apps/assistant-connection), with this conversation's company prefix as <prefix>. "
+ "That page gives the invitation and server URL to paste into the assistant; the assistant then opens a Paperclip consent screen where the person picks the company and approves access. "
+1 -5
View File
@@ -543,10 +543,8 @@ export function connectionIntentService(db: Db) {
connectionId: ready?.id ?? null,
}});
}
// A chat channel hidden by the instance setting stays findable, so the agent can say why it is
// unavailable and where it is turned on instead of searching for a route that is not offered.
if (!settings.enableChatConnectors) for (const app of APP_STORE_DEFINITIONS) {
if (!isConnectorFeatureEnabled(app, settings) || discoveryMethods(app).length
if (!isConnectorFeatureEnabled(app, settings) || discoveryMethods(app).length || findAggregatorService(app.slug) || findAggregatorService(app.name)
|| !getAvailableConnectionMethods(app).some(method => method.purpose === "channel" && method.provider !== "github")) continue;
const channelMethods = getAvailableConnectionMethods(app).filter(method => method.purpose === "channel");
const { score, nameScore } = scoreConnectionSearch(preparedQuery, [app.slug, app.name],
@@ -714,7 +712,6 @@ export function connectionIntentService(db: Db) {
}
function directSearchResult(query: string, results: ConnectionSearchResultItem[], prefix: string, suggestions = false): ConnectionsSearchResult {
// A service the catalog does not list can still be the person's own MCP server; name its secure setup page.
const ownServer = `the person adds it as their own MCP server on [Connect your own MCP server](/${prefix}/apps/byo): they enter the URL and type any API key into that page's secure field themselves. `
+ "An agent cannot enter the key. Do not create the connection yourself, ask for the key in chat or search for another route; link that page and reply.";
return { version: 1, query, results, instruction: !results.length
@@ -732,7 +729,6 @@ export function connectionIntentService(db: Db) {
: results[0]!.methods.every(method => method.purpose && method.purpose !== "tool")
? "Choose the method relevant to the task using its purpose and label. If method.management.allowed, configure through search_api/call_api using nextApiOperation. Hand off only required human authentication or consent at the saved setupPath. These methods do not use the tool connection_request card. Do not claim tools or an inbox are ready before setup finishes."
: results[0]!.state === "ready" ? "Use the installed connection. Do not create another connection request."
: results[0]!.state === "unavailable" ? "This connection is unavailable or administratively restricted. Explain the reason. Do not bypass it using another provider."
: "The user asked to connect. If method.management.allowed, use search_api/call_api with nextApiOperation to configure it. Reuse a saved draft. Use connection_request only for required human input or access. Do not ask a generic confirmation or silently change policy to bypass a failed tool call." };
}