@topolo/mcp 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -22
- package/dist/dispatch.js +143 -106
- package/dist/http.js +144 -107
- package/dist/index.js +382 -167
- package/package.json +9 -2
package/dist/index.js
CHANGED
|
@@ -44,6 +44,12 @@ var TopoloPermissionError = class extends TopoloSdkError {
|
|
|
44
44
|
this.required = required;
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
|
+
var TopoloConfirmationRequiredError = class extends TopoloSdkError {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super("confirmation_required", message);
|
|
50
|
+
this.name = "TopoloConfirmationRequiredError";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
47
53
|
var TopoloHttpError = class extends TopoloSdkError {
|
|
48
54
|
status;
|
|
49
55
|
body;
|
|
@@ -1106,6 +1112,7 @@ var APPLICATIONS = {
|
|
|
1106
1112
|
"topolo.io"
|
|
1107
1113
|
],
|
|
1108
1114
|
"routes": [
|
|
1115
|
+
"one.topolo.io",
|
|
1109
1116
|
"topolo.io",
|
|
1110
1117
|
"www.topolo.io"
|
|
1111
1118
|
],
|
|
@@ -2059,7 +2066,6 @@ var TOPOLO_RESOURCE_TYPE_HEADER = "X-Topolo-Resource-Type";
|
|
|
2059
2066
|
var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
|
|
2060
2067
|
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
2061
2068
|
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
2062
|
-
var CATALOG_CACHE_TTL_MS = 60 * 1e3;
|
|
2063
2069
|
var TopoloClient = class _TopoloClient {
|
|
2064
2070
|
credential;
|
|
2065
2071
|
agent;
|
|
@@ -2068,9 +2074,9 @@ var TopoloClient = class _TopoloClient {
|
|
|
2068
2074
|
requireConfirmForWrites;
|
|
2069
2075
|
timeoutMs;
|
|
2070
2076
|
fetchImpl;
|
|
2077
|
+
refreshCredential;
|
|
2078
|
+
refreshInFlight = null;
|
|
2071
2079
|
debug;
|
|
2072
|
-
serviceCatalogCache = null;
|
|
2073
|
-
actionCatalogCache = null;
|
|
2074
2080
|
/**
|
|
2075
2081
|
* Process-wide cache for slug → app-id lookups. Keyed by
|
|
2076
2082
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -2095,6 +2101,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2095
2101
|
this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
|
|
2096
2102
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
2097
2103
|
this.fetchImpl = options.fetch ?? fetch;
|
|
2104
|
+
this.refreshCredential = options.refreshCredential;
|
|
2098
2105
|
this.debug = options.debug;
|
|
2099
2106
|
}
|
|
2100
2107
|
emit(event) {
|
|
@@ -2112,11 +2119,11 @@ var TopoloClient = class _TopoloClient {
|
|
|
2112
2119
|
async request(opts) {
|
|
2113
2120
|
const method = opts.method ?? "GET";
|
|
2114
2121
|
if (this.requireConfirmForWrites && WRITE_METHODS.has(method) && !opts.confirm) {
|
|
2115
|
-
throw new
|
|
2122
|
+
throw new TopoloConfirmationRequiredError(
|
|
2116
2123
|
`Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
|
|
2117
2124
|
);
|
|
2118
2125
|
}
|
|
2119
|
-
const resolvedService = await this.resolveRequestService(opts.service);
|
|
2126
|
+
const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: requireHttpOrigin(opts.baseUrl) } : await this.resolveRequestService(opts.service);
|
|
2120
2127
|
const baseUrl = resolvedService.baseUrl;
|
|
2121
2128
|
const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
2122
2129
|
if (opts.query) {
|
|
@@ -2147,20 +2154,36 @@ var TopoloClient = class _TopoloClient {
|
|
|
2147
2154
|
headers.set(TOPOLO_RESOURCE_TYPE_HEADER, resourceType);
|
|
2148
2155
|
headers.set(TOPOLO_RESOURCE_ID_HEADER, resourceId);
|
|
2149
2156
|
}
|
|
2150
|
-
const controller = new AbortController();
|
|
2151
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2152
|
-
const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
|
|
2153
2157
|
const urlStr = url.toString();
|
|
2154
2158
|
const startedAt = Date.now();
|
|
2155
2159
|
this.emit({ phase: "request", method, service: resolvedService.serviceKey, path: opts.path, url: urlStr, requestId });
|
|
2160
|
+
const fetchOnce = async () => {
|
|
2161
|
+
const controller = new AbortController();
|
|
2162
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2163
|
+
const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
|
|
2164
|
+
try {
|
|
2165
|
+
return await this.fetchImpl(urlStr, {
|
|
2166
|
+
method,
|
|
2167
|
+
headers: new Headers(headers),
|
|
2168
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
|
|
2169
|
+
signal
|
|
2170
|
+
});
|
|
2171
|
+
} finally {
|
|
2172
|
+
clearTimeout(timeoutId);
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2156
2175
|
let res;
|
|
2157
2176
|
try {
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2177
|
+
const attemptedCredential = this.credential;
|
|
2178
|
+
res = await fetchOnce();
|
|
2179
|
+
if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
|
|
2180
|
+
await res.body?.cancel().catch(() => void 0);
|
|
2181
|
+
const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
|
|
2182
|
+
if (refreshed) {
|
|
2183
|
+
applyAuthHeaders(headers, refreshed);
|
|
2184
|
+
res = await fetchOnce();
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2164
2187
|
} catch (err) {
|
|
2165
2188
|
this.emit({
|
|
2166
2189
|
phase: "error",
|
|
@@ -2173,8 +2196,6 @@ var TopoloClient = class _TopoloClient {
|
|
|
2173
2196
|
error: err instanceof Error ? err.message : String(err)
|
|
2174
2197
|
});
|
|
2175
2198
|
throw err;
|
|
2176
|
-
} finally {
|
|
2177
|
-
clearTimeout(timeoutId);
|
|
2178
2199
|
}
|
|
2179
2200
|
const contentType = res.headers.get("Content-Type") ?? "";
|
|
2180
2201
|
const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
|
|
@@ -2213,6 +2234,19 @@ var TopoloClient = class _TopoloClient {
|
|
|
2213
2234
|
});
|
|
2214
2235
|
return parsed;
|
|
2215
2236
|
}
|
|
2237
|
+
async refreshRejectedAccessToken(attemptedAccessToken) {
|
|
2238
|
+
if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
|
|
2239
|
+
return this.credential;
|
|
2240
|
+
}
|
|
2241
|
+
if (!this.refreshCredential) return null;
|
|
2242
|
+
this.refreshInFlight ??= this.refreshCredential().then((credential) => {
|
|
2243
|
+
if (credential) this.credential = credential;
|
|
2244
|
+
return credential;
|
|
2245
|
+
}).catch(() => null).finally(() => {
|
|
2246
|
+
this.refreshInFlight = null;
|
|
2247
|
+
});
|
|
2248
|
+
return this.refreshInFlight;
|
|
2249
|
+
}
|
|
2216
2250
|
async resolveRequestService(service) {
|
|
2217
2251
|
const serviceKey = normalizeLookupKey(service);
|
|
2218
2252
|
if (!serviceKey) {
|
|
@@ -2238,9 +2272,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
2238
2272
|
);
|
|
2239
2273
|
}
|
|
2240
2274
|
async resolveLiveCallableService(serviceKey) {
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2243
|
-
if (!match) return null;
|
|
2275
|
+
const match = await this.getService(serviceKey).catch(() => null);
|
|
2276
|
+
if (!match || !catalogEntryMatches(match, serviceKey)) return null;
|
|
2244
2277
|
const baseUrl = resolveCatalogServiceUrl(match, {
|
|
2245
2278
|
serviceKey,
|
|
2246
2279
|
overrides: this.serviceUrls
|
|
@@ -2314,12 +2347,13 @@ var TopoloClient = class _TopoloClient {
|
|
|
2314
2347
|
* The live Auth catalog is the authority because it is scoped to the caller's
|
|
2315
2348
|
* organization and effective permissions.
|
|
2316
2349
|
*/
|
|
2317
|
-
async
|
|
2318
|
-
const
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2350
|
+
async searchServices(options = {}) {
|
|
2351
|
+
const res = await this.request({
|
|
2352
|
+
service: "developers",
|
|
2353
|
+
method: "GET",
|
|
2354
|
+
path: "/api/services/catalog",
|
|
2355
|
+
query: catalogSearchQuery(options)
|
|
2356
|
+
});
|
|
2323
2357
|
const raw = res.data?.services ?? res.services;
|
|
2324
2358
|
if (!Array.isArray(raw)) {
|
|
2325
2359
|
throw new TopoloSdkError(
|
|
@@ -2329,6 +2363,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2329
2363
|
}
|
|
2330
2364
|
const entries = [];
|
|
2331
2365
|
for (const item of raw) {
|
|
2366
|
+
if (!isRecord(item)) continue;
|
|
2332
2367
|
const s = item;
|
|
2333
2368
|
const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
|
|
2334
2369
|
if (!appId) continue;
|
|
@@ -2353,11 +2388,30 @@ var TopoloClient = class _TopoloClient {
|
|
|
2353
2388
|
apiKeyResources
|
|
2354
2389
|
});
|
|
2355
2390
|
}
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
};
|
|
2360
|
-
|
|
2391
|
+
const page = normalizeCatalogPage(
|
|
2392
|
+
res.data?.page ?? res.page
|
|
2393
|
+
);
|
|
2394
|
+
return { services: entries, page };
|
|
2395
|
+
}
|
|
2396
|
+
/** @deprecated Prefer searchServices() so callers retain the continuation cursor. */
|
|
2397
|
+
async listServices(options = {}) {
|
|
2398
|
+
return (await this.searchServices(options)).services;
|
|
2399
|
+
}
|
|
2400
|
+
async getService(application) {
|
|
2401
|
+
const needle = application.trim();
|
|
2402
|
+
if (!needle) throw new TopoloSdkError("unknown_service", "application is required");
|
|
2403
|
+
const res = await this.request({
|
|
2404
|
+
service: "developers",
|
|
2405
|
+
method: "GET",
|
|
2406
|
+
path: "/api/services/catalog",
|
|
2407
|
+
query: { application: needle }
|
|
2408
|
+
});
|
|
2409
|
+
const raw = res.data?.application ?? res.application;
|
|
2410
|
+
const entry = normalizeServiceCatalogEntry(raw);
|
|
2411
|
+
if (!entry) {
|
|
2412
|
+
throw new TopoloSdkError("unknown_service", `Unknown Topolo application "${application}".`);
|
|
2413
|
+
}
|
|
2414
|
+
return entry;
|
|
2361
2415
|
}
|
|
2362
2416
|
/**
|
|
2363
2417
|
* List callable actions from the live registry-canonical catalog (definitions
|
|
@@ -2365,16 +2419,15 @@ var TopoloClient = class _TopoloClient {
|
|
|
2365
2419
|
* caller's organization and effective permissions; no bundled/static app
|
|
2366
2420
|
* action list is used as an authorization source.
|
|
2367
2421
|
*/
|
|
2368
|
-
async
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
return this.actionCatalogCache.entries;
|
|
2422
|
+
async searchActions(options) {
|
|
2423
|
+
if (!options.service?.trim()) {
|
|
2424
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2372
2425
|
}
|
|
2373
2426
|
const res = await this.request({
|
|
2374
2427
|
service: "developers",
|
|
2375
2428
|
method: "GET",
|
|
2376
2429
|
path: "/api/actions/catalog",
|
|
2377
|
-
|
|
2430
|
+
query: { service: options.service, ...catalogSearchQuery(options) }
|
|
2378
2431
|
});
|
|
2379
2432
|
const raw = res.data?.actions ?? res.actions;
|
|
2380
2433
|
if (!Array.isArray(raw)) {
|
|
@@ -2384,36 +2437,96 @@ var TopoloClient = class _TopoloClient {
|
|
|
2384
2437
|
);
|
|
2385
2438
|
}
|
|
2386
2439
|
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2440
|
+
return {
|
|
2441
|
+
actions: entries,
|
|
2442
|
+
page: normalizeCatalogPage(
|
|
2443
|
+
res.data?.page ?? res.page
|
|
2444
|
+
)
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
async getActionCatalogSummary(service) {
|
|
2448
|
+
const boundedService = service.trim();
|
|
2449
|
+
if (!boundedService) {
|
|
2450
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2392
2451
|
}
|
|
2393
|
-
|
|
2452
|
+
const res = await this.request({
|
|
2453
|
+
service: "developers",
|
|
2454
|
+
method: "GET",
|
|
2455
|
+
path: "/api/actions/catalog",
|
|
2456
|
+
query: { service: boundedService, summary: "true" }
|
|
2457
|
+
});
|
|
2458
|
+
const raw = res.data?.summary ?? res.summary;
|
|
2459
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2460
|
+
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
|
|
2461
|
+
}
|
|
2462
|
+
const record = raw;
|
|
2463
|
+
if (record.algorithm !== "sha256-v1" || typeof record.digest !== "string" || !/^[a-f0-9]{64}$/.test(record.digest) || !Number.isSafeInteger(record.actionCount) || Number(record.actionCount) < 0) {
|
|
2464
|
+
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
|
|
2465
|
+
}
|
|
2466
|
+
return {
|
|
2467
|
+
algorithm: "sha256-v1",
|
|
2468
|
+
digest: record.digest,
|
|
2469
|
+
actionCount: Number(record.actionCount)
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
/** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
|
|
2473
|
+
async listActions(options) {
|
|
2474
|
+
if (!options) {
|
|
2475
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2476
|
+
}
|
|
2477
|
+
return (await this.searchActions(options)).actions;
|
|
2394
2478
|
}
|
|
2395
2479
|
async getAction(action) {
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
2399
|
-
if (!match) {
|
|
2480
|
+
const actionId = action.trim();
|
|
2481
|
+
if (!actionId) {
|
|
2400
2482
|
throw new TopoloSdkError(
|
|
2401
2483
|
"unknown_action",
|
|
2402
|
-
|
|
2484
|
+
"action id is required"
|
|
2403
2485
|
);
|
|
2404
2486
|
}
|
|
2487
|
+
let res;
|
|
2488
|
+
try {
|
|
2489
|
+
res = await this.request({
|
|
2490
|
+
service: "developers",
|
|
2491
|
+
method: "GET",
|
|
2492
|
+
path: "/api/actions/catalog",
|
|
2493
|
+
query: { actionId }
|
|
2494
|
+
});
|
|
2495
|
+
} catch (error) {
|
|
2496
|
+
if (error instanceof TopoloHttpError && error.status === 404) {
|
|
2497
|
+
throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
2498
|
+
}
|
|
2499
|
+
throw error;
|
|
2500
|
+
}
|
|
2501
|
+
const raw = res.data?.action ?? res.action;
|
|
2502
|
+
const match = normalizeActionCatalogEntry(raw);
|
|
2503
|
+
if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
2405
2504
|
return match;
|
|
2406
2505
|
}
|
|
2407
2506
|
async callAction(action, input = {}, options = {}) {
|
|
2408
2507
|
const entry = await this.getAction(action);
|
|
2508
|
+
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
2509
|
+
if (agentAccess === "off") {
|
|
2510
|
+
throw new TopoloPermissionError(
|
|
2511
|
+
`Topolo action "${entry.actionId}" is not available for agent invocation.`,
|
|
2512
|
+
entry.requiredPermission ? [entry.requiredPermission] : []
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
if ((agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive) && options.confirm !== true) {
|
|
2516
|
+
throw new TopoloConfirmationRequiredError(
|
|
2517
|
+
`Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2409
2520
|
const mapped = mapActionInput(entry, input);
|
|
2410
2521
|
const service = entry.appSlug || entry.appId;
|
|
2411
2522
|
return this.request({
|
|
2412
2523
|
service,
|
|
2524
|
+
...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
|
|
2413
2525
|
method: entry.method,
|
|
2414
2526
|
path: mapped.path,
|
|
2415
2527
|
...mapped.query ? { query: mapped.query } : {},
|
|
2416
2528
|
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2529
|
+
...options.resource ? { resource: options.resource } : {},
|
|
2417
2530
|
headers: {
|
|
2418
2531
|
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2419
2532
|
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
@@ -2551,6 +2664,18 @@ function serviceKeyCandidates2(slug) {
|
|
|
2551
2664
|
function normalizeLookupKey(value) {
|
|
2552
2665
|
return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
|
|
2553
2666
|
}
|
|
2667
|
+
function requireHttpOrigin(value) {
|
|
2668
|
+
let url;
|
|
2669
|
+
try {
|
|
2670
|
+
url = new URL(value);
|
|
2671
|
+
} catch {
|
|
2672
|
+
throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
|
|
2673
|
+
}
|
|
2674
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password) {
|
|
2675
|
+
throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
|
|
2676
|
+
}
|
|
2677
|
+
return url.origin;
|
|
2678
|
+
}
|
|
2554
2679
|
function deriveDevelopersFromAuth(authUrl) {
|
|
2555
2680
|
if (!authUrl) return null;
|
|
2556
2681
|
try {
|
|
@@ -2577,6 +2702,47 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2577
2702
|
}
|
|
2578
2703
|
return [...keys];
|
|
2579
2704
|
}
|
|
2705
|
+
function catalogSearchQuery(options) {
|
|
2706
|
+
const query = {};
|
|
2707
|
+
if (typeof options.query === "string" && options.query.trim()) query.query = options.query.trim();
|
|
2708
|
+
if (typeof options.cursor === "string" && options.cursor.trim()) query.cursor = options.cursor.trim();
|
|
2709
|
+
if (typeof options.limit === "number" && Number.isFinite(options.limit)) query.limit = Math.trunc(options.limit);
|
|
2710
|
+
return query;
|
|
2711
|
+
}
|
|
2712
|
+
function normalizeCatalogPage(value) {
|
|
2713
|
+
if (!isRecord(value)) return { nextCursor: null, hasMore: false };
|
|
2714
|
+
return {
|
|
2715
|
+
nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
|
|
2716
|
+
hasMore: value.hasMore === true
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
function normalizeServiceCatalogEntry(item) {
|
|
2720
|
+
if (!isRecord(item)) return null;
|
|
2721
|
+
const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
|
|
2722
|
+
if (!appId) return null;
|
|
2723
|
+
const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
|
|
2724
|
+
(permission) => typeof permission === "string" && permission.trim().length > 0
|
|
2725
|
+
) : [];
|
|
2726
|
+
const catalogApiKeyResources = normalizeApiKeyResourceMetadataList(
|
|
2727
|
+
item.api_key_resources ?? item.apiKeyResources
|
|
2728
|
+
);
|
|
2729
|
+
const apiKeyResource = normalizeApiKeyResourceMetadata(item.api_key_resource ?? item.apiKeyResource) ?? catalogApiKeyResources[0] ?? null;
|
|
2730
|
+
const apiKeyResources = catalogApiKeyResources.length > 0 ? catalogApiKeyResources : apiKeyResource ? [apiKeyResource] : [];
|
|
2731
|
+
return {
|
|
2732
|
+
appId,
|
|
2733
|
+
slug: typeof item.app_slug === "string" ? item.app_slug : null,
|
|
2734
|
+
name: typeof item.name === "string" ? item.name : null,
|
|
2735
|
+
description: typeof item.description === "string" ? item.description : null,
|
|
2736
|
+
apiBaseUrl: typeof item.api_base_url === "string" ? item.api_base_url : typeof item.apiBaseUrl === "string" ? item.apiBaseUrl : null,
|
|
2737
|
+
launchUrl: typeof item.launch_url === "string" ? item.launch_url : typeof item.launchUrl === "string" ? item.launchUrl : null,
|
|
2738
|
+
status: typeof item.status === "string" ? item.status : null,
|
|
2739
|
+
surfaceKind: typeof item.surface_kind === "string" ? item.surface_kind : null,
|
|
2740
|
+
launchable: item.launchable === true,
|
|
2741
|
+
permissions,
|
|
2742
|
+
apiKeyResource,
|
|
2743
|
+
apiKeyResources
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2580
2746
|
function normalizeActionCatalogEntry(item) {
|
|
2581
2747
|
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2582
2748
|
const a = item;
|
|
@@ -2590,6 +2756,8 @@ function normalizeActionCatalogEntry(item) {
|
|
|
2590
2756
|
}
|
|
2591
2757
|
return {
|
|
2592
2758
|
actionId,
|
|
2759
|
+
target: pickActionString(a.target_key) ?? pickActionString(a.target) ?? null,
|
|
2760
|
+
apiBaseUrl: pickActionString(a.api_base_url) ?? pickActionString(a.apiBaseUrl) ?? null,
|
|
2593
2761
|
name,
|
|
2594
2762
|
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
2595
2763
|
title: pickActionString(a.title) ?? name,
|
|
@@ -2601,6 +2769,7 @@ function normalizeActionCatalogEntry(item) {
|
|
|
2601
2769
|
path,
|
|
2602
2770
|
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
2603
2771
|
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
2772
|
+
authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
|
|
2604
2773
|
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2605
2774
|
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
2606
2775
|
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
@@ -2620,16 +2789,12 @@ function pickActionString(value) {
|
|
|
2620
2789
|
function pickAgentAccess(value) {
|
|
2621
2790
|
return value === "auto" || value === "confirm" || value === "off" ? value : null;
|
|
2622
2791
|
}
|
|
2792
|
+
function pickAuthorizationMode(value) {
|
|
2793
|
+
return value === "runtime" ? "runtime" : "static";
|
|
2794
|
+
}
|
|
2623
2795
|
function plainObject(value) {
|
|
2624
2796
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2625
2797
|
}
|
|
2626
|
-
function normalizeActionLookupKey(value) {
|
|
2627
|
-
return String(value || "").trim().toLowerCase();
|
|
2628
|
-
}
|
|
2629
|
-
function actionEntryMatches(entry, needle) {
|
|
2630
|
-
if (!needle) return false;
|
|
2631
|
-
return [entry.actionId, entry.name, entry.toolName].filter((value) => typeof value === "string" && value.length > 0).some((value) => value.toLowerCase() === needle);
|
|
2632
|
-
}
|
|
2633
2798
|
function mapActionInput(action, input) {
|
|
2634
2799
|
const remaining = { ...input || {} };
|
|
2635
2800
|
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
@@ -3058,7 +3223,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
3058
3223
|
return profileForEnvironment(config, environment).credential ?? null;
|
|
3059
3224
|
}
|
|
3060
3225
|
var REFRESH_SKEW_SECONDS = 60;
|
|
3061
|
-
async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
3226
|
+
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
3062
3227
|
const prefix = envVarPrefix(environment);
|
|
3063
3228
|
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
3064
3229
|
return resolveCredential(environment);
|
|
@@ -3069,7 +3234,7 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
|
3069
3234
|
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
3070
3235
|
if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
|
|
3071
3236
|
const now = Math.floor(Date.now() / 1e3);
|
|
3072
|
-
if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
3237
|
+
if (!options.force && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
3073
3238
|
try {
|
|
3074
3239
|
const oauth = new TopoloOAuth({
|
|
3075
3240
|
serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
|
|
@@ -3084,9 +3249,20 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
|
3084
3249
|
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
3085
3250
|
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
3086
3251
|
};
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3252
|
+
const activeContext = profile.activeContext;
|
|
3253
|
+
const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
|
|
3254
|
+
const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
|
|
3255
|
+
credential: refreshed,
|
|
3256
|
+
contexts: {
|
|
3257
|
+
...profile.contexts ?? {},
|
|
3258
|
+
[activeContext]: {
|
|
3259
|
+
...activeEntry,
|
|
3260
|
+
credential: refreshed,
|
|
3261
|
+
oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
}) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
|
|
3265
|
+
await writeConfig(updated);
|
|
3090
3266
|
return refreshed;
|
|
3091
3267
|
} catch {
|
|
3092
3268
|
return cred;
|
|
@@ -3170,6 +3346,15 @@ function splitService(servicePart) {
|
|
|
3170
3346
|
// src/tools.ts
|
|
3171
3347
|
var APPLICATION_IDS = Object.keys(APPLICATIONS).sort();
|
|
3172
3348
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
3349
|
+
function optionalResourceContext(args) {
|
|
3350
|
+
const resourceType = typeof args["resourceType"] === "string" ? args["resourceType"].trim() : "";
|
|
3351
|
+
const resourceId = typeof args["resourceId"] === "string" ? args["resourceId"].trim() : "";
|
|
3352
|
+
if (!resourceType && !resourceId) return void 0;
|
|
3353
|
+
if (!resourceType || !resourceId) {
|
|
3354
|
+
throw new Error("resourceType and resourceId must be provided together.");
|
|
3355
|
+
}
|
|
3356
|
+
return { resourceType, resourceId };
|
|
3357
|
+
}
|
|
3173
3358
|
var TOOLS = [
|
|
3174
3359
|
{
|
|
3175
3360
|
name: "topolo_whoami",
|
|
@@ -3185,18 +3370,27 @@ var TOOLS = [
|
|
|
3185
3370
|
handler: async (topolo) => topolo.identity.whoami()
|
|
3186
3371
|
},
|
|
3187
3372
|
{
|
|
3188
|
-
name: "
|
|
3189
|
-
title: "
|
|
3190
|
-
description: "
|
|
3373
|
+
name: "topolo_search_applications",
|
|
3374
|
+
title: "Search Topolo applications the agent can reach",
|
|
3375
|
+
description: "Searches the credential-scoped application registry with a bounded page and continuation cursor. Returns granted permissions, API URL, and launch URL without loading the global platform catalog.",
|
|
3191
3376
|
requiredScopes: [],
|
|
3192
3377
|
destructive: false,
|
|
3193
3378
|
inputSchema: {
|
|
3194
3379
|
type: "object",
|
|
3195
|
-
properties: {
|
|
3380
|
+
properties: {
|
|
3381
|
+
query: { type: "string" },
|
|
3382
|
+
cursor: { type: "string" },
|
|
3383
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }
|
|
3384
|
+
},
|
|
3196
3385
|
additionalProperties: false
|
|
3197
3386
|
},
|
|
3198
|
-
handler: async (topolo) => {
|
|
3199
|
-
const
|
|
3387
|
+
handler: async (topolo, args) => {
|
|
3388
|
+
const result = await topolo.client.searchServices({
|
|
3389
|
+
query: typeof args["query"] === "string" ? args["query"] : void 0,
|
|
3390
|
+
cursor: typeof args["cursor"] === "string" ? args["cursor"] : void 0,
|
|
3391
|
+
limit: typeof args["limit"] === "number" ? args["limit"] : void 0
|
|
3392
|
+
});
|
|
3393
|
+
const applications = result.services.map((entry) => {
|
|
3200
3394
|
const id = displayAppId(entry);
|
|
3201
3395
|
return {
|
|
3202
3396
|
id,
|
|
@@ -3213,13 +3407,13 @@ var TOOLS = [
|
|
|
3213
3407
|
permissions: [...entry.permissions]
|
|
3214
3408
|
};
|
|
3215
3409
|
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
3216
|
-
return {
|
|
3410
|
+
return { applications, page: result.page };
|
|
3217
3411
|
}
|
|
3218
3412
|
},
|
|
3219
3413
|
{
|
|
3220
|
-
name: "
|
|
3221
|
-
title: "
|
|
3222
|
-
description: "
|
|
3414
|
+
name: "topolo_search_actions",
|
|
3415
|
+
title: "Search Topolo actions the agent can call",
|
|
3416
|
+
description: "Searches one application's credential-scoped actions with a bounded page and continuation cursor.",
|
|
3223
3417
|
requiredScopes: [],
|
|
3224
3418
|
destructive: false,
|
|
3225
3419
|
inputSchema: {
|
|
@@ -3227,31 +3421,25 @@ var TOOLS = [
|
|
|
3227
3421
|
properties: {
|
|
3228
3422
|
service: {
|
|
3229
3423
|
type: "string",
|
|
3230
|
-
description: "
|
|
3231
|
-
}
|
|
3424
|
+
description: "Required app id, slug, or name."
|
|
3425
|
+
},
|
|
3426
|
+
query: { type: "string" },
|
|
3427
|
+
cursor: { type: "string" },
|
|
3428
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }
|
|
3232
3429
|
},
|
|
3430
|
+
required: ["service"],
|
|
3233
3431
|
additionalProperties: false
|
|
3234
3432
|
},
|
|
3235
|
-
handler: async (topolo, args) =>
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
requiredScopes: [],
|
|
3246
|
-
destructive: false,
|
|
3247
|
-
inputSchema: {
|
|
3248
|
-
type: "object",
|
|
3249
|
-
properties: {},
|
|
3250
|
-
additionalProperties: false
|
|
3251
|
-
},
|
|
3252
|
-
handler: async (topolo) => ({
|
|
3253
|
-
applications: await topolo.client.listServices()
|
|
3254
|
-
})
|
|
3433
|
+
handler: async (topolo, args) => {
|
|
3434
|
+
const service = typeof args["service"] === "string" ? args["service"].trim() : "";
|
|
3435
|
+
if (!service) throw new Error("service is required for bounded action discovery.");
|
|
3436
|
+
return topolo.client.searchActions({
|
|
3437
|
+
service,
|
|
3438
|
+
query: typeof args["query"] === "string" ? args["query"] : void 0,
|
|
3439
|
+
cursor: typeof args["cursor"] === "string" ? args["cursor"] : void 0,
|
|
3440
|
+
limit: typeof args["limit"] === "number" ? args["limit"] : void 0
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3255
3443
|
},
|
|
3256
3444
|
{
|
|
3257
3445
|
name: "topolo_get_application",
|
|
@@ -3264,7 +3452,7 @@ var TOOLS = [
|
|
|
3264
3452
|
properties: {
|
|
3265
3453
|
application: {
|
|
3266
3454
|
type: "string",
|
|
3267
|
-
description: "Application identifier: app id, slug, or name from
|
|
3455
|
+
description: "Application identifier: app id, slug, or name from topolo_search_applications."
|
|
3268
3456
|
}
|
|
3269
3457
|
},
|
|
3270
3458
|
required: ["application"],
|
|
@@ -3275,18 +3463,98 @@ var TOOLS = [
|
|
|
3275
3463
|
if (typeof application !== "string" || !application.trim()) {
|
|
3276
3464
|
throw new Error("`application` must be a non-empty string.");
|
|
3277
3465
|
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3466
|
+
return { application: await topolo.client.getService(application) };
|
|
3467
|
+
}
|
|
3468
|
+
},
|
|
3469
|
+
{
|
|
3470
|
+
name: "topolo_get_action",
|
|
3471
|
+
title: "Describe one Topolo action",
|
|
3472
|
+
description: "Returns one credential-scoped action by exact action id.",
|
|
3473
|
+
requiredScopes: [],
|
|
3474
|
+
destructive: false,
|
|
3475
|
+
inputSchema: {
|
|
3476
|
+
type: "object",
|
|
3477
|
+
properties: { actionId: { type: "string", minLength: 1 } },
|
|
3478
|
+
required: ["actionId"],
|
|
3479
|
+
additionalProperties: false
|
|
3480
|
+
},
|
|
3481
|
+
handler: async (topolo, args) => {
|
|
3482
|
+
const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
|
|
3483
|
+
if (!actionId) throw new Error("actionId is required.");
|
|
3484
|
+
return { action: await topolo.client.getAction(actionId) };
|
|
3485
|
+
}
|
|
3486
|
+
},
|
|
3487
|
+
{
|
|
3488
|
+
name: "topolo_read_action",
|
|
3489
|
+
title: "Call one read-only Topolo action",
|
|
3490
|
+
description: "Calls one exact action id only when its published contract is read-only and needs no confirmation.",
|
|
3491
|
+
requiredScopes: [],
|
|
3492
|
+
destructive: false,
|
|
3493
|
+
inputSchema: {
|
|
3494
|
+
type: "object",
|
|
3495
|
+
properties: {
|
|
3496
|
+
actionId: { type: "string", minLength: 1 },
|
|
3497
|
+
input: { type: "object", additionalProperties: true },
|
|
3498
|
+
resourceType: { type: "string", description: "Optional app-local resource alias, such as workspace." },
|
|
3499
|
+
resourceId: { type: "string", description: "Resource id. Required when resourceType is provided." }
|
|
3500
|
+
},
|
|
3501
|
+
required: ["actionId"],
|
|
3502
|
+
additionalProperties: false
|
|
3503
|
+
},
|
|
3504
|
+
handler: async (topolo, args) => {
|
|
3505
|
+
const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
|
|
3506
|
+
if (!actionId) throw new Error("actionId is required.");
|
|
3507
|
+
const input = args["input"];
|
|
3508
|
+
if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
|
|
3509
|
+
throw new Error("input must be an object.");
|
|
3510
|
+
}
|
|
3511
|
+
const action = await topolo.client.getAction(actionId);
|
|
3512
|
+
const agentAccess = action.agentAccess ?? (action.destructive ? "off" : action.readOnly && !action.requiresConfirmation ? "auto" : "confirm");
|
|
3513
|
+
if (action.method !== "GET" || !action.readOnly || action.destructive || action.requiresConfirmation || agentAccess !== "auto") {
|
|
3514
|
+
throw new Error(`Action "${actionId}" is not an automatic read-only action. Use topolo_call_action with confirmation.`);
|
|
3515
|
+
}
|
|
3516
|
+
const resource = optionalResourceContext(args);
|
|
3517
|
+
return topolo.client.callAction(
|
|
3518
|
+
actionId,
|
|
3519
|
+
input ?? {},
|
|
3520
|
+
resource ? { resource } : {}
|
|
3521
|
+
);
|
|
3522
|
+
}
|
|
3523
|
+
},
|
|
3524
|
+
{
|
|
3525
|
+
name: "topolo_call_action",
|
|
3526
|
+
title: "Call one Topolo action",
|
|
3527
|
+
description: "Calls one exact action id. Mutating actions require confirm:true.",
|
|
3528
|
+
requiredScopes: [],
|
|
3529
|
+
destructive: true,
|
|
3530
|
+
inputSchema: {
|
|
3531
|
+
type: "object",
|
|
3532
|
+
properties: {
|
|
3533
|
+
actionId: { type: "string", minLength: 1 },
|
|
3534
|
+
input: { type: "object", additionalProperties: true },
|
|
3535
|
+
resourceType: { type: "string", description: "Optional app-local resource alias, such as workspace." },
|
|
3536
|
+
resourceId: { type: "string", description: "Resource id. Required when resourceType is provided." },
|
|
3537
|
+
confirm: { type: "boolean" }
|
|
3538
|
+
},
|
|
3539
|
+
required: ["actionId"],
|
|
3540
|
+
additionalProperties: false
|
|
3541
|
+
},
|
|
3542
|
+
handler: async (topolo, args) => {
|
|
3543
|
+
const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
|
|
3544
|
+
if (!actionId) throw new Error("actionId is required.");
|
|
3545
|
+
const input = args["input"];
|
|
3546
|
+
if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
|
|
3547
|
+
throw new Error("input must be an object.");
|
|
3548
|
+
}
|
|
3549
|
+
const resource = optionalResourceContext(args);
|
|
3550
|
+
return topolo.client.callAction(
|
|
3551
|
+
actionId,
|
|
3552
|
+
input ?? {},
|
|
3553
|
+
{
|
|
3554
|
+
confirm: args["confirm"] === true,
|
|
3555
|
+
...resource ? { resource } : {}
|
|
3556
|
+
}
|
|
3282
3557
|
);
|
|
3283
|
-
if (!match) {
|
|
3284
|
-
const known = catalog.map((e) => e.slug || e.appId).sort().filter((value) => typeof value === "string" && value.length > 0);
|
|
3285
|
-
throw new Error(
|
|
3286
|
-
`Unknown application "${application}". ${formatKnownValues(known, "applications")}`
|
|
3287
|
-
);
|
|
3288
|
-
}
|
|
3289
|
-
return { application: match };
|
|
3290
3558
|
}
|
|
3291
3559
|
},
|
|
3292
3560
|
{
|
|
@@ -3300,7 +3568,7 @@ var TOOLS = [
|
|
|
3300
3568
|
properties: {
|
|
3301
3569
|
application: {
|
|
3302
3570
|
type: "string",
|
|
3303
|
-
description: "Optional application ID from
|
|
3571
|
+
description: "Optional application ID from topolo_search_applications."
|
|
3304
3572
|
}
|
|
3305
3573
|
},
|
|
3306
3574
|
additionalProperties: false
|
|
@@ -3340,7 +3608,7 @@ var TOOLS = [
|
|
|
3340
3608
|
properties: {
|
|
3341
3609
|
application: {
|
|
3342
3610
|
type: "string",
|
|
3343
|
-
description: "Optional application ID from
|
|
3611
|
+
description: "Optional application ID from topolo_search_applications."
|
|
3344
3612
|
},
|
|
3345
3613
|
failOn: {
|
|
3346
3614
|
type: "string",
|
|
@@ -3374,7 +3642,7 @@ var TOOLS = [
|
|
|
3374
3642
|
{
|
|
3375
3643
|
name: "topolo_api_call",
|
|
3376
3644
|
title: "Call any Topolo platform service",
|
|
3377
|
-
description: "Low-level passthrough to any Topolo service. Use when no domain-specific tool exists for the operation you need. The request is signed with the current credential and scoped to the caller's organization \u2014 the target service will return 403 if your scopes do not permit the operation, even though this tool is advertised. Mutating calls (non-GET methods) require confirm:true. Call
|
|
3645
|
+
description: "Low-level passthrough to any Topolo service. Use when no domain-specific tool exists for the operation you need. The request is signed with the current credential and scoped to the caller's organization \u2014 the target service will return 403 if your scopes do not permit the operation, even though this tool is advertised. Mutating calls (non-GET methods) require confirm:true. Call topolo_search_applications first to discover callable app IDs; the SDK can also resolve live Auth-catalog app ids/slugs for newly registered API surfaces.",
|
|
3378
3646
|
requiredScopes: [],
|
|
3379
3647
|
destructive: true,
|
|
3380
3648
|
inputSchema: {
|
|
@@ -3405,7 +3673,7 @@ var TOOLS = [
|
|
|
3405
3673
|
},
|
|
3406
3674
|
resourceType: {
|
|
3407
3675
|
type: "string",
|
|
3408
|
-
description: "Optional workspace resource alias from
|
|
3676
|
+
description: "Optional workspace resource alias from topolo_search_applications apiKeyResources. Aliases are app-defined, e.g. brand, mailbox, venue, merchant, or any other cataloged alias."
|
|
3409
3677
|
},
|
|
3410
3678
|
resourceId: {
|
|
3411
3679
|
type: "string",
|
|
@@ -3423,7 +3691,7 @@ var TOOLS = [
|
|
|
3423
3691
|
const service = args["service"];
|
|
3424
3692
|
const path = args["path"];
|
|
3425
3693
|
if (typeof service !== "string" || !service.trim()) {
|
|
3426
|
-
throw new Error("service is required. Call
|
|
3694
|
+
throw new Error("service is required. Call topolo_search_applications first to discover callable services.");
|
|
3427
3695
|
}
|
|
3428
3696
|
if (typeof path !== "string" || !path) {
|
|
3429
3697
|
throw new Error("path is required");
|
|
@@ -3437,12 +3705,7 @@ var TOOLS = [
|
|
|
3437
3705
|
const normalisedQuery = query && typeof query === "object" && !Array.isArray(query) ? Object.fromEntries(
|
|
3438
3706
|
Object.entries(query).map(([k, v]) => [k, String(v)])
|
|
3439
3707
|
) : void 0;
|
|
3440
|
-
const
|
|
3441
|
-
const resourceId = typeof args["resourceId"] === "string" ? args["resourceId"].trim() : "";
|
|
3442
|
-
if (resourceType && !resourceId || !resourceType && resourceId) {
|
|
3443
|
-
throw new Error("resourceType and resourceId must be provided together.");
|
|
3444
|
-
}
|
|
3445
|
-
const resource = resourceType && resourceId ? { resourceType, resourceId } : void 0;
|
|
3708
|
+
const resource = optionalResourceContext(args);
|
|
3446
3709
|
return topolo.client.request({
|
|
3447
3710
|
service: service.trim(),
|
|
3448
3711
|
method,
|
|
@@ -3459,58 +3722,8 @@ function filterToolsByScopes(tools, scopes) {
|
|
|
3459
3722
|
return tools.filter((t) => hasAnyScope(scopes, t.requiredScopes));
|
|
3460
3723
|
}
|
|
3461
3724
|
async function buildTopoloTools(topolo, scopes, tools = TOOLS) {
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
const actionTools = actionsToToolDefs(actions);
|
|
3465
|
-
return filterToolsByScopes([...baseTools, ...actionTools], scopes);
|
|
3466
|
-
}
|
|
3467
|
-
function effectiveAgentAccess(a) {
|
|
3468
|
-
if (a.agentAccess) return a.agentAccess;
|
|
3469
|
-
if (a.destructive) return "off";
|
|
3470
|
-
if (a.readOnly && !a.requiresConfirmation) return "auto";
|
|
3471
|
-
return "confirm";
|
|
3472
|
-
}
|
|
3473
|
-
function actionsToToolDefs(actions) {
|
|
3474
|
-
return actions.map((action) => ({ action, access: effectiveAgentAccess(action) })).filter(({ access }) => access !== "off").map(({ action, access }) => ({
|
|
3475
|
-
name: action.toolName || toolNameForAction(action),
|
|
3476
|
-
title: action.title,
|
|
3477
|
-
description: action.description,
|
|
3478
|
-
requiredScopes: action.requiredPermission ? [action.requiredPermission] : [],
|
|
3479
|
-
destructive: access === "confirm",
|
|
3480
|
-
inputSchema: actionInputSchema(action, access),
|
|
3481
|
-
handler: async (topolo, args) => {
|
|
3482
|
-
const confirm = Boolean(args["confirm"]);
|
|
3483
|
-
const input = { ...args };
|
|
3484
|
-
delete input["confirm"];
|
|
3485
|
-
return topolo.client.callAction(action.actionId, input, { confirm });
|
|
3486
|
-
}
|
|
3487
|
-
}));
|
|
3488
|
-
}
|
|
3489
|
-
function toolNameForAction(action) {
|
|
3490
|
-
const service = (action.appSlug || action.appId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3491
|
-
const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3492
|
-
return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
|
|
3493
|
-
}
|
|
3494
|
-
function actionInputSchema(action, access = "auto") {
|
|
3495
|
-
const schema = {
|
|
3496
|
-
type: "object",
|
|
3497
|
-
properties: {},
|
|
3498
|
-
additionalProperties: false,
|
|
3499
|
-
...action.inputSchema
|
|
3500
|
-
};
|
|
3501
|
-
const properties = schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties) ? { ...schema.properties } : {};
|
|
3502
|
-
if (access === "confirm") {
|
|
3503
|
-
properties.confirm = {
|
|
3504
|
-
type: "boolean",
|
|
3505
|
-
description: "Required for mutating actions. Explicit human-in-the-loop acknowledgement."
|
|
3506
|
-
};
|
|
3507
|
-
}
|
|
3508
|
-
return {
|
|
3509
|
-
...schema,
|
|
3510
|
-
type: "object",
|
|
3511
|
-
properties,
|
|
3512
|
-
additionalProperties: schema.additionalProperties ?? false
|
|
3513
|
-
};
|
|
3725
|
+
void topolo;
|
|
3726
|
+
return filterToolsByScopes(tools, scopes);
|
|
3514
3727
|
}
|
|
3515
3728
|
function evaluateApplicationAuditGate(report, threshold) {
|
|
3516
3729
|
const counts = {
|
|
@@ -3665,7 +3878,8 @@ function toolError(message) {
|
|
|
3665
3878
|
// src/index.ts
|
|
3666
3879
|
async function main() {
|
|
3667
3880
|
const environment = normalizeEnvironment(process.env.TOPOLO_ENV);
|
|
3668
|
-
|
|
3881
|
+
const environmentCredential = resolveCredentialFromEnv();
|
|
3882
|
+
let credential = environmentCredential;
|
|
3669
3883
|
if (!credential) {
|
|
3670
3884
|
credential = await refreshCredentialIfNeeded(environment);
|
|
3671
3885
|
}
|
|
@@ -3687,7 +3901,8 @@ async function main() {
|
|
|
3687
3901
|
...agentName ? { agentName } : {}
|
|
3688
3902
|
},
|
|
3689
3903
|
serviceUrls,
|
|
3690
|
-
requireConfirmForWrites: true
|
|
3904
|
+
requireConfirmForWrites: true,
|
|
3905
|
+
...!environmentCredential ? { refreshCredential: () => refreshCredentialIfNeeded(environment, { force: true }) } : {}
|
|
3691
3906
|
});
|
|
3692
3907
|
const scopes = await loadScopes(topolo);
|
|
3693
3908
|
const server = buildServer(topolo, scopes);
|