@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.
Files changed (3) hide show
  1. package/dist/bin/cli.js +156 -41
  2. package/dist/index.js +151 -34
  3. 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
- throw new TenancyError("WORKSPACE_KEY_MISMATCH", `This key only works in ${t.workspaceName ?? t.workspaceId ?? "its own workspace"}.`, "Run: mutagent login (for a key that can switch workspaces)");
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 TENANCY_DENIED = "TENANCY_DENIED", UUID_PATTERN, TenancyError, ENV_KEY_WORKSPACE_FIX = "Pass --workspace <name|id> or set MUTAGENT_WORKSPACE_ID", orgAssertion, workspaceFlag, keySelfProbes;
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 fetch(`${this.endpoint}${path}`, {
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
- request.headers.set("x-workspace-id", workspaceId);
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
- "x-workspace-id": workspaceId
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=7181C3F925DEA1DA64756E2164756E21
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
- throw new TenancyError("WORKSPACE_KEY_MISMATCH", `This key only works in ${t.workspaceName ?? t.workspaceId ?? "its own workspace"}.`, "Run: mutagent login (for a key that can switch workspaces)");
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 TENANCY_DENIED = "TENANCY_DENIED", UUID_PATTERN, TenancyError, ENV_KEY_WORKSPACE_FIX = "Pass --workspace <name|id> or set MUTAGENT_WORKSPACE_ID", orgAssertion, workspaceFlag, keySelfProbes;
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 fetch(`${this.endpoint}${path}`, {
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=4CBE15031C5FFF4664756E2164756E21
2042
+ //# debugId=B6CD43FAA918848864756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutagent/cli",
3
- "version": "0.2.52",
3
+ "version": "0.2.54",
4
4
  "description": "Bun-native CLI for the MutagenT AI platform - auth, provider config, and lifecycle-tool installer for AI-native workflows",
5
5
  "type": "module",
6
6
  "bin": {