@topolo/mcp 0.6.0 → 0.7.1
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 +70 -26
- package/dist/dispatch.js +144 -106
- package/dist/http.js +152 -114
- package/dist/index.js +417 -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;
|
|
@@ -66,6 +72,7 @@ var APP_API_URLS = {
|
|
|
66
72
|
bugfix: "https://bugfix.topolo.app",
|
|
67
73
|
bytes: "https://bytes.topolo.app",
|
|
68
74
|
calendar: "https://calendar.topolo.app",
|
|
75
|
+
campaigns: "https://campaigns.topolo.app",
|
|
69
76
|
capacity: "https://capacity.topolo.app",
|
|
70
77
|
chat: "https://chat.topolo.app",
|
|
71
78
|
commerce: "https://commerce.topolo.app",
|
|
@@ -324,6 +331,38 @@ var APPLICATIONS = {
|
|
|
324
331
|
}
|
|
325
332
|
]
|
|
326
333
|
},
|
|
334
|
+
"campaigns": {
|
|
335
|
+
"id": "campaigns",
|
|
336
|
+
"appDir": "TopoloCampaigns",
|
|
337
|
+
"name": "topolo-campaigns",
|
|
338
|
+
"packageName": "topolo-campaigns",
|
|
339
|
+
"productionUrl": "https://campaigns.topolo.app",
|
|
340
|
+
"services": [
|
|
341
|
+
"campaigns"
|
|
342
|
+
],
|
|
343
|
+
"deployTargets": [
|
|
344
|
+
{
|
|
345
|
+
"name": "topolo-campaigns",
|
|
346
|
+
"kind": "worker",
|
|
347
|
+
"apiUrl": "https://campaigns.topolo.app",
|
|
348
|
+
"domains": [
|
|
349
|
+
"campaigns.topolo.app"
|
|
350
|
+
],
|
|
351
|
+
"routes": [
|
|
352
|
+
"campaigns.topolo.app"
|
|
353
|
+
],
|
|
354
|
+
"appId": "campaigns"
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"name": "topolo-campaigns-api",
|
|
358
|
+
"kind": "worker",
|
|
359
|
+
"apiUrl": null,
|
|
360
|
+
"domains": [],
|
|
361
|
+
"routes": [],
|
|
362
|
+
"appId": null
|
|
363
|
+
}
|
|
364
|
+
]
|
|
365
|
+
},
|
|
327
366
|
"capacity": {
|
|
328
367
|
"id": "capacity",
|
|
329
368
|
"appDir": "TopoloCapacity",
|
|
@@ -1106,6 +1145,7 @@ var APPLICATIONS = {
|
|
|
1106
1145
|
"topolo.io"
|
|
1107
1146
|
],
|
|
1108
1147
|
"routes": [
|
|
1148
|
+
"one.topolo.io",
|
|
1109
1149
|
"topolo.io",
|
|
1110
1150
|
"www.topolo.io"
|
|
1111
1151
|
],
|
|
@@ -2059,7 +2099,6 @@ var TOPOLO_RESOURCE_TYPE_HEADER = "X-Topolo-Resource-Type";
|
|
|
2059
2099
|
var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
|
|
2060
2100
|
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
2061
2101
|
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
2062
|
-
var CATALOG_CACHE_TTL_MS = 60 * 1e3;
|
|
2063
2102
|
var TopoloClient = class _TopoloClient {
|
|
2064
2103
|
credential;
|
|
2065
2104
|
agent;
|
|
@@ -2068,9 +2107,9 @@ var TopoloClient = class _TopoloClient {
|
|
|
2068
2107
|
requireConfirmForWrites;
|
|
2069
2108
|
timeoutMs;
|
|
2070
2109
|
fetchImpl;
|
|
2110
|
+
refreshCredential;
|
|
2111
|
+
refreshInFlight = null;
|
|
2071
2112
|
debug;
|
|
2072
|
-
serviceCatalogCache = null;
|
|
2073
|
-
actionCatalogCache = null;
|
|
2074
2113
|
/**
|
|
2075
2114
|
* Process-wide cache for slug → app-id lookups. Keyed by
|
|
2076
2115
|
* `${authBaseUrl}|${slug}` so multiple `TopoloClient` instances pointing at
|
|
@@ -2095,6 +2134,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2095
2134
|
this.requireConfirmForWrites = options.requireConfirmForWrites !== false;
|
|
2096
2135
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
2097
2136
|
this.fetchImpl = options.fetch ?? fetch;
|
|
2137
|
+
this.refreshCredential = options.refreshCredential;
|
|
2098
2138
|
this.debug = options.debug;
|
|
2099
2139
|
}
|
|
2100
2140
|
emit(event) {
|
|
@@ -2112,11 +2152,11 @@ var TopoloClient = class _TopoloClient {
|
|
|
2112
2152
|
async request(opts) {
|
|
2113
2153
|
const method = opts.method ?? "GET";
|
|
2114
2154
|
if (this.requireConfirmForWrites && WRITE_METHODS.has(method) && !opts.confirm) {
|
|
2115
|
-
throw new
|
|
2155
|
+
throw new TopoloConfirmationRequiredError(
|
|
2116
2156
|
`Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
|
|
2117
2157
|
);
|
|
2118
2158
|
}
|
|
2119
|
-
const resolvedService = await this.resolveRequestService(opts.service);
|
|
2159
|
+
const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: requireHttpOrigin(opts.baseUrl) } : await this.resolveRequestService(opts.service);
|
|
2120
2160
|
const baseUrl = resolvedService.baseUrl;
|
|
2121
2161
|
const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
2122
2162
|
if (opts.query) {
|
|
@@ -2147,20 +2187,36 @@ var TopoloClient = class _TopoloClient {
|
|
|
2147
2187
|
headers.set(TOPOLO_RESOURCE_TYPE_HEADER, resourceType);
|
|
2148
2188
|
headers.set(TOPOLO_RESOURCE_ID_HEADER, resourceId);
|
|
2149
2189
|
}
|
|
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
2190
|
const urlStr = url.toString();
|
|
2154
2191
|
const startedAt = Date.now();
|
|
2155
2192
|
this.emit({ phase: "request", method, service: resolvedService.serviceKey, path: opts.path, url: urlStr, requestId });
|
|
2193
|
+
const fetchOnce = async () => {
|
|
2194
|
+
const controller = new AbortController();
|
|
2195
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2196
|
+
const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
|
|
2197
|
+
try {
|
|
2198
|
+
return await this.fetchImpl(urlStr, {
|
|
2199
|
+
method,
|
|
2200
|
+
headers: new Headers(headers),
|
|
2201
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : null,
|
|
2202
|
+
signal
|
|
2203
|
+
});
|
|
2204
|
+
} finally {
|
|
2205
|
+
clearTimeout(timeoutId);
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
2156
2208
|
let res;
|
|
2157
2209
|
try {
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2210
|
+
const attemptedCredential = this.credential;
|
|
2211
|
+
res = await fetchOnce();
|
|
2212
|
+
if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
|
|
2213
|
+
await res.body?.cancel().catch(() => void 0);
|
|
2214
|
+
const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
|
|
2215
|
+
if (refreshed) {
|
|
2216
|
+
applyAuthHeaders(headers, refreshed);
|
|
2217
|
+
res = await fetchOnce();
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2164
2220
|
} catch (err) {
|
|
2165
2221
|
this.emit({
|
|
2166
2222
|
phase: "error",
|
|
@@ -2173,8 +2229,6 @@ var TopoloClient = class _TopoloClient {
|
|
|
2173
2229
|
error: err instanceof Error ? err.message : String(err)
|
|
2174
2230
|
});
|
|
2175
2231
|
throw err;
|
|
2176
|
-
} finally {
|
|
2177
|
-
clearTimeout(timeoutId);
|
|
2178
2232
|
}
|
|
2179
2233
|
const contentType = res.headers.get("Content-Type") ?? "";
|
|
2180
2234
|
const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
|
|
@@ -2213,6 +2267,19 @@ var TopoloClient = class _TopoloClient {
|
|
|
2213
2267
|
});
|
|
2214
2268
|
return parsed;
|
|
2215
2269
|
}
|
|
2270
|
+
async refreshRejectedAccessToken(attemptedAccessToken) {
|
|
2271
|
+
if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
|
|
2272
|
+
return this.credential;
|
|
2273
|
+
}
|
|
2274
|
+
if (!this.refreshCredential) return null;
|
|
2275
|
+
this.refreshInFlight ??= this.refreshCredential().then((credential) => {
|
|
2276
|
+
if (credential) this.credential = credential;
|
|
2277
|
+
return credential;
|
|
2278
|
+
}).catch(() => null).finally(() => {
|
|
2279
|
+
this.refreshInFlight = null;
|
|
2280
|
+
});
|
|
2281
|
+
return this.refreshInFlight;
|
|
2282
|
+
}
|
|
2216
2283
|
async resolveRequestService(service) {
|
|
2217
2284
|
const serviceKey = normalizeLookupKey(service);
|
|
2218
2285
|
if (!serviceKey) {
|
|
@@ -2238,9 +2305,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
2238
2305
|
);
|
|
2239
2306
|
}
|
|
2240
2307
|
async resolveLiveCallableService(serviceKey) {
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2243
|
-
if (!match) return null;
|
|
2308
|
+
const match = await this.getService(serviceKey).catch(() => null);
|
|
2309
|
+
if (!match || !catalogEntryMatches(match, serviceKey)) return null;
|
|
2244
2310
|
const baseUrl = resolveCatalogServiceUrl(match, {
|
|
2245
2311
|
serviceKey,
|
|
2246
2312
|
overrides: this.serviceUrls
|
|
@@ -2314,12 +2380,13 @@ var TopoloClient = class _TopoloClient {
|
|
|
2314
2380
|
* The live Auth catalog is the authority because it is scoped to the caller's
|
|
2315
2381
|
* organization and effective permissions.
|
|
2316
2382
|
*/
|
|
2317
|
-
async
|
|
2318
|
-
const
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2383
|
+
async searchServices(options = {}) {
|
|
2384
|
+
const res = await this.request({
|
|
2385
|
+
service: "developers",
|
|
2386
|
+
method: "GET",
|
|
2387
|
+
path: "/api/services/catalog",
|
|
2388
|
+
query: catalogSearchQuery(options)
|
|
2389
|
+
});
|
|
2323
2390
|
const raw = res.data?.services ?? res.services;
|
|
2324
2391
|
if (!Array.isArray(raw)) {
|
|
2325
2392
|
throw new TopoloSdkError(
|
|
@@ -2329,6 +2396,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2329
2396
|
}
|
|
2330
2397
|
const entries = [];
|
|
2331
2398
|
for (const item of raw) {
|
|
2399
|
+
if (!isRecord(item)) continue;
|
|
2332
2400
|
const s = item;
|
|
2333
2401
|
const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
|
|
2334
2402
|
if (!appId) continue;
|
|
@@ -2353,11 +2421,30 @@ var TopoloClient = class _TopoloClient {
|
|
|
2353
2421
|
apiKeyResources
|
|
2354
2422
|
});
|
|
2355
2423
|
}
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
};
|
|
2360
|
-
|
|
2424
|
+
const page = normalizeCatalogPage(
|
|
2425
|
+
res.data?.page ?? res.page
|
|
2426
|
+
);
|
|
2427
|
+
return { services: entries, page };
|
|
2428
|
+
}
|
|
2429
|
+
/** @deprecated Prefer searchServices() so callers retain the continuation cursor. */
|
|
2430
|
+
async listServices(options = {}) {
|
|
2431
|
+
return (await this.searchServices(options)).services;
|
|
2432
|
+
}
|
|
2433
|
+
async getService(application) {
|
|
2434
|
+
const needle = application.trim();
|
|
2435
|
+
if (!needle) throw new TopoloSdkError("unknown_service", "application is required");
|
|
2436
|
+
const res = await this.request({
|
|
2437
|
+
service: "developers",
|
|
2438
|
+
method: "GET",
|
|
2439
|
+
path: "/api/services/catalog",
|
|
2440
|
+
query: { application: needle }
|
|
2441
|
+
});
|
|
2442
|
+
const raw = res.data?.application ?? res.application;
|
|
2443
|
+
const entry = normalizeServiceCatalogEntry(raw);
|
|
2444
|
+
if (!entry) {
|
|
2445
|
+
throw new TopoloSdkError("unknown_service", `Unknown Topolo application "${application}".`);
|
|
2446
|
+
}
|
|
2447
|
+
return entry;
|
|
2361
2448
|
}
|
|
2362
2449
|
/**
|
|
2363
2450
|
* List callable actions from the live registry-canonical catalog (definitions
|
|
@@ -2365,16 +2452,15 @@ var TopoloClient = class _TopoloClient {
|
|
|
2365
2452
|
* caller's organization and effective permissions; no bundled/static app
|
|
2366
2453
|
* action list is used as an authorization source.
|
|
2367
2454
|
*/
|
|
2368
|
-
async
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
return this.actionCatalogCache.entries;
|
|
2455
|
+
async searchActions(options) {
|
|
2456
|
+
if (!options.service?.trim()) {
|
|
2457
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2372
2458
|
}
|
|
2373
2459
|
const res = await this.request({
|
|
2374
2460
|
service: "developers",
|
|
2375
2461
|
method: "GET",
|
|
2376
2462
|
path: "/api/actions/catalog",
|
|
2377
|
-
|
|
2463
|
+
query: { service: options.service, ...catalogSearchQuery(options) }
|
|
2378
2464
|
});
|
|
2379
2465
|
const raw = res.data?.actions ?? res.actions;
|
|
2380
2466
|
if (!Array.isArray(raw)) {
|
|
@@ -2384,36 +2470,96 @@ var TopoloClient = class _TopoloClient {
|
|
|
2384
2470
|
);
|
|
2385
2471
|
}
|
|
2386
2472
|
const entries = raw.map((item) => normalizeActionCatalogEntry(item)).filter((entry) => Boolean(entry));
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2473
|
+
return {
|
|
2474
|
+
actions: entries,
|
|
2475
|
+
page: normalizeCatalogPage(
|
|
2476
|
+
res.data?.page ?? res.page
|
|
2477
|
+
)
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
async getActionCatalogSummary(service) {
|
|
2481
|
+
const boundedService = service.trim();
|
|
2482
|
+
if (!boundedService) {
|
|
2483
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2484
|
+
}
|
|
2485
|
+
const res = await this.request({
|
|
2486
|
+
service: "developers",
|
|
2487
|
+
method: "GET",
|
|
2488
|
+
path: "/api/actions/catalog",
|
|
2489
|
+
query: { service: boundedService, summary: "true" }
|
|
2490
|
+
});
|
|
2491
|
+
const raw = res.data?.summary ?? res.summary;
|
|
2492
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2493
|
+
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
|
|
2494
|
+
}
|
|
2495
|
+
const record = raw;
|
|
2496
|
+
if (record.algorithm !== "sha256-v1" || typeof record.digest !== "string" || !/^[a-f0-9]{64}$/.test(record.digest) || !Number.isSafeInteger(record.actionCount) || Number(record.actionCount) < 0) {
|
|
2497
|
+
throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
|
|
2498
|
+
}
|
|
2499
|
+
return {
|
|
2500
|
+
algorithm: "sha256-v1",
|
|
2501
|
+
digest: record.digest,
|
|
2502
|
+
actionCount: Number(record.actionCount)
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
/** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
|
|
2506
|
+
async listActions(options) {
|
|
2507
|
+
if (!options) {
|
|
2508
|
+
throw new TopoloSdkError("service_required", "service is required for bounded action discovery");
|
|
2392
2509
|
}
|
|
2393
|
-
return
|
|
2510
|
+
return (await this.searchActions(options)).actions;
|
|
2394
2511
|
}
|
|
2395
2512
|
async getAction(action) {
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
const match = actions.find((entry) => actionEntryMatches(entry, needle));
|
|
2399
|
-
if (!match) {
|
|
2513
|
+
const actionId = action.trim();
|
|
2514
|
+
if (!actionId) {
|
|
2400
2515
|
throw new TopoloSdkError(
|
|
2401
2516
|
"unknown_action",
|
|
2402
|
-
|
|
2517
|
+
"action id is required"
|
|
2403
2518
|
);
|
|
2404
2519
|
}
|
|
2520
|
+
let res;
|
|
2521
|
+
try {
|
|
2522
|
+
res = await this.request({
|
|
2523
|
+
service: "developers",
|
|
2524
|
+
method: "GET",
|
|
2525
|
+
path: "/api/actions/catalog",
|
|
2526
|
+
query: { actionId }
|
|
2527
|
+
});
|
|
2528
|
+
} catch (error) {
|
|
2529
|
+
if (error instanceof TopoloHttpError && error.status === 404) {
|
|
2530
|
+
throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
2531
|
+
}
|
|
2532
|
+
throw error;
|
|
2533
|
+
}
|
|
2534
|
+
const raw = res.data?.action ?? res.action;
|
|
2535
|
+
const match = normalizeActionCatalogEntry(raw);
|
|
2536
|
+
if (!match) throw new TopoloSdkError("unknown_action", `Unknown or unavailable Topolo action "${action}".`);
|
|
2405
2537
|
return match;
|
|
2406
2538
|
}
|
|
2407
2539
|
async callAction(action, input = {}, options = {}) {
|
|
2408
2540
|
const entry = await this.getAction(action);
|
|
2541
|
+
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
2542
|
+
if (agentAccess === "off") {
|
|
2543
|
+
throw new TopoloPermissionError(
|
|
2544
|
+
`Topolo action "${entry.actionId}" is not available for agent invocation.`,
|
|
2545
|
+
entry.requiredPermission ? [entry.requiredPermission] : []
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
if ((agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive) && options.confirm !== true) {
|
|
2549
|
+
throw new TopoloConfirmationRequiredError(
|
|
2550
|
+
`Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2409
2553
|
const mapped = mapActionInput(entry, input);
|
|
2410
2554
|
const service = entry.appSlug || entry.appId;
|
|
2411
2555
|
return this.request({
|
|
2412
2556
|
service,
|
|
2557
|
+
...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
|
|
2413
2558
|
method: entry.method,
|
|
2414
2559
|
path: mapped.path,
|
|
2415
2560
|
...mapped.query ? { query: mapped.query } : {},
|
|
2416
2561
|
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2562
|
+
...options.resource ? { resource: options.resource } : {},
|
|
2417
2563
|
headers: {
|
|
2418
2564
|
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2419
2565
|
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
@@ -2551,6 +2697,18 @@ function serviceKeyCandidates2(slug) {
|
|
|
2551
2697
|
function normalizeLookupKey(value) {
|
|
2552
2698
|
return String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
|
|
2553
2699
|
}
|
|
2700
|
+
function requireHttpOrigin(value) {
|
|
2701
|
+
let url;
|
|
2702
|
+
try {
|
|
2703
|
+
url = new URL(value);
|
|
2704
|
+
} catch {
|
|
2705
|
+
throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
|
|
2706
|
+
}
|
|
2707
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password) {
|
|
2708
|
+
throw new TopoloSdkError("invalid_action_target", `Invalid action API origin "${value}".`);
|
|
2709
|
+
}
|
|
2710
|
+
return url.origin;
|
|
2711
|
+
}
|
|
2554
2712
|
function deriveDevelopersFromAuth(authUrl) {
|
|
2555
2713
|
if (!authUrl) return null;
|
|
2556
2714
|
try {
|
|
@@ -2577,6 +2735,47 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2577
2735
|
}
|
|
2578
2736
|
return [...keys];
|
|
2579
2737
|
}
|
|
2738
|
+
function catalogSearchQuery(options) {
|
|
2739
|
+
const query = {};
|
|
2740
|
+
if (typeof options.query === "string" && options.query.trim()) query.query = options.query.trim();
|
|
2741
|
+
if (typeof options.cursor === "string" && options.cursor.trim()) query.cursor = options.cursor.trim();
|
|
2742
|
+
if (typeof options.limit === "number" && Number.isFinite(options.limit)) query.limit = Math.trunc(options.limit);
|
|
2743
|
+
return query;
|
|
2744
|
+
}
|
|
2745
|
+
function normalizeCatalogPage(value) {
|
|
2746
|
+
if (!isRecord(value)) return { nextCursor: null, hasMore: false };
|
|
2747
|
+
return {
|
|
2748
|
+
nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
|
|
2749
|
+
hasMore: value.hasMore === true
|
|
2750
|
+
};
|
|
2751
|
+
}
|
|
2752
|
+
function normalizeServiceCatalogEntry(item) {
|
|
2753
|
+
if (!isRecord(item)) return null;
|
|
2754
|
+
const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
|
|
2755
|
+
if (!appId) return null;
|
|
2756
|
+
const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
|
|
2757
|
+
(permission) => typeof permission === "string" && permission.trim().length > 0
|
|
2758
|
+
) : [];
|
|
2759
|
+
const catalogApiKeyResources = normalizeApiKeyResourceMetadataList(
|
|
2760
|
+
item.api_key_resources ?? item.apiKeyResources
|
|
2761
|
+
);
|
|
2762
|
+
const apiKeyResource = normalizeApiKeyResourceMetadata(item.api_key_resource ?? item.apiKeyResource) ?? catalogApiKeyResources[0] ?? null;
|
|
2763
|
+
const apiKeyResources = catalogApiKeyResources.length > 0 ? catalogApiKeyResources : apiKeyResource ? [apiKeyResource] : [];
|
|
2764
|
+
return {
|
|
2765
|
+
appId,
|
|
2766
|
+
slug: typeof item.app_slug === "string" ? item.app_slug : null,
|
|
2767
|
+
name: typeof item.name === "string" ? item.name : null,
|
|
2768
|
+
description: typeof item.description === "string" ? item.description : null,
|
|
2769
|
+
apiBaseUrl: typeof item.api_base_url === "string" ? item.api_base_url : typeof item.apiBaseUrl === "string" ? item.apiBaseUrl : null,
|
|
2770
|
+
launchUrl: typeof item.launch_url === "string" ? item.launch_url : typeof item.launchUrl === "string" ? item.launchUrl : null,
|
|
2771
|
+
status: typeof item.status === "string" ? item.status : null,
|
|
2772
|
+
surfaceKind: typeof item.surface_kind === "string" ? item.surface_kind : null,
|
|
2773
|
+
launchable: item.launchable === true,
|
|
2774
|
+
permissions,
|
|
2775
|
+
apiKeyResource,
|
|
2776
|
+
apiKeyResources
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2580
2779
|
function normalizeActionCatalogEntry(item) {
|
|
2581
2780
|
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2582
2781
|
const a = item;
|
|
@@ -2590,6 +2789,8 @@ function normalizeActionCatalogEntry(item) {
|
|
|
2590
2789
|
}
|
|
2591
2790
|
return {
|
|
2592
2791
|
actionId,
|
|
2792
|
+
target: pickActionString(a.target_key) ?? pickActionString(a.target) ?? null,
|
|
2793
|
+
apiBaseUrl: pickActionString(a.api_base_url) ?? pickActionString(a.apiBaseUrl) ?? null,
|
|
2593
2794
|
name,
|
|
2594
2795
|
toolName: pickActionString(a.tool_name) ?? pickActionString(a.toolName) ?? null,
|
|
2595
2796
|
title: pickActionString(a.title) ?? name,
|
|
@@ -2601,6 +2802,7 @@ function normalizeActionCatalogEntry(item) {
|
|
|
2601
2802
|
path,
|
|
2602
2803
|
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
2603
2804
|
requiredPermission: pickActionString(a.required_permission) ?? pickActionString(a.requiredPermission) ?? "",
|
|
2805
|
+
authorizationMode: pickAuthorizationMode(a.authorization_mode ?? a.authorizationMode),
|
|
2604
2806
|
inputSchema: plainObject(a.input_schema ?? a.inputSchema),
|
|
2605
2807
|
outputSchema: plainObject(a.output_schema ?? a.outputSchema),
|
|
2606
2808
|
readOnly: Boolean(a.read_only ?? a.readOnly),
|
|
@@ -2620,16 +2822,12 @@ function pickActionString(value) {
|
|
|
2620
2822
|
function pickAgentAccess(value) {
|
|
2621
2823
|
return value === "auto" || value === "confirm" || value === "off" ? value : null;
|
|
2622
2824
|
}
|
|
2825
|
+
function pickAuthorizationMode(value) {
|
|
2826
|
+
return value === "runtime" ? "runtime" : "static";
|
|
2827
|
+
}
|
|
2623
2828
|
function plainObject(value) {
|
|
2624
2829
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2625
2830
|
}
|
|
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
2831
|
function mapActionInput(action, input) {
|
|
2634
2832
|
const remaining = { ...input || {} };
|
|
2635
2833
|
const path = action.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key) => {
|
|
@@ -2815,6 +3013,7 @@ var APP_API_URLS2 = {
|
|
|
2815
3013
|
bugfix: "https://bugfix.topolo.app",
|
|
2816
3014
|
bytes: "https://bytes.topolo.app",
|
|
2817
3015
|
calendar: "https://calendar.topolo.app",
|
|
3016
|
+
campaigns: "https://campaigns.topolo.app",
|
|
2818
3017
|
capacity: "https://capacity.topolo.app",
|
|
2819
3018
|
chat: "https://chat.topolo.app",
|
|
2820
3019
|
commerce: "https://commerce.topolo.app",
|
|
@@ -3058,7 +3257,7 @@ async function resolveCredential(environment = selectedEnvironment()) {
|
|
|
3058
3257
|
return profileForEnvironment(config, environment).credential ?? null;
|
|
3059
3258
|
}
|
|
3060
3259
|
var REFRESH_SKEW_SECONDS = 60;
|
|
3061
|
-
async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
3260
|
+
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
3062
3261
|
const prefix = envVarPrefix(environment);
|
|
3063
3262
|
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
3064
3263
|
return resolveCredential(environment);
|
|
@@ -3069,7 +3268,7 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
|
3069
3268
|
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
3070
3269
|
if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
|
|
3071
3270
|
const now = Math.floor(Date.now() / 1e3);
|
|
3072
|
-
if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
3271
|
+
if (!options.force && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
3073
3272
|
try {
|
|
3074
3273
|
const oauth = new TopoloOAuth({
|
|
3075
3274
|
serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
|
|
@@ -3084,9 +3283,20 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
|
3084
3283
|
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
3085
3284
|
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
3086
3285
|
};
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3286
|
+
const activeContext = profile.activeContext;
|
|
3287
|
+
const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
|
|
3288
|
+
const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
|
|
3289
|
+
credential: refreshed,
|
|
3290
|
+
contexts: {
|
|
3291
|
+
...profile.contexts ?? {},
|
|
3292
|
+
[activeContext]: {
|
|
3293
|
+
...activeEntry,
|
|
3294
|
+
credential: refreshed,
|
|
3295
|
+
oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
}) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
|
|
3299
|
+
await writeConfig(updated);
|
|
3090
3300
|
return refreshed;
|
|
3091
3301
|
} catch {
|
|
3092
3302
|
return cred;
|
|
@@ -3170,6 +3380,15 @@ function splitService(servicePart) {
|
|
|
3170
3380
|
// src/tools.ts
|
|
3171
3381
|
var APPLICATION_IDS = Object.keys(APPLICATIONS).sort();
|
|
3172
3382
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
3383
|
+
function optionalResourceContext(args) {
|
|
3384
|
+
const resourceType = typeof args["resourceType"] === "string" ? args["resourceType"].trim() : "";
|
|
3385
|
+
const resourceId = typeof args["resourceId"] === "string" ? args["resourceId"].trim() : "";
|
|
3386
|
+
if (!resourceType && !resourceId) return void 0;
|
|
3387
|
+
if (!resourceType || !resourceId) {
|
|
3388
|
+
throw new Error("resourceType and resourceId must be provided together.");
|
|
3389
|
+
}
|
|
3390
|
+
return { resourceType, resourceId };
|
|
3391
|
+
}
|
|
3173
3392
|
var TOOLS = [
|
|
3174
3393
|
{
|
|
3175
3394
|
name: "topolo_whoami",
|
|
@@ -3185,22 +3404,32 @@ var TOOLS = [
|
|
|
3185
3404
|
handler: async (topolo) => topolo.identity.whoami()
|
|
3186
3405
|
},
|
|
3187
3406
|
{
|
|
3188
|
-
name: "
|
|
3189
|
-
title: "
|
|
3190
|
-
description: "
|
|
3407
|
+
name: "topolo_search_applications",
|
|
3408
|
+
title: "Search Topolo applications the agent can reach",
|
|
3409
|
+
description: "Searches the credential-scoped application registry with a bounded page and continuation cursor. Returns actionService as the canonical selector for topolo_search_actions, plus granted permissions, API URL, and launch URL without loading the global platform catalog.",
|
|
3191
3410
|
requiredScopes: [],
|
|
3192
3411
|
destructive: false,
|
|
3193
3412
|
inputSchema: {
|
|
3194
3413
|
type: "object",
|
|
3195
|
-
properties: {
|
|
3414
|
+
properties: {
|
|
3415
|
+
query: { type: "string" },
|
|
3416
|
+
cursor: { type: "string" },
|
|
3417
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }
|
|
3418
|
+
},
|
|
3196
3419
|
additionalProperties: false
|
|
3197
3420
|
},
|
|
3198
|
-
handler: async (topolo) => {
|
|
3199
|
-
const
|
|
3421
|
+
handler: async (topolo, args) => {
|
|
3422
|
+
const result = await topolo.client.searchServices({
|
|
3423
|
+
query: typeof args["query"] === "string" ? args["query"] : void 0,
|
|
3424
|
+
cursor: typeof args["cursor"] === "string" ? args["cursor"] : void 0,
|
|
3425
|
+
limit: typeof args["limit"] === "number" ? args["limit"] : void 0
|
|
3426
|
+
});
|
|
3427
|
+
const applications = result.services.map((entry) => {
|
|
3200
3428
|
const id = displayAppId(entry);
|
|
3201
3429
|
return {
|
|
3202
3430
|
id,
|
|
3203
3431
|
appId: entry.appId,
|
|
3432
|
+
actionService: entry.appId,
|
|
3204
3433
|
slug: entry.slug,
|
|
3205
3434
|
name: entry.name,
|
|
3206
3435
|
apiBaseUrl: entry.apiBaseUrl,
|
|
@@ -3213,13 +3442,13 @@ var TOOLS = [
|
|
|
3213
3442
|
permissions: [...entry.permissions]
|
|
3214
3443
|
};
|
|
3215
3444
|
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
3216
|
-
return {
|
|
3445
|
+
return { applications, page: result.page };
|
|
3217
3446
|
}
|
|
3218
3447
|
},
|
|
3219
3448
|
{
|
|
3220
|
-
name: "
|
|
3221
|
-
title: "
|
|
3222
|
-
description: "
|
|
3449
|
+
name: "topolo_search_actions",
|
|
3450
|
+
title: "Search Topolo actions the agent can call",
|
|
3451
|
+
description: "Searches one application's credential-scoped actions with a bounded page and continuation cursor.",
|
|
3223
3452
|
requiredScopes: [],
|
|
3224
3453
|
destructive: false,
|
|
3225
3454
|
inputSchema: {
|
|
@@ -3227,31 +3456,25 @@ var TOOLS = [
|
|
|
3227
3456
|
properties: {
|
|
3228
3457
|
service: {
|
|
3229
3458
|
type: "string",
|
|
3230
|
-
description: "
|
|
3231
|
-
}
|
|
3459
|
+
description: "Required application selector. Prefer actionService from topolo_search_applications; canonical app id, slug, and name are also accepted."
|
|
3460
|
+
},
|
|
3461
|
+
query: { type: "string" },
|
|
3462
|
+
cursor: { type: "string" },
|
|
3463
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 }
|
|
3232
3464
|
},
|
|
3465
|
+
required: ["service"],
|
|
3233
3466
|
additionalProperties: false
|
|
3234
3467
|
},
|
|
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
|
-
})
|
|
3468
|
+
handler: async (topolo, args) => {
|
|
3469
|
+
const service = typeof args["service"] === "string" ? args["service"].trim() : "";
|
|
3470
|
+
if (!service) throw new Error("service is required for bounded action discovery.");
|
|
3471
|
+
return topolo.client.searchActions({
|
|
3472
|
+
service,
|
|
3473
|
+
query: typeof args["query"] === "string" ? args["query"] : void 0,
|
|
3474
|
+
cursor: typeof args["cursor"] === "string" ? args["cursor"] : void 0,
|
|
3475
|
+
limit: typeof args["limit"] === "number" ? args["limit"] : void 0
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3255
3478
|
},
|
|
3256
3479
|
{
|
|
3257
3480
|
name: "topolo_get_application",
|
|
@@ -3264,7 +3487,7 @@ var TOOLS = [
|
|
|
3264
3487
|
properties: {
|
|
3265
3488
|
application: {
|
|
3266
3489
|
type: "string",
|
|
3267
|
-
description: "Application identifier: app id, slug, or name from
|
|
3490
|
+
description: "Application identifier: app id, slug, or name from topolo_search_applications."
|
|
3268
3491
|
}
|
|
3269
3492
|
},
|
|
3270
3493
|
required: ["application"],
|
|
@@ -3275,18 +3498,98 @@ var TOOLS = [
|
|
|
3275
3498
|
if (typeof application !== "string" || !application.trim()) {
|
|
3276
3499
|
throw new Error("`application` must be a non-empty string.");
|
|
3277
3500
|
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3501
|
+
return { application: await topolo.client.getService(application) };
|
|
3502
|
+
}
|
|
3503
|
+
},
|
|
3504
|
+
{
|
|
3505
|
+
name: "topolo_get_action",
|
|
3506
|
+
title: "Describe one Topolo action",
|
|
3507
|
+
description: "Returns one credential-scoped action by exact action id.",
|
|
3508
|
+
requiredScopes: [],
|
|
3509
|
+
destructive: false,
|
|
3510
|
+
inputSchema: {
|
|
3511
|
+
type: "object",
|
|
3512
|
+
properties: { actionId: { type: "string", minLength: 1 } },
|
|
3513
|
+
required: ["actionId"],
|
|
3514
|
+
additionalProperties: false
|
|
3515
|
+
},
|
|
3516
|
+
handler: async (topolo, args) => {
|
|
3517
|
+
const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
|
|
3518
|
+
if (!actionId) throw new Error("actionId is required.");
|
|
3519
|
+
return { action: await topolo.client.getAction(actionId) };
|
|
3520
|
+
}
|
|
3521
|
+
},
|
|
3522
|
+
{
|
|
3523
|
+
name: "topolo_read_action",
|
|
3524
|
+
title: "Call one read-only Topolo action",
|
|
3525
|
+
description: "Calls one exact action id only when its published contract is read-only and needs no confirmation.",
|
|
3526
|
+
requiredScopes: [],
|
|
3527
|
+
destructive: false,
|
|
3528
|
+
inputSchema: {
|
|
3529
|
+
type: "object",
|
|
3530
|
+
properties: {
|
|
3531
|
+
actionId: { type: "string", minLength: 1 },
|
|
3532
|
+
input: { type: "object", additionalProperties: true },
|
|
3533
|
+
resourceType: { type: "string", description: "Optional app-local resource alias, such as workspace." },
|
|
3534
|
+
resourceId: { type: "string", description: "Resource id. Required when resourceType is provided." }
|
|
3535
|
+
},
|
|
3536
|
+
required: ["actionId"],
|
|
3537
|
+
additionalProperties: false
|
|
3538
|
+
},
|
|
3539
|
+
handler: async (topolo, args) => {
|
|
3540
|
+
const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
|
|
3541
|
+
if (!actionId) throw new Error("actionId is required.");
|
|
3542
|
+
const input = args["input"];
|
|
3543
|
+
if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
|
|
3544
|
+
throw new Error("input must be an object.");
|
|
3545
|
+
}
|
|
3546
|
+
const action = await topolo.client.getAction(actionId);
|
|
3547
|
+
const agentAccess = action.agentAccess ?? (action.destructive ? "off" : action.readOnly && !action.requiresConfirmation ? "auto" : "confirm");
|
|
3548
|
+
if (action.method !== "GET" || !action.readOnly || action.destructive || action.requiresConfirmation || agentAccess !== "auto") {
|
|
3549
|
+
throw new Error(`Action "${actionId}" is not an automatic read-only action. Use topolo_call_action with confirmation.`);
|
|
3550
|
+
}
|
|
3551
|
+
const resource = optionalResourceContext(args);
|
|
3552
|
+
return topolo.client.callAction(
|
|
3553
|
+
actionId,
|
|
3554
|
+
input ?? {},
|
|
3555
|
+
resource ? { resource } : {}
|
|
3556
|
+
);
|
|
3557
|
+
}
|
|
3558
|
+
},
|
|
3559
|
+
{
|
|
3560
|
+
name: "topolo_call_action",
|
|
3561
|
+
title: "Call one Topolo action",
|
|
3562
|
+
description: "Calls one exact action id. Mutating actions require confirm:true.",
|
|
3563
|
+
requiredScopes: [],
|
|
3564
|
+
destructive: true,
|
|
3565
|
+
inputSchema: {
|
|
3566
|
+
type: "object",
|
|
3567
|
+
properties: {
|
|
3568
|
+
actionId: { type: "string", minLength: 1 },
|
|
3569
|
+
input: { type: "object", additionalProperties: true },
|
|
3570
|
+
resourceType: { type: "string", description: "Optional app-local resource alias, such as workspace." },
|
|
3571
|
+
resourceId: { type: "string", description: "Resource id. Required when resourceType is provided." },
|
|
3572
|
+
confirm: { type: "boolean" }
|
|
3573
|
+
},
|
|
3574
|
+
required: ["actionId"],
|
|
3575
|
+
additionalProperties: false
|
|
3576
|
+
},
|
|
3577
|
+
handler: async (topolo, args) => {
|
|
3578
|
+
const actionId = typeof args["actionId"] === "string" ? args["actionId"].trim() : "";
|
|
3579
|
+
if (!actionId) throw new Error("actionId is required.");
|
|
3580
|
+
const input = args["input"];
|
|
3581
|
+
if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) {
|
|
3582
|
+
throw new Error("input must be an object.");
|
|
3583
|
+
}
|
|
3584
|
+
const resource = optionalResourceContext(args);
|
|
3585
|
+
return topolo.client.callAction(
|
|
3586
|
+
actionId,
|
|
3587
|
+
input ?? {},
|
|
3588
|
+
{
|
|
3589
|
+
confirm: args["confirm"] === true,
|
|
3590
|
+
...resource ? { resource } : {}
|
|
3591
|
+
}
|
|
3282
3592
|
);
|
|
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
3593
|
}
|
|
3291
3594
|
},
|
|
3292
3595
|
{
|
|
@@ -3300,7 +3603,7 @@ var TOOLS = [
|
|
|
3300
3603
|
properties: {
|
|
3301
3604
|
application: {
|
|
3302
3605
|
type: "string",
|
|
3303
|
-
description: "Optional application ID from
|
|
3606
|
+
description: "Optional application ID from topolo_search_applications."
|
|
3304
3607
|
}
|
|
3305
3608
|
},
|
|
3306
3609
|
additionalProperties: false
|
|
@@ -3340,7 +3643,7 @@ var TOOLS = [
|
|
|
3340
3643
|
properties: {
|
|
3341
3644
|
application: {
|
|
3342
3645
|
type: "string",
|
|
3343
|
-
description: "Optional application ID from
|
|
3646
|
+
description: "Optional application ID from topolo_search_applications."
|
|
3344
3647
|
},
|
|
3345
3648
|
failOn: {
|
|
3346
3649
|
type: "string",
|
|
@@ -3374,7 +3677,7 @@ var TOOLS = [
|
|
|
3374
3677
|
{
|
|
3375
3678
|
name: "topolo_api_call",
|
|
3376
3679
|
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
|
|
3680
|
+
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
3681
|
requiredScopes: [],
|
|
3379
3682
|
destructive: true,
|
|
3380
3683
|
inputSchema: {
|
|
@@ -3405,7 +3708,7 @@ var TOOLS = [
|
|
|
3405
3708
|
},
|
|
3406
3709
|
resourceType: {
|
|
3407
3710
|
type: "string",
|
|
3408
|
-
description: "Optional workspace resource alias from
|
|
3711
|
+
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
3712
|
},
|
|
3410
3713
|
resourceId: {
|
|
3411
3714
|
type: "string",
|
|
@@ -3423,7 +3726,7 @@ var TOOLS = [
|
|
|
3423
3726
|
const service = args["service"];
|
|
3424
3727
|
const path = args["path"];
|
|
3425
3728
|
if (typeof service !== "string" || !service.trim()) {
|
|
3426
|
-
throw new Error("service is required. Call
|
|
3729
|
+
throw new Error("service is required. Call topolo_search_applications first to discover callable services.");
|
|
3427
3730
|
}
|
|
3428
3731
|
if (typeof path !== "string" || !path) {
|
|
3429
3732
|
throw new Error("path is required");
|
|
@@ -3437,12 +3740,7 @@ var TOOLS = [
|
|
|
3437
3740
|
const normalisedQuery = query && typeof query === "object" && !Array.isArray(query) ? Object.fromEntries(
|
|
3438
3741
|
Object.entries(query).map(([k, v]) => [k, String(v)])
|
|
3439
3742
|
) : 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;
|
|
3743
|
+
const resource = optionalResourceContext(args);
|
|
3446
3744
|
return topolo.client.request({
|
|
3447
3745
|
service: service.trim(),
|
|
3448
3746
|
method,
|
|
@@ -3459,58 +3757,8 @@ function filterToolsByScopes(tools, scopes) {
|
|
|
3459
3757
|
return tools.filter((t) => hasAnyScope(scopes, t.requiredScopes));
|
|
3460
3758
|
}
|
|
3461
3759
|
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
|
-
};
|
|
3760
|
+
void topolo;
|
|
3761
|
+
return filterToolsByScopes(tools, scopes);
|
|
3514
3762
|
}
|
|
3515
3763
|
function evaluateApplicationAuditGate(report, threshold) {
|
|
3516
3764
|
const counts = {
|
|
@@ -3665,7 +3913,8 @@ function toolError(message) {
|
|
|
3665
3913
|
// src/index.ts
|
|
3666
3914
|
async function main() {
|
|
3667
3915
|
const environment = normalizeEnvironment(process.env.TOPOLO_ENV);
|
|
3668
|
-
|
|
3916
|
+
const environmentCredential = resolveCredentialFromEnv();
|
|
3917
|
+
let credential = environmentCredential;
|
|
3669
3918
|
if (!credential) {
|
|
3670
3919
|
credential = await refreshCredentialIfNeeded(environment);
|
|
3671
3920
|
}
|
|
@@ -3687,7 +3936,8 @@ async function main() {
|
|
|
3687
3936
|
...agentName ? { agentName } : {}
|
|
3688
3937
|
},
|
|
3689
3938
|
serviceUrls,
|
|
3690
|
-
requireConfirmForWrites: true
|
|
3939
|
+
requireConfirmForWrites: true,
|
|
3940
|
+
...!environmentCredential ? { refreshCredential: () => refreshCredentialIfNeeded(environment, { force: true }) } : {}
|
|
3691
3941
|
});
|
|
3692
3942
|
const scopes = await loadScopes(topolo);
|
|
3693
3943
|
const server = buildServer(topolo, scopes);
|