@topolo/cli 0.11.0 → 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.
- package/dist/index.js +577 -129
- 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.
|
|
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",
|
|
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) {
|
|
@@ -1146,7 +1198,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1146
1198
|
timeoutMs;
|
|
1147
1199
|
fetchImpl;
|
|
1148
1200
|
credentialProvider;
|
|
1149
|
-
|
|
1201
|
+
credentialLoadsInFlight = /* @__PURE__ */ new Map();
|
|
1150
1202
|
refreshCredential;
|
|
1151
1203
|
refreshInFlight = null;
|
|
1152
1204
|
debug;
|
|
@@ -1203,7 +1255,16 @@ var TopoloClient = class _TopoloClient {
|
|
|
1203
1255
|
`Mutating ${method} requests require { confirm: true }. This guardrail prevents agents from issuing writes without an explicit human-in-the-loop acknowledgement.`
|
|
1204
1256
|
);
|
|
1205
1257
|
}
|
|
1206
|
-
|
|
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
|
+
});
|
|
1207
1268
|
const resolvedService = opts.baseUrl ? { serviceKey: normalizeLookupKey(opts.service), baseUrl: this.validatedServiceOrigin(opts.service, opts.baseUrl) } : await this.resolveRequestService(opts.service);
|
|
1208
1269
|
const baseUrl = resolvedService.baseUrl;
|
|
1209
1270
|
const url = new URL(opts.path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
|
|
@@ -1226,7 +1287,6 @@ var TopoloClient = class _TopoloClient {
|
|
|
1226
1287
|
if (opts.body !== void 0) headers.set("Content-Type", "application/json");
|
|
1227
1288
|
const platformAppId = resolveConfiguredAppId(resolvedService.serviceKey, this.appIds);
|
|
1228
1289
|
if (platformAppId) headers.set("X-App-ID", platformAppId);
|
|
1229
|
-
applyAuthHeaders(headers, this.credential);
|
|
1230
1290
|
const actorEnvelope = opts.actorEnvelope === void 0 ? void 0 : parseActorEnvelope(opts.actorEnvelope);
|
|
1231
1291
|
const requestId = actorEnvelope?.action.requestId ?? generateRequestId();
|
|
1232
1292
|
applyAuditHeaders(headers, this.agent, requestId);
|
|
@@ -1260,9 +1320,19 @@ var TopoloClient = class _TopoloClient {
|
|
|
1260
1320
|
}, this.timeoutMs);
|
|
1261
1321
|
const signal = opts.signal ? mergeSignals(opts.signal, controller.signal) : controller.signal;
|
|
1262
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
|
+
}
|
|
1263
1333
|
return await this.fetchImpl(urlStr, {
|
|
1264
1334
|
method,
|
|
1265
|
-
headers:
|
|
1335
|
+
headers: requestHeaders,
|
|
1266
1336
|
body: opts.rawBody ?? (opts.body !== void 0 ? JSON.stringify(opts.body) : null),
|
|
1267
1337
|
signal
|
|
1268
1338
|
});
|
|
@@ -1293,13 +1363,13 @@ var TopoloClient = class _TopoloClient {
|
|
|
1293
1363
|
};
|
|
1294
1364
|
let res;
|
|
1295
1365
|
try {
|
|
1296
|
-
const attemptedCredential =
|
|
1366
|
+
const attemptedCredential = requestCredential;
|
|
1297
1367
|
res = await fetchWithReadRetry();
|
|
1298
1368
|
if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
|
|
1299
1369
|
await res.body?.cancel().catch(() => void 0);
|
|
1300
1370
|
const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
|
|
1301
1371
|
if (refreshed) {
|
|
1302
|
-
|
|
1372
|
+
requestCredential = refreshed;
|
|
1303
1373
|
res = await fetchWithReadRetry();
|
|
1304
1374
|
}
|
|
1305
1375
|
}
|
|
@@ -1389,18 +1459,23 @@ var TopoloClient = class _TopoloClient {
|
|
|
1389
1459
|
});
|
|
1390
1460
|
return parsed;
|
|
1391
1461
|
}
|
|
1392
|
-
async loadCurrentCredential() {
|
|
1393
|
-
if (!this.credentialProvider) return;
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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;
|
|
1404
1479
|
}
|
|
1405
1480
|
async refreshRejectedAccessToken(attemptedAccessToken) {
|
|
1406
1481
|
if (this.credential.kind === "access_token" && this.credential.accessToken !== attemptedAccessToken) {
|
|
@@ -1730,6 +1805,14 @@ var TopoloClient = class _TopoloClient {
|
|
|
1730
1805
|
const entry = await this.getAction(action);
|
|
1731
1806
|
return this.executeResolvedAction(entry, input2, options);
|
|
1732
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
|
+
}
|
|
1733
1816
|
async uploadAction(action, content, input2, options) {
|
|
1734
1817
|
const entry = await this.getAction(action);
|
|
1735
1818
|
const contract = parseTopoloActionUploadContract(entry.agentContract?.upload);
|
|
@@ -1861,7 +1944,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1861
1944
|
}
|
|
1862
1945
|
return this.executeResolvedAction(entry, input2, options);
|
|
1863
1946
|
}
|
|
1864
|
-
async executeResolvedAction(entry, input2, options) {
|
|
1947
|
+
async executeResolvedAction(entry, input2, options, preserveSensitiveResult = false) {
|
|
1865
1948
|
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
1866
1949
|
if (agentAccess === "off") {
|
|
1867
1950
|
throw new TopoloPermissionError(
|
|
@@ -1919,7 +2002,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1919
2002
|
},
|
|
1920
2003
|
confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
|
|
1921
2004
|
});
|
|
1922
|
-
return redactActionResult(result);
|
|
2005
|
+
return preserveSensitiveResult ? result : redactActionResult(result);
|
|
1923
2006
|
}
|
|
1924
2007
|
async buildVerifiedAgentActorEnvelope(actionId, input2, resource) {
|
|
1925
2008
|
if (!this.actorContext) return void 0;
|
|
@@ -2395,6 +2478,12 @@ function pickString(value) {
|
|
|
2395
2478
|
}
|
|
2396
2479
|
return void 0;
|
|
2397
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
|
+
}
|
|
2398
2487
|
function extractRequired(body) {
|
|
2399
2488
|
if (body && typeof body === "object") {
|
|
2400
2489
|
const maybe = body;
|
|
@@ -3253,10 +3342,15 @@ function createTopolo(options) {
|
|
|
3253
3342
|
};
|
|
3254
3343
|
}
|
|
3255
3344
|
|
|
3256
|
-
// node_modules/.pnpm/@topolo+sdk@0.
|
|
3257
|
-
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";
|
|
3258
3352
|
import { existsSync } from "fs";
|
|
3259
|
-
import { homedir, platform } from "os";
|
|
3353
|
+
import { homedir, platform as hostPlatform } from "os";
|
|
3260
3354
|
import { dirname, join } from "path";
|
|
3261
3355
|
var TopoloSdkError2 = class extends Error {
|
|
3262
3356
|
code;
|
|
@@ -3665,6 +3759,157 @@ function topoloServiceOrigins2(serviceUrls, environment) {
|
|
|
3665
3759
|
])
|
|
3666
3760
|
);
|
|
3667
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
|
+
}
|
|
3668
3913
|
var TOPOLO_ENVIRONMENTS = TOPOLO_RUNTIME_ENVIRONMENTS;
|
|
3669
3914
|
function contextKey(contextType, organizationId) {
|
|
3670
3915
|
return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
|
|
@@ -3673,14 +3918,14 @@ var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
|
|
|
3673
3918
|
function configPath() {
|
|
3674
3919
|
const override = process.env.TOPOLO_CONFIG_PATH;
|
|
3675
3920
|
if (override) return override;
|
|
3676
|
-
const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ?
|
|
3677
|
-
return
|
|
3921
|
+
const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join2(homedir2(), "AppData", "Roaming") : join2(homedir2(), ".config"));
|
|
3922
|
+
return join2(base, "topolo", "config.json");
|
|
3678
3923
|
}
|
|
3679
3924
|
async function readConfig() {
|
|
3680
3925
|
const path = configPath();
|
|
3681
|
-
if (!
|
|
3926
|
+
if (!existsSync2(path)) return {};
|
|
3682
3927
|
try {
|
|
3683
|
-
const raw = await
|
|
3928
|
+
const raw = await readFile2(path, "utf8");
|
|
3684
3929
|
return JSON.parse(raw);
|
|
3685
3930
|
} catch {
|
|
3686
3931
|
return {};
|
|
@@ -3688,11 +3933,11 @@ async function readConfig() {
|
|
|
3688
3933
|
}
|
|
3689
3934
|
async function writeConfig(config) {
|
|
3690
3935
|
const path = configPath();
|
|
3691
|
-
await
|
|
3692
|
-
await
|
|
3936
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
3937
|
+
await writeFile2(path, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3693
3938
|
if (platform() !== "win32") {
|
|
3694
3939
|
try {
|
|
3695
|
-
await
|
|
3940
|
+
await chmod2(path, 384);
|
|
3696
3941
|
} catch {
|
|
3697
3942
|
}
|
|
3698
3943
|
}
|
|
@@ -3798,18 +4043,18 @@ var CONFIG_LOCK_TIMEOUT_MS = 5e3;
|
|
|
3798
4043
|
var CONFIG_LOCK_STALE_MS = 12e4;
|
|
3799
4044
|
async function withConfigWriteLock(operation) {
|
|
3800
4045
|
const lockPath = `${configPath()}.lock`;
|
|
3801
|
-
await
|
|
4046
|
+
await mkdir2(dirname2(lockPath), { recursive: true });
|
|
3802
4047
|
const startedAt = Date.now();
|
|
3803
4048
|
while (true) {
|
|
3804
4049
|
try {
|
|
3805
|
-
await
|
|
4050
|
+
await mkdir2(lockPath);
|
|
3806
4051
|
break;
|
|
3807
4052
|
} catch (error) {
|
|
3808
4053
|
if (error.code !== "EEXIST") throw error;
|
|
3809
4054
|
try {
|
|
3810
4055
|
const lock = await stat(lockPath);
|
|
3811
4056
|
if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
|
|
3812
|
-
await
|
|
4057
|
+
await rm2(lockPath, { recursive: true, force: true });
|
|
3813
4058
|
continue;
|
|
3814
4059
|
}
|
|
3815
4060
|
} catch (inspectionError) {
|
|
@@ -3825,7 +4070,7 @@ async function withConfigWriteLock(operation) {
|
|
|
3825
4070
|
try {
|
|
3826
4071
|
return await operation();
|
|
3827
4072
|
} finally {
|
|
3828
|
-
await
|
|
4073
|
+
await rm2(lockPath, { recursive: true, force: true });
|
|
3829
4074
|
}
|
|
3830
4075
|
}
|
|
3831
4076
|
async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
|
|
@@ -4077,14 +4322,14 @@ function normalizeError(err) {
|
|
|
4077
4322
|
|
|
4078
4323
|
// src/version.ts
|
|
4079
4324
|
import { readFileSync } from "fs";
|
|
4080
|
-
import { dirname as
|
|
4325
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
4081
4326
|
import { fileURLToPath } from "url";
|
|
4082
4327
|
function readCliVersion() {
|
|
4083
4328
|
try {
|
|
4084
|
-
const here =
|
|
4329
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
4085
4330
|
const candidates = [
|
|
4086
|
-
|
|
4087
|
-
|
|
4331
|
+
join3(here, "package.json"),
|
|
4332
|
+
join3(here, "..", "package.json")
|
|
4088
4333
|
];
|
|
4089
4334
|
for (const path of candidates) {
|
|
4090
4335
|
try {
|
|
@@ -5177,8 +5422,8 @@ function normalizeList(values, fallback = []) {
|
|
|
5177
5422
|
}
|
|
5178
5423
|
|
|
5179
5424
|
// src/commands/integrations.ts
|
|
5180
|
-
import { readdir, readFile as
|
|
5181
|
-
import { join as
|
|
5425
|
+
import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
5426
|
+
import { join as join4, relative } from "path";
|
|
5182
5427
|
function registerIntegrationsCommand(program2) {
|
|
5183
5428
|
const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
|
|
5184
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) => {
|
|
@@ -5321,7 +5566,7 @@ function detectFramework(packageJson, haystack) {
|
|
|
5321
5566
|
}
|
|
5322
5567
|
async function readPackageJson(repoRoot) {
|
|
5323
5568
|
try {
|
|
5324
|
-
return JSON.parse(await
|
|
5569
|
+
return JSON.parse(await readFile3(join4(repoRoot, "package.json"), "utf8"));
|
|
5325
5570
|
} catch {
|
|
5326
5571
|
return null;
|
|
5327
5572
|
}
|
|
@@ -5331,9 +5576,9 @@ async function collectFiles(repoRoot) {
|
|
|
5331
5576
|
const root = repoRoot;
|
|
5332
5577
|
async function walk(dir) {
|
|
5333
5578
|
if (result.length > 2e3) return;
|
|
5334
|
-
const entries = await
|
|
5579
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
5335
5580
|
for (const entry of entries) {
|
|
5336
|
-
const fullPath =
|
|
5581
|
+
const fullPath = join4(dir, entry.name);
|
|
5337
5582
|
if (entry.isDirectory()) {
|
|
5338
5583
|
if ([".git", "node_modules", "dist", "build", ".next", "coverage"].includes(entry.name)) continue;
|
|
5339
5584
|
await walk(fullPath);
|
|
@@ -5344,7 +5589,7 @@ async function collectFiles(repoRoot) {
|
|
|
5344
5589
|
if (info.size > 25e4) continue;
|
|
5345
5590
|
result.push({
|
|
5346
5591
|
path: relative(root, fullPath),
|
|
5347
|
-
content: await
|
|
5592
|
+
content: await readFile3(fullPath, "utf8")
|
|
5348
5593
|
});
|
|
5349
5594
|
}
|
|
5350
5595
|
}
|
|
@@ -5360,20 +5605,20 @@ function formatCheckResult(result) {
|
|
|
5360
5605
|
}
|
|
5361
5606
|
|
|
5362
5607
|
// src/commands/skills.ts
|
|
5363
|
-
import { spawnSync as
|
|
5364
|
-
import { existsSync as
|
|
5365
|
-
import { readFile as
|
|
5366
|
-
import { homedir as
|
|
5367
|
-
import { dirname as
|
|
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";
|
|
5368
5613
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5369
5614
|
import { createRequire } from "module";
|
|
5370
5615
|
|
|
5371
5616
|
// src/commands/agent-host.ts
|
|
5372
|
-
import { spawnSync } from "child_process";
|
|
5373
|
-
import { existsSync as
|
|
5374
|
-
import { readFile as
|
|
5375
|
-
import { homedir as
|
|
5376
|
-
import { isAbsolute, join as
|
|
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";
|
|
5377
5622
|
var SUPPORTED_AGENT_IDS = [
|
|
5378
5623
|
"claude",
|
|
5379
5624
|
"claude-desktop",
|
|
@@ -5422,41 +5667,41 @@ function selectStableNodeCommand(whichOutput, fallback = process.execPath) {
|
|
|
5422
5667
|
return candidate && isAbsolute(candidate) ? candidate : fallback;
|
|
5423
5668
|
}
|
|
5424
5669
|
function stableNodeCommand() {
|
|
5425
|
-
const probe =
|
|
5670
|
+
const probe = spawnSync2(process.platform === "win32" ? "where" : "which", ["node"], {
|
|
5426
5671
|
encoding: "utf8",
|
|
5427
5672
|
stdio: ["ignore", "pipe", "ignore"]
|
|
5428
5673
|
});
|
|
5429
5674
|
return selectStableNodeCommand(probe.status === 0 ? probe.stdout : void 0);
|
|
5430
5675
|
}
|
|
5431
|
-
function agentSkillPath(agent, homeDir =
|
|
5676
|
+
function agentSkillPath(agent, homeDir = homedir3(), projectDir) {
|
|
5432
5677
|
switch (agent) {
|
|
5433
5678
|
case "claude":
|
|
5434
|
-
return projectDir ?
|
|
5679
|
+
return projectDir ? join5(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join5(homeDir, ".claude", "skills", "topolo", "SKILL.md");
|
|
5435
5680
|
case "claude-desktop":
|
|
5436
|
-
return
|
|
5681
|
+
return join5(homeDir, ".claude", "skills", "topolo", "SKILL.md");
|
|
5437
5682
|
case "codex":
|
|
5438
|
-
return
|
|
5683
|
+
return join5(homeDir, ".codex", "skills", "topolo", "SKILL.md");
|
|
5439
5684
|
case "cursor":
|
|
5440
|
-
return
|
|
5685
|
+
return join5(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
|
|
5441
5686
|
case "grok":
|
|
5442
|
-
return
|
|
5687
|
+
return join5(projectDir ?? homeDir, ".grok", "skills", "topolo", "SKILL.md");
|
|
5443
5688
|
case "gemini":
|
|
5444
|
-
return
|
|
5689
|
+
return join5(projectDir ?? homeDir, ".gemini", "skills", "topolo", "SKILL.md");
|
|
5445
5690
|
case "copilot":
|
|
5446
|
-
return projectDir ?
|
|
5691
|
+
return projectDir ? join5(projectDir, ".github", "skills", "topolo", "SKILL.md") : join5(homeDir, ".copilot", "skills", "topolo", "SKILL.md");
|
|
5447
5692
|
case "windsurf":
|
|
5448
|
-
return projectDir ?
|
|
5693
|
+
return projectDir ? join5(projectDir, ".windsurf", "rules", "topolo.md") : void 0;
|
|
5449
5694
|
case "cline":
|
|
5450
|
-
return
|
|
5695
|
+
return join5(projectDir ?? homeDir, ".cline", "skills", "topolo", "SKILL.md");
|
|
5451
5696
|
case "goose":
|
|
5452
5697
|
case "amazon-q":
|
|
5453
5698
|
return void 0;
|
|
5454
5699
|
}
|
|
5455
5700
|
}
|
|
5456
|
-
function agentConfigPaths(agent, homeDir =
|
|
5701
|
+
function agentConfigPaths(agent, homeDir = homedir3(), projectDir) {
|
|
5457
5702
|
switch (agent) {
|
|
5458
5703
|
case "claude":
|
|
5459
|
-
return [{ path:
|
|
5704
|
+
return [{ path: join5(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
|
|
5460
5705
|
case "claude-desktop":
|
|
5461
5706
|
return [{
|
|
5462
5707
|
path: claudeDesktopConfigPath(homeDir),
|
|
@@ -5464,31 +5709,31 @@ function agentConfigPaths(agent, homeDir = homedir2(), projectDir) {
|
|
|
5464
5709
|
rootKey: "mcpServers"
|
|
5465
5710
|
}];
|
|
5466
5711
|
case "codex":
|
|
5467
|
-
return [{ path:
|
|
5712
|
+
return [{ path: join5(homeDir, ".codex", "config.toml"), format: "toml" }];
|
|
5468
5713
|
case "cursor":
|
|
5469
5714
|
return [{
|
|
5470
|
-
path: projectDir ?
|
|
5715
|
+
path: projectDir ? join5(projectDir, ".cursor", "mcp.json") : join5(homeDir, ".cursor", "mcp.json"),
|
|
5471
5716
|
format: "json",
|
|
5472
5717
|
rootKey: "mcpServers"
|
|
5473
5718
|
}];
|
|
5474
5719
|
case "grok":
|
|
5475
5720
|
return [{
|
|
5476
|
-
path: projectDir ?
|
|
5721
|
+
path: projectDir ? join5(projectDir, ".grok", "config.toml") : join5(homeDir, ".grok", "config.toml"),
|
|
5477
5722
|
format: "toml"
|
|
5478
5723
|
}];
|
|
5479
5724
|
case "gemini":
|
|
5480
5725
|
return [{
|
|
5481
|
-
path: projectDir ?
|
|
5726
|
+
path: projectDir ? join5(projectDir, ".gemini", "settings.json") : join5(homeDir, ".gemini", "settings.json"),
|
|
5482
5727
|
format: "json",
|
|
5483
5728
|
rootKey: "mcpServers"
|
|
5484
5729
|
}];
|
|
5485
5730
|
case "copilot":
|
|
5486
5731
|
return projectDir ? [
|
|
5487
|
-
{ path:
|
|
5488
|
-
{ path:
|
|
5732
|
+
{ path: join5(projectDir, ".mcp.json"), format: "json", rootKey: "mcpServers" },
|
|
5733
|
+
{ path: join5(projectDir, ".vscode", "mcp.json"), format: "json", rootKey: "servers" }
|
|
5489
5734
|
] : [
|
|
5490
5735
|
{
|
|
5491
|
-
path:
|
|
5736
|
+
path: join5(homeDir, ".copilot", "mcp-config.json"),
|
|
5492
5737
|
format: "json",
|
|
5493
5738
|
rootKey: "mcpServers"
|
|
5494
5739
|
},
|
|
@@ -5496,24 +5741,24 @@ function agentConfigPaths(agent, homeDir = homedir2(), projectDir) {
|
|
|
5496
5741
|
];
|
|
5497
5742
|
case "windsurf":
|
|
5498
5743
|
return [{
|
|
5499
|
-
path:
|
|
5744
|
+
path: join5(homeDir, ".codeium", "windsurf", "mcp_config.json"),
|
|
5500
5745
|
format: "json",
|
|
5501
5746
|
rootKey: "mcpServers"
|
|
5502
5747
|
}];
|
|
5503
5748
|
case "cline":
|
|
5504
5749
|
return [{
|
|
5505
|
-
path: projectDir ?
|
|
5750
|
+
path: projectDir ? join5(projectDir, ".cline", "mcp.json") : join5(homeDir, ".cline", "mcp.json"),
|
|
5506
5751
|
format: "json",
|
|
5507
5752
|
rootKey: "mcpServers"
|
|
5508
5753
|
}];
|
|
5509
5754
|
case "goose":
|
|
5510
|
-
return [{ path:
|
|
5755
|
+
return [{ path: join5(homeDir, ".config", "goose", "config.yaml"), format: "yaml" }];
|
|
5511
5756
|
case "amazon-q":
|
|
5512
5757
|
if (projectDir) {
|
|
5513
|
-
return [{ path:
|
|
5758
|
+
return [{ path: join5(projectDir, ".amazonq", "mcp.json"), format: "json", rootKey: "mcpServers" }];
|
|
5514
5759
|
}
|
|
5515
5760
|
return [{
|
|
5516
|
-
path:
|
|
5761
|
+
path: existsSync3(join5(homeDir, ".aws", "amazonq", "mcp.json")) ? join5(homeDir, ".aws", "amazonq", "mcp.json") : join5(homeDir, ".aws", "amazonq", "default.json"),
|
|
5517
5762
|
format: "json",
|
|
5518
5763
|
rootKey: "mcpServers"
|
|
5519
5764
|
}];
|
|
@@ -5545,23 +5790,23 @@ function agentCliNames(agent) {
|
|
|
5545
5790
|
return ["qchat", "q"];
|
|
5546
5791
|
}
|
|
5547
5792
|
}
|
|
5548
|
-
function agentConfigExists(agent, homeDir =
|
|
5549
|
-
return agentConfigPaths(agent, homeDir, projectDir).some((config) =>
|
|
5793
|
+
function agentConfigExists(agent, homeDir = homedir3(), projectDir) {
|
|
5794
|
+
return agentConfigPaths(agent, homeDir, projectDir).some((config) => existsSync3(config.path));
|
|
5550
5795
|
}
|
|
5551
5796
|
function claudeDesktopConfigPath(homeDir) {
|
|
5552
|
-
if (process.platform === "darwin") return
|
|
5553
|
-
if (process.platform === "win32") return
|
|
5554
|
-
return
|
|
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");
|
|
5555
5800
|
}
|
|
5556
5801
|
function vscodeUserConfigPath(homeDir) {
|
|
5557
|
-
if (process.platform === "darwin") return
|
|
5558
|
-
if (process.platform === "win32") return
|
|
5559
|
-
return
|
|
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");
|
|
5560
5805
|
}
|
|
5561
|
-
async function readAgentRegistrations(agent, homeDir =
|
|
5806
|
+
async function readAgentRegistrations(agent, homeDir = homedir3(), projectDir) {
|
|
5562
5807
|
const registrations = [];
|
|
5563
5808
|
for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
|
|
5564
|
-
if (!
|
|
5809
|
+
if (!existsSync3(config.path)) continue;
|
|
5565
5810
|
if (config.format === "json") {
|
|
5566
5811
|
registrations.push(...await readJsonRegistrations(config.path, config.rootKey ?? "mcpServers", agent));
|
|
5567
5812
|
} else if (config.format === "toml") {
|
|
@@ -5575,7 +5820,7 @@ async function readAgentRegistrations(agent, homeDir = homedir2(), projectDir) {
|
|
|
5575
5820
|
async function readJsonRegistrations(path, rootKey, agent) {
|
|
5576
5821
|
let parsed;
|
|
5577
5822
|
try {
|
|
5578
|
-
parsed = JSON.parse(await
|
|
5823
|
+
parsed = JSON.parse(await readFile4(path, "utf8"));
|
|
5579
5824
|
} catch (error) {
|
|
5580
5825
|
throw new Error("Cannot read malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + path + ": " + message(error));
|
|
5581
5826
|
}
|
|
@@ -5587,7 +5832,7 @@ async function readTomlRegistrations(path, agent) {
|
|
|
5587
5832
|
const registrations = /* @__PURE__ */ new Map();
|
|
5588
5833
|
let section;
|
|
5589
5834
|
let pending;
|
|
5590
|
-
for (const rawLine of (await
|
|
5835
|
+
for (const rawLine of (await readFile4(path, "utf8")).split(/\r?\n/)) {
|
|
5591
5836
|
const line = stripTomlComment(rawLine).trim();
|
|
5592
5837
|
if (pending) {
|
|
5593
5838
|
if (line.startsWith("[")) {
|
|
@@ -5626,7 +5871,7 @@ async function readTomlRegistrations(path, agent) {
|
|
|
5626
5871
|
return [...registrations.values()].filter((registration) => registration.command);
|
|
5627
5872
|
}
|
|
5628
5873
|
async function readGooseRegistrations(path) {
|
|
5629
|
-
const text2 = await
|
|
5874
|
+
const text2 = await readFile4(path, "utf8");
|
|
5630
5875
|
const registrations = [];
|
|
5631
5876
|
const lines = text2.split(/\r?\n/);
|
|
5632
5877
|
let inExtensions = false;
|
|
@@ -5822,9 +6067,9 @@ function message(error) {
|
|
|
5822
6067
|
}
|
|
5823
6068
|
|
|
5824
6069
|
// src/commands/agent-config.ts
|
|
5825
|
-
import { existsSync as
|
|
5826
|
-
import { mkdir as
|
|
5827
|
-
import { dirname as
|
|
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";
|
|
5828
6073
|
async function installAgentSkill(agent, source, homeDir, projectDir) {
|
|
5829
6074
|
const path = agentSkillPath(agent, homeDir, projectDir);
|
|
5830
6075
|
if (!path) return { installed: false };
|
|
@@ -5845,11 +6090,11 @@ async function registerAgentConfig(agent, mcp, homeDir, registration, projectDir
|
|
|
5845
6090
|
}
|
|
5846
6091
|
async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
|
|
5847
6092
|
for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
|
|
5848
|
-
if (!
|
|
6093
|
+
if (!existsSync4(config.path)) continue;
|
|
5849
6094
|
if (config.format === "json") {
|
|
5850
6095
|
await unregisterJson(config, agent, name);
|
|
5851
6096
|
} else if (config.format === "toml") {
|
|
5852
|
-
const text2 = withoutMcpServerSections(await
|
|
6097
|
+
const text2 = withoutMcpServerSections(await readFile5(config.path, "utf8"), name);
|
|
5853
6098
|
await atomicWrite(config.path, text2 ? text2 + "\n" : "");
|
|
5854
6099
|
} else {
|
|
5855
6100
|
await unregisterGoose(config.path, name);
|
|
@@ -5859,14 +6104,15 @@ async function unregisterAgentConfig(agent, homeDir, name, projectDir) {
|
|
|
5859
6104
|
function registrationEnvironment(agent, registration) {
|
|
5860
6105
|
return {
|
|
5861
6106
|
TOPOLO_AGENT_NAME: AGENT_NAME_FOR[agent],
|
|
6107
|
+
...registration.installationId ? { TOPOLO_AGENT_INSTALLATION_ID: registration.installationId } : {},
|
|
5862
6108
|
...registration.context ? { TOPOLO_CONTEXT: registration.context, TOPOLO_ENV: registration.environment } : {}
|
|
5863
6109
|
};
|
|
5864
6110
|
}
|
|
5865
6111
|
async function registerJson(config, agent, mcp, registration) {
|
|
5866
6112
|
let parsed = {};
|
|
5867
|
-
if (
|
|
6113
|
+
if (existsSync4(config.path)) {
|
|
5868
6114
|
try {
|
|
5869
|
-
parsed = JSON.parse(await
|
|
6115
|
+
parsed = JSON.parse(await readFile5(config.path, "utf8"));
|
|
5870
6116
|
} catch (error) {
|
|
5871
6117
|
throw new Error(
|
|
5872
6118
|
"Cannot update malformed " + (agent === "claude" ? "Claude" : AGENT_LABELS[agent]) + " configuration at " + config.path + ": " + (error instanceof Error ? error.message : String(error))
|
|
@@ -5892,7 +6138,7 @@ async function registerJson(config, agent, mcp, registration) {
|
|
|
5892
6138
|
async function unregisterJson(config, agent, name) {
|
|
5893
6139
|
let parsed;
|
|
5894
6140
|
try {
|
|
5895
|
-
parsed = JSON.parse(await
|
|
6141
|
+
parsed = JSON.parse(await readFile5(config.path, "utf8"));
|
|
5896
6142
|
} catch (error) {
|
|
5897
6143
|
throw new Error(
|
|
5898
6144
|
"Cannot update malformed " + AGENT_LABELS[agent] + " configuration at " + config.path + ": " + (error instanceof Error ? error.message : String(error))
|
|
@@ -5909,7 +6155,7 @@ async function unregisterJson(config, agent, name) {
|
|
|
5909
6155
|
await atomicWrite(config.path, JSON.stringify(parsed, null, 2) + "\n");
|
|
5910
6156
|
}
|
|
5911
6157
|
async function registerToml(path, agent, mcp, registration) {
|
|
5912
|
-
const existing =
|
|
6158
|
+
const existing = existsSync4(path) ? await readFile5(path, "utf8") : "";
|
|
5913
6159
|
const text2 = withoutMcpServerSections(existing, registration.name);
|
|
5914
6160
|
const args = mcp.args.length ? "\nargs = [" + mcp.args.map((item) => JSON.stringify(item)).join(", ") + "]" : "";
|
|
5915
6161
|
const environment = registrationEnvironment(agent, registration);
|
|
@@ -5918,7 +6164,7 @@ async function registerToml(path, agent, mcp, registration) {
|
|
|
5918
6164
|
await atomicWrite(path, text2 + block);
|
|
5919
6165
|
}
|
|
5920
6166
|
async function registerGoose(path, mcp, registration) {
|
|
5921
|
-
const existing =
|
|
6167
|
+
const existing = existsSync4(path) ? await readFile5(path, "utf8") : "";
|
|
5922
6168
|
const withoutTopolo = removeGooseExtension(existing, registration.name);
|
|
5923
6169
|
const environment = registrationEnvironment("goose", registration);
|
|
5924
6170
|
const block = [
|
|
@@ -5935,7 +6181,7 @@ async function registerGoose(path, mcp, registration) {
|
|
|
5935
6181
|
await atomicWrite(path, insertGooseExtension(withoutTopolo, block));
|
|
5936
6182
|
}
|
|
5937
6183
|
async function unregisterGoose(path, name) {
|
|
5938
|
-
const existing = await
|
|
6184
|
+
const existing = await readFile5(path, "utf8");
|
|
5939
6185
|
await atomicWrite(path, removeGooseExtension(existing, name));
|
|
5940
6186
|
}
|
|
5941
6187
|
function removeGooseExtension(text2, name) {
|
|
@@ -5986,10 +6232,10 @@ function formatSkillForAgent(agent, source) {
|
|
|
5986
6232
|
return "---\ntrigger: model_decision\ndescription: Use Topolo MCP for Topolo platform operations.\n---\n\n" + body;
|
|
5987
6233
|
}
|
|
5988
6234
|
async function atomicWrite(path, contents) {
|
|
5989
|
-
await
|
|
6235
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
5990
6236
|
const temporaryPath = path + ".topolo-" + process.pid + ".tmp";
|
|
5991
|
-
await
|
|
5992
|
-
await
|
|
6237
|
+
await writeFile3(temporaryPath, contents, { encoding: "utf8", mode: 384 });
|
|
6238
|
+
await rename2(temporaryPath, path);
|
|
5993
6239
|
}
|
|
5994
6240
|
function withoutMcpServerSections(text2, name) {
|
|
5995
6241
|
const section = "[mcp_servers." + name + "]";
|
|
@@ -6009,11 +6255,11 @@ function withoutMcpServerSections(text2, name) {
|
|
|
6009
6255
|
// src/commands/skills.ts
|
|
6010
6256
|
var AGENT_LABELS2 = AGENT_LABELS;
|
|
6011
6257
|
function skillsSourceDir() {
|
|
6012
|
-
const here =
|
|
6258
|
+
const here = dirname5(fileURLToPath2(import.meta.url));
|
|
6013
6259
|
const distCandidate = resolve(here, "../skills");
|
|
6014
6260
|
const srcCandidate = resolve(here, "../../skills");
|
|
6015
6261
|
for (const candidate of [distCandidate, srcCandidate]) {
|
|
6016
|
-
if (
|
|
6262
|
+
if (existsSync5(join6(candidate, "topolo", "SKILL.md"))) return candidate;
|
|
6017
6263
|
}
|
|
6018
6264
|
return distCandidate;
|
|
6019
6265
|
}
|
|
@@ -6023,7 +6269,7 @@ function resolveMcpCommand() {
|
|
|
6023
6269
|
}
|
|
6024
6270
|
function agentBinaryExists(agent) {
|
|
6025
6271
|
return agentCliNames(agent).some((bin) => {
|
|
6026
|
-
const probe =
|
|
6272
|
+
const probe = spawnSync3(process.platform === "win32" ? "where" : "which", [bin], {
|
|
6027
6273
|
stdio: "ignore"
|
|
6028
6274
|
});
|
|
6029
6275
|
return probe.status === 0;
|
|
@@ -6049,10 +6295,14 @@ function resolveMcpRegistration(options) {
|
|
|
6049
6295
|
if (!["production", "staging", "development", "local"].includes(environment)) {
|
|
6050
6296
|
throw new Error("Environment must be production, staging, development, or local.");
|
|
6051
6297
|
}
|
|
6052
|
-
|
|
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 } : {} };
|
|
6053
6303
|
}
|
|
6054
6304
|
async function installForAgent(agent, sourceDir, mcp, opts) {
|
|
6055
|
-
const source = await
|
|
6305
|
+
const source = await readFile6(join6(sourceDir, "topolo", "SKILL.md"), "utf8");
|
|
6056
6306
|
const skill = await installAgentSkill(agent, source, opts.homeDir, opts.projectDir);
|
|
6057
6307
|
if (!opts.withMcp) {
|
|
6058
6308
|
return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "skipped" };
|
|
@@ -6091,18 +6341,22 @@ function resolveTargets(opts) {
|
|
|
6091
6341
|
}))];
|
|
6092
6342
|
}
|
|
6093
6343
|
if (opts.all) return [...SUPPORTED_AGENT_IDS];
|
|
6094
|
-
const homeDir = opts.homeDir ??
|
|
6095
|
-
const detectBinaries = homeDir ===
|
|
6344
|
+
const homeDir = opts.homeDir ?? homedir4();
|
|
6345
|
+
const detectBinaries = homeDir === homedir4();
|
|
6096
6346
|
const detected = SUPPORTED_AGENT_IDS.filter((agent) => agentConfigExists(agent, homeDir, opts.project) || detectBinaries && agentBinaryExists(agent));
|
|
6097
6347
|
return [.../* @__PURE__ */ new Set(["claude", "codex", ...detected])];
|
|
6098
6348
|
}
|
|
6099
6349
|
function registerSkillsCommand(program2) {
|
|
6100
6350
|
const skills = program2.command("skills").description("Install the Topolo agent skill + MCP server into supported agent hosts");
|
|
6101
|
-
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(
|
|
6102
6352
|
async (opts) => {
|
|
6103
6353
|
const mode = resolveOutputMode(opts.json);
|
|
6104
6354
|
try {
|
|
6105
|
-
const { results, mcp } = await installAgentSurfaces({
|
|
6355
|
+
const { results, mcp } = await installAgentSurfaces({
|
|
6356
|
+
...opts,
|
|
6357
|
+
installationId: opts.installation,
|
|
6358
|
+
homeDir: opts.agentHome
|
|
6359
|
+
});
|
|
6106
6360
|
assertAgentSetupSucceeded(results);
|
|
6107
6361
|
emit({ installed: results, mcpCommand: mcp }, mode, () => formatInstall(results, mcp));
|
|
6108
6362
|
} catch (err) {
|
|
@@ -6144,12 +6398,12 @@ function registerSkillsCommand(program2) {
|
|
|
6144
6398
|
}
|
|
6145
6399
|
async function installAgentSurfaces(opts) {
|
|
6146
6400
|
const sourceDir = skillsSourceDir();
|
|
6147
|
-
if (!
|
|
6401
|
+
if (!existsSync5(join6(sourceDir, "topolo", "SKILL.md"))) {
|
|
6148
6402
|
throw new Error("Bundled skill file not found. Reinstall the CLI: npm install -g @topolo/cli");
|
|
6149
6403
|
}
|
|
6150
6404
|
const mcp = resolveMcpCommand();
|
|
6151
6405
|
const registration = resolveMcpRegistration(opts);
|
|
6152
|
-
const homeDir = opts.homeDir ??
|
|
6406
|
+
const homeDir = opts.homeDir ?? homedir4();
|
|
6153
6407
|
const results = [];
|
|
6154
6408
|
for (const agent of resolveTargets({ ...opts, homeDir, project: opts.project })) {
|
|
6155
6409
|
results.push(await installForAgent(agent, sourceDir, mcp, {
|
|
@@ -6162,10 +6416,14 @@ async function installAgentSurfaces(opts) {
|
|
|
6162
6416
|
return { results, mcp };
|
|
6163
6417
|
}
|
|
6164
6418
|
function registerSetupCommand(program2) {
|
|
6165
|
-
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) => {
|
|
6166
6420
|
const mode = resolveOutputMode(opts.json);
|
|
6167
6421
|
try {
|
|
6168
|
-
const { results, mcp } = await installAgentSurfaces({
|
|
6422
|
+
const { results, mcp } = await installAgentSurfaces({
|
|
6423
|
+
...opts,
|
|
6424
|
+
installationId: opts.installation,
|
|
6425
|
+
homeDir: opts.agentHome
|
|
6426
|
+
});
|
|
6169
6427
|
assertAgentSetupSucceeded(results);
|
|
6170
6428
|
emit({ installed: results, mcpCommand: mcp }, mode, () => formatInstall(results, mcp));
|
|
6171
6429
|
} catch (error) {
|
|
@@ -6180,8 +6438,8 @@ function assertAgentSetupSucceeded(results) {
|
|
|
6180
6438
|
}
|
|
6181
6439
|
}
|
|
6182
6440
|
async function agentSurfaceStatus(options = {}) {
|
|
6183
|
-
const homeDir = options.homeDir ??
|
|
6184
|
-
const sourceSkill = await
|
|
6441
|
+
const homeDir = options.homeDir ?? homedir4();
|
|
6442
|
+
const sourceSkill = await readFile6(join6(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
|
|
6185
6443
|
const agents = resolveTargets({
|
|
6186
6444
|
agent: options.agent,
|
|
6187
6445
|
all: options.all,
|
|
@@ -6190,7 +6448,7 @@ async function agentSurfaceStatus(options = {}) {
|
|
|
6190
6448
|
});
|
|
6191
6449
|
return Promise.all(agents.map(async (agent) => {
|
|
6192
6450
|
const skillPath = agentSkillPath(agent, homeDir, options.project);
|
|
6193
|
-
const installedSkill = skillPath &&
|
|
6451
|
+
const installedSkill = skillPath && existsSync5(skillPath) ? await readFile6(skillPath, "utf8") : void 0;
|
|
6194
6452
|
const registrations = (await readAgentRegistrations(agent, homeDir, options.project)).filter((registration) => !options.name || registration.name === options.name);
|
|
6195
6453
|
return {
|
|
6196
6454
|
agent,
|
|
@@ -6204,7 +6462,7 @@ async function agentSurfaceStatus(options = {}) {
|
|
|
6204
6462
|
}));
|
|
6205
6463
|
}
|
|
6206
6464
|
async function uninstallAgentSurfaces(options) {
|
|
6207
|
-
const homeDir = options.homeDir ??
|
|
6465
|
+
const homeDir = options.homeDir ?? homedir4();
|
|
6208
6466
|
const name = options.name?.trim() || "topolo";
|
|
6209
6467
|
resolveMcpRegistration({ name, ...name === "topolo" ? {} : { context: "uninstall-validation" } });
|
|
6210
6468
|
const removed = [];
|
|
@@ -6214,8 +6472,8 @@ async function uninstallAgentSurfaces(options) {
|
|
|
6214
6472
|
await unregisterAgentConfig(agent, homeDir, name, options.project);
|
|
6215
6473
|
const remaining = await readAgentRegistrations(agent, homeDir, options.project);
|
|
6216
6474
|
const skillPath = agentSkillPath(agent, homeDir, options.project);
|
|
6217
|
-
const skillRemoved = remaining.length === 0 && Boolean(skillPath &&
|
|
6218
|
-
if (skillRemoved && skillPath) await
|
|
6475
|
+
const skillRemoved = remaining.length === 0 && Boolean(skillPath && existsSync5(skillPath));
|
|
6476
|
+
if (skillRemoved && skillPath) await rm3(dirname5(skillPath), { recursive: true, force: true });
|
|
6219
6477
|
removed.push({ agent, name, registrationRemoved, skillRemoved });
|
|
6220
6478
|
}
|
|
6221
6479
|
return removed;
|
|
@@ -6245,7 +6503,7 @@ function formatStatus(rows) {
|
|
|
6245
6503
|
|
|
6246
6504
|
// src/commands/doctor.ts
|
|
6247
6505
|
import { access } from "fs/promises";
|
|
6248
|
-
import { homedir as
|
|
6506
|
+
import { homedir as homedir5 } from "os";
|
|
6249
6507
|
import { resolve as resolve2 } from "path";
|
|
6250
6508
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6251
6509
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
@@ -6308,7 +6566,7 @@ async function verifyMcpProtocol(command, args, env = definedEnvironment(process
|
|
|
6308
6566
|
}
|
|
6309
6567
|
async function resolveInstalledDoctorTarget(options) {
|
|
6310
6568
|
const name = options.name?.trim() || "topolo";
|
|
6311
|
-
const homeDir = options.agentHome ? resolve2(options.agentHome) :
|
|
6569
|
+
const homeDir = options.agentHome ? resolve2(options.agentHome) : homedir5();
|
|
6312
6570
|
const expected = resolveMcpCommand();
|
|
6313
6571
|
const requestedAgents = resolveTargets({
|
|
6314
6572
|
...options,
|
|
@@ -6759,6 +7017,195 @@ function formatWorkspaceMutation(result) {
|
|
|
6759
7017
|
Workspace: ${workspace.name} (${workspace.id})${workspace.isDefault ? "\nDefault: yes" : ""}`;
|
|
6760
7018
|
}
|
|
6761
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
|
+
|
|
6762
7209
|
// src/index.ts
|
|
6763
7210
|
installOutputErrorHandlers();
|
|
6764
7211
|
var program = new Command();
|
|
@@ -6812,6 +7259,7 @@ registerDoctorCommand(program);
|
|
|
6812
7259
|
registerContextCommand(program);
|
|
6813
7260
|
registerResourcesCommand(program);
|
|
6814
7261
|
registerWorkspacesCommand(program);
|
|
7262
|
+
registerInstallationsCommand(program);
|
|
6815
7263
|
program.parseAsync(argv).catch((err) => {
|
|
6816
7264
|
process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
|
|
6817
7265
|
`);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
3
|
"packageManager": "pnpm@10.17.1",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"topoloPublishedVersion": "0.
|
|
4
|
+
"version": "0.12.0",
|
|
5
|
+
"topoloPublishedVersion": "0.12.0",
|
|
6
6
|
"description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
|
|
7
7
|
"license": "UNLICENSED",
|
|
8
8
|
"author": "Topolo",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
31
|
-
"@topolo/mcp": "0.
|
|
32
|
-
"@topolo/sdk": "0.
|
|
31
|
+
"@topolo/mcp": "0.13.0",
|
|
32
|
+
"@topolo/sdk": "0.21.0",
|
|
33
33
|
"commander": "^12.1.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|