diff --git a/domains/guardrailsai.com/integrations.json b/domains/guardrailsai.com/integrations.json index d0cc4582..4cef8bf2 100644 --- a/domains/guardrailsai.com/integrations.json +++ b/domains/guardrailsai.com/integrations.json @@ -22,13 +22,15 @@ "authStatus": "unknown", "name": "Guardrails Server REST API", "slug": "guardrails-server-rest-api", - "type": "http" + "type": "http", + "url": "http://localhost:8000" }, { "authStatus": "unknown", "name": "Guardrails OpenAI-compatible endpoint", "slug": "guardrails-openai-compatible-endpoint", - "type": "http" + "type": "http", + "url": "http://localhost:8000/guards/{guard_name}/openai/v1" } ] } diff --git a/domains/lakera.ai/integrations.json b/domains/lakera.ai/integrations.json index 49fe8ed5..afdb9ca7 100644 --- a/domains/lakera.ai/integrations.json +++ b/domains/lakera.ai/integrations.json @@ -14,7 +14,8 @@ "authStatus": "none", "name": "Lakera self-hosted Kubernetes probe endpoints", "slug": "lakera-self-hosted-kubernetes-probe-endpoints", - "type": "http" + "type": "http", + "url": "http://:8000/{startupz,readyz,livez}" }, { "authStatus": "required", diff --git a/domains/portkey.ai/integrations.json b/domains/portkey.ai/integrations.json index 257dd4bf..bab87420 100644 --- a/domains/portkey.ai/integrations.json +++ b/domains/portkey.ai/integrations.json @@ -15,7 +15,8 @@ "authStatus": "unknown", "name": "Portkey Admin API", "slug": "portkey-admin-api", - "type": "http" + "type": "http", + "url": "https://api.portkey.ai/v1/admin" }, { "authStatus": "required", diff --git a/src/lib/discover.test.ts b/src/lib/discover.test.ts index 3218437c..9bea99da 100644 --- a/src/lib/discover.test.ts +++ b/src/lib/discover.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import { PROBE_KEYS } from "./conventions.ts"; import { discover, slackMcpAppManifestUrl, type ChatFn, type WebBackend } from "./discover.ts"; +import { locatorOf } from "../../worker/operations.ts"; import type { DetectionResult } from "./detect.ts"; const web: WebBackend = { @@ -189,3 +190,75 @@ describe("discover MCP onboarding overrides", () => { expect(setup).not.toContain("api.slack.com/apps?new_app=1"); }); }); + +describe("declared HTTP merge with discovered HTTP", () => { + test("merges declared and discovered HTTP surfaces when both point to the same base URL", async () => { + const chat: ChatFn = async () => ({ + message: { role: "assistant", content: null }, + toolCalls: [ + { + id: "surface-1", + name: "record_surface", + arguments: { + name: "Buddy REST API", + type: "http", + url: "https://api.buddy.works", + spec: "https://api.buddy.works/openapi.json", + authStatus: "none", + publicEvidence: ["https://docs.buddy.works/reference"], + basis: { via: "discovered", evidence: ["https://docs.buddy.works/reference"] }, + }, + }, + { + id: "finish-1", + name: "finish", + arguments: { + summary: "Buddy provides a REST API.", + description: "Buddy helps teams build software faster.", + }, + }, + ], + }); + + const detect: DetectionResult = { + domain: "buddy.works", + found: [], + probed: [], + mcp: [], + errors: [], + integrationsJson: { + url: "https://buddy.works/.well-known/integrations.json", + result: { + version: 3, + surfaces: [ + { + name: "Buddy REST API", + type: "http", + url: "https://api.buddy.works", + auth: { status: "none", basis: { via: "discovered", evidence: ["https://docs.buddy.works/reference"] } }, + }, + ], + }, + }, + }; + + const result = await discover(detect.domain, detect, chat, web); + expect(result).not.toBeNull(); + expect(result?.surfaces).toHaveLength(1); + expect(result?.surfaces[0]?.type).toBe("http"); + expect(result?.surfaces[0]?.url).toBe("https://api.buddy.works"); + }); +}); + +describe("slug continuity locator strategy", () => { + test("uses URL when available for HTTP locator continuity", () => { + expect( + locatorOf({ + slug: "buddy-rest-api", + type: "http", + url: "https://api.buddy.works", + spec: "https://api.buddy.works/openapi.json", + }), + ).toBe("http|https://api.buddy.works"); + }); +}); diff --git a/src/lib/discover.ts b/src/lib/discover.ts index 021f9857..372af4aa 100644 --- a/src/lib/discover.ts +++ b/src/lib/discover.ts @@ -202,6 +202,15 @@ export interface DiscoveryResult { surfaces: Surface[]; } +type SurfaceLocatorInput = { + type: string; + name?: string; + url?: string; + spec?: string; + command?: string; + packages?: readonly { identifier?: string }[]; +}; + // ── streamed events (partials emitted as findings are confirmed) ────────────── export type DiscoverEvent = @@ -420,7 +429,7 @@ export async function discover( return `auth references undefined credential id(s): ${missing.join(", ")}. Call record_credential for each first (or use authStatus "unknown" if the credential isn't a real user-minted one), then re-record this surface.`; } } - const key = `${s.type}|${(s.spec || s.url || s.name).toLowerCase()}`; + const key = surfaceLocator(s); if (surfaceKeys.has(key)) return `Already recorded ${s.name}.`; surfaceKeys.add(key); s.slug = assignSlug(s.name, surfaces); @@ -736,9 +745,16 @@ function markDeclaredSurface(surface: Surface, source: string): Surface { return surface; } -function surfaceLocator(s: Surface): string { - const packageId = s.packages?.[0]?.identifier; - return `${s.type}|${(s.spec || s.url || s.command || packageId || s.name).toLowerCase()}`; +export function surfaceLocatorValue(s: SurfaceLocatorInput): string | undefined { + const packageId = s.packages?.[0]?.identifier ?? undefined; + if (s.type === "cli") return s.command ?? packageId; + if (s.type === "http" || s.type === "graphql") return s.url ?? s.spec; + return s.spec ?? s.url ?? s.command ?? packageId; +} + +export function surfaceLocator(s: Surface): string { + const value = surfaceLocatorValue(s) ?? s.name ?? "unknown"; + return `${s.type}|${value.toLowerCase()}`; } function entryKey(entry: AuthEntry): string { diff --git a/worker/operations.ts b/worker/operations.ts index 2005101d..25825313 100644 --- a/worker/operations.ts +++ b/worker/operations.ts @@ -9,7 +9,7 @@ */ import { Effect, Schema } from "effect"; import { detect } from "../src/lib/detect.ts"; -import { discover, type ChatFn, type DiscoverEvent, type WebBackend } from "../src/lib/discover.ts"; +import { discover, surfaceLocatorValue, type ChatFn, type DiscoverEvent, type WebBackend } from "../src/lib/discover.ts"; import { naiveWeb } from "../src/lib/contextdev.ts"; import { Credential, CredentialType, DISCOVERY_VERSION, Surface } from "../src/lib/discovery-schema.ts"; import { canonicalDomain } from "../src/lib/domain-aliases.ts"; @@ -114,7 +114,7 @@ type SlugSurface = { }; export const locatorOf = (s: SlugSurface): string | undefined => { - const loc = s.type === "cli" ? (s.command ?? s.packages?.[0]?.identifier) : (s.url ?? s.spec); + const loc = surfaceLocatorValue(s); return loc ? `${s.type}|${loc.toLowerCase()}` : undefined; };