@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.
@@ -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\` (read before editing) \xB7
782
- \`mcp__augmented__skill_list\` \xB7 \`mcp__augmented__skill_improve\` (targeted edits)
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/mcp-http-probe.js
10966
- var MCP_ACCEPT = "application/json, text/event-stream";
10967
- var DEFAULT_TIMEOUT_MS = 1e4;
10968
- function isRpcEnvelopeFor(msg, expectedId) {
10969
- return typeof msg === "object" && msg !== null && ("result" in msg || "error" in msg) && msg["id"] === expectedId;
10970
- }
10971
- async function parseRpc(res, expectedId) {
10972
- const ct = res.headers.get("content-type") ?? "";
10973
- if (ct.includes("text/event-stream")) {
10974
- const text = await res.text();
10975
- let dataLines = [];
10976
- const tryFrame = () => {
10977
- if (dataLines.length === 0)
10978
- return null;
10979
- try {
10980
- const msg2 = JSON.parse(dataLines.join("\n"));
10981
- if (isRpcEnvelopeFor(msg2, expectedId))
10982
- return msg2;
10983
- } catch {
10984
- }
10985
- return null;
10986
- };
10987
- for (const rawLine of text.split(/\r?\n/)) {
10988
- if (rawLine.startsWith("data:")) {
10989
- dataLines.push(rawLine.slice(5).trimStart());
10990
- continue;
10991
- }
10992
- if (rawLine === "") {
10993
- const frame = tryFrame();
10994
- if (frame)
10995
- return frame;
10996
- dataLines = [];
10997
- }
10998
- }
10999
- return tryFrame();
11000
- }
11001
- const msg = await res.json().catch(() => null);
11002
- return isRpcEnvelopeFor(msg, expectedId) ? msg : null;
11003
- }
11004
- function httpStatusOutcome(status, step) {
11005
- if (status === 401 || status === 403) {
11006
- return { status: "down", message: `MCP ${step} unauthorized (${status}) \u2014 reconnect required` };
11007
- }
11008
- if (status >= 500) {
11009
- return { status: "transient_error", message: `MCP ${step} returned ${status}` };
11010
- }
11011
- return { status: "down", message: `MCP ${step} returned ${status}` };
11012
- }
11013
- async function probeMcpHttp(config, fetchImpl = fetch) {
11014
- const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
11015
- const baseHeaders = {
11016
- ...config.headers ?? {},
11017
- "Content-Type": "application/json",
11018
- Accept: MCP_ACCEPT
11019
- };
11020
- try {
11021
- const initRes = await fetchImpl(config.url, {
11022
- method: "POST",
11023
- headers: baseHeaders,
11024
- body: JSON.stringify({
11025
- jsonrpc: "2.0",
11026
- id: 1,
11027
- method: "initialize",
11028
- params: {
11029
- protocolVersion: "2025-03-26",
11030
- capabilities: {},
11031
- clientInfo: { name: "augmented-connectivity-probe", version: "1.0.0" }
11032
- }
11033
- }),
11034
- signal: AbortSignal.timeout(timeoutMs)
11035
- });
11036
- if (!initRes.ok)
11037
- return httpStatusOutcome(initRes.status, "initialize");
11038
- const sessionId = initRes.headers.get("mcp-session-id");
11039
- const initRpc = await parseRpc(initRes, 1);
11040
- if (!initRpc) {
11041
- return { status: "down", message: "MCP initialize returned a non-JSON-RPC response \u2014 not an MCP server" };
11042
- }
11043
- if ("error" in initRpc) {
11044
- const err = initRpc["error"];
11045
- return { status: "down", message: `MCP initialize error: ${err?.message ?? "unknown"}` };
11046
- }
11047
- const sessionHeaders = { ...baseHeaders, ...sessionId ? { "Mcp-Session-Id": sessionId } : {} };
11048
- const initializedRes = await fetchImpl(config.url, {
11049
- method: "POST",
11050
- headers: sessionHeaders,
11051
- body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
11052
- signal: AbortSignal.timeout(5e3)
11053
- });
11054
- if (!initializedRes.ok)
11055
- return httpStatusOutcome(initializedRes.status, "initialized");
11056
- await initializedRes.text().catch(() => "");
11057
- const listRes = await fetchImpl(config.url, {
11058
- method: "POST",
11059
- headers: sessionHeaders,
11060
- body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }),
11061
- signal: AbortSignal.timeout(timeoutMs)
11062
- });
11063
- if (!listRes.ok)
11064
- return httpStatusOutcome(listRes.status, "tools/list");
11065
- const rpc = await parseRpc(listRes, 2);
11066
- if (!rpc) {
11067
- return { status: "down", message: "MCP tools/list returned a non-JSON-RPC response \u2014 not an MCP server" };
11068
- }
11069
- if ("error" in rpc) {
11070
- const err = rpc["error"];
11071
- return { status: "down", message: `MCP tools/list error: ${err?.message ?? "unknown"}` };
11072
- }
11073
- const result = rpc["result"];
11074
- const toolCount = Array.isArray(result?.tools) ? result.tools.length : void 0;
11075
- const testTool = config.connectivityTest?.tool;
11076
- if (testTool) {
11077
- const rawArgs = config.connectivityTest?.args;
11078
- const toolArgs = rawArgs && !Array.isArray(rawArgs) ? rawArgs : {};
11079
- const callRes = await fetchImpl(config.url, {
11080
- method: "POST",
11081
- headers: sessionHeaders,
11082
- body: JSON.stringify({
11083
- jsonrpc: "2.0",
11084
- id: 3,
11085
- method: "tools/call",
11086
- params: { name: testTool, arguments: toolArgs }
11087
- }),
11088
- signal: AbortSignal.timeout(timeoutMs)
11089
- });
11090
- if (!callRes.ok)
11091
- return httpStatusOutcome(callRes.status, `tools/call ${testTool}`);
11092
- const callRpc = await parseRpc(callRes, 3);
11093
- if (!callRpc) {
11094
- return { status: "down", message: `MCP tools/call ${testTool} returned a non-JSON-RPC response \u2014 not an MCP server` };
11095
- }
11096
- if ("error" in callRpc) {
11097
- const err = callRpc["error"];
11098
- return { status: "down", message: `MCP tools/call ${testTool} error: ${err?.message ?? "unknown"}` };
11099
- }
11100
- const callResult = callRpc["result"];
11101
- if (callResult?.isError === true) {
11102
- return { status: "down", message: `MCP tool ${testTool} returned an error result` };
11103
- }
11104
- return {
11105
- status: "ok",
11106
- message: `${testTool} succeeded`,
11107
- details: { ...toolCount !== void 0 ? { toolCount } : {}, testTool }
11108
- };
11109
- }
11110
- return handshakeToolsListOutcome(toolCount);
11111
- } catch (err) {
11112
- const isAbort = err?.name === "TimeoutError" || err?.name === "AbortError";
11113
- return {
11114
- status: "transient_error",
11115
- message: isAbort ? `MCP handshake timed out after ${timeoutMs / 1e3}s` : `MCP handshake failed: ${err.message}`
11116
- };
11117
- }
11118
- }
11119
-
11120
- // ../../packages/core/dist/integrations/composio-fetch.js
11121
- var COMPOSIO_API_BASE = "https://backend.composio.dev";
11122
- var transportResolver = null;
11123
- async function getComposioTransport() {
11124
- if (!transportResolver)
11125
- return null;
11126
- try {
11127
- return await transportResolver();
11128
- } catch (err) {
11129
- console.error("[composio] transport resolver failed; using direct egress", {
11130
- error: err instanceof Error ? err.message : String(err)
11131
- });
11132
- return null;
11133
- }
11134
- }
11135
-
11136
- // ../../packages/core/dist/integrations/composio-linkage.js
11137
- function assessAuthConfigLinkage(input) {
11138
- const { accountAuthConfigId, serverAuthConfigIds, serverId } = input;
11139
- const serverLabel = serverId ? ` (${serverId})` : "";
11140
- if (serverAuthConfigIds == null || !accountAuthConfigId) {
11141
- return {
11142
- linked: null,
11143
- message: "auth_config linkage not verified \u2014 " + (serverAuthConfigIds == null ? "couldn't read the wired MCP server's auth config binding" : "Composio returned no auth_config for the connected account"),
11144
- details: {
11145
- accountAuthConfigId: accountAuthConfigId ?? null,
11146
- serverAuthConfigIds: serverAuthConfigIds ?? null,
11147
- serverId: serverId ?? null
11148
- }
11149
- };
11150
- }
11151
- if (serverAuthConfigIds.length === 0) {
11152
- return {
11153
- linked: false,
11154
- message: `The agent's wired MCP server${serverLabel} has no auth config bound, so it cannot resolve the connected account (bound to auth_config ${accountAuthConfigId}) \u2014 reconnect/rebind required.`,
11155
- details: { accountAuthConfigId, serverAuthConfigIds, serverId: serverId ?? null }
11156
- };
11157
- }
11158
- if (serverAuthConfigIds.includes(accountAuthConfigId)) {
11159
- return {
11160
- linked: true,
11161
- message: `Connected account's auth_config (${accountAuthConfigId}) matches the wired MCP server binding.`,
11162
- details: { accountAuthConfigId, serverAuthConfigIds, serverId: serverId ?? null }
11163
- };
11164
- }
11165
- return {
11166
- linked: false,
11167
- message: `The connected account is bound to auth_config ${accountAuthConfigId}, but the agent's wired MCP server${serverLabel} resolves auth_config(s) [${serverAuthConfigIds.join(", ")}] \u2014 tool calls will fail with "No connected account found". Reconnect/rebind required.`,
11168
- details: { accountAuthConfigId, serverAuthConfigIds, serverId: serverId ?? null }
11169
- };
11170
- }
11171
-
11172
- // ../../packages/core/dist/integrations/composio-account-probe.js
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 readBoundedBody(res, controller);
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
- async function readBoundedBody(res, controller) {
11208
- const stream = res.body;
11209
- if (!stream)
11210
- return await res.text();
11211
- const reader = stream.getReader();
11212
- const chunks = [];
11213
- let total = 0;
11214
- for (; ; ) {
11215
- const { done, value } = await reader.read();
11216
- if (done)
11217
- break;
11218
- if (!value)
11219
- continue;
11220
- total += value.byteLength;
11221
- if (total > BODY_MAX_BYTES) {
11222
- controller.abort();
11223
- await reader.cancel().catch(() => {
11224
- });
11225
- throw new Error(`Composio response body exceeded ${BODY_MAX_BYTES} bytes`);
11226
- }
11227
- chunks.push(value);
11228
- }
11229
- const merged = new Uint8Array(total);
11230
- let offset = 0;
11231
- for (const chunk of chunks) {
11232
- merged.set(chunk, offset);
11233
- offset += chunk.byteLength;
11234
- }
11235
- return new TextDecoder().decode(merged);
11236
- }
11237
- async function probeComposioAccount(params, fetchImpl) {
11238
- const impl = fetchImpl ?? await getComposioTransport() ?? fetch;
11239
- const { connectedAccountId, apiKey, expectedUserId } = params;
11240
- const base = params.apiBase ?? COMPOSIO_API_BASE;
11241
- if (!connectedAccountId) {
11242
- return {
11243
- status: "down",
11244
- message: "No connected account recorded \u2014 reconnect required"
11245
- };
11246
- }
11247
- if (!apiKey || !expectedUserId) {
11248
- return {
11249
- status: "transient_error",
11250
- message: "Composio probe missing api key or expected user_id"
11251
- };
11252
- }
11253
- let res;
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(impl, `${base}/api/v3/connected_accounts/${encodeURIComponent(connectedAccountId)}`, { headers: { "x-api-key": apiKey } });
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
- const isAbort = err?.name === "AbortError";
11258
- return {
11259
- status: "transient_error",
11260
- message: isAbort ? `Composio probe timed out after ${PROBE_TIMEOUT_MS / 1e3}s` : `Composio probe failed: ${err.message}`
11261
- };
11061
+ return networkOutcome(err, String(key ?? ""));
11262
11062
  }
