@topolo/cli 0.10.1 → 0.12.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 (2) hide show
  1. package/dist/index.js +964 -210
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -9,13 +9,13 @@ import { platform as platform2 } from "os";
9
9
  import { createInterface } from "readline/promises";
10
10
  import { stdin as input } from "process";
11
11
 
12
- // node_modules/.pnpm/@topolo+sdk@0.14.4/node_modules/@topolo/sdk/dist/index.js
12
+ // node_modules/.pnpm/@topolo+sdk@0.21.0/node_modules/@topolo/sdk/dist/index.js
13
13
  function applyAuthHeaders(headers, credential) {
14
14
  if (credential.kind === "api_key") {
15
15
  headers.set("X-Api-Key", credential.apiKey);
16
16
  return;
17
17
  }
18
- headers.set("Authorization", `Bearer ${credential.accessToken}`);
18
+ headers.set("Authorization", `${credential.kind === "dpop_access_token" ? "DPoP" : "Bearer"} ${credential.accessToken}`);
19
19
  }
20
20
  function applyAuditHeaders(headers, agent, requestId) {
21
21
  headers.set("X-Topolo-Client", `${agent.clientName}/${agent.clientVersion}`);
@@ -28,6 +28,58 @@ function generateRequestId() {
28
28
  }
29
29
  return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
30
30
  }
