@integrity-labs/agt-cli 0.28.941 → 0.28.942

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10962,231 +10962,9 @@ function resolveConnectivityProbe(input) {
10962
10962
  }
10963
10963
  }
10964
10964
 
10965
- // ../../packages/core/dist/integrations/mcp-http-probe.js
10966
- var MCP_ACCEPT = "application/json, text/event-stream";
10967
- var DEFAULT_TIMEOUT_MS = 1e4;
10968
- function isRpcEnvelopeFor(msg, expectedId) {
10969
- return typeof msg === "object" && msg !== null && ("result" in msg || "error" in msg) && msg["id"] === expectedId;
10970
- }
10971
- async function parseRpc(res, expectedId) {
10972
- const ct = res.headers.get("content-type") ?? "";
10973
- if (ct.includes("text/event-stream")) {
10974
- const text = await res.text();
10975
- let dataLines = [];
10976
- const tryFrame = () => {
10977
- if (dataLines.length === 0)
10978
- return null;
10979
- try {
10980
- const msg2 = JSON.parse(dataLines.join("\n"));
10981
- if (isRpcEnvelopeFor(msg2, expectedId))
10982
- return msg2;
10983
- } catch {
10984
- }
10985
- return null;
10986
- };
10987
- for (const rawLine of text.split(/\r?\n/)) {
10988
- if (rawLine.startsWith("data:")) {
10989
- dataLines.push(rawLine.slice(5).trimStart());
10990
- continue;
10991
- }
10992
- if (rawLine === "") {
10993
- const frame = tryFrame();
10994
- if (frame)
10995
- return frame;
10996
- dataLines = [];
10997
- }
10998
- }
10999
- return tryFrame();
11000
- }
11001
- const msg = await res.json().catch(() => null);
11002
- return isRpcEnvelopeFor(msg, expectedId) ? msg : null;
11003
- }
11004
- function httpStatusOutcome(status, step) {
11005
- if (status === 401 || status === 403) {
11006
- return { status: "down", message: `MCP ${step} unauthorized (${status}) \u2014 reconnect required` };
11007
- }
11008
- if (status >= 500) {
11009
- return { status: "transient_error", message: `MCP ${step} returned ${status}` };
11010
- }
11011
- return { status: "down", message: `MCP ${step} returned ${status}` };
11012
- }
11013
- async function probeMcpHttp(config, fetchImpl = fetch) {
11014
- const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
11015
- const baseHeaders = {
11016
- ...config.headers ?? {},
11017
- "Content-Type": "application/json",
11018
- Accept: MCP_ACCEPT
11019
- };
11020
- try {
11021
- const initRes = await fetchImpl(config.url, {
11022
- method: "POST",
11023
- headers: baseHeaders,
11024
- body: JSON.stringify({
11025
- jsonrpc: "2.0",
11026
- id: 1,
11027
- method: "initialize",
11028
- params: {
11029
- protocolVersion: "2025-03-26",
11030
- capabilities: {},
11031
- clientInfo: { name: "augmented-connectivity-probe", version: "1.0.0" }
11032
- }
11033
- }),
11034
- signal: AbortSignal.timeout(timeoutMs)
11035
- });
11036
- if (!initRes.ok)
11037
- return httpStatusOutcome(initRes.status, "initialize");
11038
- const sessionId = initRes.headers.get("mcp-session-id");
11039
- const initRpc = await parseRpc(initRes, 1);
11040
- if (!initRpc) {
11041
- return { status: "down", message: "MCP initialize returned a non-JSON-RPC response \u2014 not an MCP server" };
11042
- }
11043
- if ("error" in initRpc) {
11044
- const err = initRpc["error"];
11045
- return { status: "down", message: `MCP initialize error: ${err?.message ?? "unknown"}` };
11046
- }
11047
- const sessionHeaders = { ...baseHeaders, ...sessionId ? { "Mcp-Session-Id": sessionId } : {} };
11048
- const initializedRes = await fetchImpl(config.url, {
11049
- method: "POST",
11050
- headers: sessionHeaders,
11051
- body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
11052
- signal: AbortSignal.timeout(5e3)
11053
- });
11054
- if (!initializedRes.ok)
11055
- return httpStatusOutcome(initializedRes.status, "initialized");
11056
- await initializedRes.text().catch(() => "");
11057
- const listRes = await fetchImpl(config.url, {
11058
- method: "POST",
11059
- headers: sessionHeaders,
11060
- body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }),
11061
- signal: AbortSignal.timeout(timeoutMs)
11062
- });
11063
- if (!listRes.ok)
11064
- return httpStatusOutcome(listRes.status, "tools/list");
11065
- const rpc = await parseRpc(listRes, 2);
11066
- if (!rpc) {
11067
- return { status: "down", message: "MCP tools/list returned a non-JSON-RPC response \u2014 not an MCP server" };
11068
- }
11069
- if ("error" in rpc) {
11070
- const err = rpc["error"];
11071
- return { status: "down", message: `MCP tools/list error: ${err?.message ?? "unknown"}` };
11072
- }
11073
- const result = rpc["result"];
11074
- const toolCount = Array.isArray(result?.tools) ? result.tools.length : void 0;
11075
- const testTool = config.connectivityTest?.tool;
11076
- if (testTool) {
11077
- const rawArgs = config.connectivityTest?.args;
11078
- const toolArgs = rawArgs && !Array.isArray(rawArgs) ? rawArgs : {};
11079
- const callRes = await fetchImpl(config.url, {
11080
- method: "POST",
11081
- headers: sessionHeaders,
11082
- body: JSON.stringify({
11083
- jsonrpc: "2.0",
11084
- id: 3,
11085
- method: "tools/call",
11086
- params: { name: testTool, arguments: toolArgs }
11087
- }),
11088
- signal: AbortSignal.timeout(timeoutMs)
11089
- });
11090
- if (!callRes.ok)
11091
- return httpStatusOutcome(callRes.status, `tools/call ${testTool}`);
11092
- const callRpc = await parseRpc(callRes, 3);
11093
- if (!callRpc) {
11094
- return { status: "down", message: `MCP tools/call ${testTool} returned a non-JSON-RPC response \u2014 not an MCP server` };
11095
- }
11096
- if ("error" in callRpc) {
11097
- const err = callRpc["error"];
11098
- return { status: "down", message: `MCP tools/call ${testTool} error: ${err?.message ?? "unknown"}` };
11099
- }
11100
- const callResult = callRpc["result"];
11101
- if (callResult?.isError === true) {
11102
- return { status: "down", message: `MCP tool ${testTool} returned an error result` };
11103
- }
11104
- return {
11105
- status: "ok",
11106
- message: `${testTool} succeeded`,
11107
- details: { ...toolCount !== void 0 ? { toolCount } : {}, testTool }
11108
- };
11109
- }
11110
- return handshakeToolsListOutcome(toolCount);
11111
- } catch (err) {
11112
- const isAbort = err?.name === "TimeoutError" || err?.name === "AbortError";
11113
- return {
11114
- status: "transient_error",
11115
- message: isAbort ? `MCP handshake timed out after ${timeoutMs / 1e3}s` : `MCP handshake failed: ${err.message}`
11116
- };
11117
- }
11118
- }
11119
-
11120
- // ../../packages/core/dist/integrations/composio-fetch.js
11121
- var COMPOSIO_API_BASE = "https://backend.composio.dev";
11122
- var transportResolver = null;
11123
- async function getComposioTransport() {
11124
- if (!transportResolver)
11125
- return null;
11126
- try {
11127
- return await transportResolver();
11128
- } catch (err) {
11129
- console.error("[composio] transport resolver failed; using direct egress", {
11130
- error: err instanceof Error ? err.message : String(err)
11131
- });
11132
- return null;
11133
- }
11134
- }
11135
-
11136
- // ../../packages/core/dist/integrations/composio-linkage.js
11137
- function assessAuthConfigLinkage(input) {
11138
- const { accountAuthConfigId, serverAuthConfigIds, serverId } = input;
11139
- const serverLabel = serverId ? ` (${serverId})` : "";
11140
- if (serverAuthConfigIds == null || !accountAuthConfigId) {
11141
- return {
11142
- linked: null,
11143
- message: "auth_config linkage not verified \u2014 " + (serverAuthConfigIds == null ? "couldn't read the wired MCP server's auth config binding" : "Composio returned no auth_config for the connected account"),
11144
- details: {
11145
- accountAuthConfigId: accountAuthConfigId ?? null,
11146
- serverAuthConfigIds: serverAuthConfigIds ?? null,
11147
- serverId: serverId ?? null
11148
- }
11149
- };
11150
- }
11151
- if (serverAuthConfigIds.length === 0) {
11152
- return {
11153
- linked: false,
11154
- message: `The agent's wired MCP server${serverLabel} has no auth config bound, so it cannot resolve the connected account (bound to auth_config ${accountAuthConfigId}) \u2014 reconnect/rebind required.`,
11155
- details: { accountAuthConfigId, serverAuthConfigIds, serverId: serverId ?? null }
11156
- };
11157
- }
11158
- if (serverAuthConfigIds.includes(accountAuthConfigId)) {
11159
- return {
11160
- linked: true,
11161
- message: `Connected account's auth_config (${accountAuthConfigId}) matches the wired MCP server binding.`,
11162
- details: { accountAuthConfigId, serverAuthConfigIds, serverId: serverId ?? null }
11163
- };
11164
- }
11165
- return {
11166
- linked: false,
11167
- message: `The connected account is bound to auth_config ${accountAuthConfigId}, but the agent's wired MCP server${serverLabel} resolves auth_config(s) [${serverAuthConfigIds.join(", ")}] \u2014 tool calls will fail with "No connected account found". Reconnect/rebind required.`,
11168
- details: { accountAuthConfigId, serverAuthConfigIds, serverId: serverId ?? null }
11169
- };
11170
- }
11171
-
11172
- // ../../packages/core/dist/integrations/composio-account-probe.js
10965
+ // ../../packages/core/dist/integrations/connectivity-http-probes.js
11173
10966
  var PROBE_TIMEOUT_MS = 1e4;
11174
- var BODY_SNIPPET_MAX = 300;
11175
- async function readErrorBodySnippet(res, apiKey) {
11176
- let text;
11177
- try {
11178
- text = await res.text();
11179
- } catch {
11180
- return void 0;
11181
- }
11182
- const redacted = apiKey ? text.split(apiKey).join("\xABredacted\xBB") : text;
11183
- const trimmed = redacted.trim();
11184
- if (!trimmed)
11185
- return void 0;
11186
- return trimmed.length > BODY_SNIPPET_MAX ? `${trimmed.slice(0, BODY_SNIPPET_MAX)}\u2026` : trimmed;
11187
- }
11188
10967
  var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 204, 205, 304]);
