@topolo/mcp 0.11.12 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +33 -15
  2. package/dist/http.js +893 -695
  3. package/dist/index.js +1190 -76
  4. package/package.json +10 -5
package/dist/index.js CHANGED
@@ -3,13 +3,13 @@
3
3
  // src/index.ts
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
 
6
- // node_modules/@topolo/sdk/dist/index.js
6
+ // node_modules/.pnpm/@topolo+sdk@0.21.0/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);
10
10
  return;
11
11
  }
12
- headers.set("Authorization", `Bearer ${credential.accessToken}`);
12
+ headers.set("Authorization", `${credential.kind === "dpop_access_token" ? "DPoP" : "Bearer"} ${credential.accessToken}`);
13
13
  }
14
14
  function applyAuditHeaders(headers, agent, requestId) {
15
15
  headers.set("X-Topolo-Client", `${agent.clientName}/${agent.clientVersion}`);
@@ -22,6 +22,54 @@ 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
+ }
25
73
  var TopoloSdkError = class extends Error {
26
74
  code;
27
75
  constructor(code, message) {
@@ -167,10 +215,10 @@ var TopoloPermissionError = class extends TopoloSdkError {
167
215
  };
168
216
  function extractTopoloServiceErrorCode(body) {
169
217
  if (!body || typeof body !== "object" || Array.isArray(body)) return null;
170
- const record = body;
171
- const topLevel = canonicalTopoloErrorCode(record.code);
218
+ const record2 = body;
219
+ const topLevel = canonicalTopoloErrorCode(record2.code);
172
220
  if (topLevel) return topLevel;
173
- const nested = record.error;
221
+ const nested = record2.error;
174
222
  const nestedString = canonicalTopoloErrorCode(nested);
175
223
  if (nestedString) return nestedString;
176
224
  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
@@ -185,14 +233,14 @@ function serviceErrorCode(body, fallback) {
185
233
  }
186
234
  function extractTopoloServiceErrorDetails(body) {
187
235
  if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
188
- const record = body;
189
- let candidate = record.details;
190
- const nested = record.error;
236
+ const record2 = body;
237
+ let candidate = record2.details;
238
+ const nested = record2.error;
191
239
  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
192
240
  const nestedRecord = nested;
193
241
  if (candidate === void 0) candidate = nestedRecord.details ?? nestedRecord;
194
242
  }
195
- if (candidate === void 0) candidate = record;
243
+ if (candidate === void 0) candidate = record2;
196
244
  if (Array.isArray(candidate)) {
197
245
  const paths = canonicalTopoloValidationPaths(candidate);
198
246
  return paths.length ? { paths } : void 0;
@@ -244,8 +292,8 @@ function canonicalTopoloValidationPaths(issues) {
244
292
  const paths = [];
245
293
  for (const issue of issues) {
246
294
  if (!issue || typeof issue !== "object" || Array.isArray(issue)) continue;
247
- const record = issue;
248
- const path = canonicalTopoloValidationPath(record.field) ?? canonicalTopoloValidationPath(record.path);
295
+ const record2 = issue;
296
+ const path = canonicalTopoloValidationPath(record2.field) ?? canonicalTopoloValidationPath(record2.path);
249
297
  if (path && !paths.includes(path)) paths.push(path);
250
298
  if (paths.length === 32) break;
251
299
  }
@@ -712,6 +760,222 @@ function isMultipleOf(value, divisor) {
712
760
  const quotient = value / divisor;
713
761
  return Math.abs(quotient - Math.round(quotient)) < Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4;
714
762
  }
763
+ var ACTOR_ENVELOPE_VERSION = 1;
764
+ var TOPOLO_ACTOR_ENVELOPE_HEADER = "X-Topolo-Actor-Envelope";
765
+ var PRINCIPAL_TYPES = /* @__PURE__ */ new Set([
766
+ "human",
767
+ "managed_agent",
768
+ "external_agent",
769
+ "service",
770
+ "browser_automation"
771
+ ]);
772
+ var ACCOUNTABLE_SUBJECT_TYPES = /* @__PURE__ */ new Set(["human", "organization"]);
773
+ var CHANNELS = /* @__PURE__ */ new Set(["web", "mobile", "cli", "mcp", "api", "workflow", "browser_automation"]);
774
+ var ASSURANCE_LEVELS = /* @__PURE__ */ new Set(["authenticated", "workload_identity", "user_verified"]);
775
+ var HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
776
+ var ActorEnvelopeValidationError = class extends Error {
777
+ path;
778
+ constructor(path, message) {
779
+ super(`${path}: ${message}`);
780
+ this.name = "ActorEnvelopeValidationError";
781
+ this.path = path;
782
+ }
783
+ };
784
+ function parseActorEnvelope(value) {
785
+ const envelope = record(value, "actorEnvelope");
786
+ exactKeys(envelope, [
787
+ "schemaVersion",
788
+ "organizationId",
789
+ "accountableSubject",
790
+ "performer",
791
+ "delegation",
792
+ "channel",
793
+ "assurance",
794
+ "action",
795
+ "issuedAt",
796
+ "expiresAt"
797
+ ], "actorEnvelope");
798
+ if (envelope.schemaVersion !== ACTOR_ENVELOPE_VERSION) {
799
+ fail("actorEnvelope.schemaVersion", `must equal ${ACTOR_ENVELOPE_VERSION}`);
800
+ }
801
+ const accountableSubject = parseAccountableSubject(envelope.accountableSubject);
802
+ const performer = parseIdentity(envelope.performer, "actorEnvelope.performer");
803
+ const organizationId = text(envelope.organizationId, "actorEnvelope.organizationId");
804
+ const channel = member(envelope.channel, CHANNELS, "actorEnvelope.channel");
805
+ const issuedAt = timestamp(envelope.issuedAt, "actorEnvelope.issuedAt");
806
+ const expiresAt = envelope.expiresAt === void 0 ? void 0 : timestamp(envelope.expiresAt, "actorEnvelope.expiresAt");
807
+ if (expiresAt && Date.parse(expiresAt) <= Date.parse(issuedAt)) {
808
+ fail("actorEnvelope.expiresAt", "must be later than issuedAt");
809
+ }
810
+ const assuranceValue = record(envelope.assurance, "actorEnvelope.assurance");
811
+ exactKeys(assuranceValue, ["level", "methods"], "actorEnvelope.assurance");
812
+ const assurance = {
813
+ level: member(assuranceValue.level, ASSURANCE_LEVELS, "actorEnvelope.assurance.level"),
814
+ methods: textArray(assuranceValue.methods, "actorEnvelope.assurance.methods")
815
+ };
816
+ const actionValue = record(envelope.action, "actorEnvelope.action");
817
+ exactKeys(actionValue, ["requestId", "actionId", "resourceType", "resourceId", "inputHash"], "actorEnvelope.action");
818
+ const inputHash = text(actionValue.inputHash, "actorEnvelope.action.inputHash");
819
+ if (!HASH_PATTERN.test(inputHash)) fail("actorEnvelope.action.inputHash", "must be a lowercase sha256 digest");
820
+ const action = {
821
+ requestId: text(actionValue.requestId, "actorEnvelope.action.requestId"),
822
+ actionId: text(actionValue.actionId, "actorEnvelope.action.actionId"),
823
+ ...actionValue.resourceType === void 0 ? {} : { resourceType: text(actionValue.resourceType, "actorEnvelope.action.resourceType") },
824
+ ...actionValue.resourceId === void 0 ? {} : { resourceId: text(actionValue.resourceId, "actorEnvelope.action.resourceId") },
825
+ inputHash
826
+ };
827
+ if (action.resourceType === void 0 !== (action.resourceId === void 0)) {
828
+ fail("actorEnvelope.action", "resourceType and resourceId must be provided together");
829
+ }
830
+ const delegation = parseDelegation(envelope.delegation);
831
+ const directHuman = performer.principalType === "human" && performer.principalId === accountableSubject.principalId;
832
+ if (directHuman && delegation !== null) fail("actorEnvelope.delegation", "must be null for direct human action");
833
+ if (!directHuman && delegation === null) fail("actorEnvelope.delegation", "is required when performer differs from accountable subject");
834
+ if (delegation) {
835
+ const finalHop = delegation.lineage.at(-1);
836
+ if (!finalHop || finalHop.principalId !== performer.principalId || finalHop.principalType !== performer.principalType) {
837
+ fail("actorEnvelope.delegation.lineage", "must end with the performer");
838
+ }
839
+ }
840
+ if (performer.principalType === "external_agent" && (!performer.installationId || !performer.provider)) {
841
+ fail("actorEnvelope.performer", "external_agent requires installationId and provider");
842
+ }
843
+ if (performer.principalType === "managed_agent" && !performer.agentInstanceId) {
844
+ fail("actorEnvelope.performer.agentInstanceId", "is required for managed_agent");
845
+ }
846
+ if (performer.principalType === "browser_automation" && (!performer.installationId || !performer.browserSessionId)) {
847
+ fail("actorEnvelope.performer", "browser_automation requires installationId and browserSessionId");
848
+ }
849
+ return {
850
+ schemaVersion: ACTOR_ENVELOPE_VERSION,
851
+ organizationId,
852
+ accountableSubject,
853
+ performer,
854
+ delegation,
855
+ channel,
856
+ assurance,
857
+ action,
858
+ issuedAt,
859
+ ...expiresAt ? { expiresAt } : {}
860
+ };
861
+ }
862
+ function canonicalizeActorEnvelope(value) {
863
+ return canonicalJson(parseActorEnvelope(value));
864
+ }
865
+ function encodeActorEnvelopeHeader(value) {
866
+ const bytes = new TextEncoder().encode(canonicalizeActorEnvelope(value));
867
+ const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join("");
868
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
869
+ }
870
+ function canonicalJson(value) {
871
+ if (value === null) return "null";
872
+ if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
873
+ if (typeof value === "number") {
874
+ if (!Number.isFinite(value)) throw new TypeError("Canonical JSON accepts finite numbers only.");
875
+ return JSON.stringify(value);
876
+ }
877
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
878
+ if (typeof value !== "object" || !value || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) {
879
+ throw new TypeError("Canonical JSON accepts JSON objects, arrays, and primitives only.");
880
+ }
881
+ const entries = Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right));
882
+ return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
883
+ }
884
+ function parseAccountableSubject(value) {
885
+ const path = "actorEnvelope.accountableSubject";
886
+ const subject = record(value, path);
887
+ exactKeys(subject, ["principalType", "principalId", "displayName"], path);
888
+ return {
889
+ principalType: member(subject.principalType, ACCOUNTABLE_SUBJECT_TYPES, `${path}.principalType`),
890
+ principalId: text(subject.principalId, `${path}.principalId`),
891
+ ...subject.displayName === void 0 ? {} : { displayName: text(subject.displayName, `${path}.displayName`) }
892
+ };
893
+ }
894
+ async function sha256CanonicalValue(value) {
895
+ const bytes = new TextEncoder().encode(canonicalJson(value));
896
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
897
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
898
+ }
899
+ function parseIdentity(value, path) {
900
+ const identity = record(value, path);
901
+ exactKeys(identity, [
902
+ "principalType",
903
+ "principalId",
904
+ "displayName",
905
+ "installationId",
906
+ "agentInstanceId",
907
+ "browserSessionId",
908
+ "provider",
909
+ "model"
910
+ ], path);
911
+ return {
912
+ principalType: member(identity.principalType, PRINCIPAL_TYPES, `${path}.principalType`),
913
+ principalId: text(identity.principalId, `${path}.principalId`),
914
+ ...optionalTextFields(identity, path, [
915
+ "displayName",
916
+ "installationId",
917
+ "agentInstanceId",
918
+ "browserSessionId",
919
+ "provider",
920
+ "model"
921
+ ])
922
+ };
923
+ }
924
+ function parseDelegation(value) {
925
+ if (value === null) return null;
926
+ const delegation = record(value, "actorEnvelope.delegation");
927
+ exactKeys(delegation, ["taskId", "runId", "lineage"], "actorEnvelope.delegation");
928
+ if (!Array.isArray(delegation.lineage) || delegation.lineage.length === 0 || delegation.lineage.length > 16) {
929
+ fail("actorEnvelope.delegation.lineage", "must contain between 1 and 16 hops");
930
+ }
931
+ return {
932
+ taskId: text(delegation.taskId, "actorEnvelope.delegation.taskId"),
933
+ runId: text(delegation.runId, "actorEnvelope.delegation.runId"),
934
+ lineage: delegation.lineage.map((value2, index) => {
935
+ const hop = record(value2, `actorEnvelope.delegation.lineage[${index}]`);
936
+ exactKeys(hop, ["principalType", "principalId"], `actorEnvelope.delegation.lineage[${index}]`);
937
+ return {
938
+ principalType: member(hop.principalType, PRINCIPAL_TYPES, `actorEnvelope.delegation.lineage[${index}].principalType`),
939
+ principalId: text(hop.principalId, `actorEnvelope.delegation.lineage[${index}].principalId`)
940
+ };
941
+ })
942
+ };
943
+ }
944
+ function record(value, path) {
945
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail(path, "must be an object");
946
+ return value;
947
+ }
948
+ function exactKeys(value, allowed, path) {
949
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
950
+ if (unknown.length) fail(path, `contains unknown field ${unknown[0]}`);
951
+ }
952
+ function text(value, path) {
953
+ if (typeof value !== "string" || value.trim() !== value || value.length === 0 || value.length > 256) {
954
+ fail(path, "must be a non-empty trimmed string of at most 256 characters");
955
+ }
956
+ return value;
957
+ }
958
+ function timestamp(value, path) {
959
+ const result = text(value, path);
960
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(result) || Number.isNaN(Date.parse(result))) {
961
+ fail(path, "must be an RFC 3339 UTC timestamp");
962
+ }
963
+ return result;
964
+ }
965
+ function member(value, values, path) {
966
+ if (typeof value !== "string" || !values.has(value)) fail(path, "contains an unsupported value");
967
+ return value;
968
+ }
969
+ function textArray(value, path) {
970
+ if (!Array.isArray(value) || value.length === 0 || value.length > 16) fail(path, "must contain between 1 and 16 values");
971
+ return value.map((item, index) => text(item, `${path}[${index}]`));
972
+ }
973
+ function optionalTextFields(value, path, keys) {
974
+ return Object.fromEntries(keys.flatMap((key) => value[key] === void 0 ? [] : [[key, text(value[key], `${path}.${key}`)]]));
975
+ }
976
+ function fail(path, message) {
977
+ throw new ActorEnvelopeValidationError(path, message);
978
+ }
715
979
  function parseTopoloActionUploadContract(value) {
716
980
  if (!isRecord2(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
717
981
  throw new TopoloSdkError("upload_contract_unavailable", "This action does not publish a supported upload contract.");
@@ -916,6 +1180,7 @@ var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
916
1180
  var TopoloClient = class _TopoloClient {
917
1181
  credential;
918
1182
  agent;
1183
+ actorContext;
919
1184
  serviceUrls;
920
1185
  environment;
921
1186
  appIds;
@@ -923,7 +1188,7 @@ var TopoloClient = class _TopoloClient {
923
1188
  timeoutMs;
924
1189
  fetchImpl;
925
1190
  credentialProvider;
926
- credentialLoadInFlight = null;
1191
+ credentialLoadsInFlight = /* @__PURE__ */ new Map();
927
1192
  refreshCredential;
928
1193
  refreshInFlight = null;
929
1194
  debug;
@@ -946,6 +1211,11 @@ var TopoloClient = class _TopoloClient {
946
1211
  if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
947
1212
  this.credential = options.credential;
948
1213
  this.agent = options.agent;
1214
+ this.actorContext = options.actorContext ? {
1215
+ channel: options.actorContext.channel,
1216
+ taskId: options.actorContext.taskId ?? `task_${generateRequestId()}`,
1217
+ runId: options.actorContext.runId ?? `run_${generateRequestId()}`
1218
+ } : void 0;
949
1219
  this.environment = options.environment;
950
1220
  this.serviceUrls = options.environment ? topoloServiceOrigins(options.serviceUrls, options.environment) : options.serviceUrls;
951
1221
  this.appIds = options.appIds;
@@ -975,7 +1245,16 @@ var TopoloClient = class _TopoloClient {
975
1245
  `Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
976
1246
  );
977
1247
  }
978
- await this.loadCurrentCredential();
1248
+ const actionId = requestActionId(opts.headers);
1249
+ const taskId = opts.actorEnvelope?.delegation?.taskId ?? this.actorContext?.taskId;
1250
+ let requestCredential = await this.loadCurrentCredential({
1251
+ service: normalizeLookupKey(opts.service),
1252
+ method,
1253
+ path: opts.path,
1254
+ ...actionId ? { actionId } : {},
1255
+ ...taskId ? { taskId } : {},
1256
+ ...opts.resource ? { resource: opts.resource } : {}
1257
+ });
979
1258
  const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: this.validatedServiceOrigin(opts.service, opts.baseUrl) } : await this.resolveRequestService(opts.service);
980
1259
  const baseUrl = resolvedService.baseUrl;
981
1260
  const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
@@ -998,12 +1277,15 @@ var TopoloClient = class _TopoloClient {
998
1277
  if (opts.body !== void 0) headers.set("Content-Type", "application/json");
999
1278
  const platformAppId = resolveConfiguredAppId(resolvedService.serviceKey, this.appIds);
1000
1279
  if (platformAppId) headers.set("X-App-ID", platformAppId);
1001
- applyAuthHeaders(headers, this.credential);
1002
- const requestId = generateRequestId();
1280
+ const actorEnvelope = opts.actorEnvelope === void 0 ? void 0 : parseActorEnvelope(opts.actorEnvelope);
1281
+ const requestId = actorEnvelope?.action.requestId ?? generateRequestId();
1003
1282
  applyAuditHeaders(headers, this.agent, requestId);
1004
1283
  if (opts.headers) {
1005
1284
  for (const [k, v] of Object.entries(opts.headers)) headers.set(k, v);
1006
1285
  }
1286
+ if (actorEnvelope) {
1287
+ headers.set(TOPOLO_ACTOR_ENVELOPE_HEADER, encodeActorEnvelopeHeader(actorEnvelope));
1288
+ }
1007
1289
  if (opts.resource) {
1008
1290
  const resourceType = opts.resource.resourceType.trim();
1009
1291
  const resourceId = opts.resource.resourceId.trim();
@@ -1028,9 +1310,19 @@ var TopoloClient = class _TopoloClient {
1028
1310
  }, this.timeoutMs);
1029
1311
  const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
1030
1312
  try {
1313
+ const requestHeaders = new Headers(headers);
1314
+ applyAuthHeaders(requestHeaders, requestCredential);
1315
+ if (requestCredential.kind === "dpop_access_token") {
1316
+ requestHeaders.set("DPoP", await createDpopProof({
1317
+ accessToken: requestCredential.accessToken,
1318
+ method,
1319
+ targetUrl: urlStr,
1320
+ privateJwk: requestCredential.privateJwk
1321
+ }));
1322
+ }
1031
1323
  return await this.fetchImpl(urlStr, {
1032
1324
  method,
1033
- headers: new Headers(headers),
1325
+ headers: requestHeaders,
1034
1326
  body: opts.rawBody ?? (opts.body !== void 0 ? JSON.stringify(opts.body) : null),
1035
1327
  signal
1036
1328
  });
@@ -1061,13 +1353,13 @@ var TopoloClient = class _TopoloClient {
1061
1353
  };
1062
1354
  let res;
1063
1355
  try {
1064
- const attemptedCredential = this.credential;
1356
+ const attemptedCredential = requestCredential;
1065
1357
  res = await fetchWithReadRetry();
1066
1358
  if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
1067
1359
  await res.body?.cancel().catch(() => void 0);
1068
1360
  const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
1069
1361
  if (refreshed) {
1070
- applyAuthHeaders(headers, refreshed);
1362
+ requestCredential = refreshed;
1071
1363
  res = await fetchWithReadRetry();
1072
1364
  }
1073
1365
  }
@@ -1157,18 +1449,23 @@ var TopoloClient = class _TopoloClient {
1157
1449
  });
1158
1450
  return parsed;
1159
1451
  }
1160
- async loadCurrentCredential() {
1161
- if (!this.credentialProvider) return;
1162
- this.credentialLoadInFlight ??= this.credentialProvider().then((credential) => {
1163
- if (!credential) {
1164
- throw new TopoloAuthError("The configured Topolo credential is no longer available.");
1165
- }
1166
- this.credential = credential;
1167
- return credential;
1168
- }).finally(() => {
1169
- this.credentialLoadInFlight = null;
1170
- });
1171
- await this.credentialLoadInFlight;
1452
+ async loadCurrentCredential(context) {
1453
+ if (!this.credentialProvider) return this.credential;
1454
+ const key = JSON.stringify(context);
1455
+ let load = this.credentialLoadsInFlight.get(key);
1456
+ if (!load) {
1457
+ load = this.credentialProvider(context).then((credential) => {
1458
+ if (!credential) {
1459
+ throw new TopoloAuthError("The configured Topolo credential is no longer available.");
1460
+ }
1461
+ this.credential = credential;
1462
+ return credential;
1463
+ }).finally(() => {
1464
+ this.credentialLoadsInFlight.delete(key);
1465
+ });
1466
+ this.credentialLoadsInFlight.set(key, load);
1467
+ }
1468
+ return load;
1172
1469
  }
1173
1470
  async refreshRejectedAccessToken(attemptedAccessToken) {
1174
1471
  if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
@@ -1450,14 +1747,14 @@ var TopoloClient = class _TopoloClient {
1450
1747
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1451
1748
  throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
1452
1749
  }
1453
- const record = raw;
1454
- if (record.algorithm !== "sha256-v1" || typeof record.digest !== "string" || !/^[a-f0-9]{64}$/.test(record.digest) || !Number.isSafeInteger(record.actionCount) || Number(record.actionCount) < 0) {
1750
+ const record2 = raw;
1751
+ if (record2.algorithm !== "sha256-v1" || typeof record2.digest !== "string" || !/^[a-f0-9]{64}$/.test(record2.digest) || !Number.isSafeInteger(record2.actionCount) || Number(record2.actionCount) < 0) {
1455
1752
  throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
1456
1753
  }
1457
1754
  return {
1458
1755
  algorithm: "sha256-v1",
1459
- digest: record.digest,
1460
- actionCount: Number(record.actionCount)
1756
+ digest: record2.digest,
1757
+ actionCount: Number(record2.actionCount)
1461
1758
  };
1462
1759
  }
1463
1760
  /** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
@@ -1498,6 +1795,14 @@ var TopoloClient = class _TopoloClient {
1498
1795
  const entry = await this.getAction(action);
1499
1796
  return this.executeResolvedAction(entry, input, options);
1500
1797
  }
1798
+ async issueExternalAgentCredential(input) {
1799
+ const actionId = "app_topolo_auth.principal_credentials.issue";
1800
+ const entry = await this.getAction(actionId);
1801
+ if (entry.actionId !== actionId) {
1802
+ throw new TopoloSdkError("unexpected_action", "External agent credential issuance resolved an unexpected action.");
1803
+ }
1804
+ return this.executeResolvedAction(entry, input, { confirm: true }, true);
1805
+ }
1501
1806
  async uploadAction(action, content, input, options) {
1502
1807
  const entry = await this.getAction(action);
1503
1808
  const contract = parseTopoloActionUploadContract(entry.agentContract?.upload);
@@ -1629,7 +1934,7 @@ var TopoloClient = class _TopoloClient {
1629
1934
  }
1630
1935
  return this.executeResolvedAction(entry, input, options);
1631
1936
  }
1632
- async executeResolvedAction(entry, input, options) {
1937
+ async executeResolvedAction(entry, input, options, preserveSensitiveResult = false) {
1633
1938
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
1634
1939
  if (agentAccess === "off") {
1635
1940
  throw new TopoloPermissionError(
@@ -1657,6 +1962,20 @@ var TopoloClient = class _TopoloClient {
1657
1962
  );
1658
1963
  }
1659
1964
  const mapped = mapActionInput(entry, input);
1965
+ const resolvedActorEnvelope = options.actorEnvelope ?? await this.buildVerifiedAgentActorEnvelope(entry.actionId, input, options.resource);
1966
+ if (resolvedActorEnvelope) {
1967
+ const actorEnvelope = parseActorEnvelope(resolvedActorEnvelope);
1968
+ if (actorEnvelope.action.actionId !== entry.actionId) {
1969
+ throw new TopoloSdkError("actor_envelope_action_mismatch", "Actor envelope actionId does not match the executed action.");
1970
+ }
1971
+ if (actorEnvelope.action.inputHash !== await sha256CanonicalValue(input)) {
1972
+ throw new TopoloSdkError("actor_envelope_input_mismatch", "Actor envelope inputHash does not match the executed action input.");
1973
+ }
1974
+ const envelopeResource = actorEnvelope.action.resourceType && actorEnvelope.action.resourceId ? { resourceType: actorEnvelope.action.resourceType, resourceId: actorEnvelope.action.resourceId } : void 0;
1975
+ if (envelopeResource?.resourceType !== options.resource?.resourceType || envelopeResource?.resourceId !== options.resource?.resourceId) {
1976
+ throw new TopoloSdkError("actor_envelope_resource_mismatch", "Actor envelope resource does not match the executed action resource.");
1977
+ }
1978
+ }
1660
1979
  const service = entry.appSlug || entry.appId;
1661
1980
  const result = await this.request({
1662
1981
  service,
@@ -1666,13 +1985,64 @@ var TopoloClient = class _TopoloClient {
1666
1985
  ...mapped.query ? { query: mapped.query } : {},
1667
1986
  ...mapped.body !== void 0 ? { body: mapped.body } : {},
1668
1987
  ...options.resource ? { resource: options.resource } : {},
1988
+ ...resolvedActorEnvelope ? { actorEnvelope: resolvedActorEnvelope } : {},
1669
1989
  headers: {
1670
1990
  [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
1671
1991
  [TOPOLO_ACTION_NAME_HEADER]: entry.name
1672
1992
  },
1673
1993
  confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
1674
1994
  });
1675
- return redactActionResult(result);
1995
+ return preserveSensitiveResult ? result : redactActionResult(result);
1996
+ }
1997
+ async buildVerifiedAgentActorEnvelope(actionId, input, resource) {
1998
+ if (!this.actorContext) return void 0;
1999
+ const identity = await this.introspect();
2000
+ const principalType = identity.user.principalType;
2001
+ if (principalType !== "agent_employee" && principalType !== "external_agent") return void 0;
2002
+ if (!identity.organization || !identity.user.accountableUserId || !identity.user.principalId) {
2003
+ throw new TopoloSdkError(
2004
+ "incomplete_agent_identity",
2005
+ "Verified agent credentials must resolve an organization, accountable user, and principal id."
2006
+ );
2007
+ }
2008
+ const performer = principalType === "external_agent" ? {
2009
+ principalType: "external_agent",
2010
+ principalId: identity.user.principalId,
2011
+ installationId: identity.user.installationId ?? void 0,
2012
+ provider: identity.user.provider ?? void 0,
2013
+ model: identity.user.model ?? void 0
2014
+ } : {
2015
+ principalType: "managed_agent",
2016
+ principalId: identity.user.principalId,
2017
+ agentInstanceId: identity.user.agentInstanceId ?? void 0,
2018
+ model: identity.user.model ?? void 0
2019
+ };
2020
+ if (performer.principalType === "external_agent" && (!performer.installationId || !performer.provider) || performer.principalType === "managed_agent" && !performer.agentInstanceId) {
2021
+ throw new TopoloSdkError(
2022
+ "incomplete_agent_identity",
2023
+ "Verified agent credentials are missing the performer identity required for provenance."
2024
+ );
2025
+ }
2026
+ return parseActorEnvelope({
2027
+ schemaVersion: 1,
2028
+ organizationId: identity.organization.id,
2029
+ accountableSubject: { principalType: "human", principalId: identity.user.accountableUserId },
2030
+ performer,
2031
+ delegation: {
2032
+ taskId: this.actorContext.taskId,
2033
+ runId: this.actorContext.runId,
2034
+ lineage: [{ principalType: performer.principalType, principalId: performer.principalId }]
2035
+ },
2036
+ channel: this.actorContext.channel,
2037
+ assurance: { level: "workload_identity", methods: ["client_credentials"] },
2038
+ action: {
2039
+ requestId: generateRequestId(),
2040
+ actionId,
2041
+ ...resource ? resource : {},
2042
+ inputHash: await sha256CanonicalValue(input)
2043
+ },
2044
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString()
2045
+ });
1676
2046
  }
1677
2047
  /**
1678
2048
  * Introspect the current credential. Returns the caller's resolved identity,
@@ -1701,7 +2071,18 @@ var TopoloClient = class _TopoloClient {
1701
2071
  email: user.email,
1702
2072
  name: user.name ?? null,
1703
2073
  role: user.role ?? (kind === "api_key" ? "service" : "member"),
1704
- permissions: payload.permissions ?? user.permissions ?? []
2074
+ permissions: payload.permissions ?? user.permissions ?? [],
2075
+ ...normalizeAgentPrincipalType(user.principalType) ? {
2076
+ principalId: readOptionalString(user.principalId),
2077
+ principalType: normalizeAgentPrincipalType(user.principalType),
2078
+ accountableUserId: readOptionalString(user.accountableUserId),
2079
+ agentInstanceId: readOptionalString(user.agentInstanceId),
2080
+ installationId: readOptionalString(user.installationId),
2081
+ provider: readOptionalString(user.provider),
2082
+ model: readOptionalString(user.model),
2083
+ serviceScopes: stringArray(user.serviceScopes),
2084
+ policyRef: readOptionalString(user.policyRef)
2085
+ } : {}
1705
2086
  },
1706
2087
  organization: organization ? {
1707
2088
  id: organization.id,
@@ -1830,12 +2211,21 @@ function redactActionResult(value) {
1830
2211
  if (!isRecord3(value)) return value;
1831
2212
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [
1832
2213
  key,
1833
- isSensitiveActionResultKey(key) ? "[REDACTED]" : redactActionResult(item)
2214
+ isSensitiveActionResultKey(key, item) ? "[REDACTED]" : redactActionResult(item)
1834
2215
  ]));
1835
2216
  }
1836
- function isSensitiveActionResultKey(key) {
2217
+ function isSensitiveActionResultKey(key, value) {
1837
2218
  const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
1838
- return ["authorization", "cookie", "setcookie", "proxyauthorization", "capability"].includes(normalized) || /(token|secret|password|privatekey|apikey)$/.test(normalized);
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);
1839
2229
  }
1840
2230
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
1841
2231
  for (const serviceKey of serviceKeyCandidates2(slug)) {
@@ -2057,6 +2447,9 @@ function normalizeCredentialAuthorizationMode(value, kind) {
2057
2447
  if (value === "context_rbac" || value === "oauth_scopes" || value === "session_rbac") return value;
2058
2448
  return "unknown";
2059
2449
  }
2450
+ function normalizeAgentPrincipalType(value) {
2451
+ return value === "agent_employee" || value === "external_agent" ? value : null;
2452
+ }
2060
2453
  function describeError(body, fallback) {
2061
2454
  if (body && typeof body === "object") {
2062
2455
  const maybe = body;
@@ -2075,16 +2468,22 @@ function pickString(value) {
2075
2468
  }
2076
2469
  return void 0;
2077
2470
  }
2471
+ function requestActionId(headers) {
2472
+ const value = Object.entries(headers ?? {}).find(
2473
+ ([name]) => name.toLowerCase() === TOPOLO_ACTION_ID_HEADER.toLowerCase()
2474
+ )?.[1];
2475
+ return value?.trim() || void 0;
2476
+ }
2078
2477
  function extractRequired(body) {
2079
2478
  if (body && typeof body === "object") {
2080
2479
  const maybe = body;
2081
2480
  if (Array.isArray(maybe.required)) return canonicalTopoloRequired(maybe.required);
2082
2481
  for (const nested of [maybe.details, maybe.error]) {
2083
2482
  if (!nested || typeof nested !== "object" || Array.isArray(nested)) continue;
2084
- const record = nested;
2085
- if (Array.isArray(record.required)) return canonicalTopoloRequired(record.required);
2086
- if (record.details && typeof record.details === "object" && !Array.isArray(record.details)) {
2087
- const required = record.details.required;
2483
+ const record2 = nested;
2484
+ if (Array.isArray(record2.required)) return canonicalTopoloRequired(record2.required);
2485
+ if (record2.details && typeof record2.details === "object" && !Array.isArray(record2.details)) {
2486
+ const required = record2.details.required;
2088
2487
  if (Array.isArray(required)) return canonicalTopoloRequired(required);
2089
2488
  }
2090
2489
  }
@@ -2159,8 +2558,8 @@ function compactResultValue(value, depth, budget) {
2159
2558
  return value.length > ACTION_RESULT_MAX_ITEMS ? { count: value.length, items, omittedItems: value.length - ACTION_RESULT_MAX_ITEMS } : items;
2160
2559
  }
2161
2560
  if (typeof value === "object") {
2162
- const record = value;
2163
- const allKeys = Object.keys(record);
2561
+ const record2 = value;
2562
+ const allKeys = Object.keys(record2);
2164
2563
  const ranked = allKeys.sort((left, right) => resultKeyPriority(left) - resultKeyPriority(right));
2165
2564
  const prioritized = ranked.filter((key) => resultKeyPriority(key) < 2);
2166
2565
  const ordinary = ranked.filter((key) => resultKeyPriority(key) === 2);
@@ -2169,7 +2568,7 @@ function compactResultValue(value, depth, budget) {
2169
2568
  ...prioritized,
2170
2569
  ...ordinary.slice(0, ordinaryLimit)
2171
2570
  ].slice(0, ACTION_RESULT_MAX_KEYS);
2172
- const compact = Object.fromEntries(keys.map((key) => [key, compactResultValue(record[key], depth + 1, budget)]));
2571
+ const compact = Object.fromEntries(keys.map((key) => [key, compactResultValue(record2[key], depth + 1, budget)]));
2173
2572
  if (allKeys.length > keys.length) compact["_omittedKeys"] = allKeys.length - keys.length;
2174
2573
  return compact;
2175
2574
  }
@@ -2390,6 +2789,461 @@ var WorkspacesModule = class {
2390
2789
  return { organizationId: identity.organization.id, appId: service.appId };
2391
2790
  }
2392
2791
  };
2792
+ var nonEmptyString2 = { type: "string", minLength: 1 };
2793
+ var stringArray3 = { type: "array", items: nonEmptyString2 };
2794
+ var actionBinding = {
2795
+ type: "object",
2796
+ properties: {
2797
+ id: nonEmptyString2,
2798
+ label: nonEmptyString2,
2799
+ actionId: nonEmptyString2,
2800
+ resourceType: nonEmptyString2,
2801
+ resourceId: nonEmptyString2,
2802
+ input: { type: "object" },
2803
+ requiresConfirmation: { type: "boolean" },
2804
+ destructive: { type: "boolean" },
2805
+ effects: stringArray3,
2806
+ verification: stringArray3,
2807
+ recovery: {
2808
+ type: "object",
2809
+ properties: {
2810
+ mode: { enum: ["not_applicable", "retry", "compensating_action"] },
2811
+ actionId: nonEmptyString2,
2812
+ instructions: nonEmptyString2
2813
+ },
2814
+ required: ["mode", "instructions"],
2815
+ additionalProperties: false
2816
+ }
2817
+ },
2818
+ required: [
2819
+ "id",
2820
+ "label",
2821
+ "actionId",
2822
+ "resourceType",
2823
+ "input",
2824
+ "requiresConfirmation",
2825
+ "destructive",
2826
+ "effects",
2827
+ "verification",
2828
+ "recovery"
2829
+ ],
2830
+ additionalProperties: false
2831
+ };
2832
+ var dimension = {
2833
+ type: "object",
2834
+ properties: { key: nonEmptyString2, label: nonEmptyString2, value: nonEmptyString2 },
2835
+ required: ["key", "label", "value"],
2836
+ additionalProperties: false
2837
+ };
2838
+ var period = {
2839
+ type: "object",
2840
+ properties: {
2841
+ start: nonEmptyString2,
2842
+ end: nonEmptyString2,
2843
+ timezone: nonEmptyString2
2844
+ },
2845
+ required: ["start", "end", "timezone"],
2846
+ additionalProperties: false
2847
+ };
2848
+ var series = {
2849
+ type: "object",
2850
+ properties: {
2851
+ id: nonEmptyString2,
2852
+ label: nonEmptyString2,
2853
+ unit: nonEmptyString2,
2854
+ points: {
2855
+ type: "array",
2856
+ items: {
2857
+ type: "object",
2858
+ properties: {
2859
+ timestamp: nonEmptyString2,
2860
+ value: { type: "number" },
2861
+ dimensions: { type: "array", items: dimension }
2862
+ },
2863
+ required: ["timestamp", "value", "dimensions"],
2864
+ additionalProperties: false
2865
+ }
2866
+ }
2867
+ },
2868
+ required: ["id", "label", "unit", "points"],
2869
+ additionalProperties: false
2870
+ };
2871
+ var contribution = {
2872
+ type: "object",
2873
+ properties: {
2874
+ summary: nonEmptyString2,
2875
+ identity: {
2876
+ type: "object",
2877
+ properties: { iconSource: { const: "catalog" } },
2878
+ required: ["iconSource"],
2879
+ additionalProperties: false
2880
+ },
2881
+ workspaceSelector: {
2882
+ type: "object",
2883
+ properties: {
2884
+ mode: { enum: ["none", "optional", "required"] },
2885
+ resourceType: nonEmptyString2,
2886
+ label: nonEmptyString2,
2887
+ supportsMultiple: { type: "boolean" }
2888
+ },
2889
+ required: ["mode", "resourceType", "label", "supportsMultiple"],
2890
+ additionalProperties: false
2891
+ },
2892
+ metricDefinitions: {
2893
+ type: "array",
2894
+ items: {
2895
+ type: "object",
2896
+ properties: {
2897
+ key: nonEmptyString2,
2898
+ label: nonEmptyString2,
2899
+ description: nonEmptyString2,
2900
+ unit: nonEmptyString2,
2901
+ semantic: {
2902
+ enum: ["gauge", "counter", "rate", "duration", "currency", "percentage"]
2903
+ },
2904
+ aggregation: {
2905
+ enum: ["last", "sum", "average", "minimum", "maximum", "count", "distinct_count"]
2906
+ }
2907
+ },
2908
+ required: ["key", "label", "description", "unit", "semantic", "aggregation"],
2909
+ additionalProperties: false
2910
+ }
2911
+ },
2912
+ seriesDefinitions: {
2913
+ type: "array",
2914
+ items: {
2915
+ type: "object",
2916
+ properties: {
2917
+ key: nonEmptyString2,
2918
+ label: nonEmptyString2,
2919
+ metricKey: nonEmptyString2,
2920
+ unit: nonEmptyString2
2921
+ },
2922
+ required: ["key", "label", "metricKey", "unit"],
2923
+ additionalProperties: false
2924
+ }
2925
+ },
2926
+ dimensions: {
2927
+ type: "array",
2928
+ items: {
2929
+ type: "object",
2930
+ properties: {
2931
+ key: nonEmptyString2,
2932
+ label: nonEmptyString2,
2933
+ description: nonEmptyString2,
2934
+ valueType: { enum: ["string", "number", "boolean", "date", "datetime"] }
2935
+ },
2936
+ required: ["key", "label", "description", "valueType"],
2937
+ additionalProperties: false
2938
+ }
2939
+ },
2940
+ lifecycleStages: {
2941
+ type: "array",
2942
+ items: {
2943
+ type: "object",
2944
+ properties: {
2945
+ key: nonEmptyString2,
2946
+ label: nonEmptyString2,
2947
+ order: { type: "number" },
2948
+ terminal: { type: "boolean" }
2949
+ },
2950
+ required: ["key", "label", "order", "terminal"],
2951
+ additionalProperties: false
2952
+ }
2953
+ },
2954
+ events: {
2955
+ type: "array",
2956
+ items: {
2957
+ type: "object",
2958
+ properties: {
2959
+ key: nonEmptyString2,
2960
+ label: nonEmptyString2,
2961
+ description: nonEmptyString2
2962
+ },
2963
+ required: ["key", "label", "description"],
2964
+ additionalProperties: false
2965
+ }
2966
+ },
2967
+ datasets: {
2968
+ type: "array",
2969
+ minItems: 1,
2970
+ items: {
2971
+ type: "object",
2972
+ properties: {
2973
+ id: nonEmptyString2,
2974
+ label: nonEmptyString2,
2975
+ description: nonEmptyString2,
2976
+ grain: nonEmptyString2,
2977
+ metricKeys: stringArray3,
2978
+ seriesKeys: stringArray3,
2979
+ dimensionKeys: stringArray3,
2980
+ eventKeys: stringArray3,
2981
+ actionIds: stringArray3
2982
+ },
2983
+ required: [
2984
+ "id",
2985
+ "label",
2986
+ "description",
2987
+ "grain",
2988
+ "metricKeys",
2989
+ "seriesKeys",
2990
+ "dimensionKeys",
2991
+ "eventKeys",
2992
+ "actionIds"
2993
+ ],
2994
+ additionalProperties: false
2995
+ }
2996
+ },
2997
+ compatibleVisualizations: {
2998
+ type: "array",
2999
+ minItems: 1,
3000
+ items: {
3001
+ enum: ["number", "line", "area", "bar", "donut", "table", "funnel", "timeline", "actions"]
3002
+ }
3003
+ },
3004
+ defaultVisualization: {
3005
+ enum: ["number", "line", "area", "bar", "donut", "table", "funnel", "timeline", "actions"]
3006
+ }
3007
+ },
3008
+ required: [
3009
+ "summary",
3010
+ "identity",
3011
+ "workspaceSelector",
3012
+ "metricDefinitions",
3013
+ "seriesDefinitions",
3014
+ "dimensions",
3015
+ "lifecycleStages",
3016
+ "events",
3017
+ "datasets",
3018
+ "compatibleVisualizations",
3019
+ "defaultVisualization"
3020
+ ],
3021
+ additionalProperties: false
3022
+ };
3023
+ var widgetItem = {
3024
+ type: "object",
3025
+ properties: {
3026
+ id: nonEmptyString2,
3027
+ version: nonEmptyString2,
3028
+ appId: nonEmptyString2,
3029
+ serviceName: nonEmptyString2,
3030
+ lastUpdated: nonEmptyString2,
3031
+ type: { enum: ["stats", "chart", "activity", "notifications", "actions"] },
3032
+ presentation: {
3033
+ type: "object",
3034
+ properties: {
3035
+ title: nonEmptyString2,
3036
+ description: nonEmptyString2,
3037
+ contexts: {
3038
+ type: "array",
3039
+ minItems: 1,
3040
+ items: { enum: ["personal", "organization", "workspace", "resource"] }
3041
+ },
3042
+ supportedSizes: {
3043
+ type: "array",
3044
+ minItems: 1,
3045
+ items: { enum: ["compact", "medium", "wide", "full"] }
3046
+ },
3047
+ defaultSize: { enum: ["compact", "medium", "wide", "full"] }
3048
+ },
3049
+ required: ["title", "contexts", "supportedSizes", "defaultSize"],
3050
+ additionalProperties: false
3051
+ },
3052
+ resource: {
3053
+ type: "object",
3054
+ properties: {
3055
+ resourceType: nonEmptyString2,
3056
+ resourceId: nonEmptyString2,
3057
+ requiredPermissions: stringArray3
3058
+ },
3059
+ required: ["resourceType", "requiredPermissions"],
3060
+ additionalProperties: false
3061
+ },
3062
+ state: {
3063
+ type: "object",
3064
+ properties: {
3065
+ status: { enum: ["ready", "empty", "unavailable", "stale", "error"] },
3066
+ observedAt: nonEmptyString2,
3067
+ lastSuccessfulAt: nonEmptyString2,
3068
+ code: nonEmptyString2,
3069
+ message: nonEmptyString2
3070
+ },
3071
+ required: ["status", "observedAt"],
3072
+ additionalProperties: false
3073
+ },
3074
+ refresh: {
3075
+ type: "object",
3076
+ properties: {
3077
+ intervalSeconds: { type: "number" },
3078
+ timeoutSeconds: { type: "number" },
3079
+ maxAgeSeconds: { type: "number" },
3080
+ staleWhileRevalidateSeconds: { type: "number" }
3081
+ },
3082
+ required: ["intervalSeconds", "timeoutSeconds", "maxAgeSeconds", "staleWhileRevalidateSeconds"],
3083
+ additionalProperties: false
3084
+ },
3085
+ configuration: {
3086
+ type: "object",
3087
+ properties: {
3088
+ allowMultipleInstances: { type: "boolean" },
3089
+ fields: {
3090
+ type: "array",
3091
+ items: {
3092
+ type: "object",
3093
+ properties: {
3094
+ key: nonEmptyString2,
3095
+ label: nonEmptyString2,
3096
+ type: { enum: ["string", "number", "boolean", "select", "multi-select", "date-range"] },
3097
+ required: { type: "boolean" },
3098
+ options: {
3099
+ type: "array",
3100
+ items: {
3101
+ type: "object",
3102
+ properties: { value: nonEmptyString2, label: nonEmptyString2 },
3103
+ required: ["value", "label"],
3104
+ additionalProperties: false
3105
+ }
3106
+ }
3107
+ },
3108
+ required: ["key", "label", "type", "required"],
3109
+ additionalProperties: false
3110
+ }
3111
+ },
3112
+ defaults: { type: "object" }
3113
+ },
3114
+ required: ["allowMultipleInstances", "fields", "defaults"],
3115
+ additionalProperties: false
3116
+ },
3117
+ drilldowns: {
3118
+ type: "array",
3119
+ items: {
3120
+ type: "object",
3121
+ properties: { id: nonEmptyString2, label: nonEmptyString2, href: nonEmptyString2 },
3122
+ required: ["id", "label", "href"],
3123
+ additionalProperties: false
3124
+ }
3125
+ },
3126
+ controls: { type: "array", items: actionBinding },
3127
+ localization: {
3128
+ type: "object",
3129
+ properties: {
3130
+ locale: nonEmptyString2,
3131
+ titleKey: nonEmptyString2,
3132
+ descriptionKey: nonEmptyString2,
3133
+ title: nonEmptyString2,
3134
+ description: nonEmptyString2
3135
+ },
3136
+ required: ["locale", "titleKey"],
3137
+ additionalProperties: false
3138
+ },
3139
+ accessibility: {
3140
+ type: "object",
3141
+ properties: { label: nonEmptyString2, description: nonEmptyString2 },
3142
+ required: ["label"],
3143
+ additionalProperties: false
3144
+ },
3145
+ contribution,
3146
+ metrics: {
3147
+ type: "array",
3148
+ items: {
3149
+ type: "object",
3150
+ properties: {
3151
+ key: nonEmptyString2,
3152
+ label: nonEmptyString2,
3153
+ value: { type: "number" },
3154
+ unit: nonEmptyString2,
3155
+ semantic: {
3156
+ enum: ["gauge", "counter", "rate", "duration", "currency", "percentage"]
3157
+ },
3158
+ aggregation: {
3159
+ enum: ["last", "sum", "average", "minimum", "maximum", "count", "distinct_count"]
3160
+ },
3161
+ period,
3162
+ dimensions: { type: "array", items: dimension },
3163
+ series
3164
+ },
3165
+ required: ["key", "label", "value", "unit", "semantic", "aggregation", "period", "dimensions"],
3166
+ additionalProperties: false
3167
+ }
3168
+ },
3169
+ chartType: { enum: ["line", "bar", "area", "pie", "donut"] },
3170
+ series: { type: "array", items: series },
3171
+ activities: { type: "array", items: { type: "object" } },
3172
+ notifications: { type: "array", items: { type: "object" } },
3173
+ unreadCount: { type: "number" },
3174
+ actions: { type: "array", items: actionBinding }
3175
+ },
3176
+ required: [
3177
+ "id",
3178
+ "version",
3179
+ "appId",
3180
+ "serviceName",
3181
+ "lastUpdated",
3182
+ "type",
3183
+ "presentation",
3184
+ "resource",
3185
+ "state",
3186
+ "refresh",
3187
+ "configuration",
3188
+ "drilldowns",
3189
+ "controls",
3190
+ "localization",
3191
+ "accessibility",
3192
+ "contribution"
3193
+ ],
3194
+ allOf: [
3195
+ { if: { properties: { type: { const: "stats" } } }, then: { required: ["metrics"] } },
3196
+ { if: { properties: { type: { const: "chart" } } }, then: { required: ["chartType", "series"] } },
3197
+ { if: { properties: { type: { const: "activity" } } }, then: { required: ["activities"] } },
3198
+ {
3199
+ if: { properties: { type: { const: "notifications" } } },
3200
+ then: { required: ["notifications", "unreadCount"] }
3201
+ },
3202
+ { if: { properties: { type: { const: "actions" } } }, then: { required: ["actions"] } }
3203
+ ],
3204
+ additionalProperties: false
3205
+ };
3206
+ var TOPOLO_WIDGET_OUTPUT_SCHEMA = Object.freeze({
3207
+ $schema: "https://json-schema.org/draft/2020-12/schema",
3208
+ type: "object",
3209
+ properties: {
3210
+ success: { type: "boolean", const: true },
3211
+ contractVersion: { type: "string", const: "3.0.0" },
3212
+ appId: nonEmptyString2,
3213
+ serviceName: nonEmptyString2,
3214
+ generatedAt: nonEmptyString2,
3215
+ widgets: { type: "array", minItems: 1, items: widgetItem },
3216
+ workspace: {
3217
+ anyOf: [
3218
+ {
3219
+ type: "object",
3220
+ properties: {
3221
+ id: { anyOf: [{ type: "string" }, { type: "number" }] },
3222
+ name: nonEmptyString2
3223
+ },
3224
+ required: ["id", "name"],
3225
+ additionalProperties: true
3226
+ },
3227
+ { type: "null" }
3228
+ ]
3229
+ },
3230
+ workspaces: {
3231
+ type: "array",
3232
+ items: {
3233
+ type: "object",
3234
+ properties: {
3235
+ id: { anyOf: [{ type: "string" }, { type: "number" }] },
3236
+ name: nonEmptyString2,
3237
+ isDefault: { type: "boolean" }
3238
+ },
3239
+ required: ["id", "name"],
3240
+ additionalProperties: false
3241
+ }
3242
+ }
3243
+ },
3244
+ required: ["success", "contractVersion", "appId", "serviceName", "generatedAt", "widgets"],
3245
+ additionalProperties: false
3246
+ });
2393
3247
  function createTopolo(options) {
2394
3248
  const client = new TopoloClient(options);
2395
3249
  return {
@@ -2400,10 +3254,15 @@ function createTopolo(options) {
2400
3254
  };
2401
3255
  }
2402
3256
 
2403
- // node_modules/@topolo/sdk/dist/node/credential-store.js
2404
- import { mkdir, readFile, writeFile, chmod, rm, stat } from "fs/promises";
3257
+ // node_modules/.pnpm/@topolo+sdk@0.21.0/node_modules/@topolo/sdk/dist/node/credential-store.js
3258
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, rm as rm2, stat } from "fs/promises";
3259
+ import { existsSync as existsSync2 } from "fs";
3260
+ import { homedir as homedir2, platform } from "os";
3261
+ import { dirname as dirname2, join as join2 } from "path";
3262
+ import { spawnSync } from "child_process";
3263
+ import { mkdir, readFile, readdir, rename, rm, writeFile, chmod } from "fs/promises";
2405
3264
  import { existsSync } from "fs";
2406
- import { homedir, platform } from "os";
3265
+ import { homedir, platform as hostPlatform } from "os";
2407
3266
  import { dirname, join } from "path";
2408
3267
  var TopoloSdkError2 = class extends Error {
2409
3268
  code;
@@ -2523,10 +3382,10 @@ var TopoloAuthError2 = class extends TopoloSdkError2 {
2523
3382
  };
2524
3383
  function extractTopoloServiceErrorCode2(body) {
2525
3384
  if (!body || typeof body !== "object" || Array.isArray(body)) return null;
2526
- const record = body;
2527
- const topLevel = canonicalTopoloErrorCode2(record.code);
3385
+ const record2 = body;
3386
+ const topLevel = canonicalTopoloErrorCode2(record2.code);
2528
3387
  if (topLevel) return topLevel;
2529
- const nested = record.error;
3388
+ const nested = record2.error;
2530
3389
  const nestedString = canonicalTopoloErrorCode2(nested);
2531
3390
  if (nestedString) return nestedString;
2532
3391
  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
@@ -2541,14 +3400,14 @@ function serviceErrorCode2(body, fallback) {
2541
3400
  }
2542
3401
  function extractTopoloServiceErrorDetails2(body) {
2543
3402
  if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
2544
- const record = body;
2545
- let candidate = record.details;
2546
- const nested = record.error;
3403
+ const record2 = body;
3404
+ let candidate = record2.details;
3405
+ const nested = record2.error;
2547
3406
  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
2548
3407
  const nestedRecord = nested;
2549
3408
  if (candidate === void 0) candidate = nestedRecord.details ?? nestedRecord;
2550
3409
  }
2551
- if (candidate === void 0) candidate = record;
3410
+ if (candidate === void 0) candidate = record2;
2552
3411
  if (Array.isArray(candidate)) {
2553
3412
  const paths = canonicalTopoloValidationPaths2(candidate);
2554
3413
  return paths.length ? { paths } : void 0;
@@ -2600,8 +3459,8 @@ function canonicalTopoloValidationPaths2(issues) {
2600
3459
  const paths = [];
2601
3460
  for (const issue of issues) {
2602
3461
  if (!issue || typeof issue !== "object" || Array.isArray(issue)) continue;
2603
- const record = issue;
2604
- const path = canonicalTopoloValidationPath2(record.field) ?? canonicalTopoloValidationPath2(record.path);
3462
+ const record2 = issue;
3463
+ const path = canonicalTopoloValidationPath2(record2.field) ?? canonicalTopoloValidationPath2(record2.path);
2605
3464
  if (path && !paths.includes(path)) paths.push(path);
2606
3465
  if (paths.length === 32) break;
2607
3466
  }
@@ -2811,18 +3670,150 @@ function topoloServiceOrigins2(serviceUrls, environment) {
2811
3670
  ])
2812
3671
  );
2813
3672
  }
3673
+ var KEYCHAIN_SERVICE = "topolo-external-agent-installation";
3674
+ function installationStorePath() {
3675
+ const override = process.env.TOPOLO_INSTALLATIONS_PATH?.trim();
3676
+ if (override) return override;
3677
+ const base = process.env.XDG_CONFIG_HOME || (hostPlatform() === "win32" ? join(homedir(), "AppData", "Roaming") : join(homedir(), ".config"));
3678
+ return join(base, "topolo", "installations");
3679
+ }
3680
+ function installationPath(environment, id) {
3681
+ if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new TypeError("Installation id contains unsupported characters.");
3682
+ return join(installationStorePath(), environment, `${id}.json`);
3683
+ }
3684
+ function accountName(environment, id) {
3685
+ return `${environment}:${id}`;
3686
+ }
3687
+ function runOrThrow(result, operation) {
3688
+ if (result.status !== 0) {
3689
+ throw new Error(`${operation} failed: ${(result.stderr || result.error?.message || "platform credential store unavailable").trim()}`);
3690
+ }
3691
+ return result.stdout.trim();
3692
+ }
3693
+ function createPlatformInstallationSecretBackend(options = {}) {
3694
+ const platform2 = options.platform ?? hostPlatform();
3695
+ const spawn = options.spawn ?? ((command, args, spawnOptions) => spawnSync(command, args, spawnOptions));
3696
+ const root = options.root ?? installationStorePath();
3697
+ if (platform2 === "darwin") {
3698
+ return {
3699
+ async set(account, value) {
3700
+ runOrThrow(spawn("security", [
3701
+ "add-generic-password",
3702
+ "-U",
3703
+ "-a",
3704
+ account,
3705
+ "-s",
3706
+ KEYCHAIN_SERVICE,
3707
+ "-w"
3708
+ ], { input: `${value}
3709
+ `, encoding: "utf8" }), "macOS Keychain write");
3710
+ },
3711
+ async get(account) {
3712
+ const result = spawn("security", [
3713
+ "find-generic-password",
3714
+ "-a",
3715
+ account,
3716
+ "-s",
3717
+ KEYCHAIN_SERVICE,
3718
+ "-w"
3719
+ ], { encoding: "utf8" });
3720
+ if (result.status === 44) return null;
3721
+ return runOrThrow(result, "macOS Keychain read");
3722
+ },
3723
+ async delete(account) {
3724
+ const result = spawn("security", [
3725
+ "delete-generic-password",
3726
+ "-a",
3727
+ account,
3728
+ "-s",
3729
+ KEYCHAIN_SERVICE
3730
+ ], { encoding: "utf8" });
3731
+ if (result.status !== 44) runOrThrow(result, "macOS Keychain delete");
3732
+ }
3733
+ };
3734
+ }
3735
+ if (platform2 === "linux") {
3736
+ return {
3737
+ async set(account, value) {
3738
+ runOrThrow(spawn("secret-tool", [
3739
+ "store",
3740
+ "--label=Topolo external agent installation",
3741
+ "service",
3742
+ KEYCHAIN_SERVICE,
3743
+ "account",
3744
+ account
3745
+ ], { input: value, encoding: "utf8" }), "Linux Secret Service write");
3746
+ },
3747
+ async get(account) {
3748
+ const result = spawn("secret-tool", [
3749
+ "lookup",
3750
+ "service",
3751
+ KEYCHAIN_SERVICE,
3752
+ "account",
3753
+ account
3754
+ ], { encoding: "utf8" });
3755
+ if (result.status === 1) return null;
3756
+ return runOrThrow(result, "Linux Secret Service read");
3757
+ },
3758
+ async delete(account) {
3759
+ const result = spawn("secret-tool", [
3760
+ "clear",
3761
+ "service",
3762
+ KEYCHAIN_SERVICE,
3763
+ "account",
3764
+ account
3765
+ ], { encoding: "utf8" });
3766
+ if (result.status !== 1) runOrThrow(result, "Linux Secret Service delete");
3767
+ }
3768
+ };
3769
+ }
3770
+ if (platform2 === "win32") {
3771
+ const secretPath = (account) => join(root, ".secrets", `${account.replace(":", "-")}.bin`);
3772
+ const powershell = (script, path, input) => spawn(
3773
+ "powershell.exe",
3774
+ ["-NoProfile", "-NonInteractive", "-Command", script],
3775
+ { input, encoding: "utf8", env: { ...process.env, TOPOLO_SECRET_PATH: path } }
3776
+ );
3777
+ return {
3778
+ async set(account, value) {
3779
+ const path = secretPath(account);
3780
+ await mkdir(dirname(path), { recursive: true });
3781
+ const script = "$p=$env:TOPOLO_SECRET_PATH;$v=[Console]::In.ReadToEnd();$b=[Text.Encoding]::UTF8.GetBytes($v);$e=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[IO.File]::WriteAllBytes($p,$e)";
3782
+ runOrThrow(powershell(script, path, value), "Windows DPAPI write");
3783
+ },
3784
+ async get(account) {
3785
+ const path = secretPath(account);
3786
+ if (!existsSync(path)) return null;
3787
+ const script = "$p=$env:TOPOLO_SECRET_PATH;$b=[IO.File]::ReadAllBytes($p);$d=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Console]::Out.Write([Text.Encoding]::UTF8.GetString($d))";
3788
+ return runOrThrow(powershell(script, path), "Windows DPAPI read");
3789
+ },
3790
+ async delete(account) {
3791
+ await rm(secretPath(account), { force: true });
3792
+ }
3793
+ };
3794
+ }
3795
+ throw new Error(`Secure external-agent installation storage is unsupported on ${platform2}.`);
3796
+ }
3797
+ async function readExternalAgentInstallation(environment, id, backend = createPlatformInstallationSecretBackend()) {
3798
+ const path = installationPath(environment, id);
3799
+ if (!existsSync(path)) return null;
3800
+ const installation = JSON.parse(await readFile(path, "utf8"));
3801
+ const encodedSecret = await backend.get(accountName(environment, id));
3802
+ if (!encodedSecret) throw new Error(`Protected secret is missing for installation ${id}.`);
3803
+ return { installation, secret: JSON.parse(encodedSecret) };
3804
+ }
2814
3805
  var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
2815
3806
  function configPath() {
2816
3807
  const override = process.env.TOPOLO_CONFIG_PATH;
2817
3808
  if (override) return override;
2818
- const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join(homedir(), "AppData", "Roaming") : join(homedir(), ".config"));
2819
- return join(base, "topolo", "config.json");
3809
+ const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join2(homedir2(), "AppData", "Roaming") : join2(homedir2(), ".config"));
3810
+ return join2(base, "topolo", "config.json");
2820
3811
  }
2821
3812
  async function readConfig() {
2822
3813
  const path = configPath();
2823
- if (!existsSync(path)) return {};
3814
+ if (!existsSync2(path)) return {};
2824
3815
  try {
2825
- const raw = await readFile(path, "utf8");
3816
+ const raw = await readFile2(path, "utf8");
2826
3817
  return JSON.parse(raw);
2827
3818
  } catch {
2828
3819
  return {};
@@ -2830,11 +3821,11 @@ async function readConfig() {
2830
3821
  }
2831
3822
  async function writeConfig(config) {
2832
3823
  const path = configPath();
2833
- await mkdir(dirname(path), { recursive: true });
2834
- await writeFile(path, JSON.stringify(config, null, 2) + "\n", "utf8");
3824
+ await mkdir2(dirname2(path), { recursive: true });
3825
+ await writeFile2(path, JSON.stringify(config, null, 2) + "\n", "utf8");
2835
3826
  if (platform() !== "win32") {
2836
3827
  try {
2837
- await chmod(path, 384);
3828
+ await chmod2(path, 384);
2838
3829
  } catch {
2839
3830
  }
2840
3831
  }
@@ -2905,18 +3896,18 @@ var CONFIG_LOCK_TIMEOUT_MS = 5e3;
2905
3896
  var CONFIG_LOCK_STALE_MS = 12e4;
2906
3897
  async function withConfigWriteLock(operation) {
2907
3898
  const lockPath = `${configPath()}.lock`;
2908
- await mkdir(dirname(lockPath), { recursive: true });
3899
+ await mkdir2(dirname2(lockPath), { recursive: true });
2909
3900
  const startedAt = Date.now();
2910
3901
  while (true) {
2911
3902
  try {
2912
- await mkdir(lockPath);
3903
+ await mkdir2(lockPath);
2913
3904
  break;
2914
3905
  } catch (error) {
2915
3906
  if (error.code !== "EEXIST") throw error;
2916
3907
  try {
2917
3908
  const lock = await stat(lockPath);
2918
3909
  if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
2919
- await rm(lockPath, { recursive: true, force: true });
3910
+ await rm2(lockPath, { recursive: true, force: true });
2920
3911
  continue;
2921
3912
  }
2922
3913
  } catch (inspectionError) {
@@ -2932,7 +3923,7 @@ async function withConfigWriteLock(operation) {
2932
3923
  try {
2933
3924
  return await operation();
2934
3925
  } finally {
2935
- await rm(lockPath, { recursive: true, force: true });
3926
+ await rm2(lockPath, { recursive: true, force: true });
2936
3927
  }
2937
3928
  }
2938
3929
  async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
@@ -3770,14 +4761,14 @@ function displayAppId(entry) {
3770
4761
 
3771
4762
  // src/version.ts
3772
4763
  import { readFileSync } from "fs";
3773
- import { dirname as dirname2, join as join2 } from "path";
4764
+ import { dirname as dirname3, join as join3 } from "path";
3774
4765
  import { fileURLToPath } from "url";
3775
4766
  function readMcpVersion() {
3776
4767
  try {
3777
- const here = dirname2(fileURLToPath(import.meta.url));
4768
+ const here = dirname3(fileURLToPath(import.meta.url));
3778
4769
  const candidates = [
3779
- join2(here, "package.json"),
3780
- join2(here, "..", "package.json")
4770
+ join3(here, "package.json"),
4771
+ join3(here, "..", "package.json")
3781
4772
  ];
3782
4773
  for (const path of candidates) {
3783
4774
  try {
@@ -3899,6 +4890,9 @@ function toolError(context, error) {
3899
4890
  };
3900
4891
  }
3901
4892
 
4893
+ // src/context-credential.ts
4894
+ import { randomUUID } from "crypto";
4895
+
3902
4896
  // src/auth.ts
3903
4897
  function resolveCredentialFromEnv(environment = "production") {
3904
4898
  const prefix = environment === "production" ? "TOPOLO_PRODUCTION" : `TOPOLO_${environment.toUpperCase()}`;
@@ -3909,12 +4903,102 @@ function resolveCredentialFromEnv(environment = "production") {
3909
4903
  return null;
3910
4904
  }
3911
4905
 
4906
+ // src/installation-credential.ts
4907
+ function audienceFor(context) {
4908
+ const actionAudience = context.actionId?.match(/^(app_[A-Za-z0-9_]+)\./)?.[1];
4909
+ if (actionAudience) return actionAudience;
4910
+ if (context.actionId) {
4911
+ throw new TopoloMcpPublicError("The requested Topolo action has an invalid audience.");
4912
+ }
4913
+ const service = context.service.trim().toLowerCase();
4914
+ if (/^app_[a-z0-9_]+$/.test(service)) return service;
4915
+ if (!/^[a-z0-9_-]+$/.test(service)) {
4916
+ throw new TopoloMcpPublicError("The requested Topolo service cannot be bound to this installation.");
4917
+ }
4918
+ return `app_topolo_${service.replace(/-/g, "_")}`;
4919
+ }
4920
+ function responseData(value) {
4921
+ if (!value || typeof value !== "object") return {};
4922
+ const record2 = value;
4923
+ return record2.data && typeof record2.data === "object" ? record2.data : record2;
4924
+ }
4925
+ async function resolveExternalAgentInstallationCredential(environment, installationId, taskId, options = {}) {
4926
+ const stored = await (options.readInstallation ?? readExternalAgentInstallation)(environment, installationId);
4927
+ if (!stored) throw new TopoloMcpPublicError(`Installation ${installationId} is not registered on this device.`);
4928
+ if (stored.installation.id !== installationId || stored.installation.environment !== environment) {
4929
+ throw new TopoloMcpPublicError(`Installation ${installationId} is not registered for ${environment}.`);
4930
+ }
4931
+ if (!stored.installation.clientId.trim() || !stored.installation.organizationId.trim() || !/^[A-Za-z0-9_-]{43}$/.test(stored.installation.dpopJkt) || !stored.secret.clientSecret.trim() || !stored.secret.privateJwk.d) {
4932
+ throw new TopoloMcpPublicError(`Installation ${installationId} is incomplete or corrupted.`);
4933
+ }
4934
+ const fetchImpl = options.fetch ?? fetch;
4935
+ const now = options.now ?? Date.now;
4936
+ const authBaseUrl = resolveServiceUrl("auth", options.serviceUrls);
4937
+ const tokenUrl = new URL("/api/developer-oauth/token", authBaseUrl).toString();
4938
+ const cache = /* @__PURE__ */ new Map();
4939
+ const credentialProvider = async (context) => {
4940
+ const audience = audienceFor(context);
4941
+ const resource = context.resource ?? {
4942
+ resourceType: "organization",
4943
+ resourceId: stored.installation.organizationId
4944
+ };
4945
+ const boundTaskId = context.taskId?.trim() || taskId;
4946
+ const key = JSON.stringify({ audience, taskId: boundTaskId, resource });
4947
+ const cached = cache.get(key);
4948
+ if (cached && cached.expiresAt - 15e3 > now()) return cached.credential;
4949
+ const body = new URLSearchParams({
4950
+ grant_type: "client_credentials",
4951
+ client_id: stored.installation.clientId,
4952
+ client_secret: stored.secret.clientSecret,
4953
+ audience,
4954
+ task_id: boundTaskId,
4955
+ resource_type: resource.resourceType,
4956
+ resource_id: resource.resourceId,
4957
+ dpop_jkt: stored.installation.dpopJkt
4958
+ });
4959
+ const response = await fetchImpl(tokenUrl, {
4960
+ method: "POST",
4961
+ headers: {
4962
+ Accept: "application/json",
4963
+ "Content-Type": "application/x-www-form-urlencoded"
4964
+ },
4965
+ body: body.toString()
4966
+ });
4967
+ const payload = await response.json().catch(() => null);
4968
+ const data = responseData(payload);
4969
+ const accessToken = typeof data.access_token === "string" ? data.access_token : "";
4970
+ const expiresIn = Number(data.expires_in);
4971
+ if (!response.ok || !accessToken || !Number.isFinite(expiresIn) || expiresIn <= 0) {
4972
+ throw new TopoloMcpPublicError("The external-agent installation credential was rejected.");
4973
+ }
4974
+ const credential2 = {
4975
+ kind: "dpop_access_token",
4976
+ accessToken,
4977
+ privateJwk: stored.secret.privateJwk
4978
+ };
4979
+ cache.set(key, { credential: credential2, expiresAt: now() + expiresIn * 1e3 });
4980
+ return credential2;
4981
+ };
4982
+ const credential = await credentialProvider({
4983
+ service: "auth",
4984
+ method: "GET",
4985
+ path: "/api/auth/me",
4986
+ taskId,
4987
+ resource: {
4988
+ resourceType: "organization",
4989
+ resourceId: stored.installation.organizationId
4990
+ }
4991
+ });
4992
+ return { credential, credentialProvider, installation: stored.installation, taskId };
4993
+ }
4994
+
3912
4995
  // src/context-credential.ts
3913
4996
  var DEFAULT_DEPENDENCIES = {
3914
4997
  readConfig,
3915
4998
  refreshContextCredentialIfNeeded,
3916
4999
  refreshCredentialIfNeeded,
3917
- resolveCredentialFromEnv
5000
+ resolveCredentialFromEnv,
5001
+ resolveInstallationCredential: resolveExternalAgentInstallationCredential
3918
5002
  };
3919
5003
  function resolvePinnedContextKey(profile, selector) {
3920
5004
  const normalized = selector.trim().toLowerCase();
@@ -3935,9 +5019,35 @@ function resolvePinnedContextKey(profile, selector) {
3935
5019
  }
3936
5020
  throw new TopoloMcpPublicError(`TOPOLO_CONTEXT "${selector}" is not signed in for this environment`);
3937
5021
  }
3938
- async function resolveMcpCredential(environment, selector = process.env.TOPOLO_CONTEXT, dependencies = DEFAULT_DEPENDENCIES) {
5022
+ async function resolveMcpCredential(environment, selector = process.env.TOPOLO_CONTEXT, dependencies = DEFAULT_DEPENDENCIES, installationSelector = process.env.TOPOLO_AGENT_INSTALLATION_ID, taskSelector = process.env.TOPOLO_AGENT_TASK_ID) {
3939
5023
  const environmentCredential = dependencies.resolveCredentialFromEnv(environment);
3940
5024
  const requestedSelector = selector?.trim();
5025
+ const installationId = installationSelector?.trim();
5026
+ if (installationId) {
5027
+ if (requestedSelector || environmentCredential) {
5028
+ throw new TopoloMcpPublicError(
5029
+ "TOPOLO_AGENT_INSTALLATION_ID cannot be combined with TOPOLO_CONTEXT or an environment credential"
5030
+ );
5031
+ }
5032
+ const taskId = taskSelector?.trim() || `task_${randomUUID()}`;
5033
+ const config2 = await dependencies.readConfig();
5034
+ const serviceUrls = resolveServiceUrls(
5035
+ profileForEnvironment(config2, environment).serviceUrls,
5036
+ environment
5037
+ );
5038
+ const resolved = await dependencies.resolveInstallationCredential(
5039
+ environment,
5040
+ installationId,
5041
+ taskId,
5042
+ { serviceUrls }
5043
+ );
5044
+ return {
5045
+ credential: resolved.credential,
5046
+ contextKey: null,
5047
+ credentialProvider: resolved.credentialProvider,
5048
+ taskId: resolved.taskId
5049
+ };
5050
+ }
3941
5051
  if (environmentCredential) {
3942
5052
  if (requestedSelector) {
3943
5053
  throw new TopoloMcpPublicError("TOPOLO_CONTEXT cannot be combined with an environment credential");
@@ -4040,6 +5150,10 @@ async function runStdioMcp(dependencies = DEFAULT_DEPENDENCIES2) {
4040
5150
  const topolo = dependencies.createTopolo({
4041
5151
  credential,
4042
5152
  environment,
5153
+ actorContext: {
5154
+ channel: "mcp",
5155
+ ...resolvedCredential.taskId ? { taskId: resolvedCredential.taskId } : {}
5156
+ },
4043
5157
  agent: {
4044
5158
  clientName: "topolo-mcp",
4045
5159
  clientVersion: MCP_VERSION,