31
+ var encoder = new TextEncoder();
32
+ function encodeBase64Url(value) {
33
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
34
+ let binary = "";
35
+ for (const byte of bytes) binary += String.fromCharCode(byte);
36
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
37
+ }
38
+ function encodeJson(value) {
39
+ return encodeBase64Url(encoder.encode(JSON.stringify(value)));
40
+ }
41
+ async function sha256Base64Url(value) {
42
+ return encodeBase64Url(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
43
+ }
44
+ function publicJwk(privateJwk) {
45
+ const { crv, kty, x, y } = privateJwk;
46
+ if (kty !== "EC" || crv !== "P-256" || !x || !y || !privateJwk.d) {
47
+ throw new TypeError("A private P-256 JSON Web Key is required for DPoP signing.");
48
+ }
49
+ return { crv, kty, x, y };
50
+ }
51
+ function canonicalTargetUrl(value) {
52
+ const url = new URL(value);
53
+ return `${url.origin}${url.pathname}`;
54
+ }
55
+ async function calculateDpopJkt(privateJwk) {
56
+ const jwk = publicJwk(privateJwk);
57
+ return sha256Base64Url(JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }));
58
+ }
59
+ async function createDpopProof(input2) {
60
+ const jwk = publicJwk(input2.privateJwk);
61
+ const key = await crypto.subtle.importKey(
62
+ "jwk",
63
+ input2.privateJwk,
64
+ { name: "ECDSA", namedCurve: "P-256" },
65
+ false,
66
+ ["sign"]
67
+ );
68
+ const header = encodeJson({ alg: "ES256", typ: "dpop+jwt", jwk });
69
+ const claims = encodeJson({
70
+ jti: crypto.randomUUID(),
71
+ iat: Math.floor(Date.now() / 1e3),
72
+ htm: input2.method.toUpperCase(),
73
+ htu: canonicalTargetUrl(input2.targetUrl),
74
+ ath: await sha256Base64Url(input2.accessToken)
75
+ });
76
+ const signature = await crypto.subtle.sign(
77
+ { name: "ECDSA", hash: "SHA-256" },
78
+ key,
79
+ encoder.encode(`${header}.${claims}`)
80
+ );
81
+ return `${header}.${claims}.${encodeBase64Url(signature)}`;
82
+ }
31
83
  var TopoloSdkError = class extends Error {
32
84
  code;
33
85
  constructor(code, message2) {
@@ -173,10 +225,10 @@ var TopoloPermissionError = class extends TopoloSdkError {
173
225
  };
174
226
  function extractTopoloServiceErrorCode(body) {
175
227
  if (!body || typeof body !== "object" || Array.isArray(body)) return null;
176
- const record = body;
177
- const topLevel = canonicalTopoloErrorCode(record.code);
228
+ const record2 = body;
229
+ const topLevel = canonicalTopoloErrorCode(record2.code);
178
230
  if (topLevel) return topLevel;
179
- const nested = record.error;
231
+ const nested = record2.error;
180
232
  const nestedString = canonicalTopoloErrorCode(nested);
181
233
  if (nestedString) return nestedString;
182
234
  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
@@ -191,14 +243,14 @@ function serviceErrorCode(body, fallback) {
191
243
  }
192
244
  function extractTopoloServiceErrorDetails(body) {
193
245
  if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
194
- const record = body;
195
- let candidate = record.details;
196
- const nested = record.error;
246
+ const record2 = body;
247
+ let candidate = record2.details;
248
+ const nested = record2.error;
197
249
  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
198
250
  const nestedRecord = nested;
199
251
  if (candidate === void 0) candidate = nestedRecord.details ?? nestedRecord;
200
252
  }
201
- if (candidate === void 0) candidate = record;
253
+ if (candidate === void 0) candidate = record2;
202
254
  if (Array.isArray(candidate)) {
203
255
  const paths = canonicalTopoloValidationPaths(candidate);
204
256
  return paths.length ? { paths } : void 0;
@@ -250,8 +302,8 @@ function canonicalTopoloValidationPaths(issues) {
250
302
  const paths = [];
251
303
  for (const issue of issues) {
252
304
  if (!issue || typeof issue !== "object" || Array.isArray(issue)) continue;
253
- const record = issue;
254
- const path = canonicalTopoloValidationPath(record.field) ?? canonicalTopoloValidationPath(record.path);
305
+ const record2 = issue;
306
+ const path = canonicalTopoloValidationPath(record2.field) ?? canonicalTopoloValidationPath(record2.path);
255
307
  if (path && !paths.includes(path)) paths.push(path);
256
308
  if (paths.length === 32) break;
257
309
  }
@@ -718,6 +770,222 @@ function isMultipleOf(value, divisor) {
718
770
  const quotient = value / divisor;
719
771
  return Math.abs(quotient - Math.round(quotient)) < Number.EPSILON * Math.max(1, Math.abs(quotient)) * 4;
720
772
  }
773
+ var ACTOR_ENVELOPE_VERSION = 1;
774
+ var TOPOLO_ACTOR_ENVELOPE_HEADER = "X-Topolo-Actor-Envelope";
775
+ var PRINCIPAL_TYPES = /* @__PURE__ */ new Set([
776
+ "human",
777
+ "managed_agent",
778
+ "external_agent",
779
+ "service",
780
+ "browser_automation"
781
+ ]);
782
+ var ACCOUNTABLE_SUBJECT_TYPES = /* @__PURE__ */ new Set(["human", "organization"]);
783
+ var CHANNELS = /* @__PURE__ */ new Set(["web", "mobile", "cli", "mcp", "api", "workflow", "browser_automation"]);
784
+ var ASSURANCE_LEVELS = /* @__PURE__ */ new Set(["authenticated", "workload_identity", "user_verified"]);
785
+ var HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
786
+ var ActorEnvelopeValidationError = class extends Error {
787
+ path;
788
+ constructor(path, message2) {
789
+ super(`${path}: ${message2}`);
790
+ this.name = "ActorEnvelopeValidationError";
791
+ this.path = path;
792
+ }
793
+ };
794
+ function parseActorEnvelope(value) {
795
+ const envelope = record(value, "actorEnvelope");
796
+ exactKeys(envelope, [
797
+ "schemaVersion",
798
+ "organizationId",
799
+ "accountableSubject",
800
+ "performer",
801
+ "delegation",
802
+ "channel",
803
+ "assurance",
804
+ "action",
805
+ "issuedAt",
806
+ "expiresAt"
807
+ ], "actorEnvelope");
808
+ if (envelope.schemaVersion !== ACTOR_ENVELOPE_VERSION) {
809
+ fail("actorEnvelope.schemaVersion", `must equal ${ACTOR_ENVELOPE_VERSION}`);
810
+ }
811
+ const accountableSubject = parseAccountableSubject(envelope.accountableSubject);
812
+ const performer = parseIdentity(envelope.performer, "actorEnvelope.performer");
813
+ const organizationId = text(envelope.organizationId, "actorEnvelope.organizationId");
814
+ const channel = member(envelope.channel, CHANNELS, "actorEnvelope.channel");
815
+ const issuedAt = timestamp(envelope.issuedAt, "actorEnvelope.issuedAt");
816
+ const expiresAt = envelope.expiresAt === void 0 ? void 0 : timestamp(envelope.expiresAt, "actorEnvelope.expiresAt");
817
+ if (expiresAt && Date.parse(expiresAt) <= Date.parse(issuedAt)) {
818
+ fail("actorEnvelope.expiresAt", "must be later than issuedAt");
819
+ }
820
+ const assuranceValue = record(envelope.assurance, "actorEnvelope.assurance");
821
+ exactKeys(assuranceValue, ["level", "methods"], "actorEnvelope.assurance");
822
+ const assurance = {
823
+ level: member(assuranceValue.level, ASSURANCE_LEVELS, "actorEnvelope.assurance.level"),
824
+ methods: textArray(assuranceValue.methods, "actorEnvelope.assurance.methods")
825
+ };
826
+ const actionValue = record(envelope.action, "actorEnvelope.action");
827
+ exactKeys(actionValue, ["requestId", "actionId", "resourceType", "resourceId", "inputHash"], "actorEnvelope.action");
828
+ const inputHash = text(actionValue.inputHash, "actorEnvelope.action.inputHash");
829
+ if (!HASH_PATTERN.test(inputHash)) fail("actorEnvelope.action.inputHash", "must be a lowercase sha256 digest");
830
+ const action = {
831
+ requestId: text(actionValue.requestId, "actorEnvelope.action.requestId"),
832
+ actionId: text(actionValue.actionId, "actorEnvelope.action.actionId"),
833
+ ...actionValue.resourceType === void 0 ? {} : { resourceType: text(actionValue.resourceType, "actorEnvelope.action.resourceType") },
834
+ ...actionValue.resourceId === void 0 ? {} : { resourceId: text(actionValue.resourceId, "actorEnvelope.action.resourceId") },
835
+ inputHash
836
+ };
837
+ if (action.resourceType === void 0 !== (action.resourceId === void 0)) {
838
+ fail("actorEnvelope.action", "resourceType and resourceId must be provided together");
839
+ }
840
+ const delegation = parseDelegation(envelope.delegation);
841
+ const directHuman = performer.principalType === "human" && performer.principalId === accountableSubject.principalId;
842
+ if (directHuman && delegation !== null) fail("actorEnvelope.delegation", "must be null for direct human action");
843
+ if (!directHuman && delegation === null) fail("actorEnvelope.delegation", "is required when performer differs from accountable subject");
844
+ if (delegation) {
845
+ const finalHop = delegation.lineage.at(-1);
846
+ if (!finalHop || finalHop.principalId !== performer.principalId || finalHop.principalType !== performer.principalType) {
847
+ fail("actorEnvelope.delegation.lineage", "must end with the performer");
848
+ }
849
+ }
850
+ if (performer.principalType === "external_agent" && (!performer.installationId || !performer.provider)) {
851
+ fail("actorEnvelope.performer", "external_agent requires installationId and provider");
852
+ }
853
+ if (performer.principalType === "managed_agent" && !performer.agentInstanceId) {
854
+ fail("actorEnvelope.performer.agentInstanceId", "is required for managed_agent");
855
+ }
856
+ if (performer.principalType === "browser_automation" && (!performer.installationId || !performer.browserSessionId)) {
857
+ fail("actorEnvelope.performer", "browser_automation requires installationId and browserSessionId");
858
+ }
859
+ return {
860
+ schemaVersion: ACTOR_ENVELOPE_VERSION,
861
+ organizationId,
862
+ accountableSubject,
863
+ performer,
864
+ delegation,
865
+ channel,
866
+ assurance,
867
+ action,
868
+ issuedAt,
869
+ ...expiresAt ? { expiresAt } : {}
870
+ };
871
+ }
872
+ function canonicalizeActorEnvelope(value) {
873
+ return canonicalJson(parseActorEnvelope(value));
874
+ }
875
+ function encodeActorEnvelopeHeader(value) {
876
+ const bytes = new TextEncoder().encode(canonicalizeActorEnvelope(value));
877
+ const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join("");
878
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
879
+ }
880
+ function canonicalJson(value) {
881
+ if (value === null) return "null";
882
+ if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
883
+ if (typeof value === "number") {
884
+ if (!Number.isFinite(value)) throw new TypeError("Canonical JSON accepts finite numbers only.");
885
+ return JSON.stringify(value);
886
+ }
887
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
888
+ if (typeof value !== "object" || !value || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) {
889
+ throw new TypeError("Canonical JSON accepts JSON objects, arrays, and primitives only.");
890
+ }
891
+ const entries = Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right));
892
+ return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
893
+ }
894
+ function parseAccountableSubject(value) {
895
+ const path = "actorEnvelope.accountableSubject";
896
+ const subject = record(value, path);
897
+ exactKeys(subject, ["principalType", "principalId", "displayName"], path);
898
+ return {
899
+ principalType: member(subject.principalType, ACCOUNTABLE_SUBJECT_TYPES, `${path}.principalType`),
900
+ principalId: text(subject.principalId, `${path}.principalId`),
901
+ ...subject.displayName === void 0 ? {} : { displayName: text(subject.displayName, `${path}.displayName`) }
902
+ };
903
+ }
904
+ async function sha256CanonicalValue(value) {
905
+ const bytes = new TextEncoder().encode(canonicalJson(value));
906
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
907
+ return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
908
+ }
909
+ function parseIdentity(value, path) {
910
+ const identity = record(value, path);
911
+ exactKeys(identity, [
912
+ "principalType",
913
+ "principalId",
914
+ "displayName",
915
+ "installationId",
916
+ "agentInstanceId",
917
+ "browserSessionId",
918
+ "provider",
919
+ "model"
920
+ ], path);
921
+ return {
922
+ principalType: member(identity.principalType, PRINCIPAL_TYPES, `${path}.principalType`),
923
+ principalId: text(identity.principalId, `${path}.principalId`),
924
+ ...optionalTextFields(identity, path, [
925
+ "displayName",
926
+ "installationId",
927
+ "agentInstanceId",
928
+ "browserSessionId",
929
+ "provider",
930
+ "model"
931
+ ])
932
+ };
933
+ }
934
+ function parseDelegation(value) {
935
+ if (value === null) return null;
936
+ const delegation = record(value, "actorEnvelope.delegation");
937
+ exactKeys(delegation, ["taskId", "runId", "lineage"], "actorEnvelope.delegation");
938
+ if (!Array.isArray(delegation.lineage) || delegation.lineage.length === 0 || delegation.lineage.length > 16) {
939
+ fail("actorEnvelope.delegation.lineage", "must contain between 1 and 16 hops");
940
+ }
941
+ return {
942
+ taskId: text(delegation.taskId, "actorEnvelope.delegation.taskId"),
943
+ runId: text(delegation.runId, "actorEnvelope.delegation.runId"),
944
+ lineage: delegation.lineage.map((value2, index) => {
945
+ const hop = record(value2, `actorEnvelope.delegation.lineage[${index}]`);
946
+ exactKeys(hop, ["principalType", "principalId"], `actorEnvelope.delegation.lineage[${index}]`);
947
+ return {
948
+ principalType: member(hop.principalType, PRINCIPAL_TYPES, `actorEnvelope.delegation.lineage[${index}].principalType`),
949
+ principalId: text(hop.principalId, `actorEnvelope.delegation.lineage[${index}].principalId`)
950
+ };
951
+ })
952
+ };
953
+ }
954
+ function record(value, path) {
955
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail(path, "must be an object");
956
+ return value;
957
+ }
958
+ function exactKeys(value, allowed, path) {
959
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
960
+ if (unknown.length) fail(path, `contains unknown field ${unknown[0]}`);
961
+ }
962
+ function text(value, path) {
963
+ if (typeof value !== "string" || value.trim() !== value || value.length === 0 || value.length > 256) {
964
+ fail(path, "must be a non-empty trimmed string of at most 256 characters");
965
+ }
966
+ return value;
967
+ }
968
+ function timestamp(value, path) {
969
+ const result = text(value, path);
970
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(result) || Number.isNaN(Date.parse(result))) {
971
+ fail(path, "must be an RFC 3339 UTC timestamp");
972
+ }
973
+ return result;
974
+ }
975
+ function member(value, values, path) {
976
+ if (typeof value !== "string" || !values.has(value)) fail(path, "contains an unsupported value");
977
+ return value;
978
+ }
979
+ function textArray(value, path) {
980
+ if (!Array.isArray(value) || value.length === 0 || value.length > 16) fail(path, "must contain between 1 and 16 values");
981
+ return value.map((item, index) => text(item, `${path}[${index}]`));
982
+ }
983
+ function optionalTextFields(value, path, keys) {
984
+ return Object.fromEntries(keys.flatMap((key) => value[key] === void 0 ? [] : [[key, text(value[key], `${path}.${key}`)]]));
985
+ }
986
+ function fail(path, message2) {
987
+ throw new ActorEnvelopeValidationError(path, message2);
988
+ }
721
989
  function parseTopoloActionUploadContract(value) {
722
990
  if (!isRecord2(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
723
991
  throw new TopoloSdkError("upload_contract_unavailable", "This action does not publish a supported upload contract.");
@@ -922,6 +1190,7 @@ var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
922
1190
  var TopoloClient = class _TopoloClient {
923
1191
  credential;
924
1192
  agent;
1193
+ actorContext;
925
1194
  serviceUrls;
926
1195
  environment;
927
1196
  appIds;
@@ -929,7 +1198,7 @@ var TopoloClient = class _TopoloClient {
929
1198
  timeoutMs;
930
1199
  fetchImpl;
931
1200
  credentialProvider;
932
- credentialLoadInFlight = null;
1201
+ credentialLoadsInFlight = /* @__PURE__ */ new Map();
933
1202
  refreshCredential;
934
1203
  refreshInFlight = null;
935
1204
  debug;
@@ -952,6 +1221,11 @@ var TopoloClient = class _TopoloClient {
952
1221
  if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
953
1222
  this.credential = options.credential;
954
1223
  this.agent = options.agent;
1224
+ this.actorContext = options.actorContext ? {
1225
+ channel: options.actorContext.channel,
1226
+ taskId: options.actorContext.taskId ?? `task_${generateRequestId()}`,
1227
+ runId: options.actorContext.runId ?? `run_${generateRequestId()}`
1228
+ } : void 0;
955
1229
  this.environment = options.environment;
956
1230
  this.serviceUrls = options.environment ? topoloServiceOrigins(options.serviceUrls, options.environment) : options.serviceUrls;
957
1231
  this.appIds = options.appIds;
@@ -981,7 +1255,16 @@ var TopoloClient = class _TopoloClient {
981
1255
  `Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
982
1256
  );
983
1257
  }
984
- await this.loadCurrentCredential();
1258
+ const actionId = requestActionId(opts.headers);
1259
+ const taskId = opts.actorEnvelope?.delegation?.taskId ?? this.actorContext?.taskId;
1260
+ let requestCredential = await this.loadCurrentCredential({
1261
+ service: normalizeLookupKey(opts.service),
1262
+ method,
1263
+ path: opts.path,
1264
+ ...actionId ? { actionId } : {},
1265
+ ...taskId ? { taskId } : {},
1266
+ ...opts.resource ? { resource: opts.resource } : {}
1267
+ });
985
1268
  const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: this.validatedServiceOrigin(opts.service, opts.baseUrl) } : await this.resolveRequestService(opts.service);
986
1269
  const baseUrl = resolvedService.baseUrl;
987
1270
  const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
@@ -1004,12 +1287,15 @@ var TopoloClient = class _TopoloClient {
1004
1287
  if (opts.body !== void 0) headers.set("Content-Type", "application/json");
1005
1288
  const platformAppId = resolveConfiguredAppId(resolvedService.serviceKey, this.appIds);
1006
1289
  if (platformAppId) headers.set("X-App-ID", platformAppId);
1007
- applyAuthHeaders(headers, this.credential);
1008
- const requestId = generateRequestId();
1290
+ const actorEnvelope = opts.actorEnvelope === void 0 ? void 0 : parseActorEnvelope(opts.actorEnvelope);
1291
+ const requestId = actorEnvelope?.action.requestId ?? generateRequestId();
1009
1292
  applyAuditHeaders(headers, this.agent, requestId);
1010
1293
  if (opts.headers) {
1011
1294
  for (const [k, v] of Object.entries(opts.headers)) headers.set(k, v);
1012
1295
  }
1296
+ if (actorEnvelope) {
1297
+ headers.set(TOPOLO_ACTOR_ENVELOPE_HEADER, encodeActorEnvelopeHeader(actorEnvelope));
1298
+ }
1013
1299
  if (opts.resource) {
1014
1300
  const resourceType = opts.resource.resourceType.trim();
1015
1301
  const resourceId = opts.resource.resourceId.trim();
@@ -1034,9 +1320,19 @@ var TopoloClient = class _TopoloClient {
1034
1320
  }, this.timeoutMs);
1035
1321
  const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
1036
1322
  try {
1323
+ const requestHeaders = new Headers(headers);
1324
+ applyAuthHeaders(requestHeaders, requestCredential);
1325
+ if (requestCredential.kind === "dpop_access_token") {
1326
+ requestHeaders.set("DPoP", await createDpopProof({
1327
+ accessToken: requestCredential.accessToken,
1328
+ method,
1329
+ targetUrl: urlStr,
1330
+ privateJwk: requestCredential.privateJwk
1331
+ }));
1332
+ }
1037
1333
  return await this.fetchImpl(urlStr, {
1038
1334
  method,
1039
- headers: new Headers(headers),
1335
+ headers: requestHeaders,
1040
1336
  body: opts.rawBody ?? (opts.body !== void 0 ? JSON.stringify(opts.body) : null),
1041
1337
  signal
1042
1338
  });
@@ -1067,13 +1363,13 @@ var TopoloClient = class _TopoloClient {
1067
1363
  };
1068
1364
  let res;
1069
1365
  try {
1070
- const attemptedCredential = this.credential;
1366
+ const attemptedCredential = requestCredential;
1071
1367
  res = await fetchWithReadRetry();
1072
1368
  if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
1073
1369
  await res.body?.cancel().catch(() => void 0);
1074
1370
  const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
1075
1371
  if (refreshed) {
1076
- applyAuthHeaders(headers, refreshed);
1372
+ requestCredential = refreshed;
1077
1373
  res = await fetchWithReadRetry();
1078
1374
  }
1079
1375
  }
@@ -1163,18 +1459,23 @@ var TopoloClient = class _TopoloClient {
1163
1459
  });
1164
1460
  return parsed;
1165
1461
  }
1166
- async loadCurrentCredential() {
1167
- if (!this.credentialProvider) return;
1168
- this.credentialLoadInFlight ??= this.credentialProvider().then((credential) => {
1169
- if (!credential) {
1170
- throw new TopoloAuthError("The configured Topolo credential is no longer available.");
1171
- }
1172
- this.credential = credential;
1173
- return credential;
1174
- }).finally(() => {
1175
- this.credentialLoadInFlight = null;
1176
- });
1177
- await this.credentialLoadInFlight;
1462
+ async loadCurrentCredential(context) {
1463
+ if (!this.credentialProvider) return this.credential;
1464
+ const key = JSON.stringify(context);
1465
+ let load = this.credentialLoadsInFlight.get(key);
1466
+ if (!load) {
1467
+ load = this.credentialProvider(context).then((credential) => {
1468
+ if (!credential) {
1469
+ throw new TopoloAuthError("The configured Topolo credential is no longer available.");
1470
+ }
1471
+ this.credential = credential;
1472
+ return credential;
1473
+ }).finally(() => {
1474
+ this.credentialLoadsInFlight.delete(key);
1475
+ });
1476
+ this.credentialLoadsInFlight.set(key, load);
1477
+ }
1478
+ return load;
1178
1479
  }
1179
1480
  async refreshRejectedAccessToken(attemptedAccessToken) {
1180
1481
  if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
@@ -1456,14 +1757,14 @@ var TopoloClient = class _TopoloClient {
1456
1757
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1457
1758
  throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is unavailable.");
1458
1759
  }
1459
- const record = raw;
1460
- if (record.algorithm !== "sha256-v1" || typeof record.digest !== "string" || !/^[a-f0-9]{64}$/.test(record.digest) || !Number.isSafeInteger(record.actionCount) || Number(record.actionCount) < 0) {
1760
+ const record2 = raw;
1761
+ if (record2.algorithm !== "sha256-v1" || typeof record2.digest !== "string" || !/^[a-f0-9]{64}$/.test(record2.digest) || !Number.isSafeInteger(record2.actionCount) || Number(record2.actionCount) < 0) {
1461
1762
  throw new TopoloSdkError("action_catalog_summary_unavailable", "Action catalog summary is invalid.");
1462
1763
  }
1463
1764
  return {
1464
1765
  algorithm: "sha256-v1",
1465
- digest: record.digest,
1466
- actionCount: Number(record.actionCount)
1766
+ digest: record2.digest,
1767
+ actionCount: Number(record2.actionCount)
1467
1768
  };
1468
1769
  }
1469
1770
  /** @deprecated Prefer searchActions() so callers retain the continuation cursor. */
@@ -1504,6 +1805,14 @@ var TopoloClient = class _TopoloClient {
1504
1805
  const entry = await this.getAction(action);
1505
1806
  return this.executeResolvedAction(entry, input2, options);
1506
1807
  }
1808
+ async issueExternalAgentCredential(input2) {
1809
+ const actionId = "app_topolo_auth.principal_credentials.issue";
1810
+ const entry = await this.getAction(actionId);
1811
+ if (entry.actionId !== actionId) {
1812
+ throw new TopoloSdkError("unexpected_action", "External agent credential issuance resolved an unexpected action.");
1813
+ }
1814
+ return this.executeResolvedAction(entry, input2, { confirm: true }, true);
1815
+ }
1507
1816
  async uploadAction(action, content, input2, options) {
1508
1817
  const entry = await this.getAction(action);
1509
1818
  const contract = parseTopoloActionUploadContract(entry.agentContract?.upload);
@@ -1635,7 +1944,7 @@ var TopoloClient = class _TopoloClient {
1635
1944
  }
1636
1945
  return this.executeResolvedAction(entry, input2, options);
1637
1946
  }
1638
- async executeResolvedAction(entry, input2, options) {
1947
+ async executeResolvedAction(entry, input2, options, preserveSensitiveResult = false) {
1639
1948
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
1640
1949
  if (agentAccess === "off") {
1641
1950
  throw new TopoloPermissionError(
@@ -1663,6 +1972,20 @@ var TopoloClient = class _TopoloClient {
1663
1972
  );
1664
1973
  }
1665
1974
  const mapped = mapActionInput(entry, input2);
1975
+ const resolvedActorEnvelope = options.actorEnvelope ?? await this.buildVerifiedAgentActorEnvelope(entry.actionId, input2, options.resource);
1976
+ if (resolvedActorEnvelope) {
1977
+ const actorEnvelope = parseActorEnvelope(resolvedActorEnvelope);
1978
+ if (actorEnvelope.action.actionId !== entry.actionId) {
1979
+ throw new TopoloSdkError("actor_envelope_action_mismatch", "Actor envelope actionId does not match the executed action.");
1980
+ }
1981
+ if (actorEnvelope.action.inputHash !== await sha256CanonicalValue(input2)) {
1982
+ throw new TopoloSdkError("actor_envelope_input_mismatch", "Actor envelope inputHash does not match the executed action input.");
1983
+ }
1984
+ const envelopeResource = actorEnvelope.action.resourceType && actorEnvelope.action.resourceId ? { resourceType: actorEnvelope.action.resourceType, resourceId: actorEnvelope.action.resourceId } : void 0;
1985
+ if (envelopeResource?.resourceType !== options.resource?.resourceType || envelopeResource?.resourceId !== options.resource?.resourceId) {
1986
+ throw new TopoloSdkError("actor_envelope_resource_mismatch", "Actor envelope resource does not match the executed action resource.");
1987
+ }
1988
+ }
1666
1989
  const service = entry.appSlug || entry.appId;
1667
1990
  const result = await this.request({
1668
1991
  service,
@@ -1672,13 +1995,64 @@ var TopoloClient = class _TopoloClient {
1672
1995
  ...mapped.query ? { query: mapped.query } : {},
1673
1996
  ...mapped.body !== void 0 ? { body: mapped.body } : {},
1674
1997
  ...options.resource ? { resource: options.resource } : {},
1998
+ ...resolvedActorEnvelope ? { actorEnvelope: resolvedActorEnvelope } : {},
1675
1999
  headers: {
1676
2000
  [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
1677
2001
  [TOPOLO_ACTION_NAME_HEADER]: entry.name
1678
2002
  },
1679
2003
  confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
1680
2004
  });
1681
- return redactActionResult(result);
2005
+ return preserveSensitiveResult ? result : redactActionResult(result);
2006
+ }
2007
+ async buildVerifiedAgentActorEnvelope(actionId, input2, resource) {
2008
+ if (!this.actorContext) return void 0;
2009
+ const identity = await this.introspect();
2010
+ const principalType = identity.user.principalType;
2011
+ if (principalType !== "agent_employee" && principalType !== "external_agent") return void 0;
2012
+ if (!identity.organization || !identity.user.accountableUserId || !identity.user.principalId) {
2013
+ throw new TopoloSdkError(
2014
+ "incomplete_agent_identity",
2015
+ "Verified agent credentials must resolve an organization, accountable user, and principal id."
2016
+ );
2017
+ }
2018
+ const performer = principalType === "external_agent" ? {
2019
+ principalType: "external_agent",
2020
+ principalId: identity.user.principalId,
2021
+ installationId: identity.user.installationId ?? void 0,
2022
+ provider: identity.user.provider ?? void 0,
2023
+ model: identity.user.model ?? void 0
2024
+ } : {
2025
+ principalType: "managed_agent",
2026
+ principalId: identity.user.principalId,
2027
+ agentInstanceId: identity.user.agentInstanceId ?? void 0,
2028
+ model: identity.user.model ?? void 0
2029
+ };
2030
+ if (performer.principalType === "external_agent" && (!performer.installationId || !performer.provider) || performer.principalType === "managed_agent" && !performer.agentInstanceId) {
2031
+ throw new TopoloSdkError(
2032
+ "incomplete_agent_identity",
2033
+ "Verified agent credentials are missing the performer identity required for provenance."
2034
+ );
2035
+ }
2036
+ return parseActorEnvelope({
2037
+ schemaVersion: 1,
2038
+ organizationId: identity.organization.id,
2039
+ accountableSubject: { principalType: "human", principalId: identity.user.accountableUserId },
2040
+ performer,
2041
+ delegation: {
2042
+ taskId: this.actorContext.taskId,
2043
+ runId: this.actorContext.runId,
2044
+ lineage: [{ principalType: performer.principalType, principalId: performer.principalId }]
2045
+ },
2046
+ channel: this.actorContext.channel,
2047
+ assurance: { level: "workload_identity", methods: ["client_credentials"] },
2048
+ action: {
2049
+ requestId: generateRequestId(),
2050
+ actionId,
2051
+ ...resource ? resource : {},
2052
+ inputHash: await sha256CanonicalValue(input2)
2053
+ },
2054
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString()
2055
+ });
1682
2056
  }
1683
2057
  /**
1684
2058
  * Introspect the current credential. Returns the caller's resolved identity,
@@ -1707,7 +2081,18 @@ var TopoloClient = class _TopoloClient {
1707
2081
  email: user.email,
1708
2082
  name: user.name ?? null,
1709
2083
  role: user.role ?? (kind === "api_key" ? "service" : "member"),
1710
- permissions: payload.permissions ?? user.permissions ?? []
2084
+ permissions: payload.permissions ?? user.permissions ?? [],
2085
+ ...normalizeAgentPrincipalType(user.principalType) ? {
2086
+ principalId: readOptionalString(user.principalId),
2087
+ principalType: normalizeAgentPrincipalType(user.principalType),
2088
+ accountableUserId: readOptionalString(user.accountableUserId),
2089
+ agentInstanceId: readOptionalString(user.agentInstanceId),
2090
+ installationId: readOptionalString(user.installationId),
2091
+ provider: readOptionalString(user.provider),
2092
+ model: readOptionalString(user.model),
2093
+ serviceScopes: stringArray(user.serviceScopes),
2094
+ policyRef: readOptionalString(user.policyRef)
2095
+ } : {}
1711
2096
  },
1712
2097
  organization: organization ? {
1713
2098
  id: organization.id,
@@ -2072,6 +2457,9 @@ function normalizeCredentialAuthorizationMode(value, kind) {
2072
2457
  if (value === "context_rbac" || value === "oauth_scopes" || value === "session_rbac") return value;
2073
2458
  return "unknown";
2074
2459
  }
2460
+ function normalizeAgentPrincipalType(value) {
2461
+ return value === "agent_employee" || value === "external_agent" ? value : null;
2462
+ }
2075
2463
  function describeError(body, fallback) {
2076
2464
  if (body && typeof body === "object") {
2077
2465
  const maybe = body;
@@ -2090,16 +2478,22 @@ function pickString(value) {
2090
2478
  }
2091
2479
  return void 0;
2092
2480
  }
2481
+ function requestActionId(headers) {
2482
+ const value = Object.entries(headers ?? {}).find(
2483
+ ([name]) => name.toLowerCase() === TOPOLO_ACTION_ID_HEADER.toLowerCase()
2484
+ )?.[1];
2485
+ return value?.trim() || void 0;
2486
+ }
2093
2487
  function extractRequired(body) {
2094
2488
  if (body && typeof body === "object") {
2095
2489
  const maybe = body;
2096
2490
  if (Array.isArray(maybe.required)) return canonicalTopoloRequired(maybe.required);
2097
2491
  for (const nested of [maybe.details, maybe.error]) {
2098
2492
  if (!nested || typeof nested !== "object" || Array.isArray(nested)) continue;
2099
- const record = nested;
2100
- if (Array.isArray(record.required)) return canonicalTopoloRequired(record.required);
2101
- if (record.details && typeof record.details === "object" && !Array.isArray(record.details)) {
2102
- const required = record.details.required;
2493
+ const record2 = nested;
2494
+ if (Array.isArray(record2.required)) return canonicalTopoloRequired(record2.required);
2495
+ if (record2.details && typeof record2.details === "object" && !Array.isArray(record2.details)) {
2496
+ const required = record2.details.required;
2103
2497
  if (Array.isArray(required)) return canonicalTopoloRequired(required);
2104
2498
  }
2105
2499
  }
@@ -2174,8 +2568,8 @@ function compactResultValue(value, depth, budget) {
2174
2568
  return value.length > ACTION_RESULT_MAX_ITEMS ? { count: value.length, items, omittedItems: value.length - ACTION_RESULT_MAX_ITEMS } : items;
2175
2569
  }
2176
2570
  if (typeof value === "object") {
2177
- const record = value;
2178
- const allKeys = Object.keys(record);
2571
+ const record2 = value;
2572
+ const allKeys = Object.keys(record2);
2179
2573
  const ranked = allKeys.sort((left, right) => resultKeyPriority(left) - resultKeyPriority(right));
2180
2574
  const prioritized = ranked.filter((key) => resultKeyPriority(key) < 2);
2181
2575
  const ordinary = ranked.filter((key) => resultKeyPriority(key) === 2);
@@ -2184,7 +2578,7 @@ function compactResultValue(value, depth, budget) {
2184
2578
  ...prioritized,
2185
2579
  ...ordinary.slice(0, ordinaryLimit)
2186
2580
  ].slice(0, ACTION_RESULT_MAX_KEYS);
2187
- const compact = Object.fromEntries(keys.map((key) => [key, compactResultValue(record[key], depth + 1, budget)]));
2581
+ const compact = Object.fromEntries(keys.map((key) => [key, compactResultValue(record2[key], depth + 1, budget)]));
2188
2582
  if (allKeys.length > keys.length) compact["_omittedKeys"] = allKeys.length - keys.length;
2189
2583
  return compact;
2190
2584
  }
@@ -2948,10 +3342,15 @@ function createTopolo(options) {
2948
3342
  };
2949
3343
  }
2950
3344
 
2951
- // node_modules/.pnpm/@topolo+sdk@0.14.4/node_modules/@topolo/sdk/dist/node/credential-store.js
2952
- import { mkdir, readFile, writeFile, chmod, rm, stat } from "fs/promises";
3345
+ // node_modules/.pnpm/@topolo+sdk@0.21.0/node_modules/@topolo/sdk/dist/node/credential-store.js
3346
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, rm as rm2, stat } from "fs/promises";
3347
+ import { existsSync as existsSync2 } from "fs";
3348
+ import { homedir as homedir2, platform } from "os";
3349
+ import { dirname as dirname2, join as join2 } from "path";
3350
+ import { spawnSync } from "child_process";
3351
+ import { mkdir, readFile, readdir, rename, rm, writeFile, chmod } from "fs/promises";
2953
3352
  import { existsSync } from "fs";
2954
- import { homedir, platform } from "os";
3353
+ import { homedir, platform as hostPlatform } from "os";
2955
3354
  import { dirname, join } from "path";
2956
3355
  var TopoloSdkError2 = class extends Error {
2957
3356
  code;
@@ -3071,10 +3470,10 @@ var TopoloAuthError2 = class extends TopoloSdkError2 {
3071
3470
  };
3072
3471
  function extractTopoloServiceErrorCode2(body) {
3073
3472
  if (!body || typeof body !== "object" || Array.isArray(body)) return null;
3074
- const record = body;
3075
- const topLevel = canonicalTopoloErrorCode2(record.code);
3473
+ const record2 = body;
3474
+ const topLevel = canonicalTopoloErrorCode2(record2.code);
3076
3475
  if (topLevel) return topLevel;
3077
- const nested = record.error;
3476
+ const nested = record2.error;
3078
3477
  const nestedString = canonicalTopoloErrorCode2(nested);
3079
3478
  if (nestedString) return nestedString;
3080
3479
  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
@@ -3089,14 +3488,14 @@ function serviceErrorCode2(body, fallback) {
3089
3488
  }
3090
3489
  function extractTopoloServiceErrorDetails2(body) {
3091
3490
  if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
3092
- const record = body;
3093
- let candidate = record.details;
3094
- const nested = record.error;
3491
+ const record2 = body;
3492
+ let candidate = record2.details;
3493
+ const nested = record2.error;
3095
3494
  if (nested && typeof nested === "object" && !Array.isArray(nested)) {
3096
3495
  const nestedRecord = nested;
3097
3496
  if (candidate === void 0) candidate = nestedRecord.details ?? nestedRecord;
3098
3497
  }
3099
- if (candidate === void 0) candidate = record;
3498
+ if (candidate === void 0) candidate = record2;
3100
3499
  if (Array.isArray(candidate)) {
3101
3500
  const paths = canonicalTopoloValidationPaths2(candidate);
3102
3501
  return paths.length ? { paths } : void 0;
@@ -3148,8 +3547,8 @@ function canonicalTopoloValidationPaths2(issues) {
3148
3547
  const paths = [];
3149
3548
  for (const issue of issues) {
3150
3549
  if (!issue || typeof issue !== "object" || Array.isArray(issue)) continue;
3151
- const record = issue;
3152
- const path = canonicalTopoloValidationPath2(record.field) ?? canonicalTopoloValidationPath2(record.path);
3550
+ const record2 = issue;
3551
+ const path = canonicalTopoloValidationPath2(record2.field) ?? canonicalTopoloValidationPath2(record2.path);
3153
3552
  if (path && !paths.includes(path)) paths.push(path);
3154
3553
  if (paths.length === 32) break;
3155
3554
  }
@@ -3360,6 +3759,157 @@ function topoloServiceOrigins2(serviceUrls, environment) {
3360
3759
  ])
3361
3760
  );
3362
3761
  }
3762
+ var KEYCHAIN_SERVICE = "topolo-external-agent-installation";
3763
+ function installationStorePath() {
3764
+ const override = process.env.TOPOLO_INSTALLATIONS_PATH?.trim();
3765
+ if (override) return override;
3766
+ const base = process.env.XDG_CONFIG_HOME || (hostPlatform() === "win32" ? join(homedir(), "AppData", "Roaming") : join(homedir(), ".config"));
3767
+ return join(base, "topolo", "installations");
3768
+ }
3769
+ function installationPath(environment, id) {
3770
+ if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new TypeError("Installation id contains unsupported characters.");
3771
+ return join(installationStorePath(), environment, `${id}.json`);
3772
+ }
3773
+ function accountName(environment, id) {
3774
+ return `${environment}:${id}`;
3775
+ }
3776
+ function runOrThrow(result, operation) {
3777
+ if (result.status !== 0) {
3778
+ throw new Error(`${operation} failed: ${(result.stderr || result.error?.message || "platform credential store unavailable").trim()}`);
3779
+ }
3780
+ return result.stdout.trim();
3781
+ }
3782
+ function createPlatformInstallationSecretBackend(options = {}) {
3783
+ const platform22 = options.platform ?? hostPlatform();
3784
+ const spawn2 = options.spawn ?? ((command, args, spawnOptions) => spawnSync(command, args, spawnOptions));
3785
+ const root = options.root ?? installationStorePath();
3786
+ if (platform22 === "darwin") {
3787
+ return {
3788
+ async set(account, value) {
3789
+ runOrThrow(spawn2("security", [
3790
+ "add-generic-password",
3791
+ "-U",
3792
+ "-a",
3793
+ account,
3794
+ "-s",
3795
+ KEYCHAIN_SERVICE,
3796
+ "-w"
3797
+ ], { input: `${value}
3798
+ `, encoding: "utf8" }), "macOS Keychain write");
3799
+ },
3800
+ async get(account) {
3801
+ const result = spawn2("security", [
3802
+ "find-generic-password",
3803
+ "-a",
3804
+ account,
3805
+ "-s",
3806
+ KEYCHAIN_SERVICE,
3807
+ "-w"
3808
+ ], { encoding: "utf8" });
3809
+ if (result.status === 44) return null;
3810
+ return runOrThrow(result, "macOS Keychain read");
3811
+ },
3812
+ async delete(account) {
3813
+ const result = spawn2("security", [
3814
+ "delete-generic-password",
3815
+ "-a",
3816
+ account,
3817
+ "-s",
3818
+ KEYCHAIN_SERVICE
3819
+ ], { encoding: "utf8" });
3820
+ if (result.status !== 44) runOrThrow(result, "macOS Keychain delete");
3821
+ }
3822
+ };
3823
+ }
3824
+ if (platform22 === "linux") {
3825
+ return {
3826
+ async set(account, value) {
3827
+ runOrThrow(spawn2("secret-tool", [
3828
+ "store",
3829
+ "--label=Topolo external agent installation",
3830
+ "service",
3831
+ KEYCHAIN_SERVICE,
3832
+ "account",
3833
+ account
3834
+ ], { input: value, encoding: "utf8" }), "Linux Secret Service write");
3835
+ },
3836
+ async get(account) {
3837
+ const result = spawn2("secret-tool", [
3838
+ "lookup",
3839
+ "service",
3840
+ KEYCHAIN_SERVICE,
3841
+ "account",
3842
+ account
3843
+ ], { encoding: "utf8" });
3844
+ if (result.status === 1) return null;
3845
+ return runOrThrow(result, "Linux Secret Service read");
3846
+ },
3847
+ async delete(account) {
3848
+ const result = spawn2("secret-tool", [
3849
+ "clear",
3850
+ "service",
3851
+ KEYCHAIN_SERVICE,
3852
+ "account",
3853
+ account
3854
+ ], { encoding: "utf8" });
3855
+ if (result.status !== 1) runOrThrow(result, "Linux Secret Service delete");
3856
+ }
3857
+ };
3858
+ }
3859
+ if (platform22 === "win32") {
3860
+ const secretPath = (account) => join(root, ".secrets", `${account.replace(":", "-")}.bin`);
3861
+ const powershell = (script, path, input2) => spawn2(
3862
+ "powershell.exe",
3863
+ ["-NoProfile", "-NonInteractive", "-Command", script],
3864
+ { input: input2, encoding: "utf8", env: { ...process.env, TOPOLO_SECRET_PATH: path } }
3865
+ );
3866
+ return {
3867
+ async set(account, value) {
3868
+ const path = secretPath(account);
3869
+ await mkdir(dirname(path), { recursive: true });
3870
+ 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)";
3871
+ runOrThrow(powershell(script, path, value), "Windows DPAPI write");
3872
+ },
3873
+ async get(account) {
3874
+ const path = secretPath(account);
3875
+ if (!existsSync(path)) return null;
3876
+ 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))";
3877
+ return runOrThrow(powershell(script, path), "Windows DPAPI read");
3878
+ },
3879
+ async delete(account) {
3880
+ await rm(secretPath(account), { force: true });
3881
+ }
3882
+ };
3883
+ }
3884
+ throw new Error(`Secure external-agent installation storage is unsupported on ${platform22}.`);
3885
+ }
3886
+ async function writeExternalAgentInstallation(installation, secret, backend = createPlatformInstallationSecretBackend()) {
3887
+ const path = installationPath(installation.environment, installation.id);
3888
+ await backend.set(accountName(installation.environment, installation.id), JSON.stringify(secret));
3889
+ const temporary = `${path}.${process.pid}.tmp`;
3890
+ try {
3891
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
3892
+ await writeFile(temporary, `${JSON.stringify(installation, null, 2)}
3893
+ `, { encoding: "utf8", mode: 384 });
3894
+ await rename(temporary, path);
3895
+ if (hostPlatform() !== "win32") await chmod(path, 384);
3896
+ } catch (error) {
3897
+ await backend.delete(accountName(installation.environment, installation.id)).catch(() => void 0);
3898
+ await rm(temporary, { force: true });
3899
+ throw error;
3900
+ }
3901
+ }
3902
+ async function listExternalAgentInstallations(environment) {
3903
+ const directory = join(installationStorePath(), environment);
3904
+ if (!existsSync(directory)) return [];
3905
+ const entries = await readdir(directory, { withFileTypes: true });
3906
+ const installations = await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map(async (entry) => JSON.parse(await readFile(join(directory, entry.name), "utf8"))));
3907
+ return installations.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
3908
+ }
3909
+ async function deleteExternalAgentInstallation(environment, id, backend = createPlatformInstallationSecretBackend()) {
3910
+ await backend.delete(accountName(environment, id));
3911
+ await rm(installationPath(environment, id), { force: true });
3912
+ }
3363
3913
  var TOPOLO_ENVIRONMENTS = TOPOLO_RUNTIME_ENVIRONMENTS;
3364
3914
  function contextKey(contextType, organizationId) {
3365
3915
  return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
@@ -3368,14 +3918,14 @@ var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
3368
3918
  function configPath() {
3369
3919
  const override = process.env.TOPOLO_CONFIG_PATH;
3370
3920
  if (override) return override;
3371
- const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join(homedir(), "AppData", "Roaming") : join(homedir(), ".config"));
3372
- return join(base, "topolo", "config.json");
3921
+ const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join2(homedir2(), "AppData", "Roaming") : join2(homedir2(), ".config"));
3922
+ return join2(base, "topolo", "config.json");
3373
3923
  }
3374
3924
  async function readConfig() {
3375
3925
  const path = configPath();
3376
- if (!existsSync(path)) return {};
3926
+ if (!existsSync2(path)) return {};
3377
3927
  try {
3378
- const raw = await readFile(path, "utf8");
3928
+ const raw = await readFile2(path, "utf8");
3379
3929
  return JSON.parse(raw);
3380
3930
  } catch {
3381
3931
  return {};
@@ -3383,11 +3933,11 @@ async function readConfig() {
3383
3933
  }
3384
3934
  async function writeConfig(config) {
3385
3935
  const path = configPath();
3386
- await mkdir(dirname(path), { recursive: true });
3387
- await writeFile(path, JSON.stringify(config, null, 2) + "\n", "utf8");
3936
+ await mkdir2(dirname2(path), { recursive: true });
3937
+ await writeFile2(path, JSON.stringify(config, null, 2) + "\n", "utf8");
3388
3938
  if (platform() !== "win32") {
3389
3939
  try {
3390
- await chmod(path, 384);
3940
+ await chmod2(path, 384);
3391
3941
  } catch {
3392
3942
  }
3393
3943
  }
@@ -3493,18 +4043,18 @@ var CONFIG_LOCK_TIMEOUT_MS = 5e3;
3493
4043
  var CONFIG_LOCK_STALE_MS = 12e4;
3494
4044
  async function withConfigWriteLock(operation) {
3495
4045
  const lockPath = `${configPath()}.lock`;
3496
- await mkdir(dirname(lockPath), { recursive: true });
4046
+ await mkdir2(dirname2(lockPath), { recursive: true });
3497
4047
  const startedAt = Date.now();
3498
4048
  while (true) {
3499
4049
  try {
3500
- await mkdir(lockPath);
4050
+ await mkdir2(lockPath);
3501
4051
  break;
3502
4052
  } catch (error) {
3503
4053
  if (error.code !== "EEXIST") throw error;
3504
4054
  try {
3505
4055
  const lock = await stat(lockPath);
3506
4056
  if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
3507
- await rm(lockPath, { recursive: true, force: true });
4057
+ await rm2(lockPath, { recursive: true, force: true });
3508
4058
  continue;
3509
4059
  }
3510
4060
  } catch (inspectionError) {
@@ -3520,7 +4070,7 @@ async function withConfigWriteLock(operation) {
3520
4070
  try {
3521
4071
  return await operation();
3522
4072
  } finally {
3523
- await rm(lockPath, { recursive: true, force: true });
4073
+ await rm2(lockPath, { recursive: true, force: true });
3524
4074
  }
3525
4075
  }
3526
4076
  async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
@@ -3725,7 +4275,7 @@ function installOutputErrorHandlers(stdout = process.stdout, stderr = process.st
3725
4275
  stdout.on("error", handler);
3726
4276
  stderr.on("error", handler);
3727
4277
  }
3728
- function fail(err, mode) {
4278
+ function fail2(err, mode) {
3729
4279
  const payload = normalizeError(err);
3730
4280
  if (mode === "json") {
3731
4281
  process.stderr.write(JSON.stringify({ error: payload }, null, 2) + "\n");
@@ -3772,14 +4322,14 @@ function normalizeError(err) {
3772
4322
 
3773
4323
  // src/version.ts
3774
4324
  import { readFileSync } from "fs";
3775
- import { dirname as dirname2, join as join2 } from "path";
4325
+ import { dirname as dirname3, join as join3 } from "path";
3776
4326
  import { fileURLToPath } from "url";
3777
4327
  function readCliVersion() {
3778
4328
  try {
3779
- const here = dirname2(fileURLToPath(import.meta.url));
4329
+ const here = dirname3(fileURLToPath(import.meta.url));
3780
4330
  const candidates = [
3781
- join2(here, "package.json"),
3782
- join2(here, "..", "package.json")
4331
+ join3(here, "package.json"),
4332
+ join3(here, "..", "package.json")
3783
4333
  ];
3784
4334
  for (const path of candidates) {
3785
4335
  try {
@@ -3834,7 +4384,7 @@ Visit ${apiKeyIssuanceUrl(environment)} to mint one, or configure an OAuth clien
3834
4384
  openBrowser: opts.browser !== false
3835
4385
  });
3836
4386
  } catch (err) {
3837
- fail(err, mode);
4387
+ fail2(err, mode);
3838
4388
  }
3839
4389
  }
3840
4390
  );
@@ -3850,7 +4400,7 @@ Visit ${apiKeyIssuanceUrl(environment)} to mint one, or configure an OAuth clien
3850
4400
  () => `Logged out of Topolo ${environmentLabel(environment)}.`
3851
4401
  );
3852
4402
  } catch (err) {
3853
- fail(err, mode);
4403
+ fail2(err, mode);
3854
4404
  }
3855
4405
  });
3856
4406
  auth.command("status").description("Show which credential source is in use (env vs config file)").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (opts) => {
@@ -3876,7 +4426,7 @@ Visit ${apiKeyIssuanceUrl(environment)} to mint one, or configure an OAuth clien
3876
4426
  ].join("\n")
3877
4427
  );
3878
4428
  } catch (err) {
3879
- fail(err, mode);
4429
+ fail2(err, mode);
3880
4430
  }
3881
4431
  });
3882
4432
  }
@@ -4068,6 +4618,7 @@ async function buildClient(options) {
4068
4618
  return createTopolo({
4069
4619
  credential,
4070
4620
  environment,
4621
+ actorContext: { channel: "cli" },
4071
4622
  agent: {
4072
4623
  clientName: "topolo-cli",
4073
4624
  clientVersion: CLI_VERSION,
@@ -4125,7 +4676,7 @@ function registerWhoamiCommand(program2) {
4125
4676
  const info = await topolo.identity.whoami();
4126
4677
  emit(info, mode, () => formatPretty(info));
4127
4678
  } catch (err) {
4128
- fail(err, mode);
4679
+ fail2(err, mode);
4129
4680
  }
4130
4681
  });
4131
4682
  }
@@ -4206,7 +4757,7 @@ function registerApiCommand(program2) {
4206
4757
  });
4207
4758
  emit(result, mode, () => JSON.stringify(result, null, 2));
4208
4759
  } catch (err) {
4209
- fail(err, mode);
4760
+ fail2(err, mode);
4210
4761
  }
4211
4762
  }
4212
4763
  );
@@ -4270,7 +4821,7 @@ function registerActionsCommand(program2) {
4270
4821
  const output = opts.detail ? { ...result, actions: actions2 } : { actions: actions2.map(compactActionCatalogEntry), page: result.page };
4271
4822
  emit(output, mode, () => formatActionRows(actions2, result.page.nextCursor));
4272
4823
  } catch (err) {
4273
- fail(err, mode);
4824
+ fail2(err, mode);
4274
4825
  }
4275
4826
  });
4276
4827
  actions.command("get <action>").description("Describe one Topolo action by exact action id").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
@@ -4283,7 +4834,7 @@ function registerActionsCommand(program2) {
4283
4834
  const entry = await topolo.client.getAction(action);
4284
4835
  emit(entry, mode, () => formatActionDetail(entry));
4285
4836
  } catch (err) {
4286
- fail(err, mode);
4837
+ fail2(err, mode);
4287
4838
  }
4288
4839
  });
4289
4840
  actions.command("call <action>").description("Call a permission-scoped Topolo action").option("-d, --data <json-or-@file>", "JSON action input or @path to a JSON file", "{}").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Acknowledge a mutating action").option("--detail", "Return the complete action response instead of a bounded agent projection").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
@@ -4302,7 +4853,7 @@ function registerActionsCommand(program2) {
4302
4853
  const output = detailRequested(opts.detail, command) ? result : compactActionResult(action, result);
4303
4854
  emit(output, mode, () => JSON.stringify(output, null, 2));
4304
4855
  } catch (err) {
4305
- fail(err, mode);
4856
+ fail2(err, mode);
4306
4857
  }
4307
4858
  });
4308
4859
  actions.command("upload <action>").description("Upload one local file through an action-declared upload contract").requiredOption("--file <path>", "Local file to upload").option("--content-type <type>", "MIME type; inferred for common file extensions").option("-d, --data <json-or-@file>", "Additional JSON action input or @path to a JSON file", "{}").option("--complete-data <json-or-@file>", "Additional input for a declared completion action", "{}").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Acknowledge a mutating upload action").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
@@ -4326,7 +4877,7 @@ function registerActionsCommand(program2) {
4326
4877
  );
4327
4878
  emit(result, mode, () => JSON.stringify(result, null, 2));
4328
4879
  } catch (err) {
4329
- fail(err, mode);
4880
+ fail2(err, mode);
4330
4881
  }
4331
4882
  });
4332
4883
  actions.command("validate <action>").description("Validate action input locally against the published JSON Schema").option("-d, --data <json-or-@file>", "JSON action input or @path to a JSON file", "{}").option("--detail", "Include the full action contract").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
@@ -4337,7 +4888,7 @@ function registerActionsCommand(program2) {
4337
4888
  const output = detailRequested(opts.detail, command) ? result : compactActionValidation(result.action.actionId, result.validation);
4338
4889
  emit(output, mode, () => result.validation.valid ? `Valid input for ${result.action.actionId}.` : result.validation.errors.map((issue) => `${issue.path}: ${issue.message}`).join("\n"));
4339
4890
  } catch (err) {
4340
- fail(err, mode);
4891
+ fail2(err, mode);
4341
4892
  }
4342
4893
  });
4343
4894
  actions.command("prepare <action>").description("Prepare one action with identity, example, validation, exact request, verification, and rollback").option("-d, --data <json-or-@file>", "JSON action input or @path; defaults to one valid published example").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Include confirmation in the plan without executing it").option("--json", "Output compact JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
@@ -4353,7 +4904,7 @@ function registerActionsCommand(program2) {
4353
4904
  const output = compactActionPreparation(preparation, await actionExecutionContext(topolo));
4354
4905
  emit(output, mode, () => formatActionPreparation(output));
4355
4906
  } catch (err) {
4356
- fail(err, mode);
4907
+ fail2(err, mode);
4357
4908
  }
4358
4909
  });
4359
4910
  actions.command("plan <action>").description("Validate and preview the exact request, confirmation, verification, and rollback plan").option("-d, --data <json-or-@file>", "JSON action input or @path to a JSON file", "{}").option("--resource-type <type>", "App-local resource alias, such as workspace").option("--resource-id <id>", "Resource id for the selected resource type").option("--confirm", "Include confirmation in the plan without executing it").option("--detail", "Include the full action contract").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
@@ -4367,7 +4918,7 @@ function registerActionsCommand(program2) {
4367
4918
  });
4368
4919
  emit(detailRequested(opts.detail, command) ? plan : compactActionPlan(plan), mode, () => formatActionPlan(plan));
4369
4920
  } catch (err) {
4370
- fail(err, mode);
4921
+ fail2(err, mode);
4371
4922
  }
4372
4923
  });
4373
4924
  actions.command("examples <action>").description("Show published examples and public docs for one action").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
@@ -4382,7 +4933,7 @@ function registerActionsCommand(program2) {
4382
4933
  };
4383
4934
  emit(result, mode, () => result.examples.length ? JSON.stringify(result, null, 2) : `No inline examples published. Docs: ${result.docsUrl || "not published"}`);
4384
4935
  } catch (err) {
4385
- fail(err, mode);
4936
+ fail2(err, mode);
4386
4937
  }
4387
4938
  });
4388
4939
  actions.command("capabilities").description("List every published action for one application (its capability contract)").option("-s, --service <service>", "App id, slug, or name").option("--summary", "Return compact capability counts without action rows").option("--detail", "Include full schemas and agent contracts").option("--json", "Output JSON").option("--no-json", "Output human-readable table").action(async (opts, command) => {
@@ -4417,7 +4968,7 @@ function registerActionsCommand(program2) {
4417
4968
  const output = opts.summary ? summarizeActionCapabilities(resolvedService, discovered) : detailRequested(opts.detail, command) ? result : { actions: discovered.map(compactActionCatalogEntry), page: result.page };
4418
4969
  emit(output, mode, () => opts.summary ? formatActionCapabilitySummary(summarizeActionCapabilities(resolvedService, discovered)) : formatActionRows(discovered));
4419
4970
  } catch (err) {
4420
- fail(err, mode);
4971
+ fail2(err, mode);
4421
4972
  }
4422
4973
  });
4423
4974
  }
@@ -4605,7 +5156,7 @@ function registerAppsCommand(program2) {
4605
5156
  const applications = publicApplicationRows(result.services);
4606
5157
  emit({ applications, page: result.page }, mode, () => formatPublicApplicationRows(applications));
4607
5158
  } catch (error) {
4608
- fail(error, mode);
5159
+ fail2(error, mode);
4609
5160
  }
4610
5161
  });
4611
5162
  apps.command("get <application>").description("Show one application available to the current Topolo credential").option("--json", "Output JSON", true).option("--no-json", "Output human-readable details").action(async (application, opts) => {
@@ -4617,7 +5168,7 @@ function registerAppsCommand(program2) {
4617
5168
  if (!selected) throw new Error(`Application "${application}" is not a customer application.`);
4618
5169
  emit({ application: selected }, mode, () => formatPublicApplication(selected));
4619
5170
  } catch (error) {
4620
- fail(error, mode);
5171
+ fail2(error, mode);
4621
5172
  }
4622
5173
  });
4623
5174
  }
@@ -4676,7 +5227,7 @@ function registerServicesCommand(program2) {
4676
5227
  const rows = buildRows(result.services, config.serviceUrls, environment);
4677
5228
  emit({ environment, services: rows, page: result.page }, mode, () => formatRows(rows, environment, result.page.nextCursor));
4678
5229
  } catch (err) {
4679
- fail(err, mode);
5230
+ fail2(err, mode);
4680
5231
  }
4681
5232
  });
4682
5233
  }
@@ -4769,7 +5320,7 @@ function registerOAuthCommands(program2) {
4769
5320
  return clients2.map((client) => `${client.client_id} ${client.name} ${client.status}`).join("\n");
4770
5321
  });
4771
5322
  } catch (err) {
4772
- fail(err, mode);
5323
+ fail2(err, mode);
4773
5324
  }
4774
5325
  });
4775
5326
  clients.command("create").description("Create a Login with Topolo OAuth client.").requiredOption("--name <name>", "Client display name").requiredOption("--developer-id <id>", "Topolo developer id that owns this client").option("--developer-app-id <id>", "Developer app id to associate with this client").option("--description <description>", "Client description").option("--redirect-uri <uri...>", "Allowed callback URI. Repeat or pass a space-separated list.").option("--scope <scope...>", "Allowed scope. Defaults to openid profile email.").option("--public", "Create a public PKCE client instead of a confidential client").option("--homepage-url <url>", "Client homepage URL").option("--logo-url <url>", "Client logo URL").option("--json", "Emit JSON output").action(async (options) => {
@@ -4789,7 +5340,7 @@ function registerOAuthCommands(program2) {
4789
5340
  ].join("\n");
4790
5341
  });
4791
5342
  } catch (err) {
4792
- fail(err, mode);
5343
+ fail2(err, mode);
4793
5344
  }
4794
5345
  });
4795
5346
  clients.command("revoke").description("Revoke an OAuth client for all users.").argument("<id>", "OAuth client record id, for example oac_...").option("--json", "Emit JSON output").action(async (id, options) => {
@@ -4800,7 +5351,7 @@ function registerOAuthCommands(program2) {
4800
5351
  });
4801
5352
  emit(payload, mode, () => `Revoked OAuth client ${id}`);
4802
5353
  } catch (err) {
4803
- fail(err, mode);
5354
+ fail2(err, mode);
4804
5355
  }
4805
5356
  });
4806
5357
  clients.command("rotate-secret").description("Rotate a confidential OAuth client secret.").argument("<id>", "OAuth client record id, for example oac_...").option("--json", "Emit JSON output").action(async (id, options) => {
@@ -4811,7 +5362,7 @@ function registerOAuthCommands(program2) {
4811
5362
  });
4812
5363
  emit(payload, mode, () => `New client secret: ${payload.data?.client_secret ?? ""}`);
4813
5364
  } catch (err) {
4814
- fail(err, mode);
5365
+ fail2(err, mode);
4815
5366
  }
4816
5367
  });
4817
5368
  }
@@ -4856,8 +5407,8 @@ async function authFetchJson(path, init = {}) {
4856
5407
  }
4857
5408
  headers.set("Authorization", `Bearer ${credential.accessToken}`);
4858
5409
  const response = await fetch(`${authBaseUrl}${path}`, { ...init, headers });
4859
- const text = await response.text();
4860
- const payload = text ? JSON.parse(text) : {};
5410
+ const text2 = await response.text();
5411
+ const payload = text2 ? JSON.parse(text2) : {};
4861
5412
  if (!response.ok || payload.success === false) {
4862
5413
  const err = new Error(payload.message || payload.error_description || payload.error || "Topolo Auth request failed");
4863
5414
  err.code = payload.code || payload.error;
@@ -4871,8 +5422,8 @@ function normalizeList(values, fallback = []) {
4871
5422
  }
4872
5423
 
4873
5424
  // src/commands/integrations.ts
4874
- import { readdir, readFile as readFile2, stat as stat2 } from "fs/promises";
4875
- import { join as join3, relative } from "path";
5425
+ import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
5426
+ import { join as join4, relative } from "path";
4876
5427
  function registerIntegrationsCommand(program2) {
4877
5428
  const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
4878
5429
  integrations.command("prompt").description("Print an agent prompt for adding a Topolo integration.").argument("<integration>", "Integration prompt id, currently login-with-topolo").option("--framework <framework>", "auto, authjs, express, or generic", "auto").option("--json", "Emit JSON output").action((integration, options) => {
@@ -4883,7 +5434,7 @@ function registerIntegrationsCommand(program2) {
4883
5434
  const prompt = buildLoginWithTopoloPrompt(framework);
4884
5435
  emit({ integration, framework, prompt }, mode, () => prompt);
4885
5436
  } catch (err) {
4886
- fail(err, mode);
5437
+ fail2(err, mode);
4887
5438
  }
4888
5439
  });
4889
5440
  integrations.command("check").description("Check whether a repo has the Login with Topolo integration pieces.").argument("<integration>", "Integration check id, currently login-with-topolo").option("--repo-root <path>", "Repository root to inspect", ".").option("--json", "Emit JSON output").action(async (integration, options) => {
@@ -4894,7 +5445,7 @@ function registerIntegrationsCommand(program2) {
4894
5445
  emit(result, mode, () => formatCheckResult(result));
4895
5446
  if (!result.passed) process.exitCode = 1;
4896
5447
  } catch (err) {
4897
- fail(err, mode);
5448
+ fail2(err, mode);
4898
5449
  }
4899
5450
  });
4900
5451
  }
@@ -5015,7 +5566,7 @@ function detectFramework(packageJson, haystack) {
5015
5566
  }
5016
5567
  async function readPackageJson(repoRoot) {
5017
5568
  try {
5018
- return JSON.parse(await readFile2(join3(repoRoot, "package.json"), "utf8"));
5569
+ return JSON.parse(await readFile3(join4(repoRoot, "package.json"), "utf8"));
5019
5570
  } catch {
5020
5571
  return null;
5021
5572
  }
@@ -5025,9 +5576,9 @@ async function collectFiles(repoRoot) {
5025
5576
  const root = repoRoot;
5026
5577
  async function walk(dir) {
5027
5578
  if (result.length > 2e3) return;
5028
- const entries = await readdir(dir, { withFileTypes: true });
5579
+ const entries = await readdir2(dir, { withFileTypes: true });
5029
5580
  for (const entry of entries) {
5030
- const fullPath = join3(dir, entry.name);
5581
+ const fullPath = join4(dir, entry.name);
5031
5582
  if (entry.isDirectory()) {
5032
5583
  if ([".git", "node_modules", "dist", "build", ".next", "coverage"].includes(entry.name)) continue;
5033
5584
  await walk(fullPath);
@@ -5038,7 +5589,7 @@ async function collectFiles(repoRoot) {
5038
5589
  if (info.size > 25e4) continue;
5039
5590
  result.push({
5040
5591
  path: relative(root, fullPath),
5041
- content: await readFile2(fullPath, "utf8")
5592
+ content: await readFile3(fullPath, "utf8")
5042
5593
  });
5043
5594
  }
5044
5595
  }
@@ -5054,20 +5605,20 @@ function formatCheckResult(result) {
5054
5605
  }
5055
5606
 
5056
5607
  // src/commands/skills.ts
5057
- import { spawnSync as spawnSync2 } from "child_process";
5058
- import { existsSync as existsSync4 } from "fs";
5059
- import { readFile as readFile5, rm as rm2 } from "fs/promises";
5060
- import { homedir as homedir3 } from "os";
5061
- import { dirname as dirname4, join as join5, resolve } from "path";
5608
+ import { spawnSync as spawnSync3 } from "child_process";
5609
+ import { existsSync as existsSync5 } from "fs";
5610
+ import { readFile as readFile6, rm as rm3 } from "fs/promises";
5611
+ import { homedir as homedir4 } from "os";
5612
+ import { dirname as dirname5, join as join6, resolve } from "path";
5062
5613
  import { fileURLToPath as fileURLToPath2 } from "url";
5063
5614
  import { createRequire } from "module";
5064
5615
 
5065
5616
  // src/commands/agent-host.ts
5066
- import { spawnSync } from "child_process";
5067
- import { existsSync as existsSync2 } from "fs";
5068
- import { readFile as readFile3 } from "fs/promises";
5069
- import { homedir as homedir2 } from "os";
5070
- import { isAbsolute, join as join4 } from "path";
5617
+ import { spawnSync as spawnSync2 } from "child_process";
5618
+ import { existsSync as existsSync3 } from "fs";
5619
+ import { readFile as readFile4 } from "fs/promises";
5620
+ import { homedir as homedir3 } from "os";
5621
+ import { isAbsolute, join as join5 } from "path";
5071
5622
  var SUPPORTED_AGENT_IDS = [
5072
5623
  "claude",
5073
5624
  "claude-desktop",
@@ -5116,41 +5667,41 @@ function selectStableNodeCommand(whichOutput, fallback = process.execPath) {
5116
5667
  return candidate && isAbsolute(candidate) ? candidate : fallback;
5117
5668
  }
5118
5669
  function stableNodeCommand() {
5119
- const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["node"], {
5670
+ const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["node"], {
5120
5671
  encoding: "utf8",
5121
5672
  stdio: ["ignore", "pipe", "ignore"]
5122
5673
  });
5123
5674
  return selectStableNodeCommand(probe.status === 0 ? probe.stdout : void 0);
5124
5675
  }
5125
- function agentSkillPath(agent, homeDir = homedir2(), projectDir) {
5676
+ function agentSkillPath(agent, homeDir = homedir3(), projectDir) {
5126
5677
  switch (agent) {
5127
5678
  case "claude":
5128
- return projectDir ? join4(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join4(homeDir, ".claude", "skills", "topolo", "SKILL.md");
5679
+ return projectDir ? join5(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join5(homeDir, ".claude", "skills", "topolo", "SKILL.md");
5129
5680
  case "claude-desktop":
5130
- return join4(homeDir, ".claude", "skills", "topolo", "SKILL.md");
5681
+ return join5(homeDir, ".claude", "skills", "topolo", "SKILL.md");
5131
5682
  case "codex":
5132
- return join4(homeDir, ".codex", "skills", "topolo", "SKILL.md");
5683
+ return join5(homeDir, ".codex", "skills", "topolo", "SKILL.md");
5133
5684
  case "cursor":
5134
- return join4(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
5685
+ return join5(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
5135
5686
  case "grok":
5136
- return join4(projectDir ?? homeDir, ".grok", "skills", "topolo", "SKILL.md");
5687
+ return join5(projectDir ?? homeDir, ".grok", "skills", "topolo", "SKILL.md");
5137
5688
  case "gemini":
5138
- return join4(projectDir ?? homeDir, ".gemini", "skills", "topolo", "SKILL.md");
5689
+ return join5(projectDir ?? homeDir, ".gemini", "skills", "topolo", "SKILL.md");
5139
5690
  case "copilot":
5140
- return projectDir ? join4(projectDir, ".github", "skills", "topolo", "SKILL.md") : join4(homeDir, ".copilot", "skills", "topolo", "SKILL.md");
5691
+ return projectDir ? join5(projectDir, ".github", "skills", "topolo", "SKILL.md") : join5(homeDir, ".copilot", "skills", "topolo", "SKILL.md");
5141
5692
  case "windsurf":
5142
- return projectDir ? join4(projectDir, ".windsurf", "rules", "topolo.md") : void 0;
5693
+ return projectDir ? join5(projectDir, ".windsurf", "rules", "topolo.md") : void 0;
5143
5694
  case "cline":
5144
- return join4(projectDir ?? homeDir, ".cline", "skills", "topolo", "SKILL.md");
5695
+ return join5(projectDir ?? homeDir, ".cline", "skills", "topolo", "SKILL.md");
5145
5696
  case "goose":
5146
5697
  case "amazon-q":
5147
5698
  return void 0;
5148
5699
  }
5149
5700
  }
5150
- function agentConfigPaths(agent, homeDir = homedir2(), projectDir) {
5701
+ function agentConfigPaths(agent, homeDir = homedir3(), projectDir) {
5151
5702
  switch (agent) {
5152
5703
  case "claude":
5153
- return [{ path: join4(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
5704
+ return [{ path: join5(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
5154
5705
  case "claude-desktop":
5155
5706
  return [{
5156
5707
  path: claudeDesktopConfigPath(homeDir),
@@ -5158,31 +5709,31 @@ function agentConfigPaths(agent, homeDir = homedir2(), projectDir) {
5158
5709
  rootKey: "mcpServers"
5159
5710
  }];
5160
5711
  case "codex":
5161
- return [{ path: join4(homeDir, ".codex", "config.toml"), format: "toml" }];
5712
+ return [{ path: join5(homeDir, ".codex", "config.toml"), format: "toml" }];
5162
5713
  case "cursor":
5163
5714
  return [{
5164
- path: projectDir ? join4(projectDir, ".cursor", "mcp.json") : join4(homeDir, ".cursor", "mcp.json"),
5715
+ path: projectDir ? join5(projectDir, ".cursor", "mcp.json") : join5(homeDir, ".cursor", "mcp.json"),
5165
5716
  format: "json",
5166
5717
  rootKey: "mcpServers"
5167
5718
  }];
5168
5719
  case "grok":
5169
5720
  return [{
5170
- path: projectDir ? join4(projectDir, ".grok", "config.toml") : join4(homeDir, ".grok", "config.toml"),
5721
+ path: projectDir ? join5(projectDir, ".grok", "config.toml") : join5(homeDir, ".grok", "config.toml"),
5171
5722
  format: "toml"
5172
5723
  }];
5173
5724
  case "gemini":
5174
5725
  return [{
5175
- path: projectDir ? join4(projectDir, ".gemini", "settings.json") : join4(homeDir, ".gemini", "settings.json"),
5726
+ path: projectDir ? join5(projectDir, ".gemini", "settings.json") : join5(homeDir, ".gemini", "settings.json"),
5176
5727
  format: "json",
5177
5728
  rootKey: "mcpServers"
5178
5729
  }];
5179
5730
  case "copilot":
5180
5731
  return projectDir ? [
5181
- { path: join4(projectDir, ".mcp.json"), format: "json", rootKey: "mcpServers" },
5182
- { path: join4(projectDir, ".vscode", "mcp.json"), format: "json", rootKey: "servers" }
5732
+ { path: join5(projectDir, ".mcp.json"), format: "json", rootKey: "mcpServers" },
5733
+ { path: join5(projectDir, ".vscode", "mcp.json"), format: "json", rootKey: "servers" }
5183
5734
  ] : [
5184
5735
  {
5185
- path: join4(homeDir, ".copilot", "mcp-config.json"),
5736
+ path: join5(homeDir, ".copilot", "mcp-config.json"),
5186
5737
  format: "json",
5187
5738
  rootKey: "mcpServers"
5188
5739
  },
@@ -5190,24 +5741,24 @@ function agentConfigPaths(agent, homeDir = homedir2(), projectDir) {
5190
5741
  ];
5191
5742
  case "windsurf":
5192
5743
  return [{
5193
- path: join4(homeDir, ".codeium", "windsurf", "mcp_config.json"),
5744
+ path: join5(homeDir, ".codeium", "windsurf", "mcp_config.json"),
5194
5745
  format: "json",
5195
5746
  rootKey: "mcpServers"
5196
5747
  }];
5197
5748
  case "cline":
5198
5749
  return [{
5199
- path: projectDir ? join4(projectDir, ".cline", "mcp.json") : join4(homeDir, ".cline", "mcp.json"),
5750
+ path: projectDir ? join5(projectDir, ".cline", "mcp.json") : join5(homeDir, ".cline", "mcp.json"),
5200
5751
  format: "json",
5201
5752
  rootKey: "mcpServers"
5202
5753
  }];
5203
5754
  case "goose":
5204
- return [{ path: join4(homeDir, ".config", "goose", "config.yaml"), format: "yaml" }];
5755
+ return [{ path: join5(homeDir, ".config", "goose", "config.yaml"), format: "yaml" }];
5205
5756
  case "amazon-q":
5206
5757
  if (projectDir) {
5207
- return [{ path: join4(projectDir, ".amazonq", "mcp.json"), format: "json", rootKey: "mcpServers" }];
5758
+ return [{ path: join5(projectDir, ".amazonq", "mcp.json"), format: "json", rootKey: "mcpServers" }];
5208
5759
  }
5209
5760
  return [{
5210
- path: existsSync2(join4(homeDir, ".aws", "amazonq", "mcp.json")) ? join4(homeDir, ".aws", "amazonq", "mcp.json") : join4(homeDir, ".aws", "amazonq", "default.json"),
5761
+ path: existsSync3(join5(homeDir, ".aws", "amazonq", "mcp.json")) ? join5(homeDir, ".aws", "amazonq", "mcp.json") : join5(homeDir, ".aws", "amazonq", "default.json"),
5211
5762
  format: "json",
5212
5763
  rootKey: "mcpServers"
5213
5764
  }];
@@ -5239,23 +5790,23 @@ function agentCliNames(agent) {
5239
5790
  return ["qchat", "q"];
5240
5791
  }
5241
5792
  }
5242
- function agentConfigExists(agent, homeDir = homedir2(), projectDir) {
5243
- return agentConfigPaths(agent, homeDir, projectDir).some((config) => existsSync2(config.path));
5793
+ function agentConfigExists(agent, homeDir = homedir3(), projectDir) {
5794
+ return agentConfigPaths(agent, homeDir, projectDir).some((config) => existsSync3(config.path));
5244
5795
  }
5245
5796
  function claudeDesktopConfigPath(homeDir) {
5246
- if (process.platform === "darwin") return join4(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
5247
- if (process.platform === "win32") return join4(process.env.APPDATA ?? join4(homeDir, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
5248
- return join4(process.env.XDG_CONFIG_HOME ?? join4(homeDir, ".config"), "Claude", "claude_desktop_config.json");
5797
+ if (process.platform === "darwin") return join5(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
5798
+ if (process.platform === "win32") return join5(process.env.APPDATA ?? join5(homeDir, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
5799
+ return join5(process.env.XDG_CONFIG_HOME ?? join5(homeDir, ".config"), "Claude", "claude_desktop_config.json");
5249
5800
  }
5250
5801
  function vscodeUserConfigPath(homeDir) {
5251
- if (process.platform === "darwin") return join4(homeDir, "Library", "Application Support", "Code", "User", "mcp.json");
5252
- if (process.platform === "win32") return join4(process.env.APPDATA ?? join4(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
5253
- return join4(process.env.XDG_CONFIG_HOME ?? join4(homeDir, ".config"), "Code", "User", "mcp.json");
5802
+ if (process.platform === "darwin") return join5(homeDir, "Library", "Application Support", "Code", "User", "mcp.json");
5803
+ if (process.platform === "win32") return join5(process.env.APPDATA ?? join5(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
5804
+ return join5(process.env.XDG_CONFIG_HOME ?? join5(homeDir, ".config"), "Code", "User", "mcp.json");
5254
5805
  }
5255
- async function readAgentRegistrations(agent, homeDir = homedir2(), projectDir) {
5806
+ async function readAgentRegistrations(agent, homeDir = homedir3(), projectDir) {
5256
5807
  const registrations = [];
5257
5808
  for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
5258
- if (!existsSync2(config.path)) continue;
5809
+ if (!existsSync3(config.path)) continue;
5259
5810
  if (config.format === "json") {
5260
5811
  registrations.push(...await readJsonRegistrations(config.path, config.rootKey ?? "mcpServers", agent));
5261
5812
  } else if (config.format === "toml") {
@@ -5269,7 +5820,7 @@ async function readAgentRegistrations(agent, homeDir = homedir2(), projectDir) {
5269
5820
  async function readJsonRegistrations(path, rootKey, agent) {
5270
5821
  let parsed;
5271
5822
  try {
5272
- parsed = JSON.parse(await readFile3(path, "utf8"));
5823
+ parsed = JSON.parse(await readFile4(path, "utf8"));
5273
5824
  } catch (error) {
5274
5825
  throw new Error("Cannot read malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + path + ": " + message(error));
5275
5826
  }
@@ -5281,7 +5832,7 @@ async function readTomlRegistrations(path, agent) {
5281
5832
  const registrations = /* @__PURE__ */ new Map();
5282
5833
  let section;
5283
5834
  let pending;
5284
- for (const rawLine of (await readFile3(path, "utf8")).split(/\r?\n/)) {
5835
+ for (const rawLine of (await readFile4(path, "utf8")).split(/\r?\n/)) {
5285
5836
  const line = stripTomlComment(rawLine).trim();
5286
5837
  if (pending) {
5287
5838
  if (line.startsWith("[")) {
@@ -5320,9 +5871,9 @@ async function readTomlRegistrations(path, agent) {
5320
5871
  return [...registrations.values()].filter((registration) => registration.command);
5321
5872
  }
5322
5873
  async function readGooseRegistrations(path) {
5323
- const text = await readFile3(path, "utf8");
5874
+ const text2 = await readFile4(path, "utf8");
5324
5875
  const registrations = [];
5325
- const lines = text.split(/\r?\n/);
5876
+ const lines = text2.split(/\r?\n/);
5326
5877
  let inExtensions = false;
5327
5878
  let inTopolo = false;
5328
5879
  let current;
@@ -5516,9 +6067,9 @@ function message(error) {
5516
6067
  }
5517
6068
 
5518
6069
  // src/commands/agent-config.ts
5519
- import { existsSync as existsSync3 } from "fs";
5520
- import { mkdir as mkdir2, readFile as readFile4, rename, writeFile as writeFile2 } from "fs/promises";
5521
- import { dirname as dirname3 } from "path";
6070
+ import { existsSync as existsSync4 } from "fs";
6071
+ import { mkdir as mkdir3, readFile as readFile5, rename as rename2, writeFile as writeFile3 } from "fs/promises";
6072
+ import { dirname as dirname4 } from "path";
5522
6073
  async function installAgentSkill(agent, source, homeDir, projectDir) {
5523
6074
  const path = agentSkillPath(agent, homeDir, projectDir);
5524
6075
  if (!path) return { installed: false };
@@ -5539,12 +6090,12 @@ async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir
5539
6090
  }
5540
6091
  async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
5541
6092
  for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
5542
- if (!existsSync3(config.path)) continue;
6093
+ if (!existsSync4(config.path)) continue;
5543
6094
  if (config.format === "json") {
5544
6095
  await unregisterJson(config, agent, name);
5545
6096
  } else if (config.format === "toml") {
5546
- const text = withoutMcpServerSections(await readFile4(config.path, "utf8"), name);
5547
- await atomicWrite(config.path, text ? text + "\n" : "");
6097
+ const text2 = withoutMcpServerSections(await readFile5(config.path, "utf8"), name);
6098
+ await atomicWrite(config.path, text2 ? text2 + "\n" : "");
5548
6099
  } else {
5549
6100
  await unregisterGoose(config.path, name);
5550
6101
  }
@@ -5553,14 +6104,15 @@ async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
5553
6104
  function registrationEnvironment(agent, registration) {
5554
6105
  return {
5555
6106
  TOPOLO_AGENT_NAME: AGENT_NAME_FOR[agent],
6107
+ ...registration.installationId ? { TOPOLO_AGENT_INSTALLATION_ID: registration.installationId } : {},
5556
6108
  ...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {}
5557
6109
  };
5558
6110
  }
5559
6111
  async function registerJson(config, agent, mcp, registration) {
5560
6112
  let parsed = {};
5561
- if (existsSync3(config.path)) {
6113
+ if (existsSync4(config.path)) {
5562
6114
  try {
5563
- parsed = JSON.parse(await readFile4(config.path, "utf8"));
6115
+ parsed = JSON.parse(await readFile5(config.path, "utf8"));
5564
6116
  } catch (error) {
5565
6117
  throw new Error(
5566
6118
  "Cannot update malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + config.path + ": " + (error instanceof Error ? error.message : String(error))
@@ -5586,7 +6138,7 @@ async function registerJson(config, agent, mcp, registration) {
5586
6138
  async function unregisterJson(config, agent, name) {
5587
6139
  let parsed;
5588
6140
  try {
5589
- parsed = JSON.parse(await readFile4(config.path, "utf8"));
6141
+ parsed = JSON.parse(await readFile5(config.path, "utf8"));
5590
6142
  } catch (error) {
5591
6143
  throw new Error(
5592
6144
  "Cannot update malformed " + AGENT_LABELS[agent] + " configuration at " + config.path + ": " + (error instanceof Error ? error.message : String(error))
@@ -5603,16 +6155,16 @@ async function unregisterJson(config, agent, name) {
5603
6155
  await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n");
5604
6156
  }
5605
6157
  async function registerToml(path, agent, mcp, registration) {
5606
- const existing = existsSync3(path) ? await readFile4(path, "utf8") : "";
5607
- const text = withoutMcpServerSections(existing, registration.name);
6158
+ const existing = existsSync4(path) ? await readFile5(path, "utf8") : "";
6159
+ const text2 = withoutMcpServerSections(existing, registration.name);
5608
6160
  const args = mcp.args.length ? "\nargs = [" + mcp.args.map((item) => JSON.stringify(item)).join(", ") + "]" : "";
5609
6161
  const environment = registrationEnvironment(agent, registration);
5610
6162
  const environmentText = Object.entries(environment).map(([key, value]) => JSON.stringify(key) + " = " + JSON.stringify(value)).join(", ");
5611
6163
  const block = agent === "grok" ? "\n\n[mcp_servers." + registration.name + "]\ncommand = " + JSON.stringify(mcp.command) + args + "\nenv = { " + environmentText + " }\n" : "\n\n[mcp_servers." + registration.name + "]\ncommand = " + JSON.stringify(mcp.command) + args + "\n\n[mcp_servers." + registration.name + ".env]\n" + Object.entries(environment).map(([key, value]) => key + " = " + JSON.stringify(value)).join("\n") + "\n";
5612
- await atomicWrite(path, text + block);
6164
+ await atomicWrite(path, text2 + block);
5613
6165
  }
5614
6166
  async function registerGoose(path, mcp, registration) {
5615
- const existing = existsSync3(path) ? await readFile4(path, "utf8") : "";
6167
+ const existing = existsSync4(path) ? await readFile5(path, "utf8") : "";
5616
6168
  const withoutTopolo = removeGooseExtension(existing, registration.name);
5617
6169
  const environment = registrationEnvironment("goose", registration);
5618
6170
  const block = [
@@ -5629,11 +6181,11 @@ async function registerGoose(path, mcp, registration) {
5629
6181
  await atomicWrite(path, insertGooseExtension(withoutTopolo, block));
5630
6182
  }
5631
6183
  async function unregisterGoose(path, name) {
5632
- const existing = await readFile4(path, "utf8");
6184
+ const existing = await readFile5(path, "utf8");
5633
6185
  await atomicWrite(path, removeGooseExtension(existing, name));
5634
6186
  }
5635
- function removeGooseExtension(text, name) {
5636
- const lines = text.split(/\r?\n/);
6187
+ function removeGooseExtension(text2, name) {
6188
+ const lines = text2.split(/\r?\n/);
5637
6189
  const kept = [];
5638
6190
  let inExtensions = false;
5639
6191
  let skipping = false;
@@ -5653,11 +6205,11 @@ function removeGooseExtension(text, name) {
5653
6205
  }
5654
6206
  return kept.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
5655
6207
  }
5656
- function insertGooseExtension(text, block) {
5657
- if (!text.trim()) return "extensions:\n" + block + "\n";
5658
- const lines = text.split(/\r?\n/);
6208
+ function insertGooseExtension(text2, block) {
6209
+ if (!text2.trim()) return "extensions:\n" + block + "\n";
6210
+ const lines = text2.split(/\r?\n/);
5659
6211
  const extensionIndex = lines.findIndex((line) => /^extensions:\s*$/.test(line));
5660
- if (extensionIndex < 0) return text.trimEnd() + "\n\nextensions:\n" + block + "\n";
6212
+ if (extensionIndex < 0) return text2.trimEnd() + "\n\nextensions:\n" + block + "\n";
5661
6213
  let end = lines.length;
5662
6214
  for (let index = extensionIndex + 1; index < lines.length; index += 1) {
5663
6215
  if (/^\S/.test(lines[index]) && lines[index].trim()) {
@@ -5680,16 +6232,16 @@ function formatSkillForAgent(agent, source) {
5680
6232
  return "---\ntrigger: model_decision\ndescription: Use Topolo MCP for Topolo platform operations.\n---\n\n" + body;
5681
6233
  }
5682
6234
  async function atomicWrite(path, contents) {
5683
- await mkdir2(dirname3(path), { recursive: true });
6235
+ await mkdir3(dirname4(path), { recursive: true });
5684
6236
  const temporaryPath = path + ".topolo-" + process.pid + ".tmp";
5685
- await writeFile2(temporaryPath, contents, { encoding: "utf8", mode: 384 });
5686
- await rename(temporaryPath, path);
6237
+ await writeFile3(temporaryPath, contents, { encoding: "utf8", mode: 384 });
6238
+ await rename2(temporaryPath, path);
5687
6239
  }
5688
- function withoutMcpServerSections(text, name) {
6240
+ function withoutMcpServerSections(text2, name) {
5689
6241
  const section = "[mcp_servers." + name + "]";
5690
6242
  const environmentSection = "[mcp_servers." + name + ".env]";
5691
6243
  let skipping = false;
5692
- return text.split(/\r?\n/).filter((line) => {
6244
+ return text2.split(/\r?\n/).filter((line) => {
5693
6245
  const trimmed = line.trim();
5694
6246
  if (trimmed === section || trimmed === environmentSection) {
5695
6247
  skipping = true;
@@ -5703,11 +6255,11 @@ function withoutMcpServerSections(text, name) {
5703
6255
  // src/commands/skills.ts
5704
6256
  var AGENT_LABELS2 = AGENT_LABELS;
5705
6257
  function skillsSourceDir() {
5706
- const here = dirname4(fileURLToPath2(import.meta.url));
6258
+ const here = dirname5(fileURLToPath2(import.meta.url));
5707
6259
  const distCandidate = resolve(here, "../skills");
5708
6260
  const srcCandidate = resolve(here, "../../skills");
5709
6261
  for (const candidate of [distCandidate, srcCandidate]) {
5710
- if (existsSync4(join5(candidate, "topolo", "SKILL.md"))) return candidate;
6262
+ if (existsSync5(join6(candidate, "topolo", "SKILL.md"))) return candidate;
5711
6263
  }
5712
6264
  return distCandidate;
5713
6265
  }
@@ -5717,7 +6269,7 @@ function resolveMcpCommand() {
5717
6269
  }
5718
6270
  function agentBinaryExists(agent) {
5719
6271
  return agentCliNames(agent).some((bin) => {
5720
- const probe = spawnSync2(process.platform === "win32" ? "where" : "which", [bin], {
6272
+ const probe = spawnSync3(process.platform === "win32" ? "where" : "which", [bin], {
5721
6273
  stdio: "ignore"
5722
6274
  });
5723
6275
  return probe.status === 0;
@@ -5743,10 +6295,14 @@ function resolveMcpRegistration(options) {
5743
6295
  if (!["production", "staging", "development", "local"].includes(environment)) {
5744
6296
  throw new Error("Environment must be production, staging, development, or local.");
5745
6297
  }
5746
- return { name, ...context ? { context } : {}, environment };
6298
+ const installationId = options.installationId?.trim();
6299
+ if (installationId && /[\r\n]/.test(installationId)) {
6300
+ throw new Error("Installation id must be a single-line identifier.");
6301
+ }
6302
+ return { name, ...context ? { context } : {}, environment, ...installationId ? { installationId } : {} };
5747
6303
  }
5748
6304
  async function installForAgent(agent, sourceDir, mcp, opts) {
5749
- const source = await readFile5(join5(sourceDir, "topolo", "SKILL.md"), "utf8");
6305
+ const source = await readFile6(join6(sourceDir, "topolo", "SKILL.md"), "utf8");
5750
6306
  const skill = await installAgentSkill(agent, source, opts.homeDir, opts.projectDir);
5751
6307
  if (!opts.withMcp) {
5752
6308
  return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "skipped" };
@@ -5785,22 +6341,26 @@ function resolveTargets(opts) {
5785
6341
  }))];
5786
6342
  }
5787
6343
  if (opts.all) return [...SUPPORTED_AGENT_IDS];
5788
- const homeDir = opts.homeDir ?? homedir3();
5789
- const detectBinaries = homeDir === homedir3();
6344
+ const homeDir = opts.homeDir ?? homedir4();
6345
+ const detectBinaries = homeDir === homedir4();
5790
6346
  const detected = SUPPORTED_AGENT_IDS.filter((agent) => agentConfigExists(agent, homeDir, opts.project) || detectBinaries && agentBinaryExists(agent));
5791
6347
  return [.../* @__PURE__ */ new Set(["claude", "codex", ...detected])];
5792
6348
  }
5793
6349
  function registerSkillsCommand(program2) {
5794
6350
  const skills = program2.command("skills").description("Install the Topolo agent skill + MCP server into supported agent hosts");
5795
- skills.command("install").description("Copy the skill file and register the topolo MCP server (credential-free)").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id (for example cursor gemini)").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--no-mcp", "Copy the skill file but do not register the MCP server").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
6351
+ skills.command("install").description("Copy the skill file and register the topolo MCP server (credential-free)").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id (for example cursor gemini)").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--no-mcp", "Copy the skill file but do not register the MCP server").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(
5796
6352
  async (opts) => {
5797
6353
  const mode = resolveOutputMode(opts.json);
5798
6354
  try {
5799
- const { results, mcp } = await installAgentSurfaces({ ...opts, homeDir: opts.agentHome });
6355
+ const { results, mcp } = await installAgentSurfaces({
6356
+ ...opts,
6357
+ installationId: opts.installation,
6358
+ homeDir: opts.agentHome
6359
+ });
5800
6360
  assertAgentSetupSucceeded(results);
5801
6361
  emit({ installed: results, mcpCommand: mcp }, mode, () => formatInstall(results, mcp));
5802
6362
  } catch (err) {
5803
- fail(err, mode);
6363
+ fail2(err, mode);
5804
6364
  }
5805
6365
  }
5806
6366
  );
@@ -5816,7 +6376,7 @@ function registerSkillsCommand(program2) {
5816
6376
  });
5817
6377
  emit({ agents: rows }, mode, () => formatStatus(rows));
5818
6378
  } catch (err) {
5819
- fail(err, mode);
6379
+ fail2(err, mode);
5820
6380
  }
5821
6381
  });
5822
6382
  skills.command("uninstall").description("Remove the skill file and MCP registration for each agent").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Remove from every supported host").option("--project <dir>", "Remove project-scoped host files under <dir>").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Remove one registration (defaults to topolo)").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
@@ -5832,18 +6392,18 @@ function registerSkillsCommand(program2) {
5832
6392
  () => removed.map((r) => `${AGENT_LABELS2[r.agent]}: ${r.name} removed=${r.registrationRemoved}, skill removed=${r.skillRemoved}`).join("\n")
5833
6393
  );
5834
6394
  } catch (err) {
5835
- fail(err, mode);
6395
+ fail2(err, mode);
5836
6396
  }
5837
6397
  });
5838
6398
  }
5839
6399
  async function installAgentSurfaces(opts) {
5840
6400
  const sourceDir = skillsSourceDir();
5841
- if (!existsSync4(join5(sourceDir, "topolo", "SKILL.md"))) {
6401
+ if (!existsSync5(join6(sourceDir, "topolo", "SKILL.md"))) {
5842
6402
  throw new Error("Bundled skill file not found. Reinstall the CLI: npm install -g @topolo/cli");
5843
6403
  }
5844
6404
  const mcp = resolveMcpCommand();
5845
6405
  const registration = resolveMcpRegistration(opts);
5846
- const homeDir = opts.homeDir ?? homedir3();
6406
+ const homeDir = opts.homeDir ?? homedir4();
5847
6407
  const results = [];
5848
6408
  for (const agent of resolveTargets({ ...opts, homeDir, project: opts.project })) {
5849
6409
  results.push(await installForAgent(agent, sourceDir, mcp, {
@@ -5856,14 +6416,18 @@ async function installAgentSurfaces(opts) {
5856
6416
  return { results, mcp };
5857
6417
  }
5858
6418
  function registerSetupCommand(program2) {
5859
- program2.command("setup").description("Install the Topolo skill and deterministic local MCP server for supported agent hosts").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
6419
+ program2.command("setup").description("Install the Topolo skill and deterministic local MCP server for supported agent hosts").option("--claude", "Target Claude Code only").option("--codex", "Target Codex only").option("--agent <host...>", "Target one or more hosts by id").option("--all", "Configure every supported local host").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--agent-home <dir>", "Target an alternate agent home (managed installs and clean-room verification)").option("--name <name>", "Register a named pinned MCP, such as topolo-acme").option("--context <context>", "Pin the named MCP to an exact context returned by topolo context ls").option("--installation <id>", "Bind the MCP runtime to a registered external-agent installation").option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (opts) => {
5860
6420
  const mode = resolveOutputMode(opts.json);
5861
6421
  try {
5862
- const { results, mcp } = await installAgentSurfaces({ ...opts, homeDir: opts.agentHome });
6422
+ const { results, mcp } = await installAgentSurfaces({
6423
+ ...opts,
6424
+ installationId: opts.installation,
6425
+ homeDir: opts.agentHome
6426
+ });
5863
6427
  assertAgentSetupSucceeded(results);
5864
6428
  emit({ installed: results, mcpCommand: mcp }, mode, () => formatInstall(results, mcp));
5865
6429
  } catch (error) {
5866
- fail(error, mode);
6430
+ fail2(error, mode);
5867
6431
  }
5868
6432
  });
5869
6433
  }
@@ -5874,8 +6438,8 @@ function assertAgentSetupSucceeded(results) {
5874
6438
  }
5875
6439
  }
5876
6440
  async function agentSurfaceStatus(options = {}) {
5877
- const homeDir = options.homeDir ?? homedir3();
5878
- const sourceSkill = await readFile5(join5(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
6441
+ const homeDir = options.homeDir ?? homedir4();
6442
+ const sourceSkill = await readFile6(join6(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
5879
6443
  const agents = resolveTargets({
5880
6444
  agent: options.agent,
5881
6445
  all: options.all,
@@ -5884,7 +6448,7 @@ async function agentSurfaceStatus(options = {}) {
5884
6448
  });
5885
6449
  return Promise.all(agents.map(async (agent) => {
5886
6450
  const skillPath = agentSkillPath(agent, homeDir, options.project);
5887
- const installedSkill = skillPath && existsSync4(skillPath) ? await readFile5(skillPath, "utf8") : void 0;
6451
+ const installedSkill = skillPath && existsSync5(skillPath) ? await readFile6(skillPath, "utf8") : void 0;
5888
6452
  const registrations = (await readAgentRegistrations(agent, homeDir, options.project)).filter((registration) => !options.name || registration.name === options.name);
5889
6453
  return {
5890
6454
  agent,
@@ -5898,7 +6462,7 @@ async function agentSurfaceStatus(options = {}) {
5898
6462
  }));
5899
6463
  }
5900
6464
  async function uninstallAgentSurfaces(options) {
5901
- const homeDir = options.homeDir ?? homedir3();
6465
+ const homeDir = options.homeDir ?? homedir4();
5902
6466
  const name = options.name?.trim() || "topolo";
5903
6467
  resolveMcpRegistration({ name, ...name === "topolo" ? {} : { context: "uninstall-validation" } });
5904
6468
  const removed = [];
@@ -5908,8 +6472,8 @@ async function uninstallAgentSurfaces(options) {
5908
6472
  await unregisterAgentConfig(agent, homeDir, name, options.project);
5909
6473
  const remaining = await readAgentRegistrations(agent, homeDir, options.project);
5910
6474
  const skillPath = agentSkillPath(agent, homeDir, options.project);
5911
- const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync4(skillPath));
5912
- if (skillRemoved && skillPath) await rm2(dirname4(skillPath), { recursive: true, force: true });
6475
+ const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync5(skillPath));
6476
+ if (skillRemoved && skillPath) await rm3(dirname5(skillPath), { recursive: true, force: true });
5913
6477
  removed.push({ agent, name, registrationRemoved, skillRemoved });
5914
6478
  }
5915
6479
  return removed;
@@ -5939,7 +6503,7 @@ function formatStatus(rows) {
5939
6503
 
5940
6504
  // src/commands/doctor.ts
5941
6505
  import { access } from "fs/promises";
5942
- import { homedir as homedir4 } from "os";
6506
+ import { homedir as homedir5 } from "os";
5943
6507
  import { resolve as resolve2 } from "path";
5944
6508
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5945
6509
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -6002,7 +6566,7 @@ async function verifyMcpProtocol(command, args, env = definedEnvironment(process
6002
6566
  }
6003
6567
  async function resolveInstalledDoctorTarget(options) {
6004
6568
  const name = options.name?.trim() || "topolo";
6005
- const homeDir = options.agentHome ? resolve2(options.agentHome) : homedir4();
6569
+ const homeDir = options.agentHome ? resolve2(options.agentHome) : homedir5();
6006
6570
  const expected = resolveMcpCommand();
6007
6571
  const requestedAgents = resolveTargets({
6008
6572
  ...options,
@@ -6051,7 +6615,7 @@ function registerDoctorCommand(program2) {
6051
6615
  if (!result.ok) throw new Error(formatDoctor(result));
6052
6616
  emit(result, mode, () => formatDoctor(result));
6053
6617
  } catch (error) {
6054
- fail(error, mode);
6618
+ fail2(error, mode);
6055
6619
  }
6056
6620
  });
6057
6621
  }
@@ -6242,7 +6806,7 @@ function registerContextCommand(program2) {
6242
6806
  return lines.join("\n");
6243
6807
  });
6244
6808
  } catch (err) {
6245
- fail(err, mode);
6809
+ fail2(err, mode);
6246
6810
  }
6247
6811
  });
6248
6812
  context.command("use <target>").description('Switch the active context (an organization name/slug, or "personal")').option("--json", "Output JSON", true).option("--no-json", "Output human-readable text").action(async (target, opts) => {
@@ -6345,7 +6909,7 @@ function registerContextCommand(program2) {
6345
6909
  \u2713 Now acting as ${labelFor(resolved)} (${resolved.contextType}).`
6346
6910
  );
6347
6911
  } catch (err) {
6348
- fail(err, mode);
6912
+ fail2(err, mode);
6349
6913
  }
6350
6914
  });
6351
6915
  }
@@ -6362,7 +6926,7 @@ function registerResourcesCommand(program2) {
6362
6926
  const result = await topolo.client.listBindableResources(selector);
6363
6927
  emit(result, mode, () => formatResources(result));
6364
6928
  } catch (err) {
6365
- fail(err, mode);
6929
+ fail2(err, mode);
6366
6930
  }
6367
6931
  });
6368
6932
  }
@@ -6390,7 +6954,7 @@ function registerWorkspacesCommand(program2) {
6390
6954
  const result = await topolo.workspaces.list(application);
6391
6955
  emit(result, mode, () => formatWorkspaceList(result));
6392
6956
  } catch (error) {
6393
- fail(error, mode);
6957
+ fail2(error, mode);
6394
6958
  }
6395
6959
  });