11189
- var BODY_MAX_BYTES = 1024 * 1024;
11190
10968
  async function timedFetch(fetchImpl, url, init) {
11191
10969
  const controller = new AbortController();
11192
10970
  const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
@@ -11194,7 +10972,7 @@ async function timedFetch(fetchImpl, url, init) {
11194
10972
  const res = await fetchImpl(url, { ...init, signal: controller.signal });
11195
10973
  if (NULL_BODY_STATUSES.has(res.status))
11196
10974
  return res;
11197
- const body = await readBoundedBody(res, controller);
10975
+ const body = await res.text();
11198
10976
  return new Response(body, {
11199
10977
  status: res.status,
11200
10978
  statusText: res.statusText,
@@ -11204,437 +10982,372 @@ async function timedFetch(fetchImpl, url, init) {
11204
10982
  clearTimeout(timer);
11205
10983
  }
11206
10984
  }
11207
- async function readBoundedBody(res, controller) {
11208
- const stream = res.body;
11209
- if (!stream)
11210
- return await res.text();
11211
- const reader = stream.getReader();
11212
- const chunks = [];
11213
- let total = 0;
11214
- for (; ; ) {
11215
- const { done, value } = await reader.read();
11216
- if (done)
11217
- break;
11218
- if (!value)
11219
- continue;
11220
- total += value.byteLength;
11221
- if (total > BODY_MAX_BYTES) {
11222
- controller.abort();
11223
- await reader.cancel().catch(() => {
11224
- });
11225
- throw new Error(`Composio response body exceeded ${BODY_MAX_BYTES} bytes`);
11226
- }
11227
- chunks.push(value);
11228
- }
11229
- const merged = new Uint8Array(total);
11230
- let offset = 0;
11231
- for (const chunk of chunks) {
11232
- merged.set(chunk, offset);
11233
- offset += chunk.byteLength;
11234
- }
11235
- return new TextDecoder().decode(merged);
11236
- }
11237
- async function probeComposioAccount(params, fetchImpl) {
11238
- const impl = fetchImpl ?? await getComposioTransport() ?? fetch;
11239
- const { connectedAccountId, apiKey, expectedUserId } = params;
11240
- const base = params.apiBase ?? COMPOSIO_API_BASE;
11241
- if (!connectedAccountId) {
11242
- return {
11243
- status: "down",
11244
- message: "No connected account recorded \u2014 reconnect required"
11245
- };
11246
- }
11247
- if (!apiKey || !expectedUserId) {
11248
- return {
11249
- status: "transient_error",
11250
- message: "Composio probe missing api key or expected user_id"
11251
- };
11252
- }
11253
- let res;
10985
+ function isRateLimited(res) {
10986
+ const retryAfter = res.headers.get("retry-after");
10987
+ if (retryAfter !== null && retryAfter.trim() !== "")
10988
+ return true;
10989
+ const remaining = res.headers.get("x-ratelimit-remaining");
10990
+ if (remaining === null || remaining.trim() === "")
10991
+ return false;
10992
+ return Number(remaining) === 0;
10993
+ }
10994
+ function statusForHttp(httpStatus, opts) {
10995
+ if (httpStatus === 429)
10996
+ return "transient_error";
10997
+ if (httpStatus === 403 && opts?.rateLimited)
10998
+ return "transient_error";
10999
+ if (httpStatus === 401 || httpStatus === 403)
11000
+ return "down";
11001
+ if (httpStatus >= 500)
11002
+ return "transient_error";
11003
+ return "down";
11004
+ }
11005
+ function causeForHttp(httpStatus, opts) {
11006
+ if (httpStatus === 429)
11007
+ return "rate_limited";
11008
+ if (httpStatus === 403 && opts?.rateLimited)
11009
+ return "rate_limited";
11010
+ if (httpStatus === 401 || httpStatus === 403)
11011
+ return "auth_rejected";
11012
+ if (httpStatus >= 500)
11013
+ return "server_error";
11014
+ return "semantic";
11015
+ }
11016
+ function rateLimitMessage(provider, httpStatus) {
11017
+ const subject = provider ? `${provider} rate limit` : "Rate limited by the provider";
11018
+ return `${subject} (${httpStatus}) \u2014 not a credential failure`;
11019
+ }
11020
+ function redactSecret(message, secret) {
11021
+ if (typeof secret !== "string" || secret.length < 4)
11022
+ return message;
11023
+ return message.split(secret).join("[redacted]");
11024
+ }
11025
+ function networkOutcome(err, secret) {
11026
+ const isAbort = err?.name === "AbortError";
11027
+ const message = isAbort ? `Connection timed out after ${PROBE_TIMEOUT_MS / 1e3}s` : `Connection failed: ${err.message}`;
11028
+ return { status: "transient_error", cause: "unreachable", message: redactSecret(message, secret) };
11029
+ }
11030
+ async function probeLinear(creds, fetchImpl) {
11031
+ const key = creds.api_key ?? creds.access_token;
11032
+ if (!key)
11033
+ return { status: "down", message: "No Linear credential present" };
11254
11034
  try {
11255
- res = await timedFetch(impl, `${base}/api/v3/connected_accounts/${encodeURIComponent(connectedAccountId)}`, { headers: { "x-api-key": apiKey } });
11035
+ const res = await timedFetch(fetchImpl, "https://api.linear.app/graphql", {
11036
+ method: "POST",
11037
+ headers: { "Content-Type": "application/json", Authorization: String(key) },
11038
+ body: JSON.stringify({ query: "{ viewer { id name email } }" })
11039
+ });
11040
+ if (!res.ok) {
11041
+ const rateLimited = isRateLimited(res);
11042
+ const message = res.status === 429 || res.status === 403 && rateLimited ? rateLimitMessage("Linear", res.status) : `Linear API returned ${res.status}`;
11043
+ return {
11044
+ status: statusForHttp(res.status, { rateLimited }),
11045
+ cause: causeForHttp(res.status, { rateLimited }),
11046
+ message
11047
+ };
11048
+ }
11049
+ const body = await res.json();
11050
+ if (body.errors?.length)
11051
+ return { status: "down", message: body.errors[0]?.message ?? "Unknown Linear error" };
11052
+ const viewer = body.data?.viewer;
11053
+ if (!viewer)
11054
+ return { status: "down", message: "Invalid key \u2014 no viewer returned" };
11055
+ return { status: "ok", message: `Connected as ${viewer.name ?? viewer.email ?? "unknown"}` };
11256
11056
  } catch (err) {
11257
- const isAbort = err?.name === "AbortError";
11258
- return {
11259
- status: "transient_error",
11260
- message: isAbort ? `Composio probe timed out after ${PROBE_TIMEOUT_MS / 1e3}s` : `Composio probe failed: ${err.message}`
11261
- };
11057
+ return networkOutcome(err, String(key ?? ""));
11262
11058
  }
11263
- if (!res.ok) {
11264
- if (res.status >= 500) {
11059
+ }
11060
+ async function probeBearerJson(url, creds, fetchImpl, interpret, extraHeaders) {
11061
+ const token = creds.access_token ?? creds.api_key;
11062
+ if (!token)
11063
+ return { status: "down", message: "No credential present" };
11064
+ try {
11065
+ const res = await timedFetch(fetchImpl, url, { headers: { Authorization: `Bearer ${token}`, ...extraHeaders } });
11066
+ if (!res.ok) {
11067
+ const rateLimited = isRateLimited(res);
11068
+ const message = res.status === 401 ? "Token expired or revoked \u2014 reconnect required" : res.status === 429 || res.status === 403 && rateLimited ? rateLimitMessage(null, res.status) : `API returned ${res.status}`;
11265
11069
  return {
11266
- status: "transient_error",
11267
- message: `Composio unreachable (HTTP ${res.status}) \u2014 retrying`,
11268
- details: { connectedAccountId, httpStatus: res.status }
11070
+ status: statusForHttp(res.status, { rateLimited }),
11071
+ cause: causeForHttp(res.status, { rateLimited }),
11072
+ message
11269
11073
  };
11270
11074
  }
11271
- if (res.status === 401 || res.status === 403) {
11272
- const body = await readErrorBodySnippet(res, apiKey);
11075
+ return interpret(await res.json());
11076
+ } catch (err) {
11077
+ return networkOutcome(err, String(token ?? ""));
11078
+ }
11079
+ }
11080
+ async function probeBuffer(creds, fetchImpl) {
11081
+ const key = creds.api_key ?? creds.access_token;
11082
+ if (!key)
11083
+ return { status: "down", message: "No Buffer credential present" };
11084
+ try {
11085
+ const res = await timedFetch(fetchImpl, "https://api.buffer.com", {
11086
+ method: "POST",
11087
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
11088
+ body: JSON.stringify({ query: "{ account { organizations { id name } } }" })
11089
+ });
11090
+ if (!res.ok) {
11091
+ const rateLimited = isRateLimited(res);
11092
+ const message = res.status === 401 ? (
11093
+ // ENG-9809: name the mechanism, because the bare "expired or revoked" sent
11094
+ // two investigations at the wrong thing. Buffer API keys carry a MANDATORY
11095
+ // lifetime chosen at mint time — 7/30/60/90 days or 1 year, defaulting to
11096
+ // 30 days, with no "never expires" option
11097
+ // (support.buffer.com/article/984-how-to-create-your-buffer-api-key). So
11098
+ // every Buffer install on the fleet is on a countdown, and expiry is the
11099
+ // ordinary end of a key's life rather than a customer revoking one.
11100
+ //
11101
+ // All three causes are named, because this one branch answers every Buffer
11102
+ // 401 — a key that lapsed, one that was revoked, and one that was simply
11103
+ // mistyped ("Requests without a valid key will return a 401 Unauthorized",
11104
+ // developers.buffer.com/guides/authentication.html). The status code alone
11105
+ // cannot separate them, so naming expiry as the LIKELIER cause is supported;
11106
+ // asserting it is not, and a message that omitted "invalid" would send
11107
+ // someone who pasted the key wrong looking for an expiry date instead.
11108
+ "Buffer API key expired, invalid, or revoked \u2014 reconnect required. Buffer keys expire by design (30-day default, 1-year maximum), so on a key that was working until now expiry is the likeliest of the three; pick 1 year when minting the replacement."
11109
+ ) : res.status === 429 || res.status === 403 && rateLimited ? rateLimitMessage("Buffer", res.status) : `Buffer API returned ${res.status}`;
11273
11110
  return {
11274
- status: "unverified",
11275
- cause: "access_restricted",
11276
- message: `Composio refused our API key (HTTP ${res.status}) \u2014 access restriction on our side, not a problem with this account; do not reconnect` + (body ? `. Composio said: ${body}` : ""),
11277
- details: {
11278
- connectedAccountId,
11279
- httpStatus: res.status,
11280
- ...body ? { responseBody: body } : {}
11281
- }
11111
+ status: statusForHttp(res.status, { rateLimited }),
11112
+ cause: causeForHttp(res.status, { rateLimited }),
11113
+ message
11282
11114
  };
11283
11115
  }
11284
- return {
11285
- status: "down",
11286
- message: `Composio account ${connectedAccountId} not found (HTTP ${res.status}) \u2014 reconnect required`,
11287
- details: { connectedAccountId, httpStatus: res.status }
11288
- };
11116
+ const body = await res.json();
11117
+ if (body.errors?.length)
11118
+ return { status: "down", message: body.errors[0]?.message ?? "Unknown Buffer error" };
11119
+ const orgs = body.data?.account?.organizations ?? [];
11120
+ if (!orgs.length)
11121
+ return { status: "down", message: "No Buffer organizations on this account" };
11122
+ return { status: "ok", message: `Connected to ${orgs[0]?.name ?? "Buffer"}` };
11123
+ } catch (err) {
11124
+ return networkOutcome(err, String(key ?? ""));
11289
11125
  }
11290
- let data;
11126
+ }
11127
+ async function probeVercel(creds, fetchImpl) {
11128
+ const token = creds.api_key ?? creds.access_token;
11129
+ if (!token)
11130
+ return { status: "down", message: "No Vercel credential present" };
11291
11131
  try {
11292
- data = await res.json();
11132
+ const res = await timedFetch(fetchImpl, "https://api.vercel.com/v2/user", {
11133
+ headers: { Authorization: `Bearer ${token}` }
11134
+ });
11135
+ if (!res.ok) {
11136
+ const message = res.status === 429 ? rateLimitMessage("Vercel", res.status) : res.status === 401 || res.status === 403 ? `Vercel rejected the token (${res.status}) \u2014 invalid or revoked API token` : `Vercel API returned ${res.status}`;
11137
+ return {
11138
+ status: statusForHttp(res.status),
11139
+ cause: causeForHttp(res.status),
11140
+ message
11141
+ };
11142
+ }
11143
+ const body = await res.json();
11144
+ const user = body?.user;
11145
+ const identity = [user?.username, user?.email, user?.name, user?.id].find((value) => typeof value === "string" && value.trim().length > 0);
11146
+ if (!identity)
11147
+ return { status: "down", message: "Vercel returned no user for this token" };
11148
+ return { status: "ok", message: `Connected as ${identity}` };
11293
11149
  } catch (err) {
11294
- return {
11295
- status: "transient_error",
11296
- message: `Composio probe response unparseable: ${err.message}`
11297
- };
11298
- }
11299
- const accountStatus = data.status ?? "unknown";
11300
- if (accountStatus !== "ACTIVE") {
11301
- return {
11302
- status: "down",
11303
- message: `Composio account ${connectedAccountId} status=${accountStatus} \u2014 reconnect required`,
11304
- details: { connectedAccountId, status: accountStatus, boundUserId: data.user_id ?? null }
11305
- };
11306
- }
11307
- const boundUserId = data.user_id;
11308
- if (!boundUserId) {
11309
- return {
11310
- status: "down",
11311
- message: `Composio account ${connectedAccountId} is ACTIVE but returned no user_id binding \u2014 runtime queries as '${expectedUserId}', so tool calls can't be confirmed`,
11312
- details: { connectedAccountId, status: accountStatus, boundUserId: null, expectedUserId }
11313
- };
11150
+ return networkOutcome(err, String(token ?? ""));
11314
11151
  }
11315
- if (boundUserId !== expectedUserId) {
11152
+ }
11153
+ async function probeHiggsfield(creds, fetchImpl) {
11154
+ const token = creds.api_key ?? creds.access_token;
11155
+ if (!token)
11156
+ return { status: "down", message: "No Higgsfield credential present" };
11157
+ if (!token.includes(":")) {
11316
11158
  return {
11317
11159
  status: "down",
11318
- message: `Composio account ${connectedAccountId} is bound to user_id '${boundUserId}' but the agent runtime queries as '${expectedUserId}' \u2014 tool calls will fail. Reconnect to bind correctly.`,
11319
- details: { connectedAccountId, status: accountStatus, boundUserId, expectedUserId }
11160
+ message: "Higgsfield credential is not in KEY_ID:KEY_SECRET form \u2014 re-paste it from cloud.higgsfield.ai/api-keys"
11320
11161
  };
11321
11162
  }
11322
- const accountAuthConfigId = data.auth_config_id ?? data.auth_config?.id;
11323
- if (params.serverId) {
11324
- const serverAuthConfigIds = await fetchServerAuthConfigIds(impl, base, params.serverId, apiKey);
11325
- const linkage = assessAuthConfigLinkage({
11326
- accountAuthConfigId,
11327
- serverAuthConfigIds,
11328
- serverId: params.serverId
11163
+ try {
11164
+ const res = await timedFetch(fetchImpl, "https://platform.higgsfield.ai/v1/motions", {
11165
+ headers: { Authorization: `Key ${token}` }
11329
11166
  });
11330
- if (linkage.linked === false) {
11167
+ if (!res.ok) {
11168
+ const body = await res.text().catch(() => "");
11169
+ if (res.status === 403 && /credit/i.test(body)) {
11170
+ return {
11171
+ status: "degraded",
11172
+ message: "Higgsfield authenticated, but the account is out of credits \u2014 generation will fail"
11173
+ };
11174
+ }
11175
+ const message = res.status === 429 ? rateLimitMessage("Higgsfield", res.status) : res.status === 401 || res.status === 403 ? `Higgsfield rejected the key pair (${res.status}) \u2014 invalid or revoked API key` : `Higgsfield API returned ${res.status}`;
11331
11176
  return {
11332
- status: "down",
11333
- message: linkage.message,
11334
- details: { connectedAccountId, status: accountStatus, boundUserId, ...linkage.details }
11177
+ status: statusForHttp(res.status),
11178
+ cause: causeForHttp(res.status),
11179
+ message
11335
11180
  };
11336
11181
  }
11337
- }
11338
- return {
11339
- status: "ok",
11340
- message: `Connected (account ${connectedAccountId}, status=ACTIVE)`,
11341
- details: {
11342
- connectedAccountId,
11343
- status: accountStatus,
11344
- boundUserId,
11345
- ...accountAuthConfigId ? { authConfigId: accountAuthConfigId } : {}
11182
+ const motions = await res.json();
11183
+ if (!Array.isArray(motions) || motions.length === 0) {
11184
+ return { status: "down", message: "Higgsfield returned no motion presets for this key" };
11346
11185
  }
11347
- };
11186
+ return { status: "ok", message: `Higgsfield reachable \u2014 ${motions.length} motion presets` };
11187
+ } catch (err) {
11188
+ return networkOutcome(err, String(token ?? ""));
11189
+ }
11348
11190
  }
11349
- async function fetchServerAuthConfigIds(fetchImpl, base, serverId, apiKey) {
11350
- let res;
11191
+ async function probeSportsyear(creds, fetchImpl) {
11192
+ const key = creds.api_key ?? creds.access_token;
11193
+ if (!key)
11194
+ return { status: "down", message: "No Sportsyear credential present" };
11195
+ let authorization;
11351
11196
  try {
11352
- res = await timedFetch(fetchImpl, `${base}/api/v3/mcp/${encodeURIComponent(serverId)}`, { headers: { "x-api-key": apiKey } });
11197
+ authorization = basicUsernameAuthorization(String(key));
11353
11198
  } catch {
11354
- return null;
11199
+ return {
11200
+ status: "down",
11201
+ message: 'Sportsyear credential is not a bare API key (it contains ":") \u2014 paste the key alone, not email:password'
11202
+ };
11355
11203
  }
11356
- if (!res.ok)
11357
- return null;
11204
+ const encoded = authorization.slice("Basic ".length);
11358
11205
  try {
11359
- const data = await res.json();
11360
- return data.auth_config_ids ?? data.auth_configs?.map((c) => c.id).filter((id) => typeof id === "string" && id.length > 0) ?? [];
11361
- } catch {
11362
- return null;
11363
- }
11364
- }
11365
-
11366
- // ../../packages/core/dist/integrations/composio-approved-egress.json
11367
- var composio_approved_egress_default = {
11368
- $comment: [
11369
- "ADR-0032 Part 3, gate item 2 + gate item 3 (ENG-9684). THE single recorded source",
11370
- "for the addresses Composio's per-key IP allowlist is expected to hold. Read by BOTH",
11371
- "scripts/fleet-egress-enum.sh (the operator's pre-arming instrument) and the",
11372
- "ComposioEgressAddressDrift cron (the between-armings watch). They must agree, which",
11373
- "is why neither carries its own copy.",
11374
- "",
11375
- "WHY A REPO FILE AND NOT SSM. Composio's allowlist has no read API, so its contents",
11376
- "can never be queried - only inferred from a refusal. This file is therefore not a",
11377
- "cache of vendor state; it is a DECLARATION of what a human pasted into a dashboard.",
11378
- "A PR is the right ceremony for changing a security boundary nobody can read back.",
11379
- "SSM would also hit registerCron's five-parameter grant scope (ENG-9940), where a",
11380
- "sixth parameter fails silently.",
11381
- "",
11382
- "THE COMPARISON IS ON `ip` AND NOTHING ELSE. `seenAsGateway` is informational. A NAT",
11383
- "rebuild that re-adopts the same EIP keeps egress correct while changing the gateway",
11384
- "id, so comparing ids would red on the safe case - which is precisely the case Part 2's",
11385
- "EIP adoption exists to create.",
11386
- "",
11387
- "WHEN ENG-9426 LANDS THIS GOES RED UNTIL IT IS UPDATED. That is the forcing function",
11388
- "working, not a fault. ENG-9426 adds a NAT in ap-southeast-2b, i.e. a THIRD egress",
11389
- "address: add it here in the same change that lands it, or the detector correctly",
11390
- "reports that hosts are leaving from an address no key allows. Do not loosen the check."
11391
- ],
11392
- account: "711726113003",
11393
- homeRegion: "ap-southeast-2",
11394
- addresses: [
11395
- {
11396
- ip: "32.236.6.24",
11397
- stage: "prod",
11398
- seenAsGateway: "nat-00a3dac969c0641a6",
11399
- note: "Prod hosts NAT. Measured 2026-09-12 (ENG-10392): 25 of 25 active prod host instances route 0.0.0.0/0 here."
11400
- },
11401
- {
11402
- ip: "15.135.204.192",
11403
- stage: "dev",
11404
- seenAsGateway: "nat-01122e6efaf4c4eaa",
11405
- note: "Dev-stage hosts NAT, same account. 2 instances measured 2026-09-12; their host rows live in the dev Supabase project."
11206
+ const res = await timedFetch(fetchImpl, "https://sportsyear.com.au/api/v1/member", {
11207
+ headers: { Authorization: authorization, Accept: "application/json" }
11208
+ });
11209
+ if (!res.ok) {
11210
+ const message = res.status === 429 ? rateLimitMessage("Sportsyear", res.status) : res.status === 401 || res.status === 403 ? `Sportsyear rejected the API key (${res.status}) \u2014 removed, regenerated, or API access disabled on the member account` : `Sportsyear API returned ${res.status}`;
11211
+ return {
11212
+ status: statusForHttp(res.status),
11213
+ cause: causeForHttp(res.status),
11214
+ message
11215
+ };
11406
11216
  }
11407
- ]
11408
- };
11409
-
11410
- // ../../packages/core/dist/integrations/composio-approved-egress.js
11411
- var APPROVED_EGRESS_ACCOUNT = composio_approved_egress_default.account;
11412
- var APPROVED_EGRESS_HOME_REGION = composio_approved_egress_default.homeRegion;
11413
- var APPROVED_EGRESS_ADDRESSES = composio_approved_egress_default.addresses;
11414
-
11415
- // ../../packages/core/dist/integrations/composio-tool-call-probe.js
11416
- var MCP_ACCEPT2 = "application/json, text/event-stream";
11417
- var DEFAULT_TIMEOUT_MS2 = 1e4;
11418
- var READONLY_VERB_TOKENS = [
11419
- "LIST",
11420
- "GET",
11421
- "FIND",
11422
- "SEARCH",
11423
- "FETCH",
11424
- "COUNT",
11425
- "RETRIEVE",
11426
- "READ"
11427
- ];
11428
- var ACCOUNT_RESOLUTION_ERROR_PATTERNS = [
11429
- "no connected account",
11430
- "connected account not found",
11431
- "no account found",
11432
- "could not be resolved",
11433
- "auth config",
11434
- "no connection found"
11435
- ];
11436
- var UPSTREAM_AUTH_ERROR_PATTERNS = [
11437
- "authentication required",
11438
- "not authenticated",
11439
- "unauthorized",
11440
- "authentication_error",
11441
- "authentication error",
11442
- "invalid authentication",
11443
- "invalid credentials",
11444
- "invalid api key",
11445
- "invalid access token",
11446
- "token expired",
11447
- "token has expired",
11448
- "expired access token",
11449
- "expired credentials",
11450
- "permission denied",
11451
- "access denied",
11452
- "forbidden"
11453
- ];
11454
- var SITE_RESOLUTION_ERROR_PATTERNS = [
11455
- "dns resolution failed",
11456
- "getaddrinfo",
11457
- "enotfound",
11458
- "could not resolve host",
11459
- "name resolution",
11460
- "name not resolved"
11461
- ];
11462
- var SCOPE_DEFICIT_ERROR_PATTERNS = [
11463
- "insufficient_scope",
11464
- "insufficient scope",
11465
- "insufficient permission",
11466
- "missing scope",
11467
- "required scope",
11468
- "requires the scope",
11469
- "requires the following scope",
11470
- "scope is required",
11471
- "access scope",
11472
- "oauth scope",
11473
- // Shopify custom-app specifics.
11474
- "read_content",
11475
- "write_content",
11476
- "sales channel is not enabled"
11477
- ];
11478
- var QUOTA_EXHAUSTED_ERROR_PATTERNS = [
11479
- "resource_exhausted",
11480
- "resource has been exhausted",
11481
- "rate limit",
11482
- "rate_limit",
11483
- "ratelimit",
11484
- "ratescope",
11485
- "rate_scope",
11486
- "too many requests",
11487
- "quota exceeded",
11488
- "quota_exceeded",
11489
- "quotaexceeded",
11490
- "quota error",
11491
- "quotaerror",
11492
- "quota_error",
11493
- "exceeded your quota",
11494
- "out of quota",
11495
- "insufficient quota",
11496
- "check quota",
11497
- "usage limit",
11498
- "daily limit exceeded"
11499
- ];
11500
- function isReadonlyToolDescriptor(t) {
11501
- if (!t?.name)
11502
- return false;
11503
- const tokens = t.name.toUpperCase().split(/[^A-Z0-9]+/).filter(Boolean);
11504
- const hasReadVerb = tokens.some((tok) => READONLY_VERB_TOKENS.includes(tok));
11505
- if (!hasReadVerb)
11506
- return false;
11507
- const required = t.inputSchema?.required ?? [];
11508
- return !(Array.isArray(required) && required.length > 0);
11217
+ const body = await res.json();
11218
+ const member = typeof body === "object" && body !== null && !Array.isArray(body) ? body : null;
11219
+ const id = member?.id;
11220
+ if (typeof id !== "number" && !(typeof id === "string" && id.trim() !== "")) {
11221
+ return { status: "down", message: "Sportsyear returned no member account for this API key" };
11222
+ }
11223
+ const label = typeof member?.organisation === "string" && member.organisation.trim() !== "" ? member.organisation.trim() : `member ${String(id)}`;
11224
+ return { status: "ok", message: `Reached Sportsyear as ${redactSecret(redactSecret(label, String(key)), encoded)}` };
11225
+ } catch (err) {
11226
+ const outcome = networkOutcome(err, String(key));
11227
+ return outcome.message === void 0 ? outcome : { ...outcome, message: redactSecret(outcome.message, encoded) };
11228
+ }
11509
11229
  }
11510
- function pickSafeReadonlyTool(tools) {
11511
- for (const t of tools) {
11512
- if (isReadonlyToolDescriptor(t))
11513
- return t.name;
11230
+ async function probeHttpProvider(definitionId, credentials, fetchImpl = fetch) {
11231
+ switch (definitionId) {
11232
+ case "linear":
11233
+ return probeLinear(credentials, fetchImpl);
11234
+ case "buffer":
11235
+ return probeBuffer(credentials, fetchImpl);
11236
+ case "vercel":
11237
+ return probeVercel(credentials, fetchImpl);
11238
+ case "higgsfield":
11239
+ return probeHiggsfield(credentials, fetchImpl);
11240
+ case "sportsyear":
11241
+ return probeSportsyear(credentials, fetchImpl);
11242
+ case "google-workspace":
11243
+ return probeBearerJson("https://www.googleapis.com/oauth2/v2/userinfo", credentials, fetchImpl, (body) => {
11244
+ const info = body;
11245
+ return { status: "ok", message: `Connected as ${info.name ?? info.email ?? "unknown"}` };
11246
+ });
11247
+ case "xero":
11248
+ return probeBearerJson("https://api.xero.com/connections", credentials, fetchImpl, (body) => {
11249
+ const conns = body ?? [];
11250
+ if (!conns.length)
11251
+ return { status: "down", message: "No Xero organisations connected" };
11252
+ return { status: "ok", message: `Connected to ${conns[0]?.tenantName ?? "Xero"}` };
11253
+ });
11254
+ case "linkedin-ads":
11255
+ return probeBearerJson("https://api.linkedin.com/v2/userinfo", credentials, fetchImpl, (body) => {
11256
+ const user = body;
11257
+ return { status: "ok", message: `Connected as ${user.name ?? user.email ?? "unknown"}` };
11258
+ });
11259
+ case "v0":
11260
+ return probeBearerJson("https://api.v0.dev/v1/user", credentials, fetchImpl, (body) => {
11261
+ const user = body;
11262
+ return { status: "ok", message: `Connected as ${user.name ?? user.email ?? "unknown"}` };
11263
+ });
11264
+ case "sprout-social":
11265
+ return probeBearerJson("https://api.sproutsocial.com/v1/metadata/client", credentials, fetchImpl, (body) => {
11266
+ const data = body?.data;
11267
+ const clients = Array.isArray(data) ? data.filter((row) => {
11268
+ if (typeof row !== "object" || row === null)
11269
+ return false;
11270
+ const id = row.customer_id;
11271
+ return typeof id === "string" || typeof id === "number";
11272
+ }) : [];
11273
+ if (!clients.length) {
11274
+ return { status: "down", message: "Sprout Social returned no usable client account for this token" };
11275
+ }
11276
+ return {
11277
+ status: "ok",
11278
+ message: `Reached Sprout Social \u2014 ${clients.length} client account(s), incl. ${clients[0]?.name ?? "unnamed"}`
11279
+ };
11280
+ });
11281
+ case "github":
11282
+ return probeBearerJson("https://api.github.com/user", credentials, fetchImpl, (body) => {
11283
+ const u = body;
11284
+ return { status: "ok", message: `Reached GitHub as ${u.login ?? u.name ?? "unknown"}` };
11285
+ }, { "User-Agent": "augmented-team-connectivity-probe", "X-GitHub-Api-Version": "2026-03-10" });
11286
+ default:
11287
+ return null;
11514
11288
  }
11515
- return null;
11516
11289
  }
11517
- function resolveProbeTool(tools, override) {
11518
- const requested = override?.tool?.trim();
11519
- if (!requested) {
11520
- return { toolName: pickSafeReadonlyTool(tools), args: {} };
11521
- }
11522
- const match = tools.find((t) => t?.name === requested);
11523
- if (!match) {
11524
- return { toolName: pickSafeReadonlyTool(tools), args: {}, fallback: "seed-drift", requestedTool: requested };
11525
- }
11526
- if (!isReadonlyToolDescriptor(match)) {
11527
- return { toolName: pickSafeReadonlyTool(tools), args: {}, fallback: "seed-invalid", requestedTool: requested };
11528
- }
11529
- return { toolName: requested, args: override?.args ?? {}, requestedTool: requested };
11530
- }
11531
- function isAccountResolutionError(message) {
11532
- const m = message.toLowerCase();
11533
- return ACCOUNT_RESOLUTION_ERROR_PATTERNS.some((p2) => m.includes(p2));
11534
- }
11535
- function isUpstreamAuthError(message) {
11536
- const m = message.toLowerCase();
11537
- if (UPSTREAM_AUTH_ERROR_PATTERNS.some((p2) => m.includes(p2)))
11538
- return true;
11539
- if (/\b(401|403)\s+client error/.test(m))
11540
- return true;
11541
- return /(status(?:_?code)?|http_?status(?:_code)?|mercury_last_http_status_code)["'\\\s]*[:=]["'\\\s]*(401|403)\b/.test(m);
11542
- }
11543
- function isComposioFailureEnvelope(text) {
11544
- return /["'\\]*(successful|successfull)["'\\\s]*:\s*false\b/i.test(text);
11545
- }
11546
- function isSiteResolutionError(message) {
11547
- const m = message.toLowerCase();
11548
- return SITE_RESOLUTION_ERROR_PATTERNS.some((p2) => m.includes(p2));
11549
- }
11550
- function isScopeDeficitError(message) {
11551
- const m = message.toLowerCase();
11552
- if (SCOPE_DEFICIT_ERROR_PATTERNS.some((p2) => m.includes(p2)))
11553
- return true;
11554
- return m.includes("scope") && ["doesn't have", "does not have", "not have the", "not granted", "lacks the", "not authorized for"].some((p2) => m.includes(p2));
11555
- }
11556
- function isQuotaExhaustedError(message) {
11557
- const m = message.toLowerCase();
11558
- if (QUOTA_EXHAUSTED_ERROR_PATTERNS.some((p2) => m.includes(p2)))
11559
- return true;
11560
- if (/\b429\s+client error/.test(m))
11561
- return true;
11562
- return /(status(?:_?code)?|http_?status(?:_code)?|mercury_last_http_status_code|["'\\]code)["'\\\s]*[:=]["'\\\s]*429\b/.test(m);
11563
- }
11564
- function extractRetryAfterSeconds(message) {
11565
- const m = message.toLowerCase();
11566
- const patterns = [
11567
- /retry\s+in\s+(\d+)\s*(?:s\b|sec\b|secs\b|second)/,
11568
- /retry[-_ ]?after["'\\\s]*[:=]["'\\\s]*(\d+)/,
11569
- /retry_?delay["'\\\s]*[:=]["'\\\s]*"?(\d+)s?/
11570
- ];
11571
- for (const re of patterns) {
11572
- const hit = re.exec(m);
11573
- if (!hit?.[1])
11574
- continue;
11575
- const seconds = Number(hit[1]);
11576
- if (Number.isFinite(seconds) && seconds > 0)
11577
- return seconds;
11578
- }
11579
- return null;
11580
- }
11581
- function formatRetryWindow(seconds) {
11582
- if (seconds < 90)
11583
- return `${seconds} seconds`;
11584
- const minutes = seconds / 60;
11585
- if (minutes < 90)
11586
- return `about ${Math.round(minutes)} minutes`;
11587
- const hours = minutes / 60;
11588
- if (hours < 24)
11589
- return `about ${Math.round(hours * 10) / 10} hours`;
11590
- return `about ${Math.round(hours / 24 * 10) / 10} days`;
11591
- }
11592
- function classifyToolCallFailure(text) {
11593
- if (isAccountResolutionError(text))
11594
- return "account";
11595
- if (isQuotaExhaustedError(text))
11596
- return "quota";
11597
- if (isScopeDeficitError(text))
11598
- return "scope";
11599
- if (isUpstreamAuthError(text))
11600
- return "auth";
11601
- if (isSiteResolutionError(text))
11602
- return "site";
11603
- return "benign";
11290
+
11291
+ // ../../packages/core/dist/integrations/mcp-http-probe.js
11292
+ var MCP_ACCEPT = "application/json, text/event-stream";
11293
+ var DEFAULT_TIMEOUT_MS = 1e4;
11294
+ function isRpcEnvelopeFor(msg, expectedId) {
11295
+ return typeof msg === "object" && msg !== null && ("result" in msg || "error" in msg) && msg["id"] === expectedId;
11604
11296
  }
11605
- async function parseRpc2(res, expectedId) {
11297
+ async function parseRpc(res, expectedId) {
11606
11298
  const ct = res.headers.get("content-type") ?? "";
11607
11299
  if (ct.includes("text/event-stream")) {
11608
11300
  const text = await res.text();
11609
11301
  let dataLines = [];
11302
+ const tryFrame = () => {
11303
+ if (dataLines.length === 0)
11304
+ return null;
11305
+ try {
11306
+ const msg2 = JSON.parse(dataLines.join("\n"));
11307
+ if (isRpcEnvelopeFor(msg2, expectedId))
11308
+ return msg2;
11309
+ } catch {
11310
+ }
11311
+ return null;
11312
+ };
11610
11313
  for (const rawLine of text.split(/\r?\n/)) {
11611
11314
  if (rawLine.startsWith("data:")) {
11612
11315
  dataLines.push(rawLine.slice(5).trimStart());
11613
11316
  continue;
11614
11317
  }
11615
- if (rawLine === "" && dataLines.length > 0) {
11616
- try {
11617
- const msg2 = JSON.parse(dataLines.join("\n"));
11618
- if (("result" in msg2 || "error" in msg2) && msg2["id"] === expectedId)
11619
- return msg2;
11620
- } catch {
11621
- }
11318
+ if (rawLine === "") {
11319
+ const frame = tryFrame();
11320
+ if (frame)
11321
+ return frame;
11622
11322
  dataLines = [];
11623
11323
  }
11624
11324
  }
11625
- return null;
11325
+ return tryFrame();
11626
11326
  }
11627
11327
  const msg = await res.json().catch(() => null);
11628
- if (msg && ("result" in msg || "error" in msg) && msg["id"] === expectedId)
11629
- return msg;
11630
- return null;
11328
+ return isRpcEnvelopeFor(msg, expectedId) ? msg : null;
11631
11329
  }
11632
- async function probeComposioMcpToolCall(config, fetchImpl = fetch) {
11633
- const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
11330
+ function httpStatusOutcome(status, step) {
11331
+ const graded = statusForHttp(status);
11332
+ const cause = causeForHttp(status);
11333
+ if (status === 401 || status === 403) {
11334
+ return { status: graded, cause, message: `MCP ${step} unauthorized (${status}) \u2014 reconnect required` };
11335
+ }
11336
+ if (status === 429) {
11337
+ return {
11338
+ status: graded,
11339
+ cause,
11340
+ message: `MCP ${step} rate limited (${status}) \u2014 not a credential failure`
11341
+ };
11342
+ }
11343
+ return { status: graded, cause, message: `MCP ${step} returned ${status}` };
11344
+ }
11345
+ async function probeMcpHttp(config, fetchImpl = fetch) {
11346
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
11634
11347
  const baseHeaders = {
11635
11348
  ...config.headers ?? {},
11636
11349
  "Content-Type": "application/json",
11637
- Accept: MCP_ACCEPT2
11350
+ Accept: MCP_ACCEPT
11638
11351
  };
11639
11352
  try {
11640
11353
  const initRes = await fetchImpl(config.url, {
@@ -11647,16 +11360,22 @@ async function probeComposioMcpToolCall(config, fetchImpl = fetch) {
11647
11360
  params: {
11648
11361
  protocolVersion: "2025-03-26",
11649
11362
  capabilities: {},
11650
- clientInfo: { name: "augmented-toolcall-probe", version: "1.0.0" }
11363
+ clientInfo: { name: "augmented-connectivity-probe", version: "1.0.0" }
11651
11364
  }
11652
11365
  }),
11653
11366
  signal: AbortSignal.timeout(timeoutMs)
11654
11367
  });
11655
- if (!initRes.ok) {
11656
- return initRes.status >= 500 ? { status: "transient_error", message: `MCP initialize returned ${initRes.status}` } : null;
11657
- }
11368
+ if (!initRes.ok)
11369
+ return httpStatusOutcome(initRes.status, "initialize");
11658
11370
  const sessionId = initRes.headers.get("mcp-session-id");
11659
- await parseRpc2(initRes, 1);
11371
+ const initRpc = await parseRpc(initRes, 1);
11372
+ if (!initRpc) {
11373
+ return { status: "down", message: "MCP initialize returned a non-JSON-RPC response \u2014 not an MCP server" };
11374
+ }
11375
+ if ("error" in initRpc) {
11376
+ const err = initRpc["error"];
11377
+ return { status: "down", message: `MCP initialize error: ${err?.message ?? "unknown"}` };
11378
+ }
11660
11379
  const sessionHeaders = { ...baseHeaders, ...sessionId ? { "Mcp-Session-Id": sessionId } : {} };
11661
11380
  const initializedRes = await fetchImpl(config.url, {
11662
11381
  method: "POST",
@@ -11664,6 +11383,8 @@ async function probeComposioMcpToolCall(config, fetchImpl = fetch) {
11664
11383
  body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
11665
11384
  signal: AbortSignal.timeout(5e3)
11666
11385
  });
11386
+ if (!initializedRes.ok)
11387
+ return httpStatusOutcome(initializedRes.status, "initialized");
11667
11388
  await initializedRes.text().catch(() => "");
11668
11389
  const listRes = await fetchImpl(config.url, {
11669
11390
  method: "POST",
@@ -11671,430 +11392,719 @@ async function probeComposioMcpToolCall(config, fetchImpl = fetch) {
11671
11392
  body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }),
11672
11393
  signal: AbortSignal.timeout(timeoutMs)
11673
11394
  });
11674
- if (!listRes.ok) {
11675
- return listRes.status >= 500 ? { status: "transient_error", message: `MCP tools/list returned ${listRes.status}` } : null;
11676
- }
11677
- const listRpc = await parseRpc2(listRes, 2);
11678
- const tools = listRpc?.["result"]?.tools ?? [];
11679
- const resolved = resolveProbeTool(tools, { tool: config.toolName, args: config.toolArgs });
11680
- const toolName = resolved.toolName;
11681
- if (!toolName)
11682
- return null;
11683
- const baseDetails = {
11684
- tool: toolName,
11685
- ...resolved.fallback ? { override_fallback: resolved.fallback, requested_tool: resolved.requestedTool } : {}
11686
- };
11687
- const callRes = await fetchImpl(config.url, {
11688
- method: "POST",
11689
- headers: sessionHeaders,
11690
- body: JSON.stringify({
11691
- jsonrpc: "2.0",
11692
- id: 3,
11693
- method: "tools/call",
11694
- params: { name: toolName, arguments: resolved.args }
11695
- }),
11696
- signal: AbortSignal.timeout(timeoutMs)
11697
- });
11698
- if (!callRes.ok) {
11699
- return callRes.status >= 500 ? { status: "transient_error", message: `MCP tools/call returned ${callRes.status}` } : null;
11395
+ if (!listRes.ok)
11396
+ return httpStatusOutcome(listRes.status, "tools/list");
11397
+ const rpc = await parseRpc(listRes, 2);
11398
+ if (!rpc) {
11399
+ return { status: "down", message: "MCP tools/list returned a non-JSON-RPC response \u2014 not an MCP server" };
11700
11400
  }
11701
- const callRpc = await parseRpc2(callRes, 3);
11702
- {
11703
- const errText = callRpc?.["error"]?.message;
11704
- const resContent = callRpc?.["result"]?.content;
11705
- const raw = errText ?? (resContent ?? []).map((c) => c.text ?? "").join(" ").trim();
11706
- if (raw)
11707
- baseDetails.response = raw.length > 2e3 ? `${raw.slice(0, 2e3)}\u2026` : raw;
11401
+ if ("error" in rpc) {
11402
+ const err = rpc["error"];
11403
+ return { status: "down", message: `MCP tools/list error: ${err?.message ?? "unknown"}` };
11708
11404
  }
11709
- const rpcErrMsg = callRpc && "error" in callRpc ? callRpc["error"]?.message ?? "" : "";
11710
- const result = callRpc?.["result"];
11711
- const contentText2 = (result?.content ?? []).map((c) => c.text ?? "").join(" ").trim();
11712
- const failed = Boolean(rpcErrMsg) || Boolean(result?.isError) || isComposioFailureEnvelope(contentText2);
11713
- if (failed) {
11714
- const failureText = [rpcErrMsg, contentText2].filter(Boolean).join(" ");
11715
- const snippet = failureText.length > 200 ? `${failureText.slice(0, 200)}\u2026` : failureText;
11716
- const kind = classifyToolCallFailure(failureText);
11717
- if (kind === "account") {
11718
- return {
11719
- status: "down",
11720
- message: `Live tool call '${toolName}' failed to resolve the connected account: ${snippet}`,
11721
- details: baseDetails
11722
- };
11405
+ const result = rpc["result"];
11406
+ const toolCount = Array.isArray(result?.tools) ? result.tools.length : void 0;
11407
+ const testTool = config.connectivityTest?.tool;
11408
+ if (testTool) {
11409
+ const rawArgs = config.connectivityTest?.args;
11410
+ const toolArgs = rawArgs && !Array.isArray(rawArgs) ? rawArgs : {};
11411
+ const callRes = await fetchImpl(config.url, {
11412
+ method: "POST",
11413
+ headers: sessionHeaders,
11414
+ body: JSON.stringify({
11415
+ jsonrpc: "2.0",
11416
+ id: 3,
11417
+ method: "tools/call",
11418
+ params: { name: testTool, arguments: toolArgs }
11419
+ }),
11420
+ signal: AbortSignal.timeout(timeoutMs)
11421
+ });
11422
+ if (!callRes.ok)
11423
+ return httpStatusOutcome(callRes.status, `tools/call ${testTool}`);
11424
+ const callRpc = await parseRpc(callRes, 3);
11425
+ if (!callRpc) {
11426
+ return { status: "down", message: `MCP tools/call ${testTool} returned a non-JSON-RPC response \u2014 not an MCP server` };
11723
11427
  }
11724
- if (kind === "quota") {
11725
- const retryAfterSeconds = extractRetryAfterSeconds(failureText);
11726
- const retryPhrase = retryAfterSeconds ? ` The provider says to retry in ${formatRetryWindow(retryAfterSeconds)}.` : "";
11727
- return {
11728
- status: "degraded",
11729
- message: `Live tool call '${toolName}' reached the provider and was refused: a quota or rate limit is exhausted. The connection itself is valid \u2014 do NOT reconnect, this resolves when the window resets.${retryPhrase} ${snippet}`,
11730
- details: {
11731
- ...baseDetails,
11732
- reason: "quota_exhausted",
11733
- ...retryAfterSeconds ? { retry_after_seconds: retryAfterSeconds } : {}
11734
- }
11735
- };
11428
+ if ("error" in callRpc) {
11429
+ const err = callRpc["error"];
11430
+ return { status: "down", message: `MCP tools/call ${testTool} error: ${err?.message ?? "unknown"}` };
11736
11431
  }
11737
- if (kind === "scope") {
11738
- return {
11739
- status: "degraded",
11740
- message: `Live tool call '${toolName}' was refused for a missing permission \u2014 the connection is valid, but its app/token isn't granted the scope this tool needs. Grant the required scope in the provider app and re-authorise (this is NOT a reconnect): ${snippet}`,
11741
- details: { ...baseDetails, reason: "scope_deficit" }
11742
- };
11743
- }
11744
- if (kind === "auth") {
11745
- return {
11746
- status: "down",
11747
- message: `Live tool call '${toolName}' was rejected by the provider \u2014 the connection's credential is no longer valid (reconnect required): ${snippet}`,
11748
- details: { ...baseDetails, reason: "upstream_auth_rejected" }
11749
- };
11750
- }
11751
- if (kind === "site") {
11752
- return {
11753
- status: "down",
11754
- message: `Live tool call '${toolName}' couldn't reach the provider's site - the connection has no valid site URL (reconnect and make sure a site/workspace is granted): ${snippet}`,
11755
- details: { ...baseDetails, reason: "site_unresolved" }
11756
- };
11432
+ const callResult = callRpc["result"];
11433
+ if (callResult?.isError === true) {
11434
+ return { status: "down", message: `MCP tool ${testTool} returned an error result` };
11757
11435
  }
11758
11436
  return {
11759
11437
  status: "ok",
11760
- message: `Live tool call '${toolName}' resolved the account (tool error: ${snippet})`,
11761
- details: { ...baseDetails, tool_error: snippet }
11438
+ message: `${testTool} succeeded`,
11439
+ details: { ...toolCount !== void 0 ? { toolCount } : {}, testTool }
11762
11440
  };
11763
11441
  }
11764
- return { status: "ok", message: `Live tool call '${toolName}' resolved the connected account`, details: baseDetails };
11442
+ return handshakeToolsListOutcome(toolCount);
11765
11443
  } catch (err) {
11766
11444
  const isAbort = err?.name === "TimeoutError" || err?.name === "AbortError";
11767
11445
  return {
11768
11446
  status: "transient_error",
11769
- message: isAbort ? `MCP tool-call probe timed out after ${timeoutMs / 1e3}s` : `MCP tool-call probe failed: ${err.message}`
11447
+ // ENG-10574: a timeout, DNS failure or refused connection - "network
11448
+ // failure, DNS, or our own timeout". Same call `networkOutcome` makes for
11449
+ // the central lane. Not vendor-side, so escalation is unchanged.
11450
+ cause: "unreachable",
11451
+ message: isAbort ? `MCP handshake timed out after ${timeoutMs / 1e3}s` : `MCP handshake failed: ${err.message}`
11452
+ };
11453
+ }
11454
+ }
11455
+
11456
+ // ../../packages/core/dist/integrations/composio-fetch.js
11457
+ var COMPOSIO_API_BASE = "https://backend.composio.dev";
11458
+ var transportResolver = null;
11459
+ async function getComposioTransport() {
11460
+ if (!transportResolver)
11461
+ return null;
11462
+ try {
11463
+ return await transportResolver();
11464
+ } catch (err) {
11465
+ console.error("[composio] transport resolver failed; using direct egress", {
11466
+ error: err instanceof Error ? err.message : String(err)
11467
+ });
11468
+ return null;
11469
+ }
11470
+ }
11471
+
11472
+ // ../../packages/core/dist/integrations/composio-linkage.js
11473
+ function assessAuthConfigLinkage(input) {
11474
+ const { accountAuthConfigId, serverAuthConfigIds, serverId } = input;
11475
+ const serverLabel = serverId ? ` (${serverId})` : "";
11476
+ if (serverAuthConfigIds == null || !accountAuthConfigId) {
11477
+ return {
11478
+ linked: null,
11479
+ message: "auth_config linkage not verified \u2014 " + (serverAuthConfigIds == null ? "couldn't read the wired MCP server's auth config binding" : "Composio returned no auth_config for the connected account"),
11480
+ details: {
11481
+ accountAuthConfigId: accountAuthConfigId ?? null,
11482
+ serverAuthConfigIds: serverAuthConfigIds ?? null,
11483
+ serverId: serverId ?? null
11484
+ }
11485
+ };
11486
+ }
11487
+ if (serverAuthConfigIds.length === 0) {
11488
+ return {
11489
+ linked: false,
11490
+ message: `The agent's wired MCP server${serverLabel} has no auth config bound, so it cannot resolve the connected account (bound to auth_config ${accountAuthConfigId}) \u2014 reconnect/rebind required.`,
11491
+ details: { accountAuthConfigId, serverAuthConfigIds, serverId: serverId ?? null }
11492
+ };
11493
+ }
11494
+ if (serverAuthConfigIds.includes(accountAuthConfigId)) {
11495
+ return {
11496
+ linked: true,
11497
+ message: `Connected account's auth_config (${accountAuthConfigId}) matches the wired MCP server binding.`,
11498
+ details: { accountAuthConfigId, serverAuthConfigIds, serverId: serverId ?? null }
11499
+ };
11500
+ }
11501
+ return {
11502
+ linked: false,
11503
+ message: `The connected account is bound to auth_config ${accountAuthConfigId}, but the agent's wired MCP server${serverLabel} resolves auth_config(s) [${serverAuthConfigIds.join(", ")}] \u2014 tool calls will fail with "No connected account found". Reconnect/rebind required.`,
11504
+ details: { accountAuthConfigId, serverAuthConfigIds, serverId: serverId ?? null }
11505
+ };
11506
+ }
11507
+
11508
+ // ../../packages/core/dist/integrations/composio-account-probe.js
11509
+ var PROBE_TIMEOUT_MS2 = 1e4;
11510
+ var BODY_SNIPPET_MAX = 300;
11511
+ async function readErrorBodySnippet(res, apiKey) {
11512
+ let text;
11513
+ try {
11514
+ text = await res.text();
11515
+ } catch {
11516
+ return void 0;
11517
+ }
11518
+ const redacted = apiKey ? text.split(apiKey).join("\xABredacted\xBB") : text;
11519
+ const trimmed = redacted.trim();
11520
+ if (!trimmed)
11521
+ return void 0;
11522
+ return trimmed.length > BODY_SNIPPET_MAX ? `${trimmed.slice(0, BODY_SNIPPET_MAX)}\u2026` : trimmed;
11523
+ }
11524
+ var NULL_BODY_STATUSES2 = /* @__PURE__ */ new Set([101, 204, 205, 304]);
11525
+ var BODY_MAX_BYTES = 1024 * 1024;
11526
+ async function timedFetch2(fetchImpl, url, init) {
11527
+ const controller = new AbortController();
11528
+ const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS2);
11529
+ try {
11530
+ const res = await fetchImpl(url, { ...init, signal: controller.signal });
11531
+ if (NULL_BODY_STATUSES2.has(res.status))
11532
+ return res;
11533
+ const body = await readBoundedBody(res, controller);
11534
+ return new Response(body, {
11535
+ status: res.status,
11536
+ statusText: res.statusText,
11537
+ headers: res.headers
11538
+ });
11539
+ } finally {
11540
+ clearTimeout(timer);
11541
+ }
11542
+ }
11543
+ async function readBoundedBody(res, controller) {
11544
+ const stream = res.body;
11545
+ if (!stream)
11546
+ return await res.text();
11547
+ const reader = stream.getReader();
11548
+ const chunks = [];
11549
+ let total = 0;
11550
+ for (; ; ) {
11551
+ const { done, value } = await reader.read();
11552
+ if (done)
11553
+ break;
11554
+ if (!value)
11555
+ continue;
11556
+ total += value.byteLength;
11557
+ if (total > BODY_MAX_BYTES) {
11558
+ controller.abort();
11559
+ await reader.cancel().catch(() => {
11560
+ });
11561
+ throw new Error(`Composio response body exceeded ${BODY_MAX_BYTES} bytes`);
11562
+ }
11563
+ chunks.push(value);
11564
+ }
11565
+ const merged = new Uint8Array(total);
11566
+ let offset = 0;
11567
+ for (const chunk of chunks) {
11568
+ merged.set(chunk, offset);
11569
+ offset += chunk.byteLength;
11570
+ }
11571
+ return new TextDecoder().decode(merged);
11572
+ }
11573
+ async function probeComposioAccount(params, fetchImpl) {
11574
+ const impl = fetchImpl ?? await getComposioTransport() ?? fetch;
11575
+ const { connectedAccountId, apiKey, expectedUserId } = params;
11576
+ const base = params.apiBase ?? COMPOSIO_API_BASE;
11577
+ if (!connectedAccountId) {
11578
+ return {
11579
+ status: "down",
11580
+ message: "No connected account recorded \u2014 reconnect required"
11581
+ };
11582
+ }
11583
+ if (!apiKey || !expectedUserId) {
11584
+ return {
11585
+ status: "transient_error",
11586
+ message: "Composio probe missing api key or expected user_id"
11587
+ };
11588
+ }
11589
+ let res;
11590
+ try {
11591
+ res = await timedFetch2(impl, `${base}/api/v3/connected_accounts/${encodeURIComponent(connectedAccountId)}`, { headers: { "x-api-key": apiKey } });
11592
+ } catch (err) {
11593
+ const isAbort = err?.name === "AbortError";
11594
+ return {
11595
+ status: "transient_error",
11596
+ message: isAbort ? `Composio probe timed out after ${PROBE_TIMEOUT_MS2 / 1e3}s` : `Composio probe failed: ${err.message}`
11597
+ };
11598
+ }
11599
+ if (!res.ok) {
11600
+ if (res.status >= 500) {
11601
+ return {
11602
+ status: "transient_error",
11603
+ message: `Composio unreachable (HTTP ${res.status}) \u2014 retrying`,
11604
+ details: { connectedAccountId, httpStatus: res.status }
11605
+ };
11606
+ }
11607
+ if (res.status === 401 || res.status === 403) {
11608
+ const body = await readErrorBodySnippet(res, apiKey);
11609
+ return {
11610
+ status: "unverified",
11611
+ cause: "access_restricted",
11612
+ message: `Composio refused our API key (HTTP ${res.status}) \u2014 access restriction on our side, not a problem with this account; do not reconnect` + (body ? `. Composio said: ${body}` : ""),
11613
+ details: {
11614
+ connectedAccountId,
11615
+ httpStatus: res.status,
11616
+ ...body ? { responseBody: body } : {}
11617
+ }
11618
+ };
11619
+ }
11620
+ return {
11621
+ status: "down",
11622
+ message: `Composio account ${connectedAccountId} not found (HTTP ${res.status}) \u2014 reconnect required`,
11623
+ details: { connectedAccountId, httpStatus: res.status }
11624
+ };
11625
+ }
11626
+ let data;
11627
+ try {
11628
+ data = await res.json();
11629
+ } catch (err) {
11630
+ return {
11631
+ status: "transient_error",
11632
+ message: `Composio probe response unparseable: ${err.message}`
11633
+ };
11634
+ }
11635
+ const accountStatus = data.status ?? "unknown";
11636
+ if (accountStatus !== "ACTIVE") {
11637
+ return {
11638
+ status: "down",
11639
+ message: `Composio account ${connectedAccountId} status=${accountStatus} \u2014 reconnect required`,
11640
+ details: { connectedAccountId, status: accountStatus, boundUserId: data.user_id ?? null }
11641
+ };
11642
+ }
11643
+ const boundUserId = data.user_id;
11644
+ if (!boundUserId) {
11645
+ return {
11646
+ status: "down",
11647
+ message: `Composio account ${connectedAccountId} is ACTIVE but returned no user_id binding \u2014 runtime queries as '${expectedUserId}', so tool calls can't be confirmed`,
11648
+ details: { connectedAccountId, status: accountStatus, boundUserId: null, expectedUserId }
11649
+ };
11650
+ }
11651
+ if (boundUserId !== expectedUserId) {
11652
+ return {
11653
+ status: "down",
11654
+ message: `Composio account ${connectedAccountId} is bound to user_id '${boundUserId}' but the agent runtime queries as '${expectedUserId}' \u2014 tool calls will fail. Reconnect to bind correctly.`,
11655
+ details: { connectedAccountId, status: accountStatus, boundUserId, expectedUserId }
11770
11656
  };
11771
11657
  }
11658
+ const accountAuthConfigId = data.auth_config_id ?? data.auth_config?.id;
11659
+ if (params.serverId) {
11660
+ const serverAuthConfigIds = await fetchServerAuthConfigIds(impl, base, params.serverId, apiKey);
11661
+ const linkage = assessAuthConfigLinkage({
11662
+ accountAuthConfigId,
11663
+ serverAuthConfigIds,
11664
+ serverId: params.serverId
11665
+ });
11666
+ if (linkage.linked === false) {
11667
+ return {
11668
+ status: "down",
11669
+ message: linkage.message,
11670
+ details: { connectedAccountId, status: accountStatus, boundUserId, ...linkage.details }
11671
+ };
11672
+ }
11673
+ }
11674
+ return {
11675
+ status: "ok",
11676
+ message: `Connected (account ${connectedAccountId}, status=ACTIVE)`,
11677
+ details: {
11678
+ connectedAccountId,
11679
+ status: accountStatus,
11680
+ boundUserId,
11681
+ ...accountAuthConfigId ? { authConfigId: accountAuthConfigId } : {}
11682
+ }
11683
+ };
11684
+ }
11685
+ async function fetchServerAuthConfigIds(fetchImpl, base, serverId, apiKey) {
11686
+ let res;
11687
+ try {
11688
+ res = await timedFetch2(fetchImpl, `${base}/api/v3/mcp/${encodeURIComponent(serverId)}`, { headers: { "x-api-key": apiKey } });
11689
+ } catch {
11690
+ return null;
11691
+ }
11692
+ if (!res.ok)
11693
+ return null;
11694
+ try {
11695
+ const data = await res.json();
11696
+ return data.auth_config_ids ?? data.auth_configs?.map((c) => c.id).filter((id) => typeof id === "string" && id.length > 0) ?? [];
11697
+ } catch {
11698
+ return null;
11699
+ }
11700
+ }
11701
+
11702
+ // ../../packages/core/dist/integrations/composio-approved-egress.json
11703
+ var composio_approved_egress_default = {
11704
+ $comment: [
11705
+ "ADR-0032 Part 3, gate item 2 + gate item 3 (ENG-9684). THE single recorded source",
11706
+ "for the addresses Composio's per-key IP allowlist is expected to hold. Read by BOTH",
11707
+ "scripts/fleet-egress-enum.sh (the operator's pre-arming instrument) and the",
11708
+ "ComposioEgressAddressDrift cron (the between-armings watch). They must agree, which",
11709
+ "is why neither carries its own copy.",
11710
+ "",
11711
+ "WHY A REPO FILE AND NOT SSM. Composio's allowlist has no read API, so its contents",
11712
+ "can never be queried - only inferred from a refusal. This file is therefore not a",
11713
+ "cache of vendor state; it is a DECLARATION of what a human pasted into a dashboard.",
11714
+ "A PR is the right ceremony for changing a security boundary nobody can read back.",
11715
+ "SSM would also hit registerCron's five-parameter grant scope (ENG-9940), where a",
11716
+ "sixth parameter fails silently.",
11717
+ "",
11718
+ "THE COMPARISON IS ON `ip` AND NOTHING ELSE. `seenAsGateway` is informational. A NAT",
11719
+ "rebuild that re-adopts the same EIP keeps egress correct while changing the gateway",
11720
+ "id, so comparing ids would red on the safe case - which is precisely the case Part 2's",
11721
+ "EIP adoption exists to create.",
11722
+ "",
11723
+ "WHEN ENG-9426 LANDS THIS GOES RED UNTIL IT IS UPDATED. That is the forcing function",
11724
+ "working, not a fault. ENG-9426 adds a NAT in ap-southeast-2b, i.e. a THIRD egress",
11725
+ "address: add it here in the same change that lands it, or the detector correctly",
11726
+ "reports that hosts are leaving from an address no key allows. Do not loosen the check."
11727
+ ],
11728
+ account: "711726113003",
11729
+ homeRegion: "ap-southeast-2",
11730
+ addresses: [
11731
+ {
11732
+ ip: "32.236.6.24",
11733
+ stage: "prod",
11734
+ seenAsGateway: "nat-00a3dac969c0641a6",
11735
+ note: "Prod hosts NAT. Measured 2026-09-12 (ENG-10392): 25 of 25 active prod host instances route 0.0.0.0/0 here."
11736
+ },
11737
+ {
11738
+ ip: "15.135.204.192",
11739
+ stage: "dev",
11740
+ seenAsGateway: "nat-01122e6efaf4c4eaa",
11741
+ note: "Dev-stage hosts NAT, same account. 2 instances measured 2026-09-12; their host rows live in the dev Supabase project."
11742
+ }
11743
+ ]
11744
+ };
11745
+
11746
+ // ../../packages/core/dist/integrations/composio-approved-egress.js
11747
+ var APPROVED_EGRESS_ACCOUNT = composio_approved_egress_default.account;
11748
+ var APPROVED_EGRESS_HOME_REGION = composio_approved_egress_default.homeRegion;
11749
+ var APPROVED_EGRESS_ADDRESSES = composio_approved_egress_default.addresses;
11750
+
11751
+ // ../../packages/core/dist/integrations/composio-tool-call-probe.js
11752
+ var MCP_ACCEPT2 = "application/json, text/event-stream";
11753
+ var DEFAULT_TIMEOUT_MS2 = 1e4;
11754
+ var READONLY_VERB_TOKENS = [
11755
+ "LIST",
11756
+ "GET",
11757
+ "FIND",
11758
+ "SEARCH",
11759
+ "FETCH",
11760
+ "COUNT",
11761
+ "RETRIEVE",
11762
+ "READ"
11763
+ ];
11764
+ var ACCOUNT_RESOLUTION_ERROR_PATTERNS = [
11765
+ "no connected account",
11766
+ "connected account not found",
11767
+ "no account found",
11768
+ "could not be resolved",
11769
+ "auth config",
11770
+ "no connection found"
11771
+ ];
11772
+ var UPSTREAM_AUTH_ERROR_PATTERNS = [
11773
+ "authentication required",
11774
+ "not authenticated",
11775
+ "unauthorized",
11776
+ "authentication_error",
11777
+ "authentication error",
11778
+ "invalid authentication",
11779
+ "invalid credentials",
11780
+ "invalid api key",
11781
+ "invalid access token",
11782
+ "token expired",
11783
+ "token has expired",
11784
+ "expired access token",
11785
+ "expired credentials",
11786
+ "permission denied",
11787
+ "access denied",
11788
+ "forbidden"
11789
+ ];
11790
+ var SITE_RESOLUTION_ERROR_PATTERNS = [
11791
+ "dns resolution failed",
11792
+ "getaddrinfo",
11793
+ "enotfound",
11794
+ "could not resolve host",
11795
+ "name resolution",
11796
+ "name not resolved"
11797
+ ];
11798
+ var SCOPE_DEFICIT_ERROR_PATTERNS = [
11799
+ "insufficient_scope",
11800
+ "insufficient scope",
11801
+ "insufficient permission",
11802
+ "missing scope",
11803
+ "required scope",
11804
+ "requires the scope",
11805
+ "requires the following scope",
11806
+ "scope is required",
11807
+ "access scope",
11808
+ "oauth scope",
11809
+ // Shopify custom-app specifics.
11810
+ "read_content",
11811
+ "write_content",
11812
+ "sales channel is not enabled"
11813
+ ];
11814
+ var QUOTA_EXHAUSTED_ERROR_PATTERNS = [
11815
+ "resource_exhausted",
11816
+ "resource has been exhausted",
11817
+ "rate limit",
11818
+ "rate_limit",
11819
+ "ratelimit",
11820
+ "ratescope",
11821
+ "rate_scope",
11822
+ "too many requests",
11823
+ "quota exceeded",
11824
+ "quota_exceeded",
11825
+ "quotaexceeded",
11826
+ "quota error",
11827
+ "quotaerror",
11828
+ "quota_error",
11829
+ "exceeded your quota",
11830
+ "out of quota",
11831
+ "insufficient quota",
11832
+ "check quota",
11833
+ "usage limit",
11834
+ "daily limit exceeded"
11835
+ ];
11836
+ function isReadonlyToolDescriptor(t) {
11837
+ if (!t?.name)
11838
+ return false;
11839
+ const tokens = t.name.toUpperCase().split(/[^A-Z0-9]+/).filter(Boolean);
11840
+ const hasReadVerb = tokens.some((tok) => READONLY_VERB_TOKENS.includes(tok));
11841
+ if (!hasReadVerb)
11842
+ return false;
11843
+ const required = t.inputSchema?.required ?? [];
11844
+ return !(Array.isArray(required) && required.length > 0);
11845
+ }
11846
+ function pickSafeReadonlyTool(tools) {
11847
+ for (const t of tools) {
11848
+ if (isReadonlyToolDescriptor(t))
11849
+ return t.name;
11850
+ }
11851
+ return null;
11772
11852
  }
11773
-
11774
- // ../../packages/core/dist/integrations/connectivity-http-probes.js
11775
- var PROBE_TIMEOUT_MS2 = 1e4;
11776
- var NULL_BODY_STATUSES2 = /* @__PURE__ */ new Set([101, 204, 205, 304]);
11777
- async function timedFetch2(fetchImpl, url, init) {
11778
- const controller = new AbortController();
11779
- const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS2);
11780
- try {
11781
- const res = await fetchImpl(url, { ...init, signal: controller.signal });
11782
- if (NULL_BODY_STATUSES2.has(res.status))
11783
- return res;
11784
- const body = await res.text();
11785
- return new Response(body, {
11786
- status: res.status,
11787
- statusText: res.statusText,
11788
- headers: res.headers
11789
- });
11790
- } finally {
11791
- clearTimeout(timer);
11853
+ function resolveProbeTool(tools, override) {
11854
+ const requested = override?.tool?.trim();
11855
+ if (!requested) {
11856
+ return { toolName: pickSafeReadonlyTool(tools), args: {} };
11792
11857
  }
11858
+ const match = tools.find((t) => t?.name === requested);
11859
+ if (!match) {
11860
+ return { toolName: pickSafeReadonlyTool(tools), args: {}, fallback: "seed-drift", requestedTool: requested };
11861
+ }
11862
+ if (!isReadonlyToolDescriptor(match)) {
11863
+ return { toolName: pickSafeReadonlyTool(tools), args: {}, fallback: "seed-invalid", requestedTool: requested };
11864
+ }
11865
+ return { toolName: requested, args: override?.args ?? {}, requestedTool: requested };
11793
11866
  }
11794
- function isRateLimited(res) {
11795
- const retryAfter = res.headers.get("retry-after");
11796
- if (retryAfter !== null && retryAfter.trim() !== "")
11797
- return true;
11798
- const remaining = res.headers.get("x-ratelimit-remaining");
11799
- if (remaining === null || remaining.trim() === "")
11800
- return false;
11801
- return Number(remaining) === 0;
11867
+ function isAccountResolutionError(message) {
11868
+ const m = message.toLowerCase();
11869
+ return ACCOUNT_RESOLUTION_ERROR_PATTERNS.some((p2) => m.includes(p2));
11802
11870
  }
11803
- function statusForHttp(httpStatus, opts) {
11804
- if (httpStatus === 429)
11805
- return "transient_error";
11806
- if (httpStatus === 403 && opts?.rateLimited)
11807
- return "transient_error";
11808
- if (httpStatus === 401 || httpStatus === 403)
11809
- return "down";
11810
- if (httpStatus >= 500)
11811
- return "transient_error";
11812
- return "down";
11871
+ function isUpstreamAuthError(message) {
11872
+ const m = message.toLowerCase();
11873
+ if (UPSTREAM_AUTH_ERROR_PATTERNS.some((p2) => m.includes(p2)))
11874
+ return true;
11875
+ if (/\b(401|403)\s+client error/.test(m))
11876
+ return true;
11877
+ return /(status(?:_?code)?|http_?status(?:_code)?|mercury_last_http_status_code)["'\\\s]*[:=]["'\\\s]*(401|403)\b/.test(m);
11813
11878
  }
11814
- function causeForHttp(httpStatus, opts) {
11815
- if (httpStatus === 429)
11816
- return "rate_limited";
11817
- if (httpStatus === 403 && opts?.rateLimited)
11818
- return "rate_limited";
11819
- if (httpStatus === 401 || httpStatus === 403)
11820
- return "auth_rejected";
11821
- if (httpStatus >= 500)
11822
- return "server_error";
11823
- return "semantic";
11879
+ function isComposioFailureEnvelope(text) {
11880
+ return /["'\\]*(successful|successfull)["'\\\s]*:\s*false\b/i.test(text);
11824
11881
  }
11825
- function rateLimitMessage(provider, httpStatus) {
11826
- const subject = provider ? `${provider} rate limit` : "Rate limited by the provider";
11827
- return `${subject} (${httpStatus}) \u2014 not a credential failure`;
11882
+ function isSiteResolutionError(message) {
11883
+ const m = message.toLowerCase();
11884
+ return SITE_RESOLUTION_ERROR_PATTERNS.some((p2) => m.includes(p2));
11828
11885
  }
11829
- function redactSecret(message, secret) {
11830
- if (typeof secret !== "string" || secret.length < 4)
11831
- return message;
11832
- return message.split(secret).join("[redacted]");
11886
+ function isScopeDeficitError(message) {
11887
+ const m = message.toLowerCase();
11888
+ if (SCOPE_DEFICIT_ERROR_PATTERNS.some((p2) => m.includes(p2)))
11889
+ return true;
11890
+ return m.includes("scope") && ["doesn't have", "does not have", "not have the", "not granted", "lacks the", "not authorized for"].some((p2) => m.includes(p2));
11833
11891
  }
11834
- function networkOutcome(err, secret) {
11835
- const isAbort = err?.name === "AbortError";
11836
- const message = isAbort ? `Connection timed out after ${PROBE_TIMEOUT_MS2 / 1e3}s` : `Connection failed: ${err.message}`;
11837
- return { status: "transient_error", cause: "unreachable", message: redactSecret(message, secret) };
11892
+ function isQuotaExhaustedError(message) {
11893
+ const m = message.toLowerCase();
11894
+ if (QUOTA_EXHAUSTED_ERROR_PATTERNS.some((p2) => m.includes(p2)))
11895
+ return true;
11896
+ if (/\b429\s+client error/.test(m))
11897
+ return true;
11898
+ return /(status(?:_?code)?|http_?status(?:_code)?|mercury_last_http_status_code|["'\\]code)["'\\\s]*[:=]["'\\\s]*429\b/.test(m);
11838
11899
  }
11839
- async function probeLinear(creds, fetchImpl) {
11840
- const key = creds.api_key ?? creds.access_token;
11841
- if (!key)
11842
- return { status: "down", message: "No Linear credential present" };
11843
- try {
11844
- const res = await timedFetch2(fetchImpl, "https://api.linear.app/graphql", {
11845
- method: "POST",
11846
- headers: { "Content-Type": "application/json", Authorization: String(key) },
11847
- body: JSON.stringify({ query: "{ viewer { id name email } }" })
11848
- });
11849
- if (!res.ok) {
11850
- const rateLimited = isRateLimited(res);
11851
- const message = res.status === 429 || res.status === 403 && rateLimited ? rateLimitMessage("Linear", res.status) : `Linear API returned ${res.status}`;
11852
- return {
11853
- status: statusForHttp(res.status, { rateLimited }),
11854
- cause: causeForHttp(res.status, { rateLimited }),
11855
- message
11856
- };
11857
- }
11858
- const body = await res.json();
11859
- if (body.errors?.length)
11860
- return { status: "down", message: body.errors[0]?.message ?? "Unknown Linear error" };
11861
- const viewer = body.data?.viewer;
11862
- if (!viewer)
11863
- return { status: "down", message: "Invalid key \u2014 no viewer returned" };
11864
- return { status: "ok", message: `Connected as ${viewer.name ?? viewer.email ?? "unknown"}` };
11865
- } catch (err) {
11866
- return networkOutcome(err, String(key ?? ""));
11900
+ function extractRetryAfterSeconds(message) {
11901
+ const m = message.toLowerCase();
11902
+ const patterns = [
11903
+ /retry\s+in\s+(\d+)\s*(?:s\b|sec\b|secs\b|second)/,
11904
+ /retry[-_ ]?after["'\\\s]*[:=]["'\\\s]*(\d+)/,
11905
+ /retry_?delay["'\\\s]*[:=]["'\\\s]*"?(\d+)s?/
11906
+ ];
11907
+ for (const re of patterns) {
11908
+ const hit = re.exec(m);
11909
+ if (!hit?.[1])
11910
+ continue;
11911
+ const seconds = Number(hit[1]);
11912
+ if (Number.isFinite(seconds) && seconds > 0)
11913
+ return seconds;
11867
11914
  }
11915
+ return null;
11868
11916
  }
11869
- async function probeBearerJson(url, creds, fetchImpl, interpret, extraHeaders) {
11870
- const token = creds.access_token ?? creds.api_key;
11871
- if (!token)
11872
- return { status: "down", message: "No credential present" };
11873
- try {
11874
- const res = await timedFetch2(fetchImpl, url, { headers: { Authorization: `Bearer ${token}`, ...extraHeaders } });
11875
- if (!res.ok) {
11876
- const rateLimited = isRateLimited(res);
11877
- const message = res.status === 401 ? "Token expired or revoked \u2014 reconnect required" : res.status === 429 || res.status === 403 && rateLimited ? rateLimitMessage(null, res.status) : `API returned ${res.status}`;
11878
- return {
11879
- status: statusForHttp(res.status, { rateLimited }),
11880
- cause: causeForHttp(res.status, { rateLimited }),
11881
- message
11882
- };
11917
+ function formatRetryWindow(seconds) {
11918
+ if (seconds < 90)
11919
+ return `${seconds} seconds`;
11920
+ const minutes = seconds / 60;
11921
+ if (minutes < 90)
11922
+ return `about ${Math.round(minutes)} minutes`;
11923
+ const hours = minutes / 60;
11924
+ if (hours < 24)
11925
+ return `about ${Math.round(hours * 10) / 10} hours`;
11926
+ return `about ${Math.round(hours / 24 * 10) / 10} days`;
11927
+ }
11928
+ function classifyToolCallFailure(text) {
11929
+ if (isAccountResolutionError(text))
11930
+ return "account";
11931
+ if (isQuotaExhaustedError(text))
11932
+ return "quota";
11933
+ if (isScopeDeficitError(text))
11934
+ return "scope";
11935
+ if (isUpstreamAuthError(text))
11936
+ return "auth";
11937
+ if (isSiteResolutionError(text))
11938
+ return "site";
11939
+ return "benign";
11940
+ }
11941
+ async function parseRpc2(res, expectedId) {
11942
+ const ct = res.headers.get("content-type") ?? "";
11943
+ if (ct.includes("text/event-stream")) {
11944
+ const text = await res.text();
11945
+ let dataLines = [];
11946
+ for (const rawLine of text.split(/\r?\n/)) {
11947
+ if (rawLine.startsWith("data:")) {
11948
+ dataLines.push(rawLine.slice(5).trimStart());
11949
+ continue;
11950
+ }
11951
+ if (rawLine === "" && dataLines.length > 0) {
11952
+ try {
11953
+ const msg2 = JSON.parse(dataLines.join("\n"));
11954
+ if (("result" in msg2 || "error" in msg2) && msg2["id"] === expectedId)
11955
+ return msg2;
11956
+ } catch {
11957
+ }
11958
+ dataLines = [];
11959
+ }
11883
11960
  }
11884
- return interpret(await res.json());
11885
- } catch (err) {
11886
- return networkOutcome(err, String(token ?? ""));
11961
+ return null;
11887
11962
  }
11963
+ const msg = await res.json().catch(() => null);
11964
+ if (msg && ("result" in msg || "error" in msg) && msg["id"] === expectedId)
11965
+ return msg;
11966
+ return null;
11888
11967
  }
11889
- async function probeBuffer(creds, fetchImpl) {
11890
- const key = creds.api_key ?? creds.access_token;
11891
- if (!key)
11892
- return { status: "down", message: "No Buffer credential present" };
11968
+ async function probeComposioMcpToolCall(config, fetchImpl = fetch) {
11969
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
11970
+ const baseHeaders = {
11971
+ ...config.headers ?? {},
11972
+ "Content-Type": "application/json",
11973
+ Accept: MCP_ACCEPT2
11974
+ };
11893
11975
  try {
11894
- const res = await timedFetch2(fetchImpl, "https://api.buffer.com", {
11976
+ const initRes = await fetchImpl(config.url, {
11895
11977
  method: "POST",
11896
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
11897
- body: JSON.stringify({ query: "{ account { organizations { id name } } }" })
11978
+ headers: baseHeaders,
11979
+ body: JSON.stringify({
11980
+ jsonrpc: "2.0",
11981
+ id: 1,
11982
+ method: "initialize",
11983
+ params: {
11984
+ protocolVersion: "2025-03-26",
11985
+ capabilities: {},
11986
+ clientInfo: { name: "augmented-toolcall-probe", version: "1.0.0" }
11987
+ }
11988
+ }),
11989
+ signal: AbortSignal.timeout(timeoutMs)
11898
11990
  });
11899
- if (!res.ok) {
11900
- const rateLimited = isRateLimited(res);
11901
- const message = res.status === 401 ? (
11902
- // ENG-9809: name the mechanism, because the bare "expired or revoked" sent
11903
- // two investigations at the wrong thing. Buffer API keys carry a MANDATORY
11904
- // lifetime chosen at mint time — 7/30/60/90 days or 1 year, defaulting to
11905
- // 30 days, with no "never expires" option
11906
- // (support.buffer.com/article/984-how-to-create-your-buffer-api-key). So
11907
- // every Buffer install on the fleet is on a countdown, and expiry is the
11908
- // ordinary end of a key's life rather than a customer revoking one.
11909
- //
11910
- // All three causes are named, because this one branch answers every Buffer
11911
- // 401 — a key that lapsed, one that was revoked, and one that was simply
11912
- // mistyped ("Requests without a valid key will return a 401 Unauthorized",
11913
- // developers.buffer.com/guides/authentication.html). The status code alone
11914
- // cannot separate them, so naming expiry as the LIKELIER cause is supported;
11915
- // asserting it is not, and a message that omitted "invalid" would send
11916
- // someone who pasted the key wrong looking for an expiry date instead.
11917
- "Buffer API key expired, invalid, or revoked \u2014 reconnect required. Buffer keys expire by design (30-day default, 1-year maximum), so on a key that was working until now expiry is the likeliest of the three; pick 1 year when minting the replacement."
11918
- ) : res.status === 429 || res.status === 403 && rateLimited ? rateLimitMessage("Buffer", res.status) : `Buffer API returned ${res.status}`;
11919
- return {
11920
- status: statusForHttp(res.status, { rateLimited }),
11921
- cause: causeForHttp(res.status, { rateLimited }),
11922
- message
11923
- };
11991
+ if (!initRes.ok) {
11992
+ return initRes.status >= 500 ? { status: "transient_error", message: `MCP initialize returned ${initRes.status}` } : null;
11924
11993
  }
11925
- const body = await res.json();
11926
- if (body.errors?.length)
11927
- return { status: "down", message: body.errors[0]?.message ?? "Unknown Buffer error" };
11928
- const orgs = body.data?.account?.organizations ?? [];
11929
- if (!orgs.length)
11930
- return { status: "down", message: "No Buffer organizations on this account" };
11931
- return { status: "ok", message: `Connected to ${orgs[0]?.name ?? "Buffer"}` };
11932
- } catch (err) {
11933
- return networkOutcome(err, String(key ?? ""));
11934
- }
11935
- }
11936
- async function probeVercel(creds, fetchImpl) {
11937
- const token = creds.api_key ?? creds.access_token;
11938
- if (!token)
11939
- return { status: "down", message: "No Vercel credential present" };
11940
- try {
11941
- const res = await timedFetch2(fetchImpl, "https://api.vercel.com/v2/user", {
11942
- headers: { Authorization: `Bearer ${token}` }
11994
+ const sessionId = initRes.headers.get("mcp-session-id");
11995
+ await parseRpc2(initRes, 1);
11996
+ const sessionHeaders = { ...baseHeaders, ...sessionId ? { "Mcp-Session-Id": sessionId } : {} };
11997
+ const initializedRes = await fetchImpl(config.url, {
11998
+ method: "POST",
11999
+ headers: sessionHeaders,
12000
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
12001
+ signal: AbortSignal.timeout(5e3)
11943
12002
  });
11944
- if (!res.ok) {
11945
- const message = res.status === 429 ? rateLimitMessage("Vercel", res.status) : res.status === 401 || res.status === 403 ? `Vercel rejected the token (${res.status}) \u2014 invalid or revoked API token` : `Vercel API returned ${res.status}`;
11946
- return {
11947
- status: statusForHttp(res.status),
11948
- cause: causeForHttp(res.status),
11949
- message
11950
- };
12003
+ await initializedRes.text().catch(() => "");
12004
+ const listRes = await fetchImpl(config.url, {
12005
+ method: "POST",
12006
+ headers: sessionHeaders,
12007
+ body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }),
12008
+ signal: AbortSignal.timeout(timeoutMs)
12009
+ });
12010
+ if (!listRes.ok) {
12011
+ return listRes.status >= 500 ? { status: "transient_error", message: `MCP tools/list returned ${listRes.status}` } : null;
11951
12012
  }
11952
- const body = await res.json();
11953
- const user = body?.user;
11954
- const identity = [user?.username, user?.email, user?.name, user?.id].find((value) => typeof value === "string" && value.trim().length > 0);
11955
- if (!identity)
11956
- return { status: "down", message: "Vercel returned no user for this token" };
11957
- return { status: "ok", message: `Connected as ${identity}` };
11958
- } catch (err) {
11959
- return networkOutcome(err, String(token ?? ""));
11960
- }
11961
- }
11962
- async function probeHiggsfield(creds, fetchImpl) {
11963
- const token = creds.api_key ?? creds.access_token;
11964
- if (!token)
11965
- return { status: "down", message: "No Higgsfield credential present" };
11966
- if (!token.includes(":")) {
11967
- return {
11968
- status: "down",
11969
- message: "Higgsfield credential is not in KEY_ID:KEY_SECRET form \u2014 re-paste it from cloud.higgsfield.ai/api-keys"
12013
+ const listRpc = await parseRpc2(listRes, 2);
12014
+ const tools = listRpc?.["result"]?.tools ?? [];
12015
+ const resolved = resolveProbeTool(tools, { tool: config.toolName, args: config.toolArgs });
12016
+ const toolName = resolved.toolName;
12017
+ if (!toolName)
12018
+ return null;
12019
+ const baseDetails = {
12020
+ tool: toolName,
12021
+ ...resolved.fallback ? { override_fallback: resolved.fallback, requested_tool: resolved.requestedTool } : {}
11970
12022
  };
11971
- }
11972
- try {
11973
- const res = await timedFetch2(fetchImpl, "https://platform.higgsfield.ai/v1/motions", {
11974
- headers: { Authorization: `Key ${token}` }
12023
+ const callRes = await fetchImpl(config.url, {
12024
+ method: "POST",
12025
+ headers: sessionHeaders,
12026
+ body: JSON.stringify({
12027
+ jsonrpc: "2.0",
12028
+ id: 3,
12029
+ method: "tools/call",
12030
+ params: { name: toolName, arguments: resolved.args }
12031
+ }),
12032
+ signal: AbortSignal.timeout(timeoutMs)
11975
12033
  });
11976
- if (!res.ok) {
11977
- const body = await res.text().catch(() => "");
11978
- if (res.status === 403 && /credit/i.test(body)) {
12034
+ if (!callRes.ok) {
12035
+ return callRes.status >= 500 ? { status: "transient_error", message: `MCP tools/call returned ${callRes.status}` } : null;
12036
+ }
12037
+ const callRpc = await parseRpc2(callRes, 3);
12038
+ {
12039
+ const errText = callRpc?.["error"]?.message;
12040
+ const resContent = callRpc?.["result"]?.content;
12041
+ const raw = errText ?? (resContent ?? []).map((c) => c.text ?? "").join(" ").trim();
12042
+ if (raw)
12043
+ baseDetails.response = raw.length > 2e3 ? `${raw.slice(0, 2e3)}\u2026` : raw;
12044
+ }
12045
+ const rpcErrMsg = callRpc && "error" in callRpc ? callRpc["error"]?.message ?? "" : "";
12046
+ const result = callRpc?.["result"];
12047
+ const contentText2 = (result?.content ?? []).map((c) => c.text ?? "").join(" ").trim();
12048
+ const failed = Boolean(rpcErrMsg) || Boolean(result?.isError) || isComposioFailureEnvelope(contentText2);
12049
+ if (failed) {
12050
+ const failureText = [rpcErrMsg, contentText2].filter(Boolean).join(" ");
12051
+ const snippet = failureText.length > 200 ? `${failureText.slice(0, 200)}\u2026` : failureText;
12052
+ const kind = classifyToolCallFailure(failureText);
12053
+ if (kind === "account") {
12054
+ return {
12055
+ status: "down",
12056
+ message: `Live tool call '${toolName}' failed to resolve the connected account: ${snippet}`,
12057
+ details: baseDetails
12058
+ };
12059
+ }
12060
+ if (kind === "quota") {
12061
+ const retryAfterSeconds = extractRetryAfterSeconds(failureText);
12062
+ const retryPhrase = retryAfterSeconds ? ` The provider says to retry in ${formatRetryWindow(retryAfterSeconds)}.` : "";
11979
12063
  return {
11980
12064
  status: "degraded",
11981
- message: "Higgsfield authenticated, but the account is out of credits \u2014 generation will fail"
12065
+ message: `Live tool call '${toolName}' reached the provider and was refused: a quota or rate limit is exhausted. The connection itself is valid \u2014 do NOT reconnect, this resolves when the window resets.${retryPhrase} ${snippet}`,
12066
+ details: {
12067
+ ...baseDetails,
12068
+ reason: "quota_exhausted",
12069
+ ...retryAfterSeconds ? { retry_after_seconds: retryAfterSeconds } : {}
12070
+ }
12071
+ };
12072
+ }
12073
+ if (kind === "scope") {
12074
+ return {
12075
+ status: "degraded",
12076
+ message: `Live tool call '${toolName}' was refused for a missing permission \u2014 the connection is valid, but its app/token isn't granted the scope this tool needs. Grant the required scope in the provider app and re-authorise (this is NOT a reconnect): ${snippet}`,
12077
+ details: { ...baseDetails, reason: "scope_deficit" }
12078
+ };
12079
+ }
12080
+ if (kind === "auth") {
12081
+ return {
12082
+ status: "down",
12083
+ message: `Live tool call '${toolName}' was rejected by the provider \u2014 the connection's credential is no longer valid (reconnect required): ${snippet}`,
12084
+ details: { ...baseDetails, reason: "upstream_auth_rejected" }
12085
+ };
12086
+ }
12087
+ if (kind === "site") {
12088
+ return {
12089
+ status: "down",
12090
+ message: `Live tool call '${toolName}' couldn't reach the provider's site - the connection has no valid site URL (reconnect and make sure a site/workspace is granted): ${snippet}`,
12091
+ details: { ...baseDetails, reason: "site_unresolved" }
11982
12092
  };
11983
12093
  }
11984
- const message = res.status === 429 ? rateLimitMessage("Higgsfield", res.status) : res.status === 401 || res.status === 403 ? `Higgsfield rejected the key pair (${res.status}) \u2014 invalid or revoked API key` : `Higgsfield API returned ${res.status}`;
11985
12094
  return {
11986
- status: statusForHttp(res.status),
11987
- cause: causeForHttp(res.status),
11988
- message
12095
+ status: "ok",
12096
+ message: `Live tool call '${toolName}' resolved the account (tool error: ${snippet})`,
12097
+ details: { ...baseDetails, tool_error: snippet }
11989
12098
  };
11990
12099
  }
11991
- const motions = await res.json();
11992
- if (!Array.isArray(motions) || motions.length === 0) {
11993
- return { status: "down", message: "Higgsfield returned no motion presets for this key" };
11994
- }
11995
- return { status: "ok", message: `Higgsfield reachable \u2014 ${motions.length} motion presets` };
12100
+ return { status: "ok", message: `Live tool call '${toolName}' resolved the connected account`, details: baseDetails };
11996
12101
  } catch (err) {
11997
- return networkOutcome(err, String(token ?? ""));
11998
- }
11999
- }
12000
- async function probeSportsyear(creds, fetchImpl) {
12001
- const key = creds.api_key ?? creds.access_token;
12002
- if (!key)
12003
- return { status: "down", message: "No Sportsyear credential present" };
12004
- let authorization;
12005
- try {
12006
- authorization = basicUsernameAuthorization(String(key));
12007
- } catch {
12102
+ const isAbort = err?.name === "TimeoutError" || err?.name === "AbortError";
12008
12103
  return {
12009
- status: "down",
12010
- message: 'Sportsyear credential is not a bare API key (it contains ":") \u2014 paste the key alone, not email:password'
12104
+ status: "transient_error",
12105
+ message: isAbort ? `MCP tool-call probe timed out after ${timeoutMs / 1e3}s` : `MCP tool-call probe failed: ${err.message}`
12011
12106
  };
12012
12107
  }
12013
- const encoded = authorization.slice("Basic ".length);
12014
- try {
12015
- const res = await timedFetch2(fetchImpl, "https://sportsyear.com.au/api/v1/member", {
12016
- headers: { Authorization: authorization, Accept: "application/json" }
12017
- });
12018
- if (!res.ok) {
12019
- const message = res.status === 429 ? rateLimitMessage("Sportsyear", res.status) : res.status === 401 || res.status === 403 ? `Sportsyear rejected the API key (${res.status}) \u2014 removed, regenerated, or API access disabled on the member account` : `Sportsyear API returned ${res.status}`;
12020
- return {
12021
- status: statusForHttp(res.status),
12022
- cause: causeForHttp(res.status),
12023
- message
12024
- };
12025
- }
12026
- const body = await res.json();
12027
- const member = typeof body === "object" && body !== null && !Array.isArray(body) ? body : null;
12028
- const id = member?.id;
12029
- if (typeof id !== "number" && !(typeof id === "string" && id.trim() !== "")) {
12030
- return { status: "down", message: "Sportsyear returned no member account for this API key" };
12031
- }
12032
- const label = typeof member?.organisation === "string" && member.organisation.trim() !== "" ? member.organisation.trim() : `member ${String(id)}`;
12033
- return { status: "ok", message: `Reached Sportsyear as ${redactSecret(redactSecret(label, String(key)), encoded)}` };
12034
- } catch (err) {
12035
- const outcome = networkOutcome(err, String(key));
12036
- return outcome.message === void 0 ? outcome : { ...outcome, message: redactSecret(outcome.message, encoded) };
12037
- }
12038
- }
12039
- async function probeHttpProvider(definitionId, credentials, fetchImpl = fetch) {
12040
- switch (definitionId) {
12041
- case "linear":
12042
- return probeLinear(credentials, fetchImpl);
12043
- case "buffer":
12044
- return probeBuffer(credentials, fetchImpl);
12045
- case "vercel":
12046
- return probeVercel(credentials, fetchImpl);
12047
- case "higgsfield":
12048
- return probeHiggsfield(credentials, fetchImpl);
12049
- case "sportsyear":
12050
- return probeSportsyear(credentials, fetchImpl);
12051
- case "google-workspace":
12052
- return probeBearerJson("https://www.googleapis.com/oauth2/v2/userinfo", credentials, fetchImpl, (body) => {
12053
- const info = body;
12054
- return { status: "ok", message: `Connected as ${info.name ?? info.email ?? "unknown"}` };
12055
- });
12056
- case "xero":
12057
- return probeBearerJson("https://api.xero.com/connections", credentials, fetchImpl, (body) => {
12058
- const conns = body ?? [];
12059
- if (!conns.length)
12060
- return { status: "down", message: "No Xero organisations connected" };
12061
- return { status: "ok", message: `Connected to ${conns[0]?.tenantName ?? "Xero"}` };
12062
- });
12063
- case "linkedin-ads":
12064
- return probeBearerJson("https://api.linkedin.com/v2/userinfo", credentials, fetchImpl, (body) => {
12065
- const user = body;
12066
- return { status: "ok", message: `Connected as ${user.name ?? user.email ?? "unknown"}` };
12067
- });
12068
- case "v0":
12069
- return probeBearerJson("https://api.v0.dev/v1/user", credentials, fetchImpl, (body) => {
12070
- const user = body;
12071
- return { status: "ok", message: `Connected as ${user.name ?? user.email ?? "unknown"}` };
12072
- });
12073
- case "sprout-social":
12074
- return probeBearerJson("https://api.sproutsocial.com/v1/metadata/client", credentials, fetchImpl, (body) => {
12075
- const data = body?.data;
12076
- const clients = Array.isArray(data) ? data.filter((row) => {
12077
- if (typeof row !== "object" || row === null)
12078
- return false;
12079
- const id = row.customer_id;
12080
- return typeof id === "string" || typeof id === "number";
12081
- }) : [];
12082
- if (!clients.length) {
12083
- return { status: "down", message: "Sprout Social returned no usable client account for this token" };
12084
- }
12085
- return {
12086
- status: "ok",
12087
- message: `Reached Sprout Social \u2014 ${clients.length} client account(s), incl. ${clients[0]?.name ?? "unnamed"}`
12088
- };
12089
- });
12090
- case "github":
12091
- return probeBearerJson("https://api.github.com/user", credentials, fetchImpl, (body) => {
12092
- const u = body;
12093
- return { status: "ok", message: `Reached GitHub as ${u.login ?? u.name ?? "unknown"}` };
12094
- }, { "User-Agent": "augmented-team-connectivity-probe", "X-GitHub-Api-Version": "2026-03-10" });
12095
- default:
12096
- return null;
12097
- }
12098
12108
  }
12099
12109
 
12100
12110
  // ../../packages/core/dist/integrations/probe-path.js
@@ -14667,10 +14677,10 @@ export {
14667
14677
  bestConnectivityEvidence,
14668
14678
  worseConnectivityOutcome,
14669
14679
  resolveConnectivityProbe,
14680
+ probeHttpProvider,
14670
14681
  probeMcpHttp,
14671
14682
  probeComposioAccount,
14672
14683
  probeComposioMcpToolCall,
14673
- probeHttpProvider,
14674
14684
  PROBE_TOOLKIT_PATH_DIRS,
14675
14685
  PROBE_HOME_RELATIVE_PATH_SEGMENTS,
14676
14686
  PROBE_SYSTEM_PATH_FLOOR,
@@ -14769,4 +14779,4 @@ export {
14769
14779
  peekCurrentSession,
14770
14780
  readDailySessionPin
14771
14781
  };
14772
- //# sourceMappingURL=chunk-4ZRXKEXO.js.map
14782
+ //# sourceMappingURL=chunk-2TACYBMQ.js.map