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