@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.
- package/dist/bin/agt.js +5 -5
- package/dist/{chunk-4ZRXKEXO.js → chunk-2TACYBMQ.js} +998 -988
- package/dist/{chunk-4ZRXKEXO.js.map → chunk-2TACYBMQ.js.map} +1 -1
- package/dist/{chunk-5LIYT2YF.js → chunk-P6BSGYZQ.js} +2 -2
- package/dist/{chunk-QEGO2ZVT.js → chunk-QA5YZKYS.js} +12 -5
- package/dist/{chunk-QEGO2ZVT.js.map → chunk-QA5YZKYS.js.map} +1 -1
- package/dist/{claude-pair-runtime-7PLWWVWK.js → claude-pair-runtime-W2BYBJNB.js} +2 -2
- package/dist/lib/manager-worker.js +14 -14
- package/dist/{persistent-session-BBN5CBFI.js → persistent-session-V3O2E6G3.js} +3 -3
- package/dist/{responsiveness-probe-5GAOO7P4.js → responsiveness-probe-AVXRR6G6.js} +3 -3
- package/dist/{session-auth-dead-FRVOFPZL.js → session-auth-dead-2M5724BT.js} +2 -2
- package/package.json +1 -1
- /package/dist/{chunk-5LIYT2YF.js.map → chunk-P6BSGYZQ.js.map} +0 -0
- /package/dist/{claude-pair-runtime-7PLWWVWK.js.map → claude-pair-runtime-W2BYBJNB.js.map} +0 -0
- /package/dist/{persistent-session-BBN5CBFI.js.map → persistent-session-V3O2E6G3.js.map} +0 -0
- /package/dist/{responsiveness-probe-5GAOO7P4.js.map → responsiveness-probe-AVXRR6G6.js.map} +0 -0
- /package/dist/{session-auth-dead-FRVOFPZL.js.map → session-auth-dead-2M5724BT.js.map} +0 -0
|
@@ -10962,231 +10962,9 @@ function resolveConnectivityProbe(input) {
|
|
|
10962
10962
|
}
|
|
10963
10963
|
}
|
|
10964
10964
|
|
|
10965
|
-
// ../../packages/core/dist/integrations/
|
|
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
|
|
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
|
-
|
|
11208
|
-
const
|
|
11209
|
-
if (
|
|
11210
|
-
return
|
|
11211
|
-
const
|
|
11212
|
-
|
|
11213
|
-
|
|
11214
|
-
|
|
11215
|
-
|
|
11216
|
-
|
|
11217
|
-
|
|
11218
|
-
|
|
11219
|
-
|
|
11220
|
-
|
|
11221
|
-
|
|
11222
|
-
|
|
11223
|
-
|
|
11224
|
-
|
|
11225
|
-
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
11239
|
-
const
|
|
11240
|
-
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
|
|
11251
|
-
|
|
11252
|
-
|
|
11253
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
11264
|
-
|
|
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:
|
|
11267
|
-
|
|
11268
|
-
|
|
11070
|
+
status: statusForHttp(res.status, { rateLimited }),
|
|
11071
|
+
cause: causeForHttp(res.status, { rateLimited }),
|
|
11072
|
+
message
|
|
11269
11073
|
};
|
|
11270
11074
|
}
|
|
11271
|
-
|
|
11272
|
-
|
|
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:
|
|
11275
|
-
cause:
|
|
11276
|
-
message
|
|
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
|
-
|
|
11285
|
-
|
|
11286
|
-
|
|
11287
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
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 (
|
|
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:
|
|
11333
|
-
|
|
11334
|
-
|
|
11177
|
+
status: statusForHttp(res.status),
|
|
11178
|
+
cause: causeForHttp(res.status),
|
|
11179
|
+
message
|
|
11335
11180
|
};
|
|
11336
11181
|
}
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
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
|
|
11350
|
-
|
|
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
|
-
|
|
11197
|
+
authorization = basicUsernameAuthorization(String(key));
|
|
11353
11198
|
} catch {
|
|
11354
|
-
return
|
|
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
|
-
|
|
11357
|
-
return null;
|
|
11204
|
+
const encoded = authorization.slice("Basic ".length);
|
|
11358
11205
|
try {
|
|
11359
|
-
const
|
|
11360
|
-
|
|
11361
|
-
|
|
11362
|
-
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
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
|
-
|
|
11411
|
-
|
|
11412
|
-
|
|
11413
|
-
|
|
11414
|
-
|
|
11415
|
-
|
|
11416
|
-
|
|
11417
|
-
|
|
11418
|
-
|
|
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
|
|
11511
|
-
|
|
11512
|
-
|
|
11513
|
-
return
|
|
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
|
-
|
|
11518
|
-
|
|
11519
|
-
|
|
11520
|
-
|
|
11521
|
-
|
|
11522
|
-
|
|
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
|
|
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 === ""
|
|
11616
|
-
|
|
11617
|
-
|
|
11618
|
-
|
|
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
|
|
11325
|
+
return tryFrame();
|
|
11626
11326
|
}
|
|
11627
11327
|
const msg = await res.json().catch(() => null);
|
|
11628
|
-
|
|
11629
|
-
return msg;
|
|
11630
|
-
return null;
|
|
11328
|
+
return isRpcEnvelopeFor(msg, expectedId) ? msg : null;
|
|
11631
11329
|
}
|
|
11632
|
-
|
|
11633
|
-
const
|
|
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:
|
|
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-
|
|
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
|
|
11657
|
-
}
|
|
11368
|
+
if (!initRes.ok)
|
|
11369
|
+
return httpStatusOutcome(initRes.status, "initialize");
|
|
11658
11370
|
const sessionId = initRes.headers.get("mcp-session-id");
|
|
11659
|
-
await
|
|
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
|
|
11676
|
-
|
|
11677
|
-
|
|
11678
|
-
|
|
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
|
-
|
|
11702
|
-
|
|
11703
|
-
|
|
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
|
|
11710
|
-
const
|
|
11711
|
-
const
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
const
|
|
11715
|
-
const
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
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 (
|
|
11725
|
-
const
|
|
11726
|
-
|
|
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
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
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:
|
|
11761
|
-
details: { ...
|
|
11438
|
+
message: `${testTool} succeeded`,
|
|
11439
|
+
details: { ...toolCount !== void 0 ? { toolCount } : {}, testTool }
|
|
11762
11440
|
};
|
|
11763
11441
|
}
|
|
11764
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
11775
|
-
|
|
11776
|
-
|
|
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
|
|
11795
|
-
const
|
|
11796
|
-
|
|
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
|
|
11804
|
-
|
|
11805
|
-
|
|
11806
|
-
|
|
11807
|
-
|
|
11808
|
-
|
|
11809
|
-
|
|
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
|
|
11815
|
-
|
|
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
|
|
11826
|
-
const
|
|
11827
|
-
return
|
|
11882
|
+
function isSiteResolutionError(message) {
|
|
11883
|
+
const m = message.toLowerCase();
|
|
11884
|
+
return SITE_RESOLUTION_ERROR_PATTERNS.some((p2) => m.includes(p2));
|
|
11828
11885
|
}
|
|
11829
|
-
function
|
|
11830
|
-
|
|
11831
|
-
|
|
11832
|
-
|
|
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
|
|
11835
|
-
const
|
|
11836
|
-
|
|
11837
|
-
|
|
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
|
-
|
|
11840
|
-
const
|
|
11841
|
-
|
|
11842
|
-
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
|
|
11851
|
-
|
|
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
|
-
|
|
11870
|
-
|
|
11871
|
-
|
|
11872
|
-
|
|
11873
|
-
|
|
11874
|
-
|
|
11875
|
-
|
|
11876
|
-
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
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
|
|
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
|
|
11890
|
-
const
|
|
11891
|
-
|
|
11892
|
-
|
|
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
|
|
11976
|
+
const initRes = await fetchImpl(config.url, {
|
|
11895
11977
|
method: "POST",
|
|
11896
|
-
headers:
|
|
11897
|
-
body: JSON.stringify({
|
|
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 (!
|
|
11900
|
-
|
|
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
|
|
11926
|
-
|
|
11927
|
-
|
|
11928
|
-
const
|
|
11929
|
-
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
|
|
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
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
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
|
|
11953
|
-
const
|
|
11954
|
-
const
|
|
11955
|
-
|
|
11956
|
-
|
|
11957
|
-
|
|
11958
|
-
|
|
11959
|
-
|
|
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
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
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 (!
|
|
11977
|
-
|
|
11978
|
-
|
|
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:
|
|
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:
|
|
11987
|
-
|
|
11988
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
12010
|
-
message:
|
|
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-
|
|
14782
|
+
//# sourceMappingURL=chunk-2TACYBMQ.js.map
|