11263
- if (!res.ok) {
11264
- if (res.status >= 500) {
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: "transient_error",
11267
- message: `Composio unreachable (HTTP ${res.status}) \u2014 retrying`,
11268
- details: { connectedAccountId, httpStatus: res.status }
11074
+ status: statusForHttp(res.status, { rateLimited }),
11075
+ cause: causeForHttp(res.status, { rateLimited }),
11076
+ message
11269
11077
  };
11270
11078
  }
11271
- if (res.status === 401 || res.status === 403) {
11272
- const body = await readErrorBodySnippet(res, apiKey);
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: "unverified",
11275
- cause: "access_restricted",
11276
- message: `Composio refused our API key (HTTP ${res.status}) \u2014 access restriction on our side, not a problem with this account; do not reconnect` + (body ? `. Composio said: ${body}` : ""),
11277
- details: {
11278
- connectedAccountId,
11279
- httpStatus: res.status,
11280
- ...body ? { responseBody: body } : {}
11281
- }
11115
+ status: statusForHttp(res.status, { rateLimited }),
11116
+ cause: causeForHttp(res.status, { rateLimited }),
11117
+ message
11282
11118
  };
11283
11119
  }
11284
- return {
11285
- status: "down",
11286
- message: `Composio account ${connectedAccountId} not found (HTTP ${res.status}) \u2014 reconnect required`,
11287
- details: { connectedAccountId, httpStatus: res.status }
11288
- };
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
- let data;
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
- data = await res.json();
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
- if (boundUserId !== expectedUserId) {
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: `Composio account ${connectedAccountId} is bound to user_id '${boundUserId}' but the agent runtime queries as '${expectedUserId}' \u2014 tool calls will fail. Reconnect to bind correctly.`,
11319
- details: { connectedAccountId, status: accountStatus, boundUserId, expectedUserId }
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
- const accountAuthConfigId = data.auth_config_id ?? data.auth_config?.id;
11323
- if (params.serverId) {
11324
- const serverAuthConfigIds = await fetchServerAuthConfigIds(impl, base, params.serverId, apiKey);
11325
- const linkage = assessAuthConfigLinkage({
11326
- accountAuthConfigId,
11327
- serverAuthConfigIds,
11328
- serverId: params.serverId
11167
+ try {
11168
+ const res = await timedFetch(fetchImpl, "https://platform.higgsfield.ai/v1/motions", {
11169
+ headers: { Authorization: `Key ${token}` }
11329
11170
  });
11330
- if (linkage.linked === false) {
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: "down",
11333
- message: linkage.message,
11334
- details: { connectedAccountId, status: accountStatus, boundUserId, ...linkage.details }
11181
+ status: statusForHttp(res.status),
11182
+ cause: causeForHttp(res.status),
11183
+ message
11335
11184
  };
11336
11185
  }
11337
- }
11338
- return {
11339
- status: "ok",
11340
- message: `Connected (account ${connectedAccountId}, status=ACTIVE)`,
11341
- details: {
11342
- connectedAccountId,
11343
- status: accountStatus,
11344
- boundUserId,
11345
- ...accountAuthConfigId ? { authConfigId: accountAuthConfigId } : {}
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 fetchServerAuthConfigIds(fetchImpl, base, serverId, apiKey) {
11350
- let res;
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
- res = await timedFetch(fetchImpl, `${base}/api/v3/mcp/${encodeURIComponent(serverId)}`, { headers: { "x-api-key": apiKey } });
11201
+ authorization = basicUsernameAuthorization(String(key));
11353
11202
  } catch {
11354
- return null;
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
- if (!res.ok)
11357
- return null;
11208
+ const encoded = authorization.slice("Basic ".length);
11358
11209
  try {
11359
- const data = await res.json();
11360
- return data.auth_config_ids ?? data.auth_configs?.map((c) => c.id).filter((id) => typeof id === "string" && id.length > 0) ?? [];
11361
- } catch {
11362
- return null;
11363
- }
11364
- }
11365
-
11366
- // ../../packages/core/dist/integrations/composio-approved-egress.json
11367
- var composio_approved_egress_default = {
11368
- $comment: [
11369
- "ADR-0032 Part 3, gate item 2 + gate item 3 (ENG-9684). THE single recorded source",
11370
- "for the addresses Composio's per-key IP allowlist is expected to hold. Read by BOTH",
11371
- "scripts/fleet-egress-enum.sh (the operator's pre-arming instrument) and the",
11372
- "ComposioEgressAddressDrift cron (the between-armings watch). They must agree, which",
11373
- "is why neither carries its own copy.",
11374
- "",
11375
- "WHY A REPO FILE AND NOT SSM. Composio's allowlist has no read API, so its contents",
11376
- "can never be queried - only inferred from a refusal. This file is therefore not a",
11377
- "cache of vendor state; it is a DECLARATION of what a human pasted into a dashboard.",
11378
- "A PR is the right ceremony for changing a security boundary nobody can read back.",
11379
- "SSM would also hit registerCron's five-parameter grant scope (ENG-9940), where a",
11380
- "sixth parameter fails silently.",
11381
- "",
11382
- "THE COMPARISON IS ON `ip` AND NOTHING ELSE. `seenAsGateway` is informational. A NAT",
11383
- "rebuild that re-adopts the same EIP keeps egress correct while changing the gateway",
11384
- "id, so comparing ids would red on the safe case - which is precisely the case Part 2's",
11385
- "EIP adoption exists to create.",
11386
- "",
11387
- "WHEN ENG-9426 LANDS THIS GOES RED UNTIL IT IS UPDATED. That is the forcing function",
11388
- "working, not a fault. ENG-9426 adds a NAT in ap-southeast-2b, i.e. a THIRD egress",
11389
- "address: add it here in the same change that lands it, or the detector correctly",
11390
- "reports that hosts are leaving from an address no key allows. Do not loosen the check."
11391
- ],
11392
- account: "711726113003",
11393
- homeRegion: "ap-southeast-2",
11394
- addresses: [
11395
- {
11396
- ip: "32.236.6.24",
11397
- stage: "prod",
11398
- seenAsGateway: "nat-00a3dac969c0641a6",
11399
- note: "Prod hosts NAT. Measured 2026-09-12 (ENG-10392): 25 of 25 active prod host instances route 0.0.0.0/0 here."
11400
- },
11401
- {
11402
- ip: "15.135.204.192",
11403
- stage: "dev",
11404
- seenAsGateway: "nat-01122e6efaf4c4eaa",
11405
- note: "Dev-stage hosts NAT, same account. 2 instances measured 2026-09-12; their host rows live in the dev Supabase project."
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
- // ../../packages/core/dist/integrations/composio-approved-egress.js
11411
- var APPROVED_EGRESS_ACCOUNT = composio_approved_egress_default.account;
11412
- var APPROVED_EGRESS_HOME_REGION = composio_approved_egress_default.homeRegion;
11413
- var APPROVED_EGRESS_ADDRESSES = composio_approved_egress_default.addresses;
11414
-
11415
- // ../../packages/core/dist/integrations/composio-tool-call-probe.js
11416
- var MCP_ACCEPT2 = "application/json, text/event-stream";
11417
- var DEFAULT_TIMEOUT_MS2 = 1e4;
11418
- var READONLY_VERB_TOKENS = [
11419
- "LIST",
11420
- "GET",
11421
- "FIND",
11422
- "SEARCH",
11423
- "FETCH",
11424
- "COUNT",
11425
- "RETRIEVE",
11426
- "READ"
11427
- ];
11428
- var ACCOUNT_RESOLUTION_ERROR_PATTERNS = [
11429
- "no connected account",
11430
- "connected account not found",
11431
- "no account found",
11432
- "could not be resolved",
11433
- "auth config",
11434
- "no connection found"
11435
- ];
11436
- var UPSTREAM_AUTH_ERROR_PATTERNS = [
11437
- "authentication required",
11438
- "not authenticated",
11439
- "unauthorized",
11440
- "authentication_error",
11441
- "authentication error",
11442
- "invalid authentication",
11443
- "invalid credentials",
11444
- "invalid api key",
11445
- "invalid access token",
11446
- "token expired",
11447
- "token has expired",
11448
- "expired access token",
11449
- "expired credentials",
11450
- "permission denied",
11451
- "access denied",
11452
- "forbidden"
11453
- ];
11454
- var SITE_RESOLUTION_ERROR_PATTERNS = [
11455
- "dns resolution failed",
11456
- "getaddrinfo",
11457
- "enotfound",
11458
- "could not resolve host",
11459
- "name resolution",
11460
- "name not resolved"
11461
- ];
11462
- var SCOPE_DEFICIT_ERROR_PATTERNS = [
11463
- "insufficient_scope",
11464
- "insufficient scope",
11465
- "insufficient permission",
11466
- "missing scope",
11467
- "required scope",
11468
- "requires the scope",
11469
- "requires the following scope",
11470
- "scope is required",
11471
- "access scope",
11472
- "oauth scope",
11473
- // Shopify custom-app specifics.
11474
- "read_content",
11475
- "write_content",
11476
- "sales channel is not enabled"
11477
- ];
11478
- var QUOTA_EXHAUSTED_ERROR_PATTERNS = [
11479
- "resource_exhausted",
11480
- "resource has been exhausted",
11481
- "rate limit",
11482
- "rate_limit",
11483
- "ratelimit",
11484
- "ratescope",
11485
- "rate_scope",
11486
- "too many requests",
11487
- "quota exceeded",
11488
- "quota_exceeded",
11489
- "quotaexceeded",
11490
- "quota error",
11491
- "quotaerror",
11492
- "quota_error",
11493
- "exceeded your quota",
11494
- "out of quota",
11495
- "insufficient quota",
11496
- "check quota",
11497
- "usage limit",
11498
- "daily limit exceeded"
11499
- ];
11500
- function isReadonlyToolDescriptor(t) {
11501
- if (!t?.name)
11502
- return false;
11503
- const tokens = t.name.toUpperCase().split(/[^A-Z0-9]+/).filter(Boolean);
11504
- const hasReadVerb = tokens.some((tok) => READONLY_VERB_TOKENS.includes(tok));
11505
- if (!hasReadVerb)
11506
- return false;
11507
- const required = t.inputSchema?.required ?? [];
11508
- return !(Array.isArray(required) && required.length > 0);
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 pickSafeReadonlyTool(tools) {
11511
- for (const t of tools) {
11512
- if (isReadonlyToolDescriptor(t))
11513
- return t.name;
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
- function resolveProbeTool(tools, override) {
11518
- const requested = override?.tool?.trim();
11519
- if (!requested) {
11520
- return { toolName: pickSafeReadonlyTool(tools), args: {} };
11521
- }
11522
- const match = tools.find((t) => t?.name === requested);
11523
- if (!match) {
11524
- return { toolName: pickSafeReadonlyTool(tools), args: {}, fallback: "seed-drift", requestedTool: requested };
11525
- }
11526
- if (!isReadonlyToolDescriptor(match)) {
11527
- return { toolName: pickSafeReadonlyTool(tools), args: {}, fallback: "seed-invalid", requestedTool: requested };
11528
- }
11529
- return { toolName: requested, args: override?.args ?? {}, requestedTool: requested };
11530
- }
11531
- function isAccountResolutionError(message) {
11532
- const m = message.toLowerCase();
11533
- return ACCOUNT_RESOLUTION_ERROR_PATTERNS.some((p2) => m.includes(p2));
11534
- }
11535
- function isUpstreamAuthError(message) {
11536
- const m = message.toLowerCase();
11537
- if (UPSTREAM_AUTH_ERROR_PATTERNS.some((p2) => m.includes(p2)))
11538
- return true;
11539
- if (/\b(401|403)\s+client error/.test(m))
11540
- return true;
11541
- return /(status(?:_?code)?|http_?status(?:_code)?|mercury_last_http_status_code)["'\\\s]*[:=]["'\\\s]*(401|403)\b/.test(m);
11542
- }
11543
- function isComposioFailureEnvelope(text) {
11544
- return /["'\\]*(successful|successfull)["'\\\s]*:\s*false\b/i.test(text);
11545
- }
11546
- function isSiteResolutionError(message) {
11547
- const m = message.toLowerCase();
11548
- return SITE_RESOLUTION_ERROR_PATTERNS.some((p2) => m.includes(p2));
11549
- }
11550
- function isScopeDeficitError(message) {
11551
- const m = message.toLowerCase();
11552
- if (SCOPE_DEFICIT_ERROR_PATTERNS.some((p2) => m.includes(p2)))
11553
- return true;
11554
- return m.includes("scope") && ["doesn't have", "does not have", "not have the", "not granted", "lacks the", "not authorized for"].some((p2) => m.includes(p2));
11555
- }
11556
- function isQuotaExhaustedError(message) {
11557
- const m = message.toLowerCase();
11558
- if (QUOTA_EXHAUSTED_ERROR_PATTERNS.some((p2) => m.includes(p2)))
11559
- return true;
11560
- if (/\b429\s+client error/.test(m))
11561
- return true;
11562
- return /(status(?:_?code)?|http_?status(?:_code)?|mercury_last_http_status_code|["'\\]code)["'\\\s]*[:=]["'\\\s]*429\b/.test(m);
11563
- }
11564
- function extractRetryAfterSeconds(message) {
11565
- const m = message.toLowerCase();
11566
- const patterns = [
11567
- /retry\s+in\s+(\d+)\s*(?:s\b|sec\b|secs\b|second)/,
11568
- /retry[-_ ]?after["'\\\s]*[:=]["'\\\s]*(\d+)/,
11569
- /retry_?delay["'\\\s]*[:=]["'\\\s]*"?(\d+)s?/
11570
- ];
11571
- for (const re of patterns) {
11572
- const hit = re.exec(m);
11573
- if (!hit?.[1])
11574
- continue;
11575
- const seconds = Number(hit[1]);
11576
- if (Number.isFinite(seconds) && seconds > 0)
11577
- return seconds;
11578
- }
11579
- return null;
11580
- }
11581
- function formatRetryWindow(seconds) {
11582
- if (seconds < 90)
11583
- return `${seconds} seconds`;
11584
- const minutes = seconds / 60;
11585
- if (minutes < 90)
11586
- return `about ${Math.round(minutes)} minutes`;
11587
- const hours = minutes / 60;
11588
- if (hours < 24)
11589
- return `about ${Math.round(hours * 10) / 10} hours`;
11590
- return `about ${Math.round(hours / 24 * 10) / 10} days`;
11591
- }
11592
- function classifyToolCallFailure(text) {
11593
- if (isAccountResolutionError(text))
11594
- return "account";
11595
- if (isQuotaExhaustedError(text))
11596
- return "quota";
11597
- if (isScopeDeficitError(text))
11598
- return "scope";
11599
- if (isUpstreamAuthError(text))
11600
- return "auth";
11601
- if (isSiteResolutionError(text))
11602
- return "site";
11603
- return "benign";
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 parseRpc2(res, expectedId) {
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 === "" && dataLines.length > 0) {
11616
- try {
11617
- const msg2 = JSON.parse(dataLines.join("\n"));
11618
- if (("result" in msg2 || "error" in msg2) && msg2["id"] === expectedId)
11619
- return msg2;
11620
- } catch {
11621
- }
11322
+ if (rawLine === "") {
11323
+ const frame = tryFrame();
11324
+ if (frame)
11325
+ return frame;
11622
11326
  dataLines = [];
11623
11327
  }
11624
11328
  }
11625
- return null;
11329
+ return tryFrame();
11626
11330
  }
11627
11331
  const msg = await res.json().catch(() => null);
11628
- if (msg && ("result" in msg || "error" in msg) && msg["id"] === expectedId)
11629
- return msg;
11630
- return null;
11332
+ return isRpcEnvelopeFor(msg, expectedId) ? msg : null;
11631
11333
  }
11632
- async function probeComposioMcpToolCall(config, fetchImpl = fetch) {
11633
- const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
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: MCP_ACCEPT2
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-toolcall-probe", version: "1.0.0" }
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 >= 500 ? { status: "transient_error", message: `MCP initialize returned ${initRes.status}` } : null;
11657
- }
11372
+ if (!initRes.ok)
11373
+ return httpStatusOutcome(initRes.status, "initialize");
11658
11374
  const sessionId = initRes.headers.get("mcp-session-id");
11659
- await parseRpc2(initRes, 1);
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 >= 500 ? { status: "transient_error", message: `MCP tools/list returned ${listRes.status}` } : null;
11676
- }
11677
- const listRpc = await parseRpc2(listRes, 2);
11678
- const tools = listRpc?.["result"]?.tools ?? [];
11679
- const resolved = resolveProbeTool(tools, { tool: config.toolName, args: config.toolArgs });
11680
- const toolName = resolved.toolName;
11681
- if (!toolName)
11682
- return null;
11683
- const baseDetails = {
11684
- tool: toolName,
11685
- ...resolved.fallback ? { override_fallback: resolved.fallback, requested_tool: resolved.requestedTool } : {}
11686
- };
11687
- const callRes = await fetchImpl(config.url, {
11688
- method: "POST",
11689
- headers: sessionHeaders,
11690
- body: JSON.stringify({
11691
- jsonrpc: "2.0",
11692
- id: 3,
11693
- method: "tools/call",
11694
- params: { name: toolName, arguments: resolved.args }
11695
- }),
11696
- signal: AbortSignal.timeout(timeoutMs)
11697
- });
11698
- if (!callRes.ok) {
11699
- return callRes.status >= 500 ? { status: "transient_error", message: `MCP tools/call returned ${callRes.status}` } : null;
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
- const callRpc = await parseRpc2(callRes, 3);
11702
- {
11703
- const errText = callRpc?.["error"]?.message;
11704
- const resContent = callRpc?.["result"]?.content;
11705
- const raw = errText ?? (resContent ?? []).map((c) => c.text ?? "").join(" ").trim();
11706
- if (raw)
11707
- baseDetails.response = raw.length > 2e3 ? `${raw.slice(0, 2e3)}\u2026` : raw;
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 rpcErrMsg = callRpc && "error" in callRpc ? callRpc["error"]?.message ?? "" : "";
11710
- const result = callRpc?.["result"];
11711
- const contentText2 = (result?.content ?? []).map((c) => c.text ?? "").join(" ").trim();
11712
- const failed = Boolean(rpcErrMsg) || Boolean(result?.isError) || isComposioFailureEnvelope(contentText2);
11713
- if (failed) {
11714
- const failureText = [rpcErrMsg, contentText2].filter(Boolean).join(" ");
11715
- const snippet = failureText.length > 200 ? `${failureText.slice(0, 200)}\u2026` : failureText;
11716
- const kind = classifyToolCallFailure(failureText);
11717
- if (kind === "account") {
11718
- return {
11719
- status: "down",
11720
- message: `Live tool call '${toolName}' failed to resolve the connected account: ${snippet}`,
11721
- details: baseDetails
11722
- };
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 (kind === "quota") {
11725
- const retryAfterSeconds = extractRetryAfterSeconds(failureText);
11726
- const retryPhrase = retryAfterSeconds ? ` The provider says to retry in ${formatRetryWindow(retryAfterSeconds)}.` : "";
11727
- return {
11728
- status: "degraded",
11729
- message: `Live tool call '${toolName}' reached the provider and was refused: a quota or rate limit is exhausted. The connection itself is valid \u2014 do NOT reconnect, this resolves when the window resets.${retryPhrase} ${snippet}`,
11730
- details: {
11731
- ...baseDetails,
11732
- reason: "quota_exhausted",
11733
- ...retryAfterSeconds ? { retry_after_seconds: retryAfterSeconds } : {}
11734
- }
11735
- };
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
- if (kind === "scope") {
11738
- return {
11739
- status: "degraded",
11740
- message: `Live tool call '${toolName}' was refused for a missing permission \u2014 the connection is valid, but its app/token isn't granted the scope this tool needs. Grant the required scope in the provider app and re-authorise (this is NOT a reconnect): ${snippet}`,
11741
- details: { ...baseDetails, reason: "scope_deficit" }
11742
- };
11743
- }
11744
- if (kind === "auth") {
11745
- return {
11746
- status: "down",
11747
- message: `Live tool call '${toolName}' was rejected by the provider \u2014 the connection's credential is no longer valid (reconnect required): ${snippet}`,
11748
- details: { ...baseDetails, reason: "upstream_auth_rejected" }
11749
- };
11750
- }
11751
- if (kind === "site") {
11752
- return {
11753
- status: "down",
11754
- message: `Live tool call '${toolName}' couldn't reach the provider's site - the connection has no valid site URL (reconnect and make sure a site/workspace is granted): ${snippet}`,
11755
- details: { ...baseDetails, reason: "site_unresolved" }
11756
- };
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: `Live tool call '${toolName}' resolved the account (tool error: ${snippet})`,
11761
- details: { ...baseDetails, tool_error: snippet }
11442
+ message: `${testTool} succeeded`,
11443
+ details: { ...toolCount !== void 0 ? { toolCount } : {}, testTool }
11762
11444
  };
11763
11445
  }
11764
- return { status: "ok", message: `Live tool call '${toolName}' resolved the connected account`, details: baseDetails };
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
- message: isAbort ? `MCP tool-call probe timed out after ${timeoutMs / 1e3}s` : `MCP tool-call probe failed: ${err.message}`
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
- // ../../packages/core/dist/integrations/connectivity-http-probes.js
11775
- var PROBE_TIMEOUT_MS2 = 1e4;
11776
- var NULL_BODY_STATUSES2 = /* @__PURE__ */ new Set([101, 204, 205, 304]);
11777
- async function timedFetch2(fetchImpl, url, init) {
11778
- const controller = new AbortController();
11779
- const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS2);
11780
- try {
11781
- const res = await fetchImpl(url, { ...init, signal: controller.signal });
11782
- if (NULL_BODY_STATUSES2.has(res.status))
11783
- return res;
11784
- const body = await res.text();
11785
- return new Response(body, {
11786
- status: res.status,
11787
- statusText: res.statusText,
11788
- headers: res.headers
11789
- });
11790
- } finally {
11791
- clearTimeout(timer);
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 isRateLimited(res) {
11795
- const retryAfter = res.headers.get("retry-after");
11796
- if (retryAfter !== null && retryAfter.trim() !== "")
11797
- return true;
11798
- const remaining = res.headers.get("x-ratelimit-remaining");
11799
- if (remaining === null || remaining.trim() === "")
11800
- return false;
11801
- return Number(remaining) === 0;
11871
+ function isAccountResolutionError(message) {
11872
+ const m = message.toLowerCase();
11873
+ return ACCOUNT_RESOLUTION_ERROR_PATTERNS.some((p2) => m.includes(p2));
11802
11874
  }
11803
- function statusForHttp(httpStatus, opts) {
11804
- if (httpStatus === 429)
11805
- return "transient_error";
11806
- if (httpStatus === 403 && opts?.rateLimited)
11807
- return "transient_error";
11808
- if (httpStatus === 401 || httpStatus === 403)
11809
- return "down";
11810
- if (httpStatus >= 500)
11811
- return "transient_error";
11812
- return "down";
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 causeForHttp(httpStatus, opts) {
11815
- if (httpStatus === 429)
11816
- return "rate_limited";
11817
- if (httpStatus === 403 && opts?.rateLimited)
11818
- return "rate_limited";
11819
- if (httpStatus === 401 || httpStatus === 403)
11820
- return "auth_rejected";
11821
- if (httpStatus >= 500)
11822
- return "server_error";
11823
- return "semantic";
11883
+ function isComposioFailureEnvelope(text) {
11884
+ return /["'\\]*(successful|successfull)["'\\\s]*:\s*false\b/i.test(text);
11824
11885
  }
11825
- function rateLimitMessage(provider, httpStatus) {
11826
- const subject = provider ? `${provider} rate limit` : "Rate limited by the provider";
11827
- return `${subject} (${httpStatus}) \u2014 not a credential failure`;
11886
+ function isSiteResolutionError(message) {
11887
+ const m = message.toLowerCase();
11888
+ return SITE_RESOLUTION_ERROR_PATTERNS.some((p2) => m.includes(p2));
11828
11889
  }
11829
- function redactSecret(message, secret) {
11830
- if (typeof secret !== "string" || secret.length < 4)
11831
- return message;
11832
- return message.split(secret).join("[redacted]");
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 networkOutcome(err, secret) {
11835
- const isAbort = err?.name === "AbortError";
11836
- const message = isAbort ? `Connection timed out after ${PROBE_TIMEOUT_MS2 / 1e3}s` : `Connection failed: ${err.message}`;
11837
- return { status: "transient_error", cause: "unreachable", message: redactSecret(message, secret) };
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
- async function probeLinear(creds, fetchImpl) {
11840
- const key = creds.api_key ?? creds.access_token;
11841
- if (!key)
11842
- return { status: "down", message: "No Linear credential present" };
11843
- try {
11844
- const res = await timedFetch2(fetchImpl, "https://api.linear.app/graphql", {
11845
- method: "POST",
11846
- headers: { "Content-Type": "application/json", Authorization: String(key) },
11847
- body: JSON.stringify({ query: "{ viewer { id name email } }" })
11848
- });
11849
- if (!res.ok) {
11850
- const rateLimited = isRateLimited(res);
11851
- const message = res.status === 429 || res.status === 403 && rateLimited ? rateLimitMessage("Linear", res.status) : `Linear API returned ${res.status}`;
11852
- return {
11853
- status: statusForHttp(res.status, { rateLimited }),
11854
- cause: causeForHttp(res.status, { rateLimited }),
11855
- message
11856
- };
11857
- }
11858
- const body = await res.json();
11859
- if (body.errors?.length)
11860
- return { status: "down", message: body.errors[0]?.message ?? "Unknown Linear error" };
11861
- const viewer = body.data?.viewer;
11862
- if (!viewer)
11863
- return { status: "down", message: "Invalid key \u2014 no viewer returned" };
11864
- return { status: "ok", message: `Connected as ${viewer.name ?? viewer.email ?? "unknown"}` };
11865
- } catch (err) {
11866
- return networkOutcome(err, String(key ?? ""));
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
- async function probeBearerJson(url, creds, fetchImpl, interpret, extraHeaders) {
11870
- const token = creds.access_token ?? creds.api_key;
11871
- if (!token)
11872
- return { status: "down", message: "No credential present" };
11873
- try {
11874
- const res = await timedFetch2(fetchImpl, url, { headers: { Authorization: `Bearer ${token}`, ...extraHeaders } });
11875
- if (!res.ok) {
11876
- const rateLimited = isRateLimited(res);
11877
- const message = res.status === 401 ? "Token expired or revoked \u2014 reconnect required" : res.status === 429 || res.status === 403 && rateLimited ? rateLimitMessage(null, res.status) : `API returned ${res.status}`;
11878
- return {
11879
- status: statusForHttp(res.status, { rateLimited }),
11880
- cause: causeForHttp(res.status, { rateLimited }),
11881
- message
11882
- };
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 interpret(await res.json());
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 probeBuffer(creds, fetchImpl) {
11890
- const key = creds.api_key ?? creds.access_token;
11891
- if (!key)
11892
- return { status: "down", message: "No Buffer credential present" };
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 res = await timedFetch2(fetchImpl, "https://api.buffer.com", {
11980
+ const initRes = await fetchImpl(config.url, {
11895
11981
  method: "POST",
11896
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
11897
- body: JSON.stringify({ query: "{ account { organizations { id name } } }" })
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 (!res.ok) {
11900
- const rateLimited = isRateLimited(res);
11901
- const message = res.status === 401 ? (
11902
- // ENG-9809: name the mechanism, because the bare "expired or revoked" sent
11903
- // two investigations at the wrong thing. Buffer API keys carry a MANDATORY
11904
- // lifetime chosen at mint time — 7/30/60/90 days or 1 year, defaulting to
11905
- // 30 days, with no "never expires" option
11906
- // (support.buffer.com/article/984-how-to-create-your-buffer-api-key). So
11907
- // every Buffer install on the fleet is on a countdown, and expiry is the
11908
- // ordinary end of a key's life rather than a customer revoking one.
11909
- //
11910
- // All three causes are named, because this one branch answers every Buffer
11911
- // 401 — a key that lapsed, one that was revoked, and one that was simply
11912
- // mistyped ("Requests without a valid key will return a 401 Unauthorized",
11913
- // developers.buffer.com/guides/authentication.html). The status code alone
11914
- // cannot separate them, so naming expiry as the LIKELIER cause is supported;
11915
- // asserting it is not, and a message that omitted "invalid" would send
11916
- // someone who pasted the key wrong looking for an expiry date instead.
11917
- "Buffer API key expired, invalid, or revoked \u2014 reconnect required. Buffer keys expire by design (30-day default, 1-year maximum), so on a key that was working until now expiry is the likeliest of the three; pick 1 year when minting the replacement."
11918
- ) : res.status === 429 || res.status === 403 && rateLimited ? rateLimitMessage("Buffer", res.status) : `Buffer API returned ${res.status}`;
11919
- return {
11920
- status: statusForHttp(res.status, { rateLimited }),
11921
- cause: causeForHttp(res.status, { rateLimited }),
11922
- message
11923
- };
11995
+ if (!initRes.ok) {
11996
+ return initRes.status >= 500 ? { status: "transient_error", message: `MCP initialize returned ${initRes.status}` } : null;
11924
11997
  }
11925
- const body = await res.json();
11926
- if (body.errors?.length)
11927
- return { status: "down", message: body.errors[0]?.message ?? "Unknown Buffer error" };
11928
- const orgs = body.data?.account?.organizations ?? [];
11929
- if (!orgs.length)
11930
- return { status: "down", message: "No Buffer organizations on this account" };
11931
- return { status: "ok", message: `Connected to ${orgs[0]?.name ?? "Buffer"}` };
11932
- } catch (err) {
11933
- return networkOutcome(err, String(key ?? ""));
11934
- }
11935
- }
11936
- async function probeVercel(creds, fetchImpl) {
11937
- const token = creds.api_key ?? creds.access_token;
11938
- if (!token)
11939
- return { status: "down", message: "No Vercel credential present" };
11940
- try {
11941
- const res = await timedFetch2(fetchImpl, "https://api.vercel.com/v2/user", {
11942
- headers: { Authorization: `Bearer ${token}` }
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
- if (!res.ok) {
11945
- const message = res.status === 429 ? rateLimitMessage("Vercel", res.status) : res.status === 401 || res.status === 403 ? `Vercel rejected the token (${res.status}) \u2014 invalid or revoked API token` : `Vercel API returned ${res.status}`;
11946
- return {
11947
- status: statusForHttp(res.status),
11948
- cause: causeForHttp(res.status),
11949
- message
11950
- };
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 body = await res.json();
11953
- const user = body?.user;
11954
- const identity = [user?.username, user?.email, user?.name, user?.id].find((value) => typeof value === "string" && value.trim().length > 0);
11955
- if (!identity)
11956
- return { status: "down", message: "Vercel returned no user for this token" };
11957
- return { status: "ok", message: `Connected as ${identity}` };
11958
- } catch (err) {
11959
- return networkOutcome(err, String(token ?? ""));
11960
- }
11961
- }
11962
- async function probeHiggsfield(creds, fetchImpl) {
11963
- const token = creds.api_key ?? creds.access_token;
11964
- if (!token)
11965
- return { status: "down", message: "No Higgsfield credential present" };
11966
- if (!token.includes(":")) {
11967
- return {
11968
- status: "down",
11969
- message: "Higgsfield credential is not in KEY_ID:KEY_SECRET form \u2014 re-paste it from cloud.higgsfield.ai/api-keys"
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
- try {
11973
- const res = await timedFetch2(fetchImpl, "https://platform.higgsfield.ai/v1/motions", {
11974
- headers: { Authorization: `Key ${token}` }
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 (!res.ok) {
11977
- const body = await res.text().catch(() => "");
11978
- if (res.status === 403 && /credit/i.test(body)) {
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: "Higgsfield authenticated, but the account is out of credits \u2014 generation will fail"
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: statusForHttp(res.status),
11987
- cause: causeForHttp(res.status),
11988
- message
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
- const motions = await res.json();
11992
- if (!Array.isArray(motions) || motions.length === 0) {
11993
- return { status: "down", message: "Higgsfield returned no motion presets for this key" };
11994
- }
11995
- return { status: "ok", message: `Higgsfield reachable \u2014 ${motions.length} motion presets` };
12104
+ return { status: "ok", message: `Live tool call '${toolName}' resolved the connected account`, details: baseDetails };
11996
12105
  } catch (err) {
11997
- return networkOutcome(err, String(token ?? ""));
11998
- }
11999
- }
12000
- async function probeSportsyear(creds, fetchImpl) {
12001
- const key = creds.api_key ?? creds.access_token;
12002
- if (!key)
12003
- return { status: "down", message: "No Sportsyear credential present" };
12004
- let authorization;
12005
- try {
12006
- authorization = basicUsernameAuthorization(String(key));
12007
- } catch {
12106
+ const isAbort = err?.name === "TimeoutError" || err?.name === "AbortError";
12008
12107
  return {
12009
- status: "down",
12010
- message: 'Sportsyear credential is not a bare API key (it contains ":") \u2014 paste the key alone, not email:password'
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-4ZRXKEXO.js.map
14786
+ //# sourceMappingURL=chunk-HF2GMFMJ.js.map