@mutagent/cli 0.2.52 → 0.2.54
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/bin/cli.js +156 -41
- package/dist/index.js +151 -34
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -893,6 +893,35 @@ var init_config = __esm(() => {
|
|
|
893
893
|
CREDENTIALS_FILE = join2(CREDENTIALS_DIR, "credentials.json");
|
|
894
894
|
});
|
|
895
895
|
|
|
896
|
+
// src/lib/tenancy-error.ts
|
|
897
|
+
function shellQuote(value) {
|
|
898
|
+
if (/^[A-Za-z0-9_-]+$/.test(value))
|
|
899
|
+
return value;
|
|
900
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
901
|
+
}
|
|
902
|
+
function workspaceDenied(ctx) {
|
|
903
|
+
const shown = ctx.workspaceName ?? ctx.workspaceId ?? "workspace";
|
|
904
|
+
const own = ctx.keyScope === "workspace" ? ctx.keyWorkspace : undefined;
|
|
905
|
+
if (ctx.workspaceOrigin === "selection" && own && own.id !== ctx.workspaceId) {
|
|
906
|
+
return new TenancyError(TENANCY_DENIED, `This API key only works in workspace ${own.name}; your saved selection is ${shown}.`, `Run: mutagent workspaces use ${shellQuote(own.name)}`);
|
|
907
|
+
}
|
|
908
|
+
if (ctx.workspaceOrigin === "selection") {
|
|
909
|
+
return new TenancyError(TENANCY_DENIED, `Your saved workspace ${shown} is no longer available to you.`, "Run: mutagent workspaces use <name>");
|
|
910
|
+
}
|
|
911
|
+
const where = ctx.organizationName ? ` in ${ctx.organizationName}` : "";
|
|
912
|
+
return new TenancyError(TENANCY_DENIED, `No workspace "${shown}" among your workspaces${where}.`, "See: mutagent workspaces list");
|
|
913
|
+
}
|
|
914
|
+
var TENANCY_DENIED = "TENANCY_DENIED", TenancyError;
|
|
915
|
+
var init_tenancy_error = __esm(() => {
|
|
916
|
+
init_errors();
|
|
917
|
+
TenancyError = class TenancyError extends MutagentError {
|
|
918
|
+
constructor(code, what, fix) {
|
|
919
|
+
super(code, `${what} ${fix}`, fix.replace(/^(Run|See|To switch): /, ""));
|
|
920
|
+
this.name = "TenancyError";
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
});
|
|
924
|
+
|
|
896
925
|
// src/lib/organization-scope.ts
|
|
897
926
|
function looksLikeWorkspaceId(value) {
|
|
898
927
|
return UUID_PATTERN.test(value);
|
|
@@ -901,11 +930,6 @@ function tenancyHeaders(t) {
|
|
|
901
930
|
const headers = {};
|
|
902
931
|
if (!t.workspaceId)
|
|
903
932
|
return headers;
|
|
904
|
-
if (t.keyScope === "workspace") {
|
|
905
|
-
if (t.workspaceOrigin === "override")
|
|
906
|
-
headers["x-workspace-id"] = t.workspaceId;
|
|
907
|
-
return headers;
|
|
908
|
-
}
|
|
909
933
|
headers["x-workspace-id"] = t.workspaceId;
|
|
910
934
|
if (t.keyScope === "user" && t.organizationId)
|
|
911
935
|
headers["x-organization-id"] = t.organizationId;
|
|
@@ -915,7 +939,6 @@ function configTenancyHeaders(config) {
|
|
|
915
939
|
return tenancyHeaders({
|
|
916
940
|
keyScope: config.keyScope,
|
|
917
941
|
workspaceId: config.defaultWorkspace,
|
|
918
|
-
workspaceOrigin: config.workspaceOrigin,
|
|
919
942
|
organizationId: config.defaultOrganization
|
|
920
943
|
});
|
|
921
944
|
}
|
|
@@ -930,14 +953,6 @@ function parseBody(body) {
|
|
|
930
953
|
function deadKeyError() {
|
|
931
954
|
return new TenancyError("AUTH_EXPIRED", "Your CLI key expired or was revoked.", "Run: mutagent login");
|
|
932
955
|
}
|
|
933
|
-
function workspaceDenied(ctx) {
|
|
934
|
-
const shown = ctx.workspaceName ?? ctx.workspaceId ?? "workspace";
|
|
935
|
-
if (ctx.workspaceOrigin === "selection") {
|
|
936
|
-
return new TenancyError(TENANCY_DENIED, `Your saved workspace ${shown} is no longer available to you.`, "Run: mutagent workspaces use <name>");
|
|
937
|
-
}
|
|
938
|
-
const where = ctx.organizationName ? ` in ${ctx.organizationName}` : "";
|
|
939
|
-
return new TenancyError(TENANCY_DENIED, `No workspace "${shown}" among your workspaces${where}.`, "See: mutagent workspaces list");
|
|
940
|
-
}
|
|
941
956
|
function tenancyFailure(status, body, ctx) {
|
|
942
957
|
const parsed = parseBody(body);
|
|
943
958
|
if (status === 401)
|
|
@@ -1003,11 +1018,6 @@ function matchesOrganization(value, org) {
|
|
|
1003
1018
|
return true;
|
|
1004
1019
|
return org.name !== undefined && value.toLowerCase() === org.name.toLowerCase();
|
|
1005
1020
|
}
|
|
1006
|
-
function shellQuote(value) {
|
|
1007
|
-
if (/^[A-Za-z0-9_-]+$/.test(value))
|
|
1008
|
-
return value;
|
|
1009
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1010
|
-
}
|
|
1011
1021
|
function orgMismatch(value, org) {
|
|
1012
1022
|
const shown = org.slug ? `${org.name} (${org.slug})` : org.name;
|
|
1013
1023
|
return new TenancyError("ORG_MISMATCH", `Your key is for ${shown}.`, `Run: mutagent login --org ${shellQuote(value)}`);
|
|
@@ -1052,6 +1062,17 @@ ${apiKey}`;
|
|
|
1052
1062
|
probe.catch(() => keySelfProbes.delete(cacheKey));
|
|
1053
1063
|
return probe;
|
|
1054
1064
|
}
|
|
1065
|
+
function confirmKeyScope(apiKey, endpoint) {
|
|
1066
|
+
const cacheKey = `${endpoint}
|
|
1067
|
+
${apiKey}`;
|
|
1068
|
+
const done = keyScopeConfirmations.get(cacheKey);
|
|
1069
|
+
if (done)
|
|
1070
|
+
return done;
|
|
1071
|
+
const read = readKeyScope(apiKey, endpoint, !envKeyReplacesFile());
|
|
1072
|
+
keyScopeConfirmations.set(cacheKey, read);
|
|
1073
|
+
keySelfProbes.set(cacheKey, read);
|
|
1074
|
+
return read;
|
|
1075
|
+
}
|
|
1055
1076
|
async function readKeyScope(apiKey, endpoint, fromFile) {
|
|
1056
1077
|
try {
|
|
1057
1078
|
const self = await fetchKeySelf(apiKey, endpoint);
|
|
@@ -1119,7 +1140,14 @@ async function findWorkspace(wanted, t) {
|
|
|
1119
1140
|
if (t.workspaceId && (wanted === t.workspaceId || wanted === t.workspaceName)) {
|
|
1120
1141
|
return { id: t.workspaceId, name: t.workspaceName ?? t.workspaceId };
|
|
1121
1142
|
}
|
|
1122
|
-
|
|
1143
|
+
const self = await confirmKeyScope(t.apiKey, t.endpoint);
|
|
1144
|
+
if (self && self.scope !== "workspace") {
|
|
1145
|
+
return findWorkspace(wanted, { ...t, keyScope: self.scope, organization: self.organization ?? t.organization });
|
|
1146
|
+
}
|
|
1147
|
+
const own = self?.workspace ?? (t.workspaceId ? { id: t.workspaceId, name: t.workspaceName ?? t.workspaceId } : undefined);
|
|
1148
|
+
if (own && (wanted === own.id || wanted === own.name))
|
|
1149
|
+
return { id: own.id, name: own.name };
|
|
1150
|
+
throw new TenancyError("WORKSPACE_KEY_MISMATCH", `This key only works in ${own?.name ?? "its own workspace"}.`, "Run: mutagent login (for a key that can switch workspaces)");
|
|
1123
1151
|
}
|
|
1124
1152
|
const rows = await listKeyWorkspaces(t.apiKey, t.endpoint, t);
|
|
1125
1153
|
const byId = rows.find((w) => w.id === wanted);
|
|
@@ -1136,18 +1164,14 @@ async function findWorkspace(wanted, t) {
|
|
|
1136
1164
|
const orgs = await fetchOrganizations(t.apiKey, t.endpoint);
|
|
1137
1165
|
throw otherOrganizationHint(wanted, t.organization, orgs);
|
|
1138
1166
|
}
|
|
1139
|
-
var
|
|
1167
|
+
var UUID_PATTERN, ENV_KEY_WORKSPACE_FIX = "Pass --workspace <name|id> or set MUTAGENT_WORKSPACE_ID", orgAssertion, workspaceFlag, keySelfProbes, keyScopeConfirmations;
|
|
1140
1168
|
var init_organization_scope = __esm(() => {
|
|
1141
1169
|
init_errors();
|
|
1142
1170
|
init_config();
|
|
1171
|
+
init_tenancy_error();
|
|
1143
1172
|
UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1144
|
-
TenancyError = class TenancyError extends MutagentError {
|
|
1145
|
-
constructor(code, what, fix) {
|
|
1146
|
-
super(code, `${what} ${fix}`, fix.replace(/^(Run|See|To switch): /, ""));
|
|
1147
|
-
this.name = "TenancyError";
|
|
1148
|
-
}
|
|
1149
|
-
};
|
|
1150
1173
|
keySelfProbes = new Map;
|
|
1174
|
+
keyScopeConfirmations = new Map;
|
|
1151
1175
|
});
|
|
1152
1176
|
|
|
1153
1177
|
// src/lib/sdk-debug.ts
|
|
@@ -1464,6 +1488,85 @@ function buildCreateProviderBody(data) {
|
|
|
1464
1488
|
return body;
|
|
1465
1489
|
}
|
|
1466
1490
|
|
|
1491
|
+
// src/lib/key-scope-heal.ts
|
|
1492
|
+
function isOrgKeyWithoutWorkspace(status, body) {
|
|
1493
|
+
return status === 400 && body.toLowerCase().includes(ORG_KEY_NEEDS_WORKSPACE);
|
|
1494
|
+
}
|
|
1495
|
+
function codeOf(body) {
|
|
1496
|
+
try {
|
|
1497
|
+
return JSON.parse(body)?.code;
|
|
1498
|
+
} catch {
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
async function readKeySelf(key, fetchImpl) {
|
|
1503
|
+
try {
|
|
1504
|
+
const response = await fetchImpl(`${key.endpoint}/api/api-keys/self`, { headers: { "x-api-key": key.apiKey } });
|
|
1505
|
+
const self = response.ok ? await response.json() : undefined;
|
|
1506
|
+
return self?.scope === "workspace" || self?.scope === "organization" || self?.scope === "user" ? self : undefined;
|
|
1507
|
+
} catch {
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
async function explainSelectionDenial(status, body, ctx, key, fetchImpl = fetch) {
|
|
1512
|
+
if (status !== 403 || ctx.workspaceOrigin !== "selection" || ctx.keyScope !== "workspace" || ctx.keyWorkspace)
|
|
1513
|
+
return;
|
|
1514
|
+
if (codeOf(body) !== TENANCY_DENIED)
|
|
1515
|
+
return;
|
|
1516
|
+
const self = await readKeySelf(key, fetchImpl);
|
|
1517
|
+
if (!self)
|
|
1518
|
+
return;
|
|
1519
|
+
ctx.keyScope = self.scope;
|
|
1520
|
+
if (self.scope === "workspace") {
|
|
1521
|
+
ctx.keyWorkspace = self.workspace ?? undefined;
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
if (!envKeyReplacesFile())
|
|
1525
|
+
recordKeyScope(self);
|
|
1526
|
+
}
|
|
1527
|
+
function sameHeaders(a, b) {
|
|
1528
|
+
const keys = Object.keys(a);
|
|
1529
|
+
return keys.length === Object.keys(b).length && keys.every((k) => a[k] === b[k]);
|
|
1530
|
+
}
|
|
1531
|
+
async function fetchWithScopeHeal(input, init, target) {
|
|
1532
|
+
const spare = input instanceof Request ? input.clone() : undefined;
|
|
1533
|
+
const response = await (init == null ? fetch(input) : fetch(input, init));
|
|
1534
|
+
if (response.status === 403 && target.tenancy) {
|
|
1535
|
+
const denied = await response.clone().text().catch(() => "");
|
|
1536
|
+
await explainSelectionDenial(403, denied, target.tenancy, target);
|
|
1537
|
+
return response;
|
|
1538
|
+
}
|
|
1539
|
+
if (response.status !== 400)
|
|
1540
|
+
return response;
|
|
1541
|
+
const body = await response.clone().text().catch(() => "");
|
|
1542
|
+
if (!isOrgKeyWithoutWorkspace(response.status, body))
|
|
1543
|
+
return response;
|
|
1544
|
+
const before = target.headers();
|
|
1545
|
+
const self = await confirmKeyScope(target.apiKey, target.endpoint).catch(() => {
|
|
1546
|
+
return;
|
|
1547
|
+
});
|
|
1548
|
+
if (!self)
|
|
1549
|
+
return response;
|
|
1550
|
+
target.adopt(self);
|
|
1551
|
+
const after = target.headers();
|
|
1552
|
+
if (sameHeaders(before, after))
|
|
1553
|
+
return response;
|
|
1554
|
+
if (spare) {
|
|
1555
|
+
const headers = new Headers(spare.headers);
|
|
1556
|
+
for (const [name, value] of Object.entries(after))
|
|
1557
|
+
headers.set(name, value);
|
|
1558
|
+
return fetch(new Request(spare, { headers }));
|
|
1559
|
+
}
|
|
1560
|
+
const headers = { ...init?.headers, ...after };
|
|
1561
|
+
return fetch(input, { ...init, headers });
|
|
1562
|
+
}
|
|
1563
|
+
var ORG_KEY_NEEDS_WORKSPACE = "x-workspace-id header required";
|
|
1564
|
+
var init_key_scope_heal = __esm(() => {
|
|
1565
|
+
init_config();
|
|
1566
|
+
init_organization_scope();
|
|
1567
|
+
init_tenancy_error();
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1467
1570
|
// src/lib/sdk-mappers.ts
|
|
1468
1571
|
import { LlmProviderType as SDKProviderEnum } from "@mutagent/sdk/models";
|
|
1469
1572
|
function isProviderType(value) {
|
|
@@ -1574,7 +1677,7 @@ class SDKClientWrapper {
|
|
|
1574
1677
|
workspaceOrigin: opts.workspaceOrigin ?? (opts.workspaceId ? "selection" : undefined),
|
|
1575
1678
|
organizationName: opts.organizationName
|
|
1576
1679
|
};
|
|
1577
|
-
const httpClient = new HTTPClient;
|
|
1680
|
+
const httpClient = new HTTPClient({ fetcher: (input, init) => fetchWithScopeHeal(input, init, this.scopeHealTarget()) });
|
|
1578
1681
|
httpClient.addHook("beforeRequest", (req) => {
|
|
1579
1682
|
for (const [name, value] of Object.entries(this.tenancyHeaders()))
|
|
1580
1683
|
req.headers.set(name, value);
|
|
@@ -1644,7 +1747,7 @@ class SDKClientWrapper {
|
|
|
1644
1747
|
}
|
|
1645
1748
|
let response;
|
|
1646
1749
|
try {
|
|
1647
|
-
response = await
|
|
1750
|
+
response = await fetchWithScopeHeal(`${this.endpoint}${path}`, {
|
|
1648
1751
|
...options,
|
|
1649
1752
|
headers: {
|
|
1650
1753
|
"x-api-key": this.apiKey,
|
|
@@ -1652,7 +1755,7 @@ class SDKClientWrapper {
|
|
|
1652
1755
|
...this.tenancyHeaders(),
|
|
1653
1756
|
...extraHeaders
|
|
1654
1757
|
}
|
|
1655
|
-
});
|
|
1758
|
+
}, this.scopeHealTarget());
|
|
1656
1759
|
} catch {
|
|
1657
1760
|
throw unreachableServerError(this.endpoint);
|
|
1658
1761
|
}
|
|
@@ -1661,9 +1764,8 @@ class SDKClientWrapper {
|
|
|
1661
1764
|
const tenancy = tenancyFailure(response.status, error, this.tenancy);
|
|
1662
1765
|
if (tenancy)
|
|
1663
1766
|
throw tenancy;
|
|
1664
|
-
if (response.status === 401)
|
|
1767
|
+
if (response.status === 401)
|
|
1665
1768
|
throw new AuthenticationError;
|
|
1666
|
-
}
|
|
1667
1769
|
if (response.status === 403) {
|
|
1668
1770
|
throw new ApiError(403, error || "Access denied");
|
|
1669
1771
|
}
|
|
@@ -1826,10 +1928,24 @@ class SDKClientWrapper {
|
|
|
1826
1928
|
return tenancyHeaders({
|
|
1827
1929
|
keyScope: this.tenancy.keyScope,
|
|
1828
1930
|
workspaceId: this.workspaceId,
|
|
1829
|
-
workspaceOrigin: this.tenancy.workspaceOrigin,
|
|
1830
1931
|
organizationId: this.organizationId
|
|
1831
1932
|
});
|
|
1832
1933
|
}
|
|
1934
|
+
scopeHealTarget() {
|
|
1935
|
+
return {
|
|
1936
|
+
apiKey: this.apiKey,
|
|
1937
|
+
endpoint: this.endpoint,
|
|
1938
|
+
headers: () => this.tenancyHeaders(),
|
|
1939
|
+
tenancy: this.tenancy,
|
|
1940
|
+
adopt: (self) => {
|
|
1941
|
+
this.tenancy.keyScope = self.scope;
|
|
1942
|
+
if (!this.workspaceId && self.scope === "workspace" && self.workspace)
|
|
1943
|
+
this.workspaceId = self.workspace.id;
|
|
1944
|
+
if (self.scope === "user" && self.organization)
|
|
1945
|
+
this.organizationId ??= self.organization.id;
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1833
1949
|
requireSelectedWorkspace() {
|
|
1834
1950
|
if (!this.workspaceId)
|
|
1835
1951
|
throw noWorkspaceSelected();
|
|
@@ -1944,6 +2060,7 @@ var init_sdk_client = __esm(() => {
|
|
|
1944
2060
|
init_key_check();
|
|
1945
2061
|
init_organization_scope();
|
|
1946
2062
|
init_sdk_debug();
|
|
2063
|
+
init_key_scope_heal();
|
|
1947
2064
|
init_known_secrets();
|
|
1948
2065
|
init_sdk_mappers();
|
|
1949
2066
|
init_organization_scope();
|
|
@@ -2140,7 +2257,8 @@ async function createAgentDeploymentAPI() {
|
|
|
2140
2257
|
const workspaceId = context.requireSelectedWorkspace();
|
|
2141
2258
|
const httpClient = new HTTPClient2;
|
|
2142
2259
|
httpClient.addHook("beforeRequest", (request) => {
|
|
2143
|
-
|
|
2260
|
+
for (const [name, value] of Object.entries(tenancyHeaders({ workspaceId })))
|
|
2261
|
+
request.headers.set(name, value);
|
|
2144
2262
|
return request;
|
|
2145
2263
|
});
|
|
2146
2264
|
const sdk = new Mutagent2({ security: { apiKey }, serverURL: endpoint, httpClient, timeoutMs: Math.max(config.timeout, 120000), retryConfig: { strategy: "none" } });
|
|
@@ -18011,7 +18129,6 @@ async function connectAgentSpec() {
|
|
|
18011
18129
|
const headers = tenancyHeaders({
|
|
18012
18130
|
keyScope: resolved.keyScope,
|
|
18013
18131
|
workspaceId: resolved.workspaceId,
|
|
18014
|
-
workspaceOrigin: resolved.workspaceOrigin,
|
|
18015
18132
|
organizationId: resolved.organization?.id
|
|
18016
18133
|
});
|
|
18017
18134
|
const httpClient = new HTTPClient3;
|
|
@@ -21219,7 +21336,6 @@ the answer is verified, not read from local config.
|
|
|
21219
21336
|
const headers = tenancyHeaders({
|
|
21220
21337
|
keyScope: tenancy.keyScope,
|
|
21221
21338
|
workspaceId: tenancy.workspaceId,
|
|
21222
|
-
workspaceOrigin: tenancy.workspaceOrigin,
|
|
21223
21339
|
organizationId: tenancy.organization?.id
|
|
21224
21340
|
});
|
|
21225
21341
|
const self = await fetchKeySelf(apiKey, tenancy.endpoint, { headers, ctx: tenancy });
|
|
@@ -27250,6 +27366,7 @@ init_secure_file();
|
|
|
27250
27366
|
init_errors();
|
|
27251
27367
|
init_known_secrets();
|
|
27252
27368
|
init_organization_scope();
|
|
27369
|
+
init_key_scope_heal();
|
|
27253
27370
|
import { existsSync as existsSync15, readFileSync as readFileSync16, mkdirSync as mkdirSync5, rmSync as rmSync2 } from "fs";
|
|
27254
27371
|
import { homedir as homedir6 } from "os";
|
|
27255
27372
|
import { join as join21, dirname as dirname8 } from "path";
|
|
@@ -27335,6 +27452,7 @@ async function exchangeToken(input, deps = {}) {
|
|
|
27335
27452
|
if (response.status === 403) {
|
|
27336
27453
|
const context = { ...input.tenancy, workspaceId: input.workspaceId };
|
|
27337
27454
|
const body = await response.text().catch(() => "");
|
|
27455
|
+
await explainSelectionDenial(403, body, context, input, doFetch);
|
|
27338
27456
|
throw tenancyFailure(403, body, context) ?? workspaceDenied(context);
|
|
27339
27457
|
}
|
|
27340
27458
|
if (!response.ok) {
|
|
@@ -27422,7 +27540,6 @@ async function resolveConnection() {
|
|
|
27422
27540
|
const headers = tenancyHeaders({
|
|
27423
27541
|
keyScope: tenancy.keyScope,
|
|
27424
27542
|
workspaceId,
|
|
27425
|
-
workspaceOrigin: tenancy.workspaceOrigin,
|
|
27426
27543
|
organizationId: tenancy.organization?.id
|
|
27427
27544
|
});
|
|
27428
27545
|
return {
|
|
@@ -31702,7 +31819,7 @@ async function resolveGatewayConnection() {
|
|
|
31702
31819
|
const headers = {
|
|
31703
31820
|
"x-api-key": apiKey,
|
|
31704
31821
|
"Content-Type": "application/json",
|
|
31705
|
-
|
|
31822
|
+
...tenancyHeaders({ workspaceId })
|
|
31706
31823
|
};
|
|
31707
31824
|
return {
|
|
31708
31825
|
endpoint: tenancy.endpoint,
|
|
@@ -34782,7 +34899,6 @@ async function connectReports(opts) {
|
|
|
34782
34899
|
const headers = tenancyHeaders({
|
|
34783
34900
|
keyScope: tenancy.keyScope,
|
|
34784
34901
|
workspaceId: tenancy.workspaceId,
|
|
34785
|
-
workspaceOrigin: tenancy.workspaceOrigin,
|
|
34786
34902
|
organizationId: tenancy.organization?.id
|
|
34787
34903
|
});
|
|
34788
34904
|
const httpClient = new HTTPClient5;
|
|
@@ -36555,7 +36671,6 @@ async function connectIntegrations() {
|
|
|
36555
36671
|
const headers = tenancyHeaders({
|
|
36556
36672
|
keyScope: tenancy.keyScope,
|
|
36557
36673
|
workspaceId: tenancy.workspaceId,
|
|
36558
|
-
workspaceOrigin: tenancy.workspaceOrigin,
|
|
36559
36674
|
organizationId: tenancy.organization?.id
|
|
36560
36675
|
});
|
|
36561
36676
|
const tenancyCtx = {
|
|
@@ -38100,4 +38215,4 @@ if (process.env.CLI_VERSION) {
|
|
|
38100
38215
|
}
|
|
38101
38216
|
runCli(process.argv.slice(2), cliVersion);
|
|
38102
38217
|
|
|
38103
|
-
//# debugId=
|
|
38218
|
+
//# debugId=C914B921636AC8AB64756E2164756E21
|
package/dist/index.js
CHANGED
|
@@ -1043,6 +1043,35 @@ var init_key_check = __esm(() => {
|
|
|
1043
1043
|
};
|
|
1044
1044
|
});
|
|
1045
1045
|
|
|
1046
|
+
// src/lib/tenancy-error.ts
|
|
1047
|
+
function shellQuote(value) {
|
|
1048
|
+
if (/^[A-Za-z0-9_-]+$/.test(value))
|
|
1049
|
+
return value;
|
|
1050
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1051
|
+
}
|
|
1052
|
+
function workspaceDenied(ctx) {
|
|
1053
|
+
const shown = ctx.workspaceName ?? ctx.workspaceId ?? "workspace";
|
|
1054
|
+
const own = ctx.keyScope === "workspace" ? ctx.keyWorkspace : undefined;
|
|
1055
|
+
if (ctx.workspaceOrigin === "selection" && own && own.id !== ctx.workspaceId) {
|
|
1056
|
+
return new TenancyError(TENANCY_DENIED, `This API key only works in workspace ${own.name}; your saved selection is ${shown}.`, `Run: mutagent workspaces use ${shellQuote(own.name)}`);
|
|
1057
|
+
}
|
|
1058
|
+
if (ctx.workspaceOrigin === "selection") {
|
|
1059
|
+
return new TenancyError(TENANCY_DENIED, `Your saved workspace ${shown} is no longer available to you.`, "Run: mutagent workspaces use <name>");
|
|
1060
|
+
}
|
|
1061
|
+
const where = ctx.organizationName ? ` in ${ctx.organizationName}` : "";
|
|
1062
|
+
return new TenancyError(TENANCY_DENIED, `No workspace "${shown}" among your workspaces${where}.`, "See: mutagent workspaces list");
|
|
1063
|
+
}
|
|
1064
|
+
var TENANCY_DENIED = "TENANCY_DENIED", TenancyError;
|
|
1065
|
+
var init_tenancy_error = __esm(() => {
|
|
1066
|
+
init_errors();
|
|
1067
|
+
TenancyError = class TenancyError extends MutagentError {
|
|
1068
|
+
constructor(code, what, fix) {
|
|
1069
|
+
super(code, `${what} ${fix}`, fix.replace(/^(Run|See|To switch): /, ""));
|
|
1070
|
+
this.name = "TenancyError";
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1046
1075
|
// src/lib/organization-scope.ts
|
|
1047
1076
|
function looksLikeWorkspaceId(value) {
|
|
1048
1077
|
return UUID_PATTERN.test(value);
|
|
@@ -1051,11 +1080,6 @@ function tenancyHeaders(t) {
|
|
|
1051
1080
|
const headers = {};
|
|
1052
1081
|
if (!t.workspaceId)
|
|
1053
1082
|
return headers;
|
|
1054
|
-
if (t.keyScope === "workspace") {
|
|
1055
|
-
if (t.workspaceOrigin === "override")
|
|
1056
|
-
headers["x-workspace-id"] = t.workspaceId;
|
|
1057
|
-
return headers;
|
|
1058
|
-
}
|
|
1059
1083
|
headers["x-workspace-id"] = t.workspaceId;
|
|
1060
1084
|
if (t.keyScope === "user" && t.organizationId)
|
|
1061
1085
|
headers["x-organization-id"] = t.organizationId;
|
|
@@ -1065,7 +1089,6 @@ function configTenancyHeaders(config) {
|
|
|
1065
1089
|
return tenancyHeaders({
|
|
1066
1090
|
keyScope: config.keyScope,
|
|
1067
1091
|
workspaceId: config.defaultWorkspace,
|
|
1068
|
-
workspaceOrigin: config.workspaceOrigin,
|
|
1069
1092
|
organizationId: config.defaultOrganization
|
|
1070
1093
|
});
|
|
1071
1094
|
}
|
|
@@ -1080,14 +1103,6 @@ function parseBody(body) {
|
|
|
1080
1103
|
function deadKeyError() {
|
|
1081
1104
|
return new TenancyError("AUTH_EXPIRED", "Your CLI key expired or was revoked.", "Run: mutagent login");
|
|
1082
1105
|
}
|
|
1083
|
-
function workspaceDenied(ctx) {
|
|
1084
|
-
const shown = ctx.workspaceName ?? ctx.workspaceId ?? "workspace";
|
|
1085
|
-
if (ctx.workspaceOrigin === "selection") {
|
|
1086
|
-
return new TenancyError(TENANCY_DENIED, `Your saved workspace ${shown} is no longer available to you.`, "Run: mutagent workspaces use <name>");
|
|
1087
|
-
}
|
|
1088
|
-
const where = ctx.organizationName ? ` in ${ctx.organizationName}` : "";
|
|
1089
|
-
return new TenancyError(TENANCY_DENIED, `No workspace "${shown}" among your workspaces${where}.`, "See: mutagent workspaces list");
|
|
1090
|
-
}
|
|
1091
1106
|
function tenancyFailure(status, body, ctx) {
|
|
1092
1107
|
const parsed = parseBody(body);
|
|
1093
1108
|
if (status === 401)
|
|
@@ -1153,11 +1168,6 @@ function matchesOrganization(value, org) {
|
|
|
1153
1168
|
return true;
|
|
1154
1169
|
return org.name !== undefined && value.toLowerCase() === org.name.toLowerCase();
|
|
1155
1170
|
}
|
|
1156
|
-
function shellQuote(value) {
|
|
1157
|
-
if (/^[A-Za-z0-9_-]+$/.test(value))
|
|
1158
|
-
return value;
|
|
1159
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1160
|
-
}
|
|
1161
1171
|
function orgMismatch(value, org) {
|
|
1162
1172
|
const shown = org.slug ? `${org.name} (${org.slug})` : org.name;
|
|
1163
1173
|
return new TenancyError("ORG_MISMATCH", `Your key is for ${shown}.`, `Run: mutagent login --org ${shellQuote(value)}`);
|
|
@@ -1202,6 +1212,17 @@ ${apiKey}`;
|
|
|
1202
1212
|
probe.catch(() => keySelfProbes.delete(cacheKey));
|
|
1203
1213
|
return probe;
|
|
1204
1214
|
}
|
|
1215
|
+
function confirmKeyScope(apiKey, endpoint) {
|
|
1216
|
+
const cacheKey = `${endpoint}
|
|
1217
|
+
${apiKey}`;
|
|
1218
|
+
const done = keyScopeConfirmations.get(cacheKey);
|
|
1219
|
+
if (done)
|
|
1220
|
+
return done;
|
|
1221
|
+
const read = readKeyScope(apiKey, endpoint, !envKeyReplacesFile());
|
|
1222
|
+
keyScopeConfirmations.set(cacheKey, read);
|
|
1223
|
+
keySelfProbes.set(cacheKey, read);
|
|
1224
|
+
return read;
|
|
1225
|
+
}
|
|
1205
1226
|
async function readKeyScope(apiKey, endpoint, fromFile) {
|
|
1206
1227
|
try {
|
|
1207
1228
|
const self = await fetchKeySelf(apiKey, endpoint);
|
|
@@ -1269,7 +1290,14 @@ async function findWorkspace(wanted, t) {
|
|
|
1269
1290
|
if (t.workspaceId && (wanted === t.workspaceId || wanted === t.workspaceName)) {
|
|
1270
1291
|
return { id: t.workspaceId, name: t.workspaceName ?? t.workspaceId };
|
|
1271
1292
|
}
|
|
1272
|
-
|
|
1293
|
+
const self = await confirmKeyScope(t.apiKey, t.endpoint);
|
|
1294
|
+
if (self && self.scope !== "workspace") {
|
|
1295
|
+
return findWorkspace(wanted, { ...t, keyScope: self.scope, organization: self.organization ?? t.organization });
|
|
1296
|
+
}
|
|
1297
|
+
const own = self?.workspace ?? (t.workspaceId ? { id: t.workspaceId, name: t.workspaceName ?? t.workspaceId } : undefined);
|
|
1298
|
+
if (own && (wanted === own.id || wanted === own.name))
|
|
1299
|
+
return { id: own.id, name: own.name };
|
|
1300
|
+
throw new TenancyError("WORKSPACE_KEY_MISMATCH", `This key only works in ${own?.name ?? "its own workspace"}.`, "Run: mutagent login (for a key that can switch workspaces)");
|
|
1273
1301
|
}
|
|
1274
1302
|
const rows = await listKeyWorkspaces(t.apiKey, t.endpoint, t);
|
|
1275
1303
|
const byId = rows.find((w) => w.id === wanted);
|
|
@@ -1286,18 +1314,14 @@ async function findWorkspace(wanted, t) {
|
|
|
1286
1314
|
const orgs = await fetchOrganizations(t.apiKey, t.endpoint);
|
|
1287
1315
|
throw otherOrganizationHint(wanted, t.organization, orgs);
|
|
1288
1316
|
}
|
|
1289
|
-
var
|
|
1317
|
+
var UUID_PATTERN, ENV_KEY_WORKSPACE_FIX = "Pass --workspace <name|id> or set MUTAGENT_WORKSPACE_ID", orgAssertion, workspaceFlag, keySelfProbes, keyScopeConfirmations;
|
|
1290
1318
|
var init_organization_scope = __esm(() => {
|
|
1291
1319
|
init_errors();
|
|
1292
1320
|
init_config();
|
|
1321
|
+
init_tenancy_error();
|
|
1293
1322
|
UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1294
|
-
TenancyError = class TenancyError extends MutagentError {
|
|
1295
|
-
constructor(code, what, fix) {
|
|
1296
|
-
super(code, `${what} ${fix}`, fix.replace(/^(Run|See|To switch): /, ""));
|
|
1297
|
-
this.name = "TenancyError";
|
|
1298
|
-
}
|
|
1299
|
-
};
|
|
1300
1323
|
keySelfProbes = new Map;
|
|
1324
|
+
keyScopeConfirmations = new Map;
|
|
1301
1325
|
});
|
|
1302
1326
|
|
|
1303
1327
|
// src/lib/provider-request.ts
|
|
@@ -1417,6 +1441,85 @@ var init_sdk_debug = __esm(() => {
|
|
|
1417
1441
|
};
|
|
1418
1442
|
});
|
|
1419
1443
|
|
|
1444
|
+
// src/lib/key-scope-heal.ts
|
|
1445
|
+
function isOrgKeyWithoutWorkspace(status, body) {
|
|
1446
|
+
return status === 400 && body.toLowerCase().includes(ORG_KEY_NEEDS_WORKSPACE);
|
|
1447
|
+
}
|
|
1448
|
+
function codeOf(body) {
|
|
1449
|
+
try {
|
|
1450
|
+
return JSON.parse(body)?.code;
|
|
1451
|
+
} catch {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async function readKeySelf(key, fetchImpl) {
|
|
1456
|
+
try {
|
|
1457
|
+
const response = await fetchImpl(`${key.endpoint}/api/api-keys/self`, { headers: { "x-api-key": key.apiKey } });
|
|
1458
|
+
const self = response.ok ? await response.json() : undefined;
|
|
1459
|
+
return self?.scope === "workspace" || self?.scope === "organization" || self?.scope === "user" ? self : undefined;
|
|
1460
|
+
} catch {
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
async function explainSelectionDenial(status, body, ctx, key, fetchImpl = fetch) {
|
|
1465
|
+
if (status !== 403 || ctx.workspaceOrigin !== "selection" || ctx.keyScope !== "workspace" || ctx.keyWorkspace)
|
|
1466
|
+
return;
|
|
1467
|
+
if (codeOf(body) !== TENANCY_DENIED)
|
|
1468
|
+
return;
|
|
1469
|
+
const self = await readKeySelf(key, fetchImpl);
|
|
1470
|
+
if (!self)
|
|
1471
|
+
return;
|
|
1472
|
+
ctx.keyScope = self.scope;
|
|
1473
|
+
if (self.scope === "workspace") {
|
|
1474
|
+
ctx.keyWorkspace = self.workspace ?? undefined;
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
if (!envKeyReplacesFile())
|
|
1478
|
+
recordKeyScope(self);
|
|
1479
|
+
}
|
|
1480
|
+
function sameHeaders(a, b) {
|
|
1481
|
+
const keys = Object.keys(a);
|
|
1482
|
+
return keys.length === Object.keys(b).length && keys.every((k) => a[k] === b[k]);
|
|
1483
|
+
}
|
|
1484
|
+
async function fetchWithScopeHeal(input, init, target) {
|
|
1485
|
+
const spare = input instanceof Request ? input.clone() : undefined;
|
|
1486
|
+
const response = await (init == null ? fetch(input) : fetch(input, init));
|
|
1487
|
+
if (response.status === 403 && target.tenancy) {
|
|
1488
|
+
const denied = await response.clone().text().catch(() => "");
|
|
1489
|
+
await explainSelectionDenial(403, denied, target.tenancy, target);
|
|
1490
|
+
return response;
|
|
1491
|
+
}
|
|
1492
|
+
if (response.status !== 400)
|
|
1493
|
+
return response;
|
|
1494
|
+
const body = await response.clone().text().catch(() => "");
|
|
1495
|
+
if (!isOrgKeyWithoutWorkspace(response.status, body))
|
|
1496
|
+
return response;
|
|
1497
|
+
const before = target.headers();
|
|
1498
|
+
const self = await confirmKeyScope(target.apiKey, target.endpoint).catch(() => {
|
|
1499
|
+
return;
|
|
1500
|
+
});
|
|
1501
|
+
if (!self)
|
|
1502
|
+
return response;
|
|
1503
|
+
target.adopt(self);
|
|
1504
|
+
const after = target.headers();
|
|
1505
|
+
if (sameHeaders(before, after))
|
|
1506
|
+
return response;
|
|
1507
|
+
if (spare) {
|
|
1508
|
+
const headers = new Headers(spare.headers);
|
|
1509
|
+
for (const [name, value] of Object.entries(after))
|
|
1510
|
+
headers.set(name, value);
|
|
1511
|
+
return fetch(new Request(spare, { headers }));
|
|
1512
|
+
}
|
|
1513
|
+
const headers = { ...init?.headers, ...after };
|
|
1514
|
+
return fetch(input, { ...init, headers });
|
|
1515
|
+
}
|
|
1516
|
+
var ORG_KEY_NEEDS_WORKSPACE = "x-workspace-id header required";
|
|
1517
|
+
var init_key_scope_heal = __esm(() => {
|
|
1518
|
+
init_config();
|
|
1519
|
+
init_organization_scope();
|
|
1520
|
+
init_tenancy_error();
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1420
1523
|
// src/lib/sdk-mappers.ts
|
|
1421
1524
|
import { LlmProviderType as SDKProviderEnum } from "@mutagent/sdk/models";
|
|
1422
1525
|
function isProviderType(value) {
|
|
@@ -1527,7 +1630,7 @@ class SDKClientWrapper {
|
|
|
1527
1630
|
workspaceOrigin: opts.workspaceOrigin ?? (opts.workspaceId ? "selection" : undefined),
|
|
1528
1631
|
organizationName: opts.organizationName
|
|
1529
1632
|
};
|
|
1530
|
-
const httpClient = new HTTPClient;
|
|
1633
|
+
const httpClient = new HTTPClient({ fetcher: (input, init) => fetchWithScopeHeal(input, init, this.scopeHealTarget()) });
|
|
1531
1634
|
httpClient.addHook("beforeRequest", (req) => {
|
|
1532
1635
|
for (const [name, value] of Object.entries(this.tenancyHeaders()))
|
|
1533
1636
|
req.headers.set(name, value);
|
|
@@ -1597,7 +1700,7 @@ class SDKClientWrapper {
|
|
|
1597
1700
|
}
|
|
1598
1701
|
let response;
|
|
1599
1702
|
try {
|
|
1600
|
-
response = await
|
|
1703
|
+
response = await fetchWithScopeHeal(`${this.endpoint}${path}`, {
|
|
1601
1704
|
...options,
|
|
1602
1705
|
headers: {
|
|
1603
1706
|
"x-api-key": this.apiKey,
|
|
@@ -1605,7 +1708,7 @@ class SDKClientWrapper {
|
|
|
1605
1708
|
...this.tenancyHeaders(),
|
|
1606
1709
|
...extraHeaders
|
|
1607
1710
|
}
|
|
1608
|
-
});
|
|
1711
|
+
}, this.scopeHealTarget());
|
|
1609
1712
|
} catch {
|
|
1610
1713
|
throw unreachableServerError(this.endpoint);
|
|
1611
1714
|
}
|
|
@@ -1614,9 +1717,8 @@ class SDKClientWrapper {
|
|
|
1614
1717
|
const tenancy = tenancyFailure(response.status, error, this.tenancy);
|
|
1615
1718
|
if (tenancy)
|
|
1616
1719
|
throw tenancy;
|
|
1617
|
-
if (response.status === 401)
|
|
1720
|
+
if (response.status === 401)
|
|
1618
1721
|
throw new AuthenticationError;
|
|
1619
|
-
}
|
|
1620
1722
|
if (response.status === 403) {
|
|
1621
1723
|
throw new ApiError(403, error || "Access denied");
|
|
1622
1724
|
}
|
|
@@ -1779,10 +1881,24 @@ class SDKClientWrapper {
|
|
|
1779
1881
|
return tenancyHeaders({
|
|
1780
1882
|
keyScope: this.tenancy.keyScope,
|
|
1781
1883
|
workspaceId: this.workspaceId,
|
|
1782
|
-
workspaceOrigin: this.tenancy.workspaceOrigin,
|
|
1783
1884
|
organizationId: this.organizationId
|
|
1784
1885
|
});
|
|
1785
1886
|
}
|
|
1887
|
+
scopeHealTarget() {
|
|
1888
|
+
return {
|
|
1889
|
+
apiKey: this.apiKey,
|
|
1890
|
+
endpoint: this.endpoint,
|
|
1891
|
+
headers: () => this.tenancyHeaders(),
|
|
1892
|
+
tenancy: this.tenancy,
|
|
1893
|
+
adopt: (self) => {
|
|
1894
|
+
this.tenancy.keyScope = self.scope;
|
|
1895
|
+
if (!this.workspaceId && self.scope === "workspace" && self.workspace)
|
|
1896
|
+
this.workspaceId = self.workspace.id;
|
|
1897
|
+
if (self.scope === "user" && self.organization)
|
|
1898
|
+
this.organizationId ??= self.organization.id;
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1786
1902
|
requireSelectedWorkspace() {
|
|
1787
1903
|
if (!this.workspaceId)
|
|
1788
1904
|
throw noWorkspaceSelected();
|
|
@@ -1897,6 +2013,7 @@ var init_sdk_client = __esm(() => {
|
|
|
1897
2013
|
init_key_check();
|
|
1898
2014
|
init_organization_scope();
|
|
1899
2015
|
init_sdk_debug();
|
|
2016
|
+
init_key_scope_heal();
|
|
1900
2017
|
init_known_secrets();
|
|
1901
2018
|
init_sdk_mappers();
|
|
1902
2019
|
init_organization_scope();
|
|
@@ -1922,4 +2039,4 @@ export {
|
|
|
1922
2039
|
version
|
|
1923
2040
|
};
|
|
1924
2041
|
|
|
1925
|
-
//# debugId=
|
|
2042
|
+
//# debugId=B6CD43FAA918848864756E2164756E21
|
package/package.json
CHANGED