6396
6960
  workspaces.command("create <application>").description("Create a workspace when the target application policy permits it").requiredOption("--name <name>", "Workspace display name").option("--default", "Make the new workspace the application default").option("--confirm", "Acknowledge workspace creation").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (application, opts) => {
@@ -6404,7 +6968,7 @@ function registerWorkspacesCommand(program2) {
6404
6968
  });
6405
6969
  emit(result, mode, () => formatWorkspaceMutation(result));
6406
6970
  } catch (error) {
6407
- fail(error, mode);
6971
+ fail2(error, mode);
6408
6972
  }
6409
6973
  });
6410
6974
  workspaces.command("rename <application> <workspace-id>").description("Rename a caller-authorized workspace without changing its stable id").requiredOption("--name <name>", "New workspace display name").option("--confirm", "Acknowledge workspace rename").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (application, workspaceId, opts) => {
@@ -6414,7 +6978,7 @@ function registerWorkspacesCommand(program2) {
6414
6978
  const result = await topolo.workspaces.rename(application, workspaceId, opts.name.trim(), opts.confirm === true);
6415
6979
  emit(result, mode, () => formatWorkspaceMutation(result));
6416
6980
  } catch (error) {
6417
- fail(error, mode);
6981
+ fail2(error, mode);
6418
6982
  }
6419
6983
  });
