@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.
- package/dist/index.js +964 -210
- 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) {
|
|
@@ -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
|
|
177
|
-
const topLevel = canonicalTopoloErrorCode(
|
|
228
|
+
const record2 = body;
|
|
229
|
+
const topLevel = canonicalTopoloErrorCode(record2.code);
|
|
178
230
|
if (topLevel) return topLevel;
|
|
179
|
-
const nested =
|
|
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
|
|
195
|
-
let candidate =
|
|
196
|
-
const nested =
|
|
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 =
|
|
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
|
|
254
|
-
const path = canonicalTopoloValidationPath(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
|
1460
|
-
if (
|
|
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:
|
|
1466
|
-
actionCount: Number(
|
|
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
|
|
2100
|
-
if (Array.isArray(
|
|
2101
|
-
if (
|
|
2102
|
-
const 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
|
|
2178
|
-
const allKeys = Object.keys(
|
|
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(
|
|
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.
|
|
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
|
|
3075
|
-
const topLevel = canonicalTopoloErrorCode2(
|
|
3473
|
+
const record2 = body;
|
|
3474
|
+
const topLevel = canonicalTopoloErrorCode2(record2.code);
|
|
3076
3475
|
if (topLevel) return topLevel;
|
|
3077
|
-
const nested =
|
|
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
|
|
3093
|
-
let candidate =
|
|
3094
|
-
const nested =
|
|
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 =
|
|
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
|
|
3152
|
-
const path = canonicalTopoloValidationPath2(
|
|
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" ?
|
|
3372
|
-
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");
|
|
3373
3923
|
}
|
|
3374
3924
|
async function readConfig() {
|
|
3375
3925
|
const path = configPath();
|
|
3376
|
-
if (!
|
|
3926
|
+
if (!existsSync2(path)) return {};
|
|
3377
3927
|
try {
|
|
3378
|
-
const raw = await
|
|
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
|
|
3387
|
-
await
|
|
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
|
|
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
|
|
4046
|
+
await mkdir2(dirname2(lockPath), { recursive: true });
|
|
3497
4047
|
const startedAt = Date.now();
|
|
3498
4048
|
while (true) {
|
|
3499
4049
|
try {
|
|
3500
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
4329
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
3780
4330
|
const candidates = [
|
|
3781
|
-
|
|
3782
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4860
|
-
const payload =
|
|
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
|
|
4875
|
-
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";
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
5579
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
5029
5580
|
for (const entry of entries) {
|
|
5030
|
-
const fullPath =
|
|
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
|
|
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
|
|
5058
|
-
import { existsSync as
|
|
5059
|
-
import { readFile as
|
|
5060
|
-
import { homedir as
|
|
5061
|
-
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";
|
|
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
|
|
5068
|
-
import { readFile as
|
|
5069
|
-
import { homedir as
|
|
5070
|
-
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";
|
|
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 =
|
|
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 =
|
|
5676
|
+
function agentSkillPath(agent, homeDir = homedir3(), projectDir) {
|
|
5126
5677
|
switch (agent) {
|
|
5127
5678
|
case "claude":
|
|
5128
|
-
return projectDir ?
|
|
5679
|
+
return projectDir ? join5(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join5(homeDir, ".claude", "skills", "topolo", "SKILL.md");
|
|
5129
5680
|
case "claude-desktop":
|
|
5130
|
-
return
|
|
5681
|
+
return join5(homeDir, ".claude", "skills", "topolo", "SKILL.md");
|
|
5131
5682
|
case "codex":
|
|
5132
|
-
return
|
|
5683
|
+
return join5(homeDir, ".codex", "skills", "topolo", "SKILL.md");
|
|
5133
5684
|
case "cursor":
|
|
5134
|
-
return
|
|
5685
|
+
return join5(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
|
|
5135
5686
|
case "grok":
|
|
5136
|
-
return
|
|
5687
|
+
return join5(projectDir ?? homeDir, ".grok", "skills", "topolo", "SKILL.md");
|
|
5137
5688
|
case "gemini":
|
|
5138
|
-
return
|
|
5689
|
+
return join5(projectDir ?? homeDir, ".gemini", "skills", "topolo", "SKILL.md");
|
|
5139
5690
|
case "copilot":
|
|
5140
|
-
return projectDir ?
|
|
5691
|
+
return projectDir ? join5(projectDir, ".github", "skills", "topolo", "SKILL.md") : join5(homeDir, ".copilot", "skills", "topolo", "SKILL.md");
|
|
5141
5692
|
case "windsurf":
|
|
5142
|
-
return projectDir ?
|
|
5693
|
+
return projectDir ? join5(projectDir, ".windsurf", "rules", "topolo.md") : void 0;
|
|
5143
5694
|
case "cline":
|
|
5144
|
-
return
|
|
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 =
|
|
5701
|
+
function agentConfigPaths(agent, homeDir = homedir3(), projectDir) {
|
|
5151
5702
|
switch (agent) {
|
|
5152
5703
|
case "claude":
|
|
5153
|
-
return [{ path:
|
|
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:
|
|
5712
|
+
return [{ path: join5(homeDir, ".codex", "config.toml"), format: "toml" }];
|
|
5162
5713
|
case "cursor":
|
|
5163
5714
|
return [{
|
|
5164
|
-
path: projectDir ?
|
|
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 ?
|
|
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 ?
|
|
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:
|
|
5182
|
-
{ path:
|
|
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:
|
|
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:
|
|
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 ?
|
|
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:
|
|
5755
|
+
return [{ path: join5(homeDir, ".config", "goose", "config.yaml"), format: "yaml" }];
|
|
5205
5756
|
case "amazon-q":
|
|
5206
5757
|
if (projectDir) {
|
|
5207
|
-
return [{ path:
|
|
5758
|
+
return [{ path: join5(projectDir, ".amazonq", "mcp.json"), format: "json", rootKey: "mcpServers" }];
|
|
5208
5759
|
}
|
|
5209
5760
|
return [{
|
|
5210
|
-
path:
|
|
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 =
|
|
5243
|
-
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));
|
|
5244
5795
|
}
|
|
5245
5796
|
function claudeDesktopConfigPath(homeDir) {
|
|
5246
|
-
if (process.platform === "darwin") return
|
|
5247
|
-
if (process.platform === "win32") return
|
|
5248
|
-
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");
|
|
5249
5800
|
}
|
|
5250
5801
|
function vscodeUserConfigPath(homeDir) {
|
|
5251
|
-
if (process.platform === "darwin") return
|
|
5252
|
-
if (process.platform === "win32") return
|
|
5253
|
-
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");
|
|
5254
5805
|
}
|
|
5255
|
-
async function readAgentRegistrations(agent, homeDir =
|
|
5806
|
+
async function readAgentRegistrations(agent, homeDir = homedir3(), projectDir) {
|
|
5256
5807
|
const registrations = [];
|
|
5257
5808
|
for (const config of agentConfigPaths(agent, homeDir, projectDir)) {
|
|
5258
|
-
if (!
|
|
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
|
|
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
|
|
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
|
|
5874
|
+
const text2 = await readFile4(path, "utf8");
|
|
5324
5875
|
const registrations = [];
|
|
5325
|
-
const lines =
|
|
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
|
|
5520
|
-
import { mkdir as
|
|
5521
|
-
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";
|
|
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 (!
|
|
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
|
|
5547
|
-
await atomicWrite(config.path,
|
|
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 (
|
|
6113
|
+
if (existsSync4(config.path)) {
|
|
5562
6114
|
try {
|
|
5563
|
-
parsed = JSON.parse(await
|
|
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
|
|
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 =
|
|
5607
|
-
const
|
|
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,
|
|
6164
|
+
await atomicWrite(path, text2 + block);
|
|
5613
6165
|
}
|
|
5614
6166
|
async function registerGoose(path, mcp, registration) {
|
|
5615
|
-
const existing =
|
|
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
|
|
6184
|
+
const existing = await readFile5(path, "utf8");
|
|
5633
6185
|
await atomicWrite(path, removeGooseExtension(existing, name));
|
|
5634
6186
|
}
|
|
5635
|
-
function removeGooseExtension(
|
|
5636
|
-
const lines =
|
|
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(
|
|
5657
|
-
if (!
|
|
5658
|
-
const lines =
|
|
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
|
|
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
|
|
6235
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
5684
6236
|
const temporaryPath = path + ".topolo-" + process.pid + ".tmp";
|
|
5685
|
-
await
|
|
5686
|
-
await
|
|
6237
|
+
await writeFile3(temporaryPath, contents, { encoding: "utf8", mode: 384 });
|
|
6238
|
+
await rename2(temporaryPath, path);
|
|
5687
6239
|
}
|
|
5688
|
-
function withoutMcpServerSections(
|
|
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
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 ??
|
|
5789
|
-
const detectBinaries = homeDir ===
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6395
|
+
fail2(err, mode);
|
|
5836
6396
|
}
|
|
5837
6397
|
});
|
|
5838
6398
|
}
|
|
5839
6399
|
async function installAgentSurfaces(opts) {
|
|
5840
6400
|
const sourceDir = skillsSourceDir();
|
|
5841
|
-
if (!
|
|
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 ??
|
|
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({
|
|
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
|
-
|
|
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 ??
|
|
5878
|
-
const sourceSkill = await
|
|
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 &&
|
|
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 ??
|
|
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 &&
|
|
5912
|
-
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 });
|
|
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
|
|
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) :
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
`);
|