@topolo/mcp 0.14.1 → 0.14.3
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 +13 -0
- package/dist/dispatch.js +36 -2
- package/dist/http.js +36 -2
- package/dist/index.js +363 -203
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -55,6 +55,19 @@ entry point resolved from the installed CLI dependency graph. Advanced hosts
|
|
|
55
55
|
may execute the exported `@topolo/mcp/stdio` entry directly, but customer setup
|
|
56
56
|
must not use an `npx` launcher because that makes startup network-dependent.
|
|
57
57
|
|
|
58
|
+
## Protected action handoff
|
|
59
|
+
|
|
60
|
+
Local stdio action results return `topolo-secret:<id>` references instead of
|
|
61
|
+
bearer credentials. Pass these references unchanged to subsequent published
|
|
62
|
+
actions; SDK 0.21.3 resolves them internally using the OS protected store and
|
|
63
|
+
masks them in plans and diagnostics. Entries are bound to the verified
|
|
64
|
+
principal, organization, API key identity when present, and environment.
|
|
65
|
+
References expire after 30 minutes; accessing an expired entry deletes it.
|
|
66
|
+
Release finished workflow references with `topolo_release_action_secret`
|
|
67
|
+
and `confirm: true`. This deletes local storage only, not the upstream grant.
|
|
68
|
+
HTTP/Worker hosts remain redacted unless they explicitly configure a protected
|
|
69
|
+
SDK store; local references cannot be transferred to a different host.
|
|
70
|
+
|
|
58
71
|
## Supported env vars
|
|
59
72
|
|
|
60
73
|
| Var | Purpose |
|
package/dist/dispatch.js
CHANGED
|
@@ -63,6 +63,28 @@ function publicMcpErrorMessage(error, fallback) {
|
|
|
63
63
|
return error instanceof TopoloMcpPublicError ? error.message : fallback;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// src/action-secret-tools.ts
|
|
67
|
+
var actionSecretTools = [{
|
|
68
|
+
name: "topolo_release_action_secret",
|
|
69
|
+
title: "Release a protected action reference",
|
|
70
|
+
description: "Deletes only a local protected handoff reference for the verified identity and environment. Does not revoke the upstream credential. Requires confirm:true.",
|
|
71
|
+
requiredScopes: [],
|
|
72
|
+
destructive: true,
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: { reference: { type: "string", minLength: 1 }, confirm: { type: "boolean" } },
|
|
76
|
+
required: ["reference", "confirm"],
|
|
77
|
+
additionalProperties: false
|
|
78
|
+
},
|
|
79
|
+
handler: async (topolo, args) => {
|
|
80
|
+
const reference = typeof args.reference === "string" ? args.reference.trim() : "";
|
|
81
|
+
if (!reference) throw new TopoloMcpPublicError("reference is required.");
|
|
82
|
+
if (args.confirm !== true) throw new TopoloMcpPublicError("Releasing a protected action reference requires confirm:true.");
|
|
83
|
+
await topolo.client.releaseActionSecret(reference);
|
|
84
|
+
return { released: true };
|
|
85
|
+
}
|
|
86
|
+
}];
|
|
87
|
+
|
|
66
88
|
// src/tools.ts
|
|
67
89
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
68
90
|
var AGENT_INVOCATIONS_CREATE = "app_topolo_agent.agent_invocations.create";
|
|
@@ -125,6 +147,11 @@ function agentRunResult(invocation) {
|
|
|
125
147
|
state: "approval_required",
|
|
126
148
|
invocationId: invocation["invocationId"],
|
|
127
149
|
verificationUrl: approval["verificationUrl"],
|
|
150
|
+
continuation: {
|
|
151
|
+
tool: "topolo_run_agent",
|
|
152
|
+
reuseSameInvocation: true,
|
|
153
|
+
arguments: { waitForApproval: true, waitSeconds: 120 }
|
|
154
|
+
},
|
|
128
155
|
approval,
|
|
129
156
|
invocation
|
|
130
157
|
};
|
|
@@ -136,6 +163,7 @@ function agentRunResult(invocation) {
|
|
|
136
163
|
};
|
|
137
164
|
}
|
|
138
165
|
var TOOLS = [
|
|
166
|
+
...actionSecretTools,
|
|
139
167
|
{
|
|
140
168
|
name: "topolo_whoami",
|
|
141
169
|
title: "Describe the current Topolo credential",
|
|
@@ -385,7 +413,7 @@ var TOOLS = [
|
|
|
385
413
|
{
|
|
386
414
|
name: "topolo_run_agent",
|
|
387
415
|
title: "Run a Topolo agent to a durable checkpoint",
|
|
388
|
-
description: "Creates or replays one idempotent Topolo Agent invocation, polls its published status, and returns either the terminal result or an approval_required handoff with the exact Topolo verification URL. Reusing the same idempotencyKey never starts a duplicate invocation.",
|
|
416
|
+
description: "Creates or replays one idempotent Topolo Agent invocation, polls its published status, and returns either the terminal result or an approval_required handoff with the exact Topolo verification URL. Reusing the same idempotencyKey never starts a duplicate invocation. After showing the verification URL, immediately call this tool again with the same invocation and waitForApproval=true so the terminal result returns here.",
|
|
389
417
|
requiredScopes: [],
|
|
390
418
|
destructive: true,
|
|
391
419
|
inputSchema: {
|
|
@@ -404,6 +432,11 @@ var TOOLS = [
|
|
|
404
432
|
maximum: 120,
|
|
405
433
|
default: 30,
|
|
406
434
|
description: "Maximum time to poll. Returns state=running when the bounded wait elapses."
|
|
435
|
+
},
|
|
436
|
+
waitForApproval: {
|
|
437
|
+
type: "boolean",
|
|
438
|
+
default: false,
|
|
439
|
+
description: "Set only when resuming an approval_required handoff already shown to the human. Keeps polling the same idempotent invocation while approval is pending."
|
|
407
440
|
}
|
|
408
441
|
},
|
|
409
442
|
required: ["invocation"],
|
|
@@ -417,6 +450,7 @@ var TOOLS = [
|
|
|
417
450
|
}
|
|
418
451
|
const resource = optionalResourceContext(args);
|
|
419
452
|
const waitSeconds = typeof args["waitSeconds"] === "number" ? Math.max(0, Math.min(120, Math.floor(args["waitSeconds"]))) : 30;
|
|
453
|
+
const waitForApproval = args["waitForApproval"] === true;
|
|
420
454
|
const createResult = await topolo.client.callAction(
|
|
421
455
|
AGENT_INVOCATIONS_CREATE,
|
|
422
456
|
invocationInput,
|
|
@@ -424,7 +458,7 @@ var TOOLS = [
|
|
|
424
458
|
);
|
|
425
459
|
let invocation = invocationFrom(createResult);
|
|
426
460
|
const deadline = Date.now() + waitSeconds * 1e3;
|
|
427
|
-
while (invocation["terminal"] !== true && invocation["status"] !== "awaiting_approval" && Date.now() < deadline) {
|
|
461
|
+
while (invocation["terminal"] !== true && (invocation["status"] !== "awaiting_approval" || waitForApproval) && Date.now() < deadline) {
|
|
428
462
|
const recommended = typeof invocation["recommendedPollAfterSeconds"] === "number" ? invocation["recommendedPollAfterSeconds"] : 2;
|
|
429
463
|
const delayMs = Math.min(
|
|
430
464
|
Math.max(250, recommended * 1e3),
|
package/dist/http.js
CHANGED
|
@@ -17790,6 +17790,28 @@ function splitService(servicePart) {
|
|
|
17790
17790
|
return [servicePart.slice(0, idx), servicePart.slice(idx + 1)];
|
|
17791
17791
|
}
|
|
17792
17792
|
|
|
17793
|
+
// src/action-secret-tools.ts
|
|
17794
|
+
var actionSecretTools = [{
|
|
17795
|
+
name: "topolo_release_action_secret",
|
|
17796
|
+
title: "Release a protected action reference",
|
|
17797
|
+
description: "Deletes only a local protected handoff reference for the verified identity and environment. Does not revoke the upstream credential. Requires confirm:true.",
|
|
17798
|
+
requiredScopes: [],
|
|
17799
|
+
destructive: true,
|
|
17800
|
+
inputSchema: {
|
|
17801
|
+
type: "object",
|
|
17802
|
+
properties: { reference: { type: "string", minLength: 1 }, confirm: { type: "boolean" } },
|
|
17803
|
+
required: ["reference", "confirm"],
|
|
17804
|
+
additionalProperties: false
|
|
17805
|
+
},
|
|
17806
|
+
handler: async (topolo, args) => {
|
|
17807
|
+
const reference = typeof args.reference === "string" ? args.reference.trim() : "";
|
|
17808
|
+
if (!reference) throw new TopoloMcpPublicError("reference is required.");
|
|
17809
|
+
if (args.confirm !== true) throw new TopoloMcpPublicError("Releasing a protected action reference requires confirm:true.");
|
|
17810
|
+
await topolo.client.releaseActionSecret(reference);
|
|
17811
|
+
return { released: true };
|
|
17812
|
+
}
|
|
17813
|
+
}];
|
|
17814
|
+
|
|
17793
17815
|
// src/tools.ts
|
|
17794
17816
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
17795
17817
|
var AGENT_INVOCATIONS_CREATE = "app_topolo_agent.agent_invocations.create";
|
|
@@ -17852,6 +17874,11 @@ function agentRunResult(invocation) {
|
|
|
17852
17874
|
state: "approval_required",
|
|
17853
17875
|
invocationId: invocation["invocationId"],
|
|
17854
17876
|
verificationUrl: approval["verificationUrl"],
|
|
17877
|
+
continuation: {
|
|
17878
|
+
tool: "topolo_run_agent",
|
|
17879
|
+
reuseSameInvocation: true,
|
|
17880
|
+
arguments: { waitForApproval: true, waitSeconds: 120 }
|
|
17881
|
+
},
|
|
17855
17882
|
approval,
|
|
17856
17883
|
invocation
|
|
17857
17884
|
};
|
|
@@ -17863,6 +17890,7 @@ function agentRunResult(invocation) {
|
|
|
17863
17890
|
};
|
|
17864
17891
|
}
|
|
17865
17892
|
var TOOLS = [
|
|
17893
|
+
...actionSecretTools,
|
|
17866
17894
|
{
|
|
17867
17895
|
name: "topolo_whoami",
|
|
17868
17896
|
title: "Describe the current Topolo credential",
|
|
@@ -18112,7 +18140,7 @@ var TOOLS = [
|
|
|
18112
18140
|
{
|
|
18113
18141
|
name: "topolo_run_agent",
|
|
18114
18142
|
title: "Run a Topolo agent to a durable checkpoint",
|
|
18115
|
-
description: "Creates or replays one idempotent Topolo Agent invocation, polls its published status, and returns either the terminal result or an approval_required handoff with the exact Topolo verification URL. Reusing the same idempotencyKey never starts a duplicate invocation.",
|
|
18143
|
+
description: "Creates or replays one idempotent Topolo Agent invocation, polls its published status, and returns either the terminal result or an approval_required handoff with the exact Topolo verification URL. Reusing the same idempotencyKey never starts a duplicate invocation. After showing the verification URL, immediately call this tool again with the same invocation and waitForApproval=true so the terminal result returns here.",
|
|
18116
18144
|
requiredScopes: [],
|
|
18117
18145
|
destructive: true,
|
|
18118
18146
|
inputSchema: {
|
|
@@ -18131,6 +18159,11 @@ var TOOLS = [
|
|
|
18131
18159
|
maximum: 120,
|
|
18132
18160
|
default: 30,
|
|
18133
18161
|
description: "Maximum time to poll. Returns state=running when the bounded wait elapses."
|
|
18162
|
+
},
|
|
18163
|
+
waitForApproval: {
|
|
18164
|
+
type: "boolean",
|
|
18165
|
+
default: false,
|
|
18166
|
+
description: "Set only when resuming an approval_required handoff already shown to the human. Keeps polling the same idempotent invocation while approval is pending."
|
|
18134
18167
|
}
|
|
18135
18168
|
},
|
|
18136
18169
|
required: ["invocation"],
|
|
@@ -18144,6 +18177,7 @@ var TOOLS = [
|
|
|
18144
18177
|
}
|
|
18145
18178
|
const resource = optionalResourceContext(args);
|
|
18146
18179
|
const waitSeconds = typeof args["waitSeconds"] === "number" ? Math.max(0, Math.min(120, Math.floor(args["waitSeconds"]))) : 30;
|
|
18180
|
+
const waitForApproval = args["waitForApproval"] === true;
|
|
18147
18181
|
const createResult = await topolo.client.callAction(
|
|
18148
18182
|
AGENT_INVOCATIONS_CREATE,
|
|
18149
18183
|
invocationInput,
|
|
@@ -18151,7 +18185,7 @@ var TOOLS = [
|
|
|
18151
18185
|
);
|
|
18152
18186
|
let invocation = invocationFrom(createResult);
|
|
18153
18187
|
const deadline = Date.now() + waitSeconds * 1e3;
|
|
18154
|
-
while (invocation["terminal"] !== true && invocation["status"] !== "awaiting_approval" && Date.now() < deadline) {
|
|
18188
|
+
while (invocation["terminal"] !== true && (invocation["status"] !== "awaiting_approval" || waitForApproval) && Date.now() < deadline) {
|
|
18155
18189
|
const recommended = typeof invocation["recommendedPollAfterSeconds"] === "number" ? invocation["recommendedPollAfterSeconds"] : 2;
|
|
18156
18190
|
const delayMs = Math.min(
|
|
18157
18191
|
Math.max(250, recommended * 1e3),
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
|
|
6
|
-
// node_modules/.pnpm/@topolo+sdk@0.21.
|
|
6
|
+
// node_modules/.pnpm/@topolo+sdk@0.21.3/node_modules/@topolo/sdk/dist/index.js
|
|
7
7
|
function applyAuthHeaders(headers, credential) {
|
|
8
8
|
if (credential.kind === "api_key") {
|
|
9
9
|
headers.set("X-Api-Key", credential.apiKey);
|
|
@@ -22,54 +22,6 @@ function generateRequestId() {
|
|
|
22
22
|
}
|
|
23
23
|
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
24
24
|
}
|
|
25
|
-
var encoder = new TextEncoder();
|
|
26
|
-
function encodeBase64Url(value) {
|
|
27
|
-
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
28
|
-
let binary = "";
|
|
29
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
30
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
31
|
-
}
|
|
32
|
-
function encodeJson(value) {
|
|
33
|
-
return encodeBase64Url(encoder.encode(JSON.stringify(value)));
|
|
34
|
-
}
|
|
35
|
-
async function sha256Base64Url(value) {
|
|
36
|
-
return encodeBase64Url(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
37
|
-
}
|
|
38
|
-
function publicJwk(privateJwk) {
|
|
39
|
-
const { crv, kty, x, y } = privateJwk;
|
|
40
|
-
if (kty !== "EC" || crv !== "P-256" || !x || !y || !privateJwk.d) {
|
|
41
|
-
throw new TypeError("A private P-256 JSON Web Key is required for DPoP signing.");
|
|
42
|
-
}
|
|
43
|
-
return { crv, kty, x, y };
|
|
44
|
-
}
|
|
45
|
-
function canonicalTargetUrl(value) {
|
|
46
|
-
const url = new URL(value);
|
|
47
|
-
return `${url.origin}${url.pathname}`;
|
|
48
|
-
}
|
|
49
|
-
async function createDpopProof(input) {
|
|
50
|
-
const jwk = publicJwk(input.privateJwk);
|
|
51
|
-
const key = await crypto.subtle.importKey(
|
|
52
|
-
"jwk",
|
|
53
|
-
input.privateJwk,
|
|
54
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
55
|
-
false,
|
|
56
|
-
["sign"]
|
|
57
|
-
);
|
|
58
|
-
const header = encodeJson({ alg: "ES256", typ: "dpop+jwt", jwk });
|
|
59
|
-
const claims = encodeJson({
|
|
60
|
-
jti: crypto.randomUUID(),
|
|
61
|
-
iat: Math.floor(Date.now() / 1e3),
|
|
62
|
-
htm: input.method.toUpperCase(),
|
|
63
|
-
htu: canonicalTargetUrl(input.targetUrl),
|
|
64
|
-
ath: await sha256Base64Url(input.accessToken)
|
|
65
|
-
});
|
|
66
|
-
const signature = await crypto.subtle.sign(
|
|
67
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
68
|
-
key,
|
|
69
|
-
encoder.encode(`${header}.${claims}`)
|
|
70
|
-
);
|
|
71
|
-
return `${header}.${claims}.${encodeBase64Url(signature)}`;
|
|
72
|
-
}
|
|
73
25
|
var TopoloSdkError = class extends Error {
|
|
74
26
|
code;
|
|
75
27
|
constructor(code, message) {
|
|
@@ -391,6 +343,131 @@ var TopoloHttpError = class extends TopoloSdkError {
|
|
|
391
343
|
);
|
|
392
344
|
}
|
|
393
345
|
};
|
|
346
|
+
var ACTION_SECRET_PREFIX = "topolo-secret:";
|
|
347
|
+
function isActionSecretReference(value) {
|
|
348
|
+
return typeof value === "string" && value.startsWith(ACTION_SECRET_PREFIX);
|
|
349
|
+
}
|
|
350
|
+
function isRecord(value) {
|
|
351
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
352
|
+
}
|
|
353
|
+
function sensitive(key, value) {
|
|
354
|
+
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
355
|
+
if (normalized === "capability") {
|
|
356
|
+
if (typeof value !== "string") return value !== null && value !== void 0 && typeof value !== "boolean" && typeof value !== "number";
|
|
357
|
+
const text2 = value.trim();
|
|
358
|
+
return Boolean(text2) && !/^[a-z][a-z0-9_-]{0,31}$/.test(text2);
|
|
359
|
+
}
|
|
360
|
+
return ["authorization", "cookie", "setcookie", "proxyauthorization"].includes(normalized) || /(token|secret|password|privatekey|apikey)$/.test(normalized);
|
|
361
|
+
}
|
|
362
|
+
function maskActionSecrets(value, secrets) {
|
|
363
|
+
let masked = value;
|
|
364
|
+
for (const [secret, reference] of secrets) {
|
|
365
|
+
const encoded = encodeURIComponent(secret);
|
|
366
|
+
const variants = [.../* @__PURE__ */ new Set([secret, encoded])].map((item) => item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
367
|
+
masked = masked.replace(new RegExp(variants.join("|"), "g"), (match) => match === secret ? reference : encodeURIComponent(reference));
|
|
368
|
+
}
|
|
369
|
+
return masked;
|
|
370
|
+
}
|
|
371
|
+
async function resolveActionSecrets(value, store, secrets) {
|
|
372
|
+
if (isActionSecretReference(value)) {
|
|
373
|
+
if (!store) throw new TopoloSdkError("action_secret_store_required", "Protected action references require a secret store.");
|
|
374
|
+
const resolved = await store.load(value);
|
|
375
|
+
if (typeof resolved === "string" && resolved) secrets.set(resolved, value);
|
|
376
|
+
return resolved;
|
|
377
|
+
}
|
|
378
|
+
if (Array.isArray(value)) return Promise.all(value.map((item) => resolveActionSecrets(item, store, secrets)));
|
|
379
|
+
if (!isRecord(value)) return value;
|
|
380
|
+
return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, item]) => [key, await resolveActionSecrets(item, store, secrets)])));
|
|
381
|
+
}
|
|
382
|
+
async function protectActionResult(value, store) {
|
|
383
|
+
const secrets = /* @__PURE__ */ new Map();
|
|
384
|
+
async function protect(item, key = "") {
|
|
385
|
+
if (isActionSecretReference(item)) return item;
|
|
386
|
+
if (sensitive(key, item)) {
|
|
387
|
+
const reference = store ? await store.save(item) : "[REDACTED]";
|
|
388
|
+
if (typeof item === "string" && item) secrets.set(item, reference);
|
|
389
|
+
return reference;
|
|
390
|
+
}
|
|
391
|
+
if (Array.isArray(item)) return Promise.all(item.map((child) => protect(child)));
|
|
392
|
+
if (!isRecord(item)) return item;
|
|
393
|
+
return Object.fromEntries(await Promise.all(Object.entries(item).map(async ([name, child]) => [name, await protect(child, name)])));
|
|
394
|
+
}
|
|
395
|
+
const protectedValue = await protect(value);
|
|
396
|
+
async function protectEmbedded(item) {
|
|
397
|
+
if (typeof item === "string" && !isActionSecretReference(item) && maskActionSecrets(item, secrets) !== item) {
|
|
398
|
+
return store ? store.save(item) : maskActionSecrets(item, secrets);
|
|
399
|
+
}
|
|
400
|
+
if (Array.isArray(item)) return Promise.all(item.map(protectEmbedded));
|
|
401
|
+
if (!isRecord(item)) return item;
|
|
402
|
+
return Object.fromEntries(await Promise.all(Object.entries(item).map(async ([key, child]) => [key, await protectEmbedded(child)])));
|
|
403
|
+
}
|
|
404
|
+
return protectEmbedded(protectedValue);
|
|
405
|
+
}
|
|
406
|
+
function maskActionSecretValue(value, secrets) {
|
|
407
|
+
if (value instanceof Error) return maskActionSecretError(value, secrets);
|
|
408
|
+
if (typeof value === "string") return maskActionSecrets(value, secrets);
|
|
409
|
+
if (Array.isArray(value)) return value.map((item) => maskActionSecretValue(item, secrets));
|
|
410
|
+
if (!isRecord(value)) return value;
|
|
411
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, maskActionSecretValue(item, secrets)]));
|
|
412
|
+
}
|
|
413
|
+
function maskActionSecretError(error, secrets) {
|
|
414
|
+
if (!(error instanceof Error)) return maskActionSecretValue(error, secrets);
|
|
415
|
+
const masked = Object.create(Object.getPrototypeOf(error));
|
|
416
|
+
for (const key of Object.getOwnPropertyNames(error)) {
|
|
417
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, key);
|
|
418
|
+
if ("value" in descriptor) descriptor.value = maskActionSecretValue(descriptor.value, secrets);
|
|
419
|
+
Object.defineProperty(masked, key, descriptor);
|
|
420
|
+
}
|
|
421
|
+
return masked;
|
|
422
|
+
}
|
|
423
|
+
var encoder = new TextEncoder();
|
|
424
|
+
function encodeBase64Url(value) {
|
|
425
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
426
|
+
let binary = "";
|
|
427
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
428
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
429
|
+
}
|
|
430
|
+
function encodeJson(value) {
|
|
431
|
+
return encodeBase64Url(encoder.encode(JSON.stringify(value)));
|
|
432
|
+
}
|
|
433
|
+
async function sha256Base64Url(value) {
|
|
434
|
+
return encodeBase64Url(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
435
|
+
}
|
|
436
|
+
function publicJwk(privateJwk) {
|
|
437
|
+
const { crv, kty, x, y } = privateJwk;
|
|
438
|
+
if (kty !== "EC" || crv !== "P-256" || !x || !y || !privateJwk.d) {
|
|
439
|
+
throw new TypeError("A private P-256 JSON Web Key is required for DPoP signing.");
|
|
440
|
+
}
|
|
441
|
+
return { crv, kty, x, y };
|
|
442
|
+
}
|
|
443
|
+
function canonicalTargetUrl(value) {
|
|
444
|
+
const url = new URL(value);
|
|
445
|
+
return `${url.origin}${url.pathname}`;
|
|
446
|
+
}
|
|
447
|
+
async function createDpopProof(input) {
|
|
448
|
+
const jwk = publicJwk(input.privateJwk);
|
|
449
|
+
const key = await crypto.subtle.importKey(
|
|
450
|
+
"jwk",
|
|
451
|
+
input.privateJwk,
|
|
452
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
453
|
+
false,
|
|
454
|
+
["sign"]
|
|
455
|
+
);
|
|
456
|
+
const header = encodeJson({ alg: "ES256", typ: "dpop+jwt", jwk });
|
|
457
|
+
const claims = encodeJson({
|
|
458
|
+
jti: crypto.randomUUID(),
|
|
459
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
460
|
+
htm: input.method.toUpperCase(),
|
|
461
|
+
htu: canonicalTargetUrl(input.targetUrl),
|
|
462
|
+
ath: await sha256Base64Url(input.accessToken)
|
|
463
|
+
});
|
|
464
|
+
const signature = await crypto.subtle.sign(
|
|
465
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
466
|
+
key,
|
|
467
|
+
encoder.encode(`${header}.${claims}`)
|
|
468
|
+
);
|
|
469
|
+
return `${header}.${claims}.${encodeBase64Url(signature)}`;
|
|
470
|
+
}
|
|
394
471
|
function hostnameHasSuffix(hostname, suffix) {
|
|
395
472
|
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
396
473
|
}
|
|
@@ -564,9 +641,9 @@ function inspectSchema(schema, path, root, depth) {
|
|
|
564
641
|
return { path, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
|
|
565
642
|
}
|
|
566
643
|
const children = [];
|
|
567
|
-
if (
|
|
568
|
-
if (
|
|
569
|
-
if (
|
|
644
|
+
if (isRecord2(schema.properties)) children.push(...Object.entries(schema.properties));
|
|
645
|
+
if (isRecord2(schema.$defs)) children.push(...Object.entries(schema.$defs));
|
|
646
|
+
if (isRecord2(schema.definitions)) children.push(...Object.entries(schema.definitions));
|
|
570
647
|
children.push(
|
|
571
648
|
["additionalProperties", schema.additionalProperties],
|
|
572
649
|
["propertyNames", schema.propertyNames],
|
|
@@ -576,7 +653,7 @@ function inspectSchema(schema, path, root, depth) {
|
|
|
576
653
|
...arraySchemaEntries("oneOf", schema.oneOf)
|
|
577
654
|
);
|
|
578
655
|
for (const [key, child] of children) {
|
|
579
|
-
if (!
|
|
656
|
+
if (!isRecord2(child)) continue;
|
|
580
657
|
const issue = inspectSchema(child, `${path}.${key}`, root, depth + 1);
|
|
581
658
|
if (issue) return issue;
|
|
582
659
|
}
|
|
@@ -600,7 +677,7 @@ function validateNode(schema, value, path, root, errors, depth) {
|
|
|
600
677
|
}
|
|
601
678
|
if (Array.isArray(schema.allOf)) {
|
|
602
679
|
for (const candidate of schema.allOf) {
|
|
603
|
-
if (
|
|
680
|
+
if (isRecord2(candidate)) validateNode(candidate, value, path, root, errors, depth + 1);
|
|
604
681
|
}
|
|
605
682
|
}
|
|
606
683
|
if (Array.isArray(schema.anyOf) && !schema.anyOf.some((candidate) => candidateMatches(candidate, value, root, depth))) {
|
|
@@ -630,7 +707,7 @@ function validateNode(schema, value, path, root, errors, depth) {
|
|
|
630
707
|
if (typeof value === "string") validateString(schema, value, path, errors);
|
|
631
708
|
if (typeof value === "number") validateNumber(schema, value, path, errors);
|
|
632
709
|
if (Array.isArray(value)) validateArray(schema, value, path, root, errors, depth);
|
|
633
|
-
if (
|
|
710
|
+
if (isRecord2(value)) validateObject(schema, value, path, root, errors, depth);
|
|
634
711
|
}
|
|
635
712
|
function validateObject(schema, value, path, root, errors, depth) {
|
|
636
713
|
if (typeof schema.minProperties === "number" && Object.keys(value).length < schema.minProperties) {
|
|
@@ -639,25 +716,25 @@ function validateObject(schema, value, path, root, errors, depth) {
|
|
|
639
716
|
if (typeof schema.maxProperties === "number" && Object.keys(value).length > schema.maxProperties) {
|
|
640
717
|
errors.push({ path, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
|
|
641
718
|
}
|
|
642
|
-
const properties =
|
|
719
|
+
const properties = isRecord2(schema.properties) ? schema.properties : {};
|
|
643
720
|
const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
|
|
644
721
|
for (const key of required) {
|
|
645
722
|
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
646
723
|
errors.push({ path: `${path}.${key}`, code: "required", message: "Required property is missing." });
|
|
647
724
|
}
|
|
648
725
|
}
|
|
649
|
-
if (
|
|
726
|
+
if (isRecord2(schema.propertyNames)) {
|
|
650
727
|
for (const key of Object.keys(value)) {
|
|
651
728
|
validateNode(schema.propertyNames, key, `${path}.${key}`, root, errors, depth + 1);
|
|
652
729
|
}
|
|
653
730
|
}
|
|
654
731
|
for (const [key, child] of Object.entries(value)) {
|
|
655
732
|
const childSchema = properties[key];
|
|
656
|
-
if (
|
|
733
|
+
if (isRecord2(childSchema)) {
|
|
657
734
|
validateNode(childSchema, child, `${path}.${key}`, root, errors, depth + 1);
|
|
658
735
|
} else if (schema.additionalProperties === false) {
|
|
659
736
|
errors.push({ path: `${path}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
|
|
660
|
-
} else if (
|
|
737
|
+
} else if (isRecord2(schema.additionalProperties)) {
|
|
661
738
|
validateNode(schema.additionalProperties, child, `${path}.${key}`, root, errors, depth + 1);
|
|
662
739
|
}
|
|
663
740
|
}
|
|
@@ -673,7 +750,7 @@ function validateArray(schema, value, path, root, errors, depth) {
|
|
|
673
750
|
const unique = new Set(value.map((item) => JSON.stringify(item)));
|
|
674
751
|
if (unique.size !== value.length) errors.push({ path, code: "unique_items", message: "Expected every item to be unique." });
|
|
675
752
|
}
|
|
676
|
-
if (
|
|
753
|
+
if (isRecord2(schema.items)) {
|
|
677
754
|
value.forEach((item, index) => validateNode(schema.items, item, `${path}[${index}]`, root, errors, depth + 1));
|
|
678
755
|
}
|
|
679
756
|
}
|
|
@@ -711,7 +788,7 @@ function validateNumber(schema, value, path, errors) {
|
|
|
711
788
|
}
|
|
712
789
|
}
|
|
713
790
|
function candidateMatches(candidate, value, root, depth) {
|
|
714
|
-
if (!
|
|
791
|
+
if (!isRecord2(candidate)) return false;
|
|
715
792
|
const issues = [];
|
|
716
793
|
validateNode(candidate, value, "$", root, issues, depth + 1);
|
|
717
794
|
return issues.length === 0;
|
|
@@ -725,24 +802,24 @@ function resolveLocalReference(root, reference) {
|
|
|
725
802
|
current = current[Number(token)];
|
|
726
803
|
continue;
|
|
727
804
|
}
|
|
728
|
-
if (!
|
|
805
|
+
if (!isRecord2(current) || !Object.prototype.hasOwnProperty.call(current, token)) return null;
|
|
729
806
|
current = current[token];
|
|
730
807
|
}
|
|
731
|
-
return
|
|
808
|
+
return isRecord2(current) ? current : null;
|
|
732
809
|
}
|
|
733
810
|
function matchesType(type, value) {
|
|
734
811
|
if (type === void 0) return true;
|
|
735
812
|
if (Array.isArray(type)) return type.some((candidate) => matchesType(candidate, value));
|
|
736
813
|
if (type === "null") return value === null;
|
|
737
814
|
if (type === "array") return Array.isArray(value);
|
|
738
|
-
if (type === "object") return
|
|
815
|
+
if (type === "object") return isRecord2(value);
|
|
739
816
|
if (type === "integer") return typeof value === "number" && Number.isInteger(value);
|
|
740
817
|
return typeof value === type;
|
|
741
818
|
}
|
|
742
819
|
function sameJsonValue(left, right) {
|
|
743
820
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
744
821
|
}
|
|
745
|
-
function
|
|
822
|
+
function isRecord2(value) {
|
|
746
823
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
747
824
|
}
|
|
748
825
|
function isUrl(value) {
|
|
@@ -977,7 +1054,7 @@ function fail(path, message) {
|
|
|
977
1054
|
throw new ActorEnvelopeValidationError(path, message);
|
|
978
1055
|
}
|
|
979
1056
|
function parseTopoloActionUploadContract(value) {
|
|
980
|
-
if (!
|
|
1057
|
+
if (!isRecord3(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
|
|
981
1058
|
throw new TopoloSdkError("upload_contract_unavailable", "This action does not publish a supported upload contract.");
|
|
982
1059
|
}
|
|
983
1060
|
const contentTypeField = requiredField(value.contentTypeField, "contentTypeField");
|
|
@@ -1056,17 +1133,17 @@ function prepareTopoloUrlSessionUploadInput(contractValue, byteLength, input, op
|
|
|
1056
1133
|
};
|
|
1057
1134
|
}
|
|
1058
1135
|
function topoloUploadResponseValue(response, path) {
|
|
1059
|
-
return path.split(".").reduce((value, segment) =>
|
|
1136
|
+
return path.split(".").reduce((value, segment) => isRecord3(value) ? value[segment] : void 0, response);
|
|
1060
1137
|
}
|
|
1061
1138
|
function redactTopoloUrlSessionResult(response, uploadUrlPath) {
|
|
1062
1139
|
const redacted = cloneUploadValue(response);
|
|
1063
1140
|
let target = redacted;
|
|
1064
1141
|
const segments = uploadUrlPath.split(".");
|
|
1065
1142
|
for (const segment of segments.slice(0, -1)) {
|
|
1066
|
-
target =
|
|
1143
|
+
target = isRecord3(target) ? target[segment] : void 0;
|
|
1067
1144
|
}
|
|
1068
1145
|
const finalSegment = segments.at(-1);
|
|
1069
|
-
if (finalSegment &&
|
|
1146
|
+
if (finalSegment && isRecord3(target) && finalSegment in target) target[finalSegment] = "[REDACTED]";
|
|
1070
1147
|
redactUploadHeaders(redacted);
|
|
1071
1148
|
return redacted;
|
|
1072
1149
|
}
|
|
@@ -1090,7 +1167,7 @@ function mapUploadInput(contract, contentBase64, input, options) {
|
|
|
1090
1167
|
}
|
|
1091
1168
|
function cloneUploadValue(value) {
|
|
1092
1169
|
if (Array.isArray(value)) return value.map(cloneUploadValue);
|
|
1093
|
-
if (!
|
|
1170
|
+
if (!isRecord3(value)) return value;
|
|
1094
1171
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneUploadValue(item)]));
|
|
1095
1172
|
}
|
|
1096
1173
|
function redactUploadHeaders(value) {
|
|
@@ -1098,9 +1175,9 @@ function redactUploadHeaders(value) {
|
|
|
1098
1175
|
value.forEach(redactUploadHeaders);
|
|
1099
1176
|
return;
|
|
1100
1177
|
}
|
|
1101
|
-
if (!
|
|
1178
|
+
if (!isRecord3(value)) return;
|
|
1102
1179
|
for (const [key, item] of Object.entries(value)) {
|
|
1103
|
-
if (key.toLowerCase() === "headers" &&
|
|
1180
|
+
if (key.toLowerCase() === "headers" && isRecord3(item)) {
|
|
1104
1181
|
for (const header of Object.keys(item)) {
|
|
1105
1182
|
if (["authorization", "cookie", "set-cookie", "proxy-authorization", "x-api-key"].includes(header.toLowerCase())) {
|
|
1106
1183
|
item[header] = "[REDACTED]";
|
|
@@ -1129,7 +1206,7 @@ function decodedBase64Size(contentBase64) {
|
|
|
1129
1206
|
return contentBase64.length / 4 * 3 - padding;
|
|
1130
1207
|
}
|
|
1131
1208
|
function parseCompleteContract(value) {
|
|
1132
|
-
if (!
|
|
1209
|
+
if (!isRecord3(value)) throw new TopoloSdkError("invalid_upload_contract", "The action upload completion contract is invalid.");
|
|
1133
1210
|
return {
|
|
1134
1211
|
actionId: requiredPath(value.actionId, "complete.actionId"),
|
|
1135
1212
|
inputField: requiredField(value.inputField, "complete.inputField"),
|
|
@@ -1167,7 +1244,7 @@ function encodeTopoloActionBase64(bytes) {
|
|
|
1167
1244
|
}
|
|
1168
1245
|
return btoa(binary);
|
|
1169
1246
|
}
|
|
1170
|
-
function
|
|
1247
|
+
function isRecord3(value) {
|
|
1171
1248
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1172
1249
|
}
|
|
1173
1250
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
@@ -1178,6 +1255,8 @@ var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
|
|
|
1178
1255
|
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1179
1256
|
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
1180
1257
|
var TopoloClient = class _TopoloClient {
|
|
1258
|
+
actionSecretStore;
|
|
1259
|
+
actionSecrets = /* @__PURE__ */ new Map();
|
|
1181
1260
|
credential;
|
|
1182
1261
|
agent;
|
|
1183
1262
|
actorContext;
|
|
@@ -1207,6 +1286,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1207
1286
|
_TopoloClient.appIdCache.clear();
|
|
1208
1287
|
}
|
|
1209
1288
|
constructor(options) {
|
|
1289
|
+
this.actionSecretStore = options.actionSecretStore;
|
|
1210
1290
|
if (!options.credential) throw new TopoloAuthError("credential is required");
|
|
1211
1291
|
if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
|
|
1212
1292
|
this.credential = options.credential;
|
|
@@ -1228,6 +1308,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1228
1308
|
}
|
|
1229
1309
|
emit(event) {
|
|
1230
1310
|
if (!this.debug) return;
|
|
1311
|
+
event = maskActionSecretValue(event, this.actionSecrets);
|
|
1231
1312
|
try {
|
|
1232
1313
|
this.debug(event);
|
|
1233
1314
|
} catch {
|
|
@@ -1613,7 +1694,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1613
1694
|
}
|
|
1614
1695
|
const entries = [];
|
|
1615
1696
|
for (const item of raw) {
|
|
1616
|
-
if (!
|
|
1697
|
+
if (!isRecord4(item)) continue;
|
|
1617
1698
|
const s = item;
|
|
1618
1699
|
const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
|
|
1619
1700
|
if (!appId) continue;
|
|
@@ -1688,8 +1769,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
1688
1769
|
"app_topolo_auth.service_api_key_resources.list",
|
|
1689
1770
|
{ appId: service.appId }
|
|
1690
1771
|
);
|
|
1691
|
-
const payload =
|
|
1692
|
-
if (!
|
|
1772
|
+
const payload = isRecord4(response) && isRecord4(response.data) ? response.data : response;
|
|
1773
|
+
if (!isRecord4(payload) || !Array.isArray(payload.resources)) {
|
|
1693
1774
|
throw new TopoloSdkError("resource_catalog_unavailable", "TopoloAuth did not return bindable resources.");
|
|
1694
1775
|
}
|
|
1695
1776
|
return {
|
|
@@ -1870,17 +1951,20 @@ var TopoloClient = class _TopoloClient {
|
|
|
1870
1951
|
}
|
|
1871
1952
|
async validateActionInput(action, input = {}) {
|
|
1872
1953
|
const entry = await this.getAction(action);
|
|
1873
|
-
|
|
1954
|
+
const resolved = await resolveActionSecrets(input, this.actionSecretStore, this.actionSecrets);
|
|
1955
|
+
return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, resolved) };
|
|
1874
1956
|
}
|
|
1875
1957
|
async planAction(action, input = {}, options = {}) {
|
|
1876
1958
|
const entry = await this.getAction(action);
|
|
1877
|
-
|
|
1959
|
+
const resolved = await resolveActionSecrets(input, this.actionSecretStore, this.actionSecrets);
|
|
1960
|
+
return this.planResolvedAction(entry, resolved, options);
|
|
1878
1961
|
}
|
|
1879
1962
|
async prepareAction(action, input, options = {}) {
|
|
1880
1963
|
const entry = await this.getAction(action);
|
|
1881
1964
|
const example = (entry.agentContract?.examples ?? []).find((candidate) => Boolean(candidate?.input) && typeof candidate.input === "object" && !Array.isArray(candidate.input) && validateTopoloActionInput(entry.inputSchema, candidate.input).valid) ?? null;
|
|
1882
1965
|
const selectedInput = input ?? example?.input ?? {};
|
|
1883
|
-
const
|
|
1966
|
+
const resolved = await resolveActionSecrets(selectedInput, this.actionSecretStore, this.actionSecrets);
|
|
1967
|
+
const plan = this.planResolvedAction(entry, resolved, options);
|
|
1884
1968
|
return {
|
|
1885
1969
|
action: entry,
|
|
1886
1970
|
example,
|
|
@@ -1917,7 +2001,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1917
2001
|
confirmationRequired,
|
|
1918
2002
|
confirmationProvided: options.confirm === true,
|
|
1919
2003
|
executable: validation.valid && agentAccess !== "off" && (!confirmationRequired || options.confirm === true),
|
|
1920
|
-
request,
|
|
2004
|
+
request: maskActionSecretValue(request, this.actionSecrets),
|
|
1921
2005
|
verification: stringArray(entry.agentContract?.verify),
|
|
1922
2006
|
rollbackActionId: pickActionString(entry.agentContract?.rollbackActionId),
|
|
1923
2007
|
nextActionIds: stringArray(entry.agentContract?.nextActionIds)
|
|
@@ -1947,6 +2031,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1947
2031
|
`Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
|
|
1948
2032
|
);
|
|
1949
2033
|
}
|
|
2034
|
+
input = await resolveActionSecrets(input, this.actionSecretStore, this.actionSecrets);
|
|
1950
2035
|
const validation = validateTopoloActionInput(entry.inputSchema, input);
|
|
1951
2036
|
if (!validation.valid) {
|
|
1952
2037
|
throw new TopoloSdkError(
|
|
@@ -1977,22 +2062,31 @@ var TopoloClient = class _TopoloClient {
|
|
|
1977
2062
|
}
|
|
1978
2063
|
}
|
|
1979
2064
|
const service = entry.appSlug || entry.appId;
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2065
|
+
let result;
|
|
2066
|
+
try {
|
|
2067
|
+
result = await this.request({
|
|
2068
|
+
service,
|
|
2069
|
+
...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
|
|
2070
|
+
method: entry.method,
|
|
2071
|
+
path: mapped.path,
|
|
2072
|
+
...mapped.query ? { query: mapped.query } : {},
|
|
2073
|
+
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2074
|
+
...options.resource ? { resource: options.resource } : {},
|
|
2075
|
+
...resolvedActorEnvelope ? { actorEnvelope: resolvedActorEnvelope } : {},
|
|
2076
|
+
headers: {
|
|
2077
|
+
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2078
|
+
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
2079
|
+
},
|
|
2080
|
+
confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
|
|
2081
|
+
});
|
|
2082
|
+
} catch (error) {
|
|
2083
|
+
throw maskActionSecretError(error, this.actionSecrets);
|
|
2084
|
+
}
|
|
2085
|
+
return preserveSensitiveResult ? result : await protectActionResult(result, this.actionSecretStore);
|
|
2086
|
+
}
|
|
2087
|
+
async releaseActionSecret(reference) {
|
|
2088
|
+
if (!this.actionSecretStore) throw new TopoloSdkError("action_secret_store_required", "Protected action references require a secret store.");
|
|
2089
|
+
await this.actionSecretStore.delete(reference);
|
|
1996
2090
|
}
|
|
1997
2091
|
async buildVerifiedAgentActorEnvelope(actionId, input, resource) {
|
|
1998
2092
|
if (!this.actorContext) return void 0;
|
|
@@ -2139,7 +2233,7 @@ function resolveConfiguredAppId(slug, appIds) {
|
|
|
2139
2233
|
return null;
|
|
2140
2234
|
}
|
|
2141
2235
|
function normalizeApiKeyResourceMetadata(input) {
|
|
2142
|
-
if (!
|
|
2236
|
+
if (!isRecord4(input)) return null;
|
|
2143
2237
|
const resourceType = readString(input.resourceType ?? input.resource_type);
|
|
2144
2238
|
const label = readString(input.label);
|
|
2145
2239
|
const emptyLabel = readString(input.emptyLabel ?? input.empty_label);
|
|
@@ -2173,7 +2267,7 @@ function normalizeApiKeyResourceMetadataList(input) {
|
|
|
2173
2267
|
return resources;
|
|
2174
2268
|
}
|
|
2175
2269
|
function normalizeBindableResource(input) {
|
|
2176
|
-
if (!
|
|
2270
|
+
if (!isRecord4(input)) return null;
|
|
2177
2271
|
const resourceType = readString(input.resourceType ?? input.resource_type);
|
|
2178
2272
|
const resourceId = readString(input.resourceId ?? input.resource_id);
|
|
2179
2273
|
if (!resourceType || !resourceId) return null;
|
|
@@ -2184,13 +2278,13 @@ function normalizeBindableResource(input) {
|
|
|
2184
2278
|
slug: readOptionalString(input.slug),
|
|
2185
2279
|
status: readOptionalString(input.status),
|
|
2186
2280
|
organizationId: readOptionalString(input.organizationId ?? input.organization_id),
|
|
2187
|
-
metadata:
|
|
2281
|
+
metadata: isRecord4(input.metadata) ? input.metadata : {}
|
|
2188
2282
|
};
|
|
2189
2283
|
}
|
|
2190
2284
|
function normalizeResourceBindings(input) {
|
|
2191
2285
|
if (!Array.isArray(input)) return [];
|
|
2192
2286
|
return input.flatMap((binding) => {
|
|
2193
|
-
if (!
|
|
2287
|
+
if (!isRecord4(binding)) return [];
|
|
2194
2288
|
const resourceType = readString(binding.resourceType ?? binding.resource_type);
|
|
2195
2289
|
const resourceId = readString(binding.resourceId ?? binding.resource_id);
|
|
2196
2290
|
return resourceType && resourceId ? [{ resourceType, resourceId }] : [];
|
|
@@ -2203,30 +2297,9 @@ function readOptionalString(value) {
|
|
|
2203
2297
|
const normalized = readString(value);
|
|
2204
2298
|
return normalized || null;
|
|
2205
2299
|
}
|
|
2206
|
-
function
|
|
2300
|
+
function isRecord4(value) {
|
|
2207
2301
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2208
2302
|
}
|
|
2209
|
-
function redactActionResult(value) {
|
|
2210
|
-
if (Array.isArray(value)) return value.map(redactActionResult);
|
|
2211
|
-
if (!isRecord3(value)) return value;
|
|
2212
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
2213
|
-
key,
|
|
2214
|
-
isSensitiveActionResultKey(key, item) ? "[REDACTED]" : redactActionResult(item)
|
|
2215
|
-
]));
|
|
2216
|
-
}
|
|
2217
|
-
function isSensitiveActionResultKey(key, value) {
|
|
2218
|
-
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2219
|
-
if (normalized === "capability") return isCredentialShaped(value);
|
|
2220
|
-
return ["authorization", "cookie", "setcookie", "proxyauthorization"].includes(normalized) || /(token|secret|password|privatekey|apikey)$/.test(normalized);
|
|
2221
|
-
}
|
|
2222
|
-
var PLAIN_ENUM_VALUE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
2223
|
-
function isCredentialShaped(value) {
|
|
2224
|
-
if (typeof value !== "string") return value !== null && value !== void 0 && typeof value !== "boolean" && typeof value !== "number";
|
|
2225
|
-
const trimmed = value.trim();
|
|
2226
|
-
if (!trimmed) return false;
|
|
2227
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return true;
|
|
2228
|
-
return !PLAIN_ENUM_VALUE.test(trimmed);
|
|
2229
|
-
}
|
|
2230
2303
|
function resolveConfiguredServiceUrl(slug, serviceUrls) {
|
|
2231
2304
|
for (const serviceKey of serviceKeyCandidates2(slug)) {
|
|
2232
2305
|
const configured = serviceUrls?.[serviceKey];
|
|
@@ -2296,14 +2369,14 @@ function catalogSearchQuery(options) {
|
|
|
2296
2369
|
return query;
|
|
2297
2370
|
}
|
|
2298
2371
|
function normalizeCatalogPage(value) {
|
|
2299
|
-
if (!
|
|
2372
|
+
if (!isRecord4(value)) return { nextCursor: null, hasMore: false };
|
|
2300
2373
|
return {
|
|
2301
2374
|
nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
|
|
2302
2375
|
hasMore: value.hasMore === true
|
|
2303
2376
|
};
|
|
2304
2377
|
}
|
|
2305
2378
|
function normalizeServiceCatalogEntry(item) {
|
|
2306
|
-
if (!
|
|
2379
|
+
if (!isRecord4(item)) return null;
|
|
2307
2380
|
const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
|
|
2308
2381
|
if (!appId) return null;
|
|
2309
2382
|
const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
|
|
@@ -3254,11 +3327,13 @@ function createTopolo(options) {
|
|
|
3254
3327
|
};
|
|
3255
3328
|
}
|
|
3256
3329
|
|
|
3257
|
-
// node_modules/.pnpm/@topolo+sdk@0.21.
|
|
3330
|
+
// node_modules/.pnpm/@topolo+sdk@0.21.3/node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
3258
3331
|
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, rm as rm2, stat } from "fs/promises";
|
|
3259
3332
|
import { existsSync as existsSync2 } from "fs";
|
|
3260
3333
|
import { homedir as homedir2, platform } from "os";
|
|
3261
|
-
import { dirname as dirname2, join as
|
|
3334
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
3335
|
+
import { createHash, randomUUID } from "crypto";
|
|
3336
|
+
import { join as join2 } from "path";
|
|
3262
3337
|
import { spawnSync } from "child_process";
|
|
3263
3338
|
import { mkdir, readFile, readdir, rename, rm, writeFile, chmod } from "fs/promises";
|
|
3264
3339
|
import { existsSync } from "fs";
|
|
@@ -3610,66 +3685,7 @@ var TopoloOAuth = class {
|
|
|
3610
3685
|
return parsed;
|
|
3611
3686
|
}
|
|
3612
3687
|
};
|
|
3613
|
-
var
|
|
3614
|
-
production: {
|
|
3615
|
-
auth: "https://auth.topolo.app",
|
|
3616
|
-
developers: "https://developers.topolo.app"
|
|
3617
|
-
},
|
|
3618
|
-
staging: {
|
|
3619
|
-
auth: "https://auth.stg.topolo.us",
|
|
3620
|
-
developers: "https://developers.stg.topolo.us"
|
|
3621
|
-
},
|
|
3622
|
-
development: {
|
|
3623
|
-
auth: "https://auth.topolo.dev",
|
|
3624
|
-
developers: "https://developers.topolo.dev"
|
|
3625
|
-
},
|
|
3626
|
-
local: {}
|
|
3627
|
-
};
|
|
3628
|
-
function hostnameHasSuffix2(hostname, suffix) {
|
|
3629
|
-
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
3630
|
-
}
|
|
3631
|
-
function workersDevAccountMatches2(hostname, account) {
|
|
3632
|
-
if (!hostname.endsWith(".workers.dev")) return false;
|
|
3633
|
-
const labels = hostname.slice(0, -".workers.dev".length).split(".");
|
|
3634
|
-
return labels.length >= 2 && labels.at(-1) === account;
|
|
3635
|
-
}
|
|
3636
|
-
function originMatchesEnvironment2(url, environment) {
|
|
3637
|
-
const hostname = url.hostname.toLowerCase();
|
|
3638
|
-
if (environment === "local") {
|
|
3639
|
-
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
3640
|
-
}
|
|
3641
|
-
if (url.protocol !== "https:") return false;
|
|
3642
|
-
if (environment === "production") {
|
|
3643
|
-
return hostnameHasSuffix2(hostname, "topolo.app") || hostnameHasSuffix2(hostname, "topolo.io") || hostname === "api.ol0.me" || workersDevAccountMatches2(hostname, "topolo");
|
|
3644
|
-
}
|
|
3645
|
-
if (environment === "staging") {
|
|
3646
|
-
return hostnameHasSuffix2(hostname, "stg.topolo.us") || workersDevAccountMatches2(hostname, "topolo-staging");
|
|
3647
|
-
}
|
|
3648
|
-
return hostnameHasSuffix2(hostname, "topolo.dev") || workersDevAccountMatches2(hostname, "topolo-development");
|
|
3649
|
-
}
|
|
3650
|
-
function assertTopoloServiceUrlEnvironment2(service, value, environment) {
|
|
3651
|
-
let url;
|
|
3652
|
-
try {
|
|
3653
|
-
url = new URL(value);
|
|
3654
|
-
} catch {
|
|
3655
|
-
throw new TopoloSdkError2("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
|
|
3656
|
-
}
|
|
3657
|
-
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment2(url, environment)) {
|
|
3658
|
-
throw new TopoloSdkError2(
|
|
3659
|
-
"environment_mismatch",
|
|
3660
|
-
`Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
|
|
3661
|
-
);
|
|
3662
|
-
}
|
|
3663
|
-
return url.origin;
|
|
3664
|
-
}
|
|
3665
|
-
function topoloServiceOrigins2(serviceUrls, environment) {
|
|
3666
|
-
return Object.fromEntries(
|
|
3667
|
-
Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
|
|
3668
|
-
service,
|
|
3669
|
-
assertTopoloServiceUrlEnvironment2(service, value, environment)
|
|
3670
|
-
])
|
|
3671
|
-
);
|
|
3672
|
-
}
|
|
3688
|
+
var ACTION_SECRET_PREFIX2 = "topolo-secret:";
|
|
3673
3689
|
var KEYCHAIN_SERVICE = "topolo-external-agent-installation";
|
|
3674
3690
|
var MACOS_KEYCHAIN_CHUNK_SIZE = 96;
|
|
3675
3691
|
var MACOS_KEYCHAIN_CHUNK_PREFIX = "topolo-keychain-chunks:v1:";
|
|
@@ -3712,6 +3728,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3712
3728
|
const platform2 = options.platform ?? hostPlatform();
|
|
3713
3729
|
const spawn = options.spawn ?? ((command, args, spawnOptions) => spawnSync(command, args, spawnOptions));
|
|
3714
3730
|
const root = options.root ?? installationStorePath();
|
|
3731
|
+
const service = options.service ?? KEYCHAIN_SERVICE;
|
|
3715
3732
|
if (platform2 === "darwin") {
|
|
3716
3733
|
const write = (account, value) => runOrThrow(spawn("/usr/bin/expect", ["-c", MACOS_KEYCHAIN_WRITE_SCRIPT], {
|
|
3717
3734
|
input: `${value}
|
|
@@ -3720,7 +3737,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3720
3737
|
env: {
|
|
3721
3738
|
...process.env,
|
|
3722
3739
|
TOPOLO_KEYCHAIN_ACCOUNT: account,
|
|
3723
|
-
TOPOLO_KEYCHAIN_SERVICE:
|
|
3740
|
+
TOPOLO_KEYCHAIN_SERVICE: service
|
|
3724
3741
|
}
|
|
3725
3742
|
}), "macOS Keychain write");
|
|
3726
3743
|
const read = (account) => {
|
|
@@ -3729,7 +3746,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3729
3746
|
"-a",
|
|
3730
3747
|
account,
|
|
3731
3748
|
"-s",
|
|
3732
|
-
|
|
3749
|
+
service,
|
|
3733
3750
|
"-w"
|
|
3734
3751
|
], { encoding: "utf8" });
|
|
3735
3752
|
if (result.status === 44) return null;
|
|
@@ -3741,7 +3758,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3741
3758
|
"-a",
|
|
3742
3759
|
account,
|
|
3743
3760
|
"-s",
|
|
3744
|
-
|
|
3761
|
+
service
|
|
3745
3762
|
], { encoding: "utf8" });
|
|
3746
3763
|
if (result.status !== 44) runOrThrow(result, "macOS Keychain delete");
|
|
3747
3764
|
};
|
|
@@ -3788,9 +3805,9 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3788
3805
|
async set(account, value) {
|
|
3789
3806
|
runOrThrow(spawn("secret-tool", [
|
|
3790
3807
|
"store",
|
|
3791
|
-
"--label=Topolo
|
|
3808
|
+
"--label=Topolo protected credential",
|
|
3792
3809
|
"service",
|
|
3793
|
-
|
|
3810
|
+
service,
|
|
3794
3811
|
"account",
|
|
3795
3812
|
account
|
|
3796
3813
|
], { input: value, encoding: "utf8" }), "Linux Secret Service write");
|
|
@@ -3799,7 +3816,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3799
3816
|
const result = spawn("secret-tool", [
|
|
3800
3817
|
"lookup",
|
|
3801
3818
|
"service",
|
|
3802
|
-
|
|
3819
|
+
service,
|
|
3803
3820
|
"account",
|
|
3804
3821
|
account
|
|
3805
3822
|
], { encoding: "utf8" });
|
|
@@ -3810,7 +3827,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3810
3827
|
const result = spawn("secret-tool", [
|
|
3811
3828
|
"clear",
|
|
3812
3829
|
"service",
|
|
3813
|
-
|
|
3830
|
+
service,
|
|
3814
3831
|
"account",
|
|
3815
3832
|
account
|
|
3816
3833
|
], { encoding: "utf8" });
|
|
@@ -3853,12 +3870,119 @@ async function readExternalAgentInstallation(environment, id, backend = createPl
|
|
|
3853
3870
|
if (!encodedSecret) throw new Error(`Protected secret is missing for installation ${id}.`);
|
|
3854
3871
|
return { installation, secret: JSON.parse(encodedSecret) };
|
|
3855
3872
|
}
|
|
3873
|
+
function createProtectedActionSecretStore(options) {
|
|
3874
|
+
const backend = options.backend ?? createPlatformInstallationSecretBackend({
|
|
3875
|
+
service: "topolo-action-handoff",
|
|
3876
|
+
root: join2(installationStorePath(), "action-handoff")
|
|
3877
|
+
});
|
|
3878
|
+
const now = options.now ?? Date.now;
|
|
3879
|
+
async function account(reference) {
|
|
3880
|
+
if (!/^topolo-secret:[0-9a-f-]{36}$/.test(reference)) throw new TopoloSdkError2("invalid_action_secret_reference", "Invalid protected action reference.");
|
|
3881
|
+
const identity = await options.identity();
|
|
3882
|
+
if (!identity.user.id) throw new TopoloSdkError2("action_secret_identity_required", "A verified identity is required for protected action handoff.");
|
|
3883
|
+
if (identity.kind === "api_key" && !identity.apiKey?.id) throw new TopoloSdkError2("action_secret_identity_required", "A verified API key identity is required for protected action handoff.");
|
|
3884
|
+
const scope = createHash("sha256").update(JSON.stringify([
|
|
3885
|
+
options.environment,
|
|
3886
|
+
identity.user.principalId ?? identity.user.id,
|
|
3887
|
+
identity.organization?.id ?? null,
|
|
3888
|
+
identity.apiKey?.id ?? null
|
|
3889
|
+
])).digest("hex");
|
|
3890
|
+
return `${scope}-${reference.slice(ACTION_SECRET_PREFIX2.length)}`;
|
|
3891
|
+
}
|
|
3892
|
+
return {
|
|
3893
|
+
async save(value) {
|
|
3894
|
+
const reference = `${ACTION_SECRET_PREFIX2}${randomUUID()}`;
|
|
3895
|
+
await backend.set(await account(reference), JSON.stringify({ value, expiresAt: now() + 30 * 60 * 1e3 }));
|
|
3896
|
+
return reference;
|
|
3897
|
+
},
|
|
3898
|
+
async load(reference) {
|
|
3899
|
+
const key = await account(reference);
|
|
3900
|
+
const encoded = await backend.get(key);
|
|
3901
|
+
if (!encoded) throw new TopoloSdkError2("action_secret_unavailable", "Protected action reference is missing or belongs to another identity or environment.");
|
|
3902
|
+
let record2;
|
|
3903
|
+
try {
|
|
3904
|
+
record2 = JSON.parse(encoded);
|
|
3905
|
+
} catch {
|
|
3906
|
+
throw new TopoloSdkError2("action_secret_invalid", "Protected action reference is corrupt.");
|
|
3907
|
+
}
|
|
3908
|
+
if (!Number.isFinite(record2?.expiresAt) || !Object.hasOwn(record2, "value")) throw new TopoloSdkError2("action_secret_invalid", "Protected action reference is corrupt.");
|
|
3909
|
+
if (record2.expiresAt <= now()) {
|
|
3910
|
+
await backend.delete(key);
|
|
3911
|
+
throw new TopoloSdkError2("action_secret_expired", "Protected action reference expired. Repeat the originating action if its contract permits.");
|
|
3912
|
+
}
|
|
3913
|
+
return record2.value;
|
|
3914
|
+
},
|
|
3915
|
+
async delete(reference) {
|
|
3916
|
+
await backend.delete(await account(reference));
|
|
3917
|
+
}
|
|
3918
|
+
};
|
|
3919
|
+
}
|
|
3920
|
+
var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
|
|
3921
|
+
production: {
|
|
3922
|
+
auth: "https://auth.topolo.app",
|
|
3923
|
+
developers: "https://developers.topolo.app"
|
|
3924
|
+
},
|
|
3925
|
+
staging: {
|
|
3926
|
+
auth: "https://auth.stg.topolo.us",
|
|
3927
|
+
developers: "https://developers.stg.topolo.us"
|
|
3928
|
+
},
|
|
3929
|
+
development: {
|
|
3930
|
+
auth: "https://auth.topolo.dev",
|
|
3931
|
+
developers: "https://developers.topolo.dev"
|
|
3932
|
+
},
|
|
3933
|
+
local: {}
|
|
3934
|
+
};
|
|
3935
|
+
function hostnameHasSuffix2(hostname, suffix) {
|
|
3936
|
+
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
3937
|
+
}
|
|
3938
|
+
function workersDevAccountMatches2(hostname, account) {
|
|
3939
|
+
if (!hostname.endsWith(".workers.dev")) return false;
|
|
3940
|
+
const labels = hostname.slice(0, -".workers.dev".length).split(".");
|
|
3941
|
+
return labels.length >= 2 && labels.at(-1) === account;
|
|
3942
|
+
}
|
|
3943
|
+
function originMatchesEnvironment2(url, environment) {
|
|
3944
|
+
const hostname = url.hostname.toLowerCase();
|
|
3945
|
+
if (environment === "local") {
|
|
3946
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
3947
|
+
}
|
|
3948
|
+
if (url.protocol !== "https:") return false;
|
|
3949
|
+
if (environment === "production") {
|
|
3950
|
+
return hostnameHasSuffix2(hostname, "topolo.app") || hostnameHasSuffix2(hostname, "topolo.io") || hostname === "api.ol0.me" || workersDevAccountMatches2(hostname, "topolo");
|
|
3951
|
+
}
|
|
3952
|
+
if (environment === "staging") {
|
|
3953
|
+
return hostnameHasSuffix2(hostname, "stg.topolo.us") || workersDevAccountMatches2(hostname, "topolo-staging");
|
|
3954
|
+
}
|
|
3955
|
+
return hostnameHasSuffix2(hostname, "topolo.dev") || workersDevAccountMatches2(hostname, "topolo-development");
|
|
3956
|
+
}
|
|
3957
|
+
function assertTopoloServiceUrlEnvironment2(service, value, environment) {
|
|
3958
|
+
let url;
|
|
3959
|
+
try {
|
|
3960
|
+
url = new URL(value);
|
|
3961
|
+
} catch {
|
|
3962
|
+
throw new TopoloSdkError2("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
|
|
3963
|
+
}
|
|
3964
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment2(url, environment)) {
|
|
3965
|
+
throw new TopoloSdkError2(
|
|
3966
|
+
"environment_mismatch",
|
|
3967
|
+
`Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
|
|
3968
|
+
);
|
|
3969
|
+
}
|
|
3970
|
+
return url.origin;
|
|
3971
|
+
}
|
|
3972
|
+
function topoloServiceOrigins2(serviceUrls, environment) {
|
|
3973
|
+
return Object.fromEntries(
|
|
3974
|
+
Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
|
|
3975
|
+
service,
|
|
3976
|
+
assertTopoloServiceUrlEnvironment2(service, value, environment)
|
|
3977
|
+
])
|
|
3978
|
+
);
|
|
3979
|
+
}
|
|
3856
3980
|
var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
|
|
3857
3981
|
function configPath() {
|
|
3858
3982
|
const override = process.env.TOPOLO_CONFIG_PATH;
|
|
3859
3983
|
if (override) return override;
|
|
3860
|
-
const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ?
|
|
3861
|
-
return
|
|
3984
|
+
const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join3(homedir2(), "AppData", "Roaming") : join3(homedir2(), ".config"));
|
|
3985
|
+
return join3(base, "topolo", "config.json");
|
|
3862
3986
|
}
|
|
3863
3987
|
async function readConfig() {
|
|
3864
3988
|
const path = configPath();
|
|
@@ -4152,6 +4276,28 @@ function publicMcpErrorMessage(error, fallback) {
|
|
|
4152
4276
|
return error instanceof TopoloMcpPublicError ? error.message : fallback;
|
|
4153
4277
|
}
|
|
4154
4278
|
|
|
4279
|
+
// src/action-secret-tools.ts
|
|
4280
|
+
var actionSecretTools = [{
|
|
4281
|
+
name: "topolo_release_action_secret",
|
|
4282
|
+
title: "Release a protected action reference",
|
|
4283
|
+
description: "Deletes only a local protected handoff reference for the verified identity and environment. Does not revoke the upstream credential. Requires confirm:true.",
|
|
4284
|
+
requiredScopes: [],
|
|
4285
|
+
destructive: true,
|
|
4286
|
+
inputSchema: {
|
|
4287
|
+
type: "object",
|
|
4288
|
+
properties: { reference: { type: "string", minLength: 1 }, confirm: { type: "boolean" } },
|
|
4289
|
+
required: ["reference", "confirm"],
|
|
4290
|
+
additionalProperties: false
|
|
4291
|
+
},
|
|
4292
|
+
handler: async (topolo, args) => {
|
|
4293
|
+
const reference = typeof args.reference === "string" ? args.reference.trim() : "";
|
|
4294
|
+
if (!reference) throw new TopoloMcpPublicError("reference is required.");
|
|
4295
|
+
if (args.confirm !== true) throw new TopoloMcpPublicError("Releasing a protected action reference requires confirm:true.");
|
|
4296
|
+
await topolo.client.releaseActionSecret(reference);
|
|
4297
|
+
return { released: true };
|
|
4298
|
+
}
|
|
4299
|
+
}];
|
|
4300
|
+
|
|
4155
4301
|
// src/tools.ts
|
|
4156
4302
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
4157
4303
|
var AGENT_INVOCATIONS_CREATE = "app_topolo_agent.agent_invocations.create";
|
|
@@ -4214,6 +4360,11 @@ function agentRunResult(invocation) {
|
|
|
4214
4360
|
state: "approval_required",
|
|
4215
4361
|
invocationId: invocation["invocationId"],
|
|
4216
4362
|
verificationUrl: approval["verificationUrl"],
|
|
4363
|
+
continuation: {
|
|
4364
|
+
tool: "topolo_run_agent",
|
|
4365
|
+
reuseSameInvocation: true,
|
|
4366
|
+
arguments: { waitForApproval: true, waitSeconds: 120 }
|
|
4367
|
+
},
|
|
4217
4368
|
approval,
|
|
4218
4369
|
invocation
|
|
4219
4370
|
};
|
|
@@ -4225,6 +4376,7 @@ function agentRunResult(invocation) {
|
|
|
4225
4376
|
};
|
|
4226
4377
|
}
|
|
4227
4378
|
var TOOLS = [
|
|
4379
|
+
...actionSecretTools,
|
|
4228
4380
|
{
|
|
4229
4381
|
name: "topolo_whoami",
|
|
4230
4382
|
title: "Describe the current Topolo credential",
|
|
@@ -4474,7 +4626,7 @@ var TOOLS = [
|
|
|
4474
4626
|
{
|
|
4475
4627
|
name: "topolo_run_agent",
|
|
4476
4628
|
title: "Run a Topolo agent to a durable checkpoint",
|
|
4477
|
-
description: "Creates or replays one idempotent Topolo Agent invocation, polls its published status, and returns either the terminal result or an approval_required handoff with the exact Topolo verification URL. Reusing the same idempotencyKey never starts a duplicate invocation.",
|
|
4629
|
+
description: "Creates or replays one idempotent Topolo Agent invocation, polls its published status, and returns either the terminal result or an approval_required handoff with the exact Topolo verification URL. Reusing the same idempotencyKey never starts a duplicate invocation. After showing the verification URL, immediately call this tool again with the same invocation and waitForApproval=true so the terminal result returns here.",
|
|
4478
4630
|
requiredScopes: [],
|
|
4479
4631
|
destructive: true,
|
|
4480
4632
|
inputSchema: {
|
|
@@ -4493,6 +4645,11 @@ var TOOLS = [
|
|
|
4493
4645
|
maximum: 120,
|
|
4494
4646
|
default: 30,
|
|
4495
4647
|
description: "Maximum time to poll. Returns state=running when the bounded wait elapses."
|
|
4648
|
+
},
|
|
4649
|
+
waitForApproval: {
|
|
4650
|
+
type: "boolean",
|
|
4651
|
+
default: false,
|
|
4652
|
+
description: "Set only when resuming an approval_required handoff already shown to the human. Keeps polling the same idempotent invocation while approval is pending."
|
|
4496
4653
|
}
|
|
4497
4654
|
},
|
|
4498
4655
|
required: ["invocation"],
|
|
@@ -4506,6 +4663,7 @@ var TOOLS = [
|
|
|
4506
4663
|
}
|
|
4507
4664
|
const resource = optionalResourceContext(args);
|
|
4508
4665
|
const waitSeconds = typeof args["waitSeconds"] === "number" ? Math.max(0, Math.min(120, Math.floor(args["waitSeconds"]))) : 30;
|
|
4666
|
+
const waitForApproval = args["waitForApproval"] === true;
|
|
4509
4667
|
const createResult = await topolo.client.callAction(
|
|
4510
4668
|
AGENT_INVOCATIONS_CREATE,
|
|
4511
4669
|
invocationInput,
|
|
@@ -4513,7 +4671,7 @@ var TOOLS = [
|
|
|
4513
4671
|
);
|
|
4514
4672
|
let invocation = invocationFrom(createResult);
|
|
4515
4673
|
const deadline = Date.now() + waitSeconds * 1e3;
|
|
4516
|
-
while (invocation["terminal"] !== true && invocation["status"] !== "awaiting_approval" && Date.now() < deadline) {
|
|
4674
|
+
while (invocation["terminal"] !== true && (invocation["status"] !== "awaiting_approval" || waitForApproval) && Date.now() < deadline) {
|
|
4517
4675
|
const recommended = typeof invocation["recommendedPollAfterSeconds"] === "number" ? invocation["recommendedPollAfterSeconds"] : 2;
|
|
4518
4676
|
const delayMs = Math.min(
|
|
4519
4677
|
Math.max(250, recommended * 1e3),
|
|
@@ -4901,14 +5059,14 @@ function displayAppId(entry) {
|
|
|
4901
5059
|
|
|
4902
5060
|
// src/version.ts
|
|
4903
5061
|
import { readFileSync } from "fs";
|
|
4904
|
-
import { dirname as dirname3, join as
|
|
5062
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
4905
5063
|
import { fileURLToPath } from "url";
|
|
4906
5064
|
function readMcpVersion() {
|
|
4907
5065
|
try {
|
|
4908
5066
|
const here = dirname3(fileURLToPath(import.meta.url));
|
|
4909
5067
|
const candidates = [
|
|
4910
|
-
|
|
4911
|
-
|
|
5068
|
+
join4(here, "package.json"),
|
|
5069
|
+
join4(here, "..", "package.json")
|
|
4912
5070
|
];
|
|
4913
5071
|
for (const path of candidates) {
|
|
4914
5072
|
try {
|
|
@@ -5031,7 +5189,7 @@ function toolError(context, error) {
|
|
|
5031
5189
|
}
|
|
5032
5190
|
|
|
5033
5191
|
// src/context-credential.ts
|
|
5034
|
-
import { randomUUID } from "crypto";
|
|
5192
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
5035
5193
|
|
|
5036
5194
|
// src/auth.ts
|
|
5037
5195
|
function resolveCredentialFromEnv(environment = "production") {
|
|
@@ -5175,7 +5333,7 @@ async function resolveMcpCredential(environment, selector = process.env.TOPOLO_C
|
|
|
5175
5333
|
"TOPOLO_AGENT_INSTALLATION_ID cannot be combined with TOPOLO_CONTEXT or an environment credential"
|
|
5176
5334
|
);
|
|
5177
5335
|
}
|
|
5178
|
-
const taskId = taskSelector?.trim() || `task_${
|
|
5336
|
+
const taskId = taskSelector?.trim() || `task_${randomUUID2()}`;
|
|
5179
5337
|
const config2 = await dependencies.readConfig();
|
|
5180
5338
|
const serviceUrls = resolveServiceUrls(
|
|
5181
5339
|
profileForEnvironment(config2, environment).serviceUrls,
|
|
@@ -5274,6 +5432,7 @@ var DEFAULT_DEPENDENCIES2 = {
|
|
|
5274
5432
|
resolveAgentName,
|
|
5275
5433
|
resolveServiceUrls,
|
|
5276
5434
|
createTopolo,
|
|
5435
|
+
createProtectedActionSecretStore,
|
|
5277
5436
|
buildServer,
|
|
5278
5437
|
createTransport: createStdioTransport,
|
|
5279
5438
|
advertisedToolNames,
|
|
@@ -5296,6 +5455,7 @@ async function runStdioMcp(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
5296
5455
|
const topolo = dependencies.createTopolo({
|
|
5297
5456
|
credential,
|
|
5298
5457
|
environment,
|
|
5458
|
+
actionSecretStore: dependencies.createProtectedActionSecretStore({ environment, identity: () => topolo.client.introspect() }),
|
|
5299
5459
|
actorContext: {
|
|
5300
5460
|
channel: "mcp",
|
|
5301
5461
|
...resolvedCredential.taskId ? { taskId: resolvedCredential.taskId } : {}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/mcp",
|
|
3
3
|
"packageManager": "pnpm@10.17.1",
|
|
4
|
-
"version": "0.14.
|
|
5
|
-
"topoloPublishedVersion": "0.14.
|
|
4
|
+
"version": "0.14.3",
|
|
5
|
+
"topoloPublishedVersion": "0.14.3",
|
|
6
6
|
"description": "Model Context Protocol server for the Topolo platform. Exposes scope-gated tools that third-party agents (Claude, Codex, etc.) can call natively.",
|
|
7
7
|
"license": "UNLICENSED",
|
|
8
8
|
"author": "Topolo",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
39
|
-
"@topolo/sdk": "0.21.
|
|
39
|
+
"@topolo/sdk": "0.21.3"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^20.12.0",
|