6420
6984
  workspaces.command("default <application> <workspace-id>").description("Set the default workspace for one application").option("--confirm", "Acknowledge the default-workspace change").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (application, workspaceId, opts) => {
@@ -6424,7 +6988,7 @@ function registerWorkspacesCommand(program2) {
6424
6988
  const result = await topolo.workspaces.setDefault(application, workspaceId, opts.confirm === true);
6425
6989
  emit(result, mode, () => formatWorkspaceMutation(result));
6426
6990
  } catch (error) {
6427
- fail(error, mode);
6991
+ fail2(error, mode);
6428
6992
  }
6429
6993
  });
6430
6994
  }
@@ -6453,6 +7017,195 @@ function formatWorkspaceMutation(result) {
6453
7017
  Workspace: ${workspace.name} (${workspace.id})${workspace.isDefault ? "\nDefault: yes" : ""}`;
6454
7018
  }
6455
7019
 
7020
+ // src/commands/installations.ts
7021
+ var CREATE_PRINCIPAL = "app_topolo_auth.organization_principals.create";
7022
+ var UPDATE_PRINCIPAL = "app_topolo_auth.organization_principals.update";
7023
+ var REVOKE_CREDENTIAL = "app_topolo_auth.principal_credentials.revoke";
7024
+ var INSTALLATION_BOOTSTRAP_GRANT = "app_topolo_auth:identity";
7025
+ function actionData(value) {
7026
+ if (!value || typeof value !== "object") throw new Error("Topolo returned an invalid action response.");
7027
+ const record2 = value;
7028
+ const data = record2.data;
7029
+ return data && typeof data === "object" ? data : record2;
7030
+ }
7031
+ function requiredRecord(value, name) {
7032
+ if (!value || typeof value !== "object") throw new Error(`Topolo did not return ${name}.`);
7033
+ return value;
7034
+ }
7035
+ function requiredString(record2, key) {
7036
+ const value = record2[key];
7037
+ if (typeof value !== "string" || !value.trim()) throw new Error(`Topolo did not return ${key}.`);
7038
+ return value;
7039
+ }
7040
+ async function createInstallationKey() {
7041
+ const keyPair = await crypto.subtle.generateKey(
7042
+ { name: "ECDSA", namedCurve: "P-256" },
7043
+ true,
7044
+ ["sign", "verify"]
7045
+ );
7046
+ const privateJwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
7047
+ return { privateJwk, dpopJkt: await calculateDpopJkt(privateJwk) };
7048
+ }
7049
+ async function registerExternalAgentInstallation(input2, dependencies) {
7050
+ const organizationId = dependencies.identity.organization?.id;
7051
+ if (!organizationId) throw new Error("Select an organization context before registering an installation.");
7052
+ const serviceGrants = Array.from(/* @__PURE__ */ new Set([
7053
+ INSTALLATION_BOOTSTRAP_GRANT,
7054
+ ...input2.serviceGrants.map((grant) => grant.trim()).filter(Boolean)
7055
+ ]));
7056
+ const { privateJwk, dpopJkt } = await createInstallationKey();
7057
+ const principalResult = actionData(await dependencies.client.callAction(CREATE_PRINCIPAL, {
7058
+ orgId: organizationId,
7059
+ principalType: "external_agent",
7060
+ displayName: input2.displayName,
7061
+ accountableUserId: dependencies.identity.user.id,
7062
+ provider: input2.provider,
7063
+ ...input2.model ? { model: input2.model } : {},
7064
+ serviceGrants
7065
+ }, { confirm: true }));
7066
+ const principal = requiredRecord(principalResult.principal, "principal");
7067
+ const principalId = requiredString(principal, "id");
7068
+ const installationId = typeof principal.installationId === "string" ? principal.installationId : principalId;
7069
+ let credentialId = null;
7070
+ try {
7071
+ const credentialResult = actionData(await dependencies.client.issueExternalAgentCredential({
7072
+ orgId: organizationId,
7073
+ principalId,
7074
+ displayName: `${input2.displayName} installation credential`,
7075
+ expiresAt: input2.expiresAt ?? null,
7076
+ dpopJkt
7077
+ }));
7078
+ const credential = requiredRecord(credentialResult.credential, "credential");
7079
+ credentialId = requiredString(credential, "id");
7080
+ const installation = {
7081
+ id: installationId,
7082
+ environment: dependencies.environment,
7083
+ organizationId,
7084
+ principalId,
7085
+ credentialId,
7086
+ clientId: requiredString(credential, "clientId"),
7087
+ displayName: input2.displayName,
7088
+ provider: input2.provider,
7089
+ host: input2.host,
7090
+ ...input2.model ? { model: input2.model } : {},
7091
+ dpopJkt,
7092
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
7093
+ };
7094
+ await (dependencies.write ?? writeExternalAgentInstallation)(installation, {
7095
+ clientSecret: requiredString(credential, "clientSecret"),
7096
+ privateJwk
7097
+ });
7098
+ return installation;
7099
+ } catch (error) {
7100
+ if (credentialId) {
7101
+ await dependencies.client.callAction(REVOKE_CREDENTIAL, {
7102
+ orgId: organizationId,
7103
+ principalId,
7104
+ credentialId
7105
+ }, { confirm: true }).catch(() => void 0);
7106
+ }
7107
+ await dependencies.client.callAction(UPDATE_PRINCIPAL, {
7108
+ orgId: organizationId,
7109
+ principalId,
7110
+ status: "suspended"
7111
+ }, { confirm: true }).catch(() => void 0);
7112
+ throw error;
7113
+ }
7114
+ }
7115
+ async function revokeInstallation(client, installation) {
7116
+ await client.callAction(REVOKE_CREDENTIAL, {
7117
+ orgId: installation.organizationId,
7118
+ principalId: installation.principalId,
7119
+ credentialId: installation.credentialId
7120
+ }, { confirm: true });
7121
+ await client.callAction(UPDATE_PRINCIPAL, {
7122
+ orgId: installation.organizationId,
7123
+ principalId: installation.principalId,
7124
+ status: "suspended"
7125
+ }, { confirm: true });
7126
+ await deleteExternalAgentInstallation(installation.environment, installation.id);
7127
+ }
7128
+ function parseExpiry(value) {
7129
+ if (value === void 0) return void 0;
7130
+ if (value === "never") return null;
7131
+ const timestamp2 = Math.floor(new Date(value).getTime() / 1e3);
7132
+ if (!Number.isSafeInteger(timestamp2) || timestamp2 <= Math.floor(Date.now() / 1e3)) {
7133
+ throw new Error('--expires-at must be a future ISO-8601 timestamp or "never".');
7134
+ }
7135
+ return timestamp2;
7136
+ }
7137
+ async function requiredClient3() {
7138
+ const client = await buildClient({ requireConfirmForWrites: true });
7139
+ if (!client) throw new Error("No credential configured. Run `topolo auth login`.");
7140
+ return client;
7141
+ }
7142
+ function registerInstallationsCommand(program2) {
7143
+ const installations = program2.command("installations").description("Register and manage accountable external agent installations");
7144
+ installations.command("register").requiredOption("--name <name>", "Human-readable installation name").requiredOption("--provider <provider>", "Agent provider, such as openai or anthropic").requiredOption("--host <host>", "Agent host, such as codex or claude").option("--model <model>", "Model identifier recorded for audit attribution").option("--grant <app:scope...>", "One or more explicit application grants", []).option("--expires-at <timestamp>", "Credential expiry in ISO-8601 form, or never", "never").option("--project <dir>", "Write project-scoped host files under <dir> where supported").option("--agent-home <dir>", "Target an alternate agent home").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (opts) => {
7145
+ const mode = resolveOutputMode(opts.json);
7146
+ try {
7147
+ const host = opts.host.trim();
7148
+ if (!SUPPORTED_AGENT_IDS.includes(host)) {
7149
+ throw new Error(`Unsupported agent host "${opts.host}". Supported hosts: ${SUPPORTED_AGENT_IDS.join(", ")}.`);
7150
+ }
7151
+ const topolo = await requiredClient3();
7152
+ const installation = await registerExternalAgentInstallation({
7153
+ displayName: opts.name.trim(),
7154
+ provider: opts.provider.trim(),
7155
+ host,
7156
+ model: opts.model?.trim(),
7157
+ serviceGrants: opts.grant,
7158
+ expiresAt: parseExpiry(opts.expiresAt)
7159
+ }, {
7160
+ client: topolo.client,
7161
+ identity: await topolo.identity.whoami(),
7162
+ environment: selectedEnvironment()
7163
+ });
7164
+ try {
7165
+ const configured = await installAgentSurfaces({
7166
+ agent: [host],
7167
+ project: opts.project,
7168
+ homeDir: opts.agentHome,
7169
+ installationId: installation.id
7170
+ });
7171
+ assertAgentSetupSucceeded(configured.results);
7172
+ } catch (error) {
7173
+ await revokeInstallation(topolo.client, installation).catch(() => void 0);
7174
+ throw error;
7175
+ }
7176
+ emit(
7177
+ { installation },
7178
+ mode,
7179
+ () => `Registered ${installation.displayName} (${installation.id}) for ${installation.host}.`
7180
+ );
7181
+ } catch (error) {
7182
+ fail2(error, mode);
7183
+ }
7184
+ });
7185
+ installations.command("list").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (opts) => {
7186
+ const mode = resolveOutputMode(opts.json);
7187
+ try {
7188
+ const rows = await listExternalAgentInstallations(selectedEnvironment());
7189
+ emit({ installations: rows }, mode, () => rows.length ? rows.map((row) => `${row.id} ${row.displayName} ${row.host} ${row.provider}`).join("\n") : "No external agent installations are registered on this device.");
7190
+ } catch (error) {
7191
+ fail2(error, mode);
7192
+ }
7193
+ });
7194
+ installations.command("revoke <installation-id>").description("Revoke the remote credential and remove its protected local installation").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (installationId, opts) => {
7195
+ const mode = resolveOutputMode(opts.json);
7196
+ try {
7197
+ const environment = selectedEnvironment();
7198
+ const row = (await listExternalAgentInstallations(environment)).find((installation) => installation.id === installationId);
7199
+ if (!row) throw new Error(`Installation ${installationId} was not found on this device.`);
7200
+ const topolo = await requiredClient3();
7201
+ await revokeInstallation(topolo.client, row);
7202
+ emit({ revoked: true, installationId }, mode, () => `Revoked ${installationId}.`);
7203
+ } catch (error) {
7204
+ fail2(error, mode);
7205
+ }
7206
+ });
7207
+ }
7208
+
6456
7209
  // src/index.ts
6457
7210
  installOutputErrorHandlers();
6458
7211
  var program = new Command();
@@ -6506,6 +7259,7 @@ registerDoctorCommand(program);
6506
7259
  registerContextCommand(program);
6507
7260
  registerResourcesCommand(program);
6508
7261
  registerWorkspacesCommand(program);
7262
+ registerInstallationsCommand(program);
6509
7263
  program.parseAsync(argv).catch((err) => {
6510
7264
  process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
6511
7265
  `);