@m8t-stack/cli 0.2.48 → 0.2.52

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/cli.js CHANGED
@@ -453,7 +453,13 @@ function emptyChecks() {
453
453
  installationsListable: { ok: false }
454
454
  };
455
455
  }
456
+ function isPermanentKvRbacDenial(e) {
457
+ const msg = e.message ?? "";
458
+ return /assignment:\s*\(not found\)/i.test(msg);
459
+ }
456
460
  function isTransientErr(e) {
461
+ if (isPermanentKvRbacDenial(e))
462
+ return false;
457
463
  const any = e;
458
464
  const code = any.statusCode ?? any.status;
459
465
  if (typeof code === "number" && (code === 429 || code === 403 || code >= 500))
@@ -474,7 +480,12 @@ async function runChecksOnce(args) {
474
480
  checks.kvSecretsReadable = { ok: true };
475
481
  } catch (e) {
476
482
  checks.kvSecretsReadable = { ok: false, error: e.message };
477
- return { ok: false, transient: isTransientErr(e), checks };
483
+ return {
484
+ ok: false,
485
+ transient: isTransientErr(e),
486
+ ...isPermanentKvRbacDenial(e) ? { permanentReason: "kv_rbac_denied" } : {},
487
+ checks
488
+ };
478
489
  }
479
490
  const appIdNum = Number(appId);
480
491
  if (Number.isInteger(appIdNum) && appIdNum > 0) {
@@ -556,6 +567,30 @@ async function checkAppHealth(args) {
556
567
  }
557
568
  return last;
558
569
  }
570
+ function appHealthFailureCopy(health) {
571
+ if (health.permanentReason === "kv_rbac_denied") {
572
+ return {
573
+ message: "You do not have access to this platform's Key Vault, so the GitHub App secrets could not be read.",
574
+ hint: "This is a permanent permission gap, not a blip \u2014 retrying will not fix it. Run `m8t prereqs --fix` to grant yourself Key Vault secrets access (or ask whoever administers the subscription to run it for you), then re-run this command.",
575
+ category: "unknown",
576
+ retryable: false
577
+ };
578
+ }
579
+ if (health.transient === true) {
580
+ return {
581
+ message: "GitHub App health check failed transiently (Key Vault / GitHub API blip).",
582
+ hint: "This is usually a transient Azure/GitHub blip \u2014 retrying. If it persists, run `m8t brain check-app`.",
583
+ category: "transient",
584
+ retryable: true
585
+ };
586
+ }
587
+ return {
588
+ message: "GitHub App is not configured correctly.",
589
+ hint: "Run `m8t brain check-app` for details.",
590
+ category: "unknown",
591
+ retryable: false
592
+ };
593
+ }
559
594
  var init_check = __esm({
560
595
  "../../packages/github-app-auth/dist/esm/check.js"() {
561
596
  "use strict";
@@ -1110,7 +1145,7 @@ async function grantKeyVaultSecretsUser(args) {
1110
1145
  properties: {
1111
1146
  roleDefinitionId: `/subscriptions/${args.subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/${KV_SECRETS_USER_ROLE_ID}`,
1112
1147
  principalId: args.principalId,
1113
- principalType: "ServicePrincipal"
1148
+ principalType: args.principalType ?? "ServicePrincipal"
1114
1149
  }
1115
1150
  },
1116
1151
  okStatuses: [409]
@@ -1329,7 +1364,7 @@ var init_enable_hosted_brain = __esm({
1329
1364
  import { Builtins, Cli } from "clipanion";
1330
1365
 
1331
1366
  // src/lib/package-version.ts
1332
- var CLI_VERSION = "0.2.48";
1367
+ var CLI_VERSION = "0.2.52";
1333
1368
 
1334
1369
  // src/lib/render-error.ts
1335
1370
  init_errors();
@@ -2054,10 +2089,10 @@ function ReadableStreamToAsyncIterable(stream) {
2054
2089
  return {
2055
2090
  async next() {
2056
2091
  try {
2057
- const result = await reader.read();
2058
- if (result?.done)
2092
+ const result2 = await reader.read();
2093
+ if (result2?.done)
2059
2094
  reader.releaseLock();
2060
- return result;
2095
+ return result2;
2061
2096
  } catch (e) {
2062
2097
  reader.releaseLock();
2063
2098
  throw e;
@@ -2799,9 +2834,9 @@ var Stream = class _Stream {
2799
2834
  return {
2800
2835
  next: () => {
2801
2836
  if (queue.length === 0) {
2802
- const result = iterator.next();
2803
- left.push(result);
2804
- right.push(result);
2837
+ const result2 = iterator.next();
2838
+ left.push(result2);
2839
+ right.push(result2);
2805
2840
  }
2806
2841
  return queue.shift();
2807
2842
  }
@@ -4384,23 +4419,23 @@ var AbstractChatCompletionRunner = class extends EventStream {
4384
4419
  }
4385
4420
  if (singleFunctionToCall || params.parallel_tool_calls === false) {
4386
4421
  for (const toolCall of message.tool_calls) {
4387
- const result = await runToolCall(toolCall);
4388
- if (result.message)
4389
- this._addMessage(result.message);
4390
- if (singleFunctionToCall && result.functionCalled) {
4422
+ const result2 = await runToolCall(toolCall);
4423
+ if (result2.message)
4424
+ this._addMessage(result2.message);
4425
+ if (singleFunctionToCall && result2.functionCalled) {
4391
4426
  await afterCompletion?.(chatCompletion, runner);
4392
4427
  return;
4393
4428
  }
4394
4429
  }
4395
4430
  } else {
4396
4431
  const results = await Promise.allSettled(message.tool_calls.map(runToolCall));
4397
- for (const result of results) {
4398
- if (result.status === "rejected")
4399
- throw result.reason;
4432
+ for (const result2 of results) {
4433
+ if (result2.status === "rejected")
4434
+ throw result2.reason;
4400
4435
  }
4401
- for (const result of results) {
4402
- if (result.status === "fulfilled" && result.value.message) {
4403
- this._addMessage(result.value.message);
4436
+ for (const result2 of results) {
4437
+ if (result2.status === "fulfilled" && result2.value.message) {
4438
+ this._addMessage(result2.value.message);
4404
4439
  }
4405
4440
  }
4406
4441
  }
@@ -11213,17 +11248,17 @@ Uploads.Parts = Parts;
11213
11248
  // ../../node_modules/.pnpm/openai@6.48.0_ws@8.21.1_zod@4.4.3/node_modules/openai/lib/Util.mjs
11214
11249
  var allSettledWithThrow = async (promises) => {
11215
11250
  const results = await Promise.allSettled(promises);
11216
- const rejected = results.filter((result) => result.status === "rejected");
11251
+ const rejected = results.filter((result2) => result2.status === "rejected");
11217
11252
  if (rejected.length) {
11218
- for (const result of rejected) {
11219
- console.error(result.reason);
11253
+ for (const result2 of rejected) {
11254
+ console.error(result2.reason);
11220
11255
  }
11221
11256
  throw new Error(`${rejected.length} promise(s) failed - see the above errors`);
11222
11257
  }
11223
11258
  const values = [];
11224
- for (const result of results) {
11225
- if (result.status === "fulfilled") {
11226
- values.push(result.value);
11259
+ for (const result2 of results) {
11260
+ if (result2.status === "fulfilled") {
11261
+ values.push(result2.value);
11227
11262
  }
11228
11263
  }
11229
11264
  return values;
@@ -13985,17 +14020,17 @@ var TeamRemoveCommand = class extends M8tCommand {
13985
14020
  }
13986
14021
  }
13987
14022
  const ctx = await resolveGatewayContext({ interactive, subscriptionId: this.subscription, resourceGroup: this.resourceGroup });
13988
- const result = await apiCall(
14023
+ const result2 = await apiCall(
13989
14024
  { gatewayUrl: ctx.gatewayUrl, gatewayClientId: ctx.gatewayClientId },
13990
14025
  { method: "DELETE", path: `/api/team/${encodeURIComponent(this.handle)}` },
13991
14026
  (msg) => this.context.stderr.write(msg + "\n")
13992
14027
  );
13993
14028
  if (mode === "json") {
13994
- this.context.stdout.write(renderJson(result) + "\n");
14029
+ this.context.stdout.write(renderJson(result2) + "\n");
13995
14030
  return 0;
13996
14031
  }
13997
14032
  this.context.stdout.write(
13998
- `${colors.success("\u2713")} removed ${colors.field(result.deleted.handle)} (${result.deleted.rows.toString()} row${result.deleted.rows === 1 ? "" : "s"}).
14033
+ `${colors.success("\u2713")} removed ${colors.field(result2.deleted.handle)} (${result2.deleted.rows.toString()} row${result2.deleted.rows === 1 ? "" : "s"}).
13999
14034
  `
14000
14035
  );
14001
14036
  return 0;
@@ -14717,10 +14752,10 @@ var BrainCheckAppCommand = class extends M8tCommand {
14717
14752
  );
14718
14753
  const kvUri = discoverKvUri(process.env, typeof this.kvUri === "string" ? this.kvUri : void 0);
14719
14754
  const credential2 = new DefaultAzureCredential3();
14720
- const result = await checkAppHealth({ credential: credential2, kvUri });
14755
+ const result2 = await checkAppHealth({ credential: credential2, kvUri });
14721
14756
  if (mode === "json") {
14722
- this.context.stdout.write(renderJson(result) + "\n");
14723
- return result.ok ? 0 : 1;
14757
+ this.context.stdout.write(renderJson(result2) + "\n");
14758
+ return result2.ok ? 0 : 1;
14724
14759
  }
14725
14760
  this.context.stdout.write(`${colors.field("GitHub App health check")} (${kvUri})
14726
14761
 
@@ -14731,18 +14766,18 @@ var BrainCheckAppCommand = class extends M8tCommand {
14731
14766
  this.context.stdout.write(` ${status} ${label.padEnd(28)} ${colors.dim(detail)}
14732
14767
  `);
14733
14768
  };
14734
- row("KV secrets readable", result.checks.kvSecretsReadable);
14735
- row("App ID numeric", result.checks.appIdNumeric);
14736
- row("Private key valid", result.checks.privateKeyValid);
14737
- row("App JWT accepted", result.checks.appJwtAccepted);
14738
- row("Installations listable", result.checks.installationsListable);
14769
+ row("KV secrets readable", result2.checks.kvSecretsReadable);
14770
+ row("App ID numeric", result2.checks.appIdNumeric);
14771
+ row("Private key valid", result2.checks.privateKeyValid);
14772
+ row("App JWT accepted", result2.checks.appJwtAccepted);
14773
+ row("Installations listable", result2.checks.installationsListable);
14739
14774
  this.context.stdout.write("\n");
14740
14775
  this.context.stdout.write(
14741
- result.ok ? `${colors.success("\u2713")} all checks passed.
14776
+ result2.ok ? `${colors.success("\u2713")} all checks passed.
14742
14777
  ` : `${colors.error("\u2717")} one or more checks failed \u2014 see above.
14743
14778
  `
14744
14779
  );
14745
- return result.ok ? 0 : 1;
14780
+ return result2.ok ? 0 : 1;
14746
14781
  }
14747
14782
  };
14748
14783
 
@@ -15810,8 +15845,7 @@ var BrainCreateCommand = class extends M8tCommand {
15810
15845
  if (!health.ok) {
15811
15846
  throw new LocalCliError({
15812
15847
  code: "APP_HEALTH_FAILED",
15813
- message: "GitHub App is not configured correctly.",
15814
- hint: "Run `m8t brain check-app` for details, then re-run this command."
15848
+ ...appHealthFailureCopy(health)
15815
15849
  });
15816
15850
  }
15817
15851
  }
@@ -15835,7 +15869,7 @@ var BrainCreateCommand = class extends M8tCommand {
15835
15869
  });
15836
15870
  }
15837
15871
  const tmpDir = fs11.mkdtempSync(path10.join(os4.tmpdir(), `m8t-brain-create-${worker}-`));
15838
- let result;
15872
+ let result2;
15839
15873
  let resolvedInstallationId = null;
15840
15874
  const force = this.reuse === true ? "reuse" : this.newName === true ? "new" : void 0;
15841
15875
  const interactive = !useAppPath && this.context.stdout.isTTY === true;
@@ -15950,7 +15984,7 @@ var BrainCreateCommand = class extends M8tCommand {
15950
15984
  `);
15951
15985
  }
15952
15986
  }
15953
- result = await linkBrain({
15987
+ result2 = await linkBrain({
15954
15988
  credential: credential2,
15955
15989
  kvUri,
15956
15990
  projectEndpoint: project.endpoint,
@@ -15975,14 +16009,14 @@ var BrainCreateCommand = class extends M8tCommand {
15975
16009
  repo: `${owner}/${finalName}`,
15976
16010
  branch,
15977
16011
  installationId: resolvedInstallationId,
15978
- connectionName: result.connectionName,
15979
- foundryVersion: result.foundryVersion
16012
+ connectionName: result2.connectionName,
16013
+ foundryVersion: result2.foundryVersion
15980
16014
  }) + "\n"
15981
16015
  );
15982
16016
  return 0;
15983
16017
  }
15984
16018
  this.context.stdout.write(
15985
- `${colors.success("\u2713")} created + linked ${colors.field(worker)} \u2194 ${colors.field(`${owner}/${finalName}`)} (agent version ${result.foundryVersion ?? ""}).
16019
+ `${colors.success("\u2713")} created + linked ${colors.field(worker)} \u2194 ${colors.field(`${owner}/${finalName}`)} (agent version ${result2.foundryVersion ?? ""}).
15986
16020
  `
15987
16021
  );
15988
16022
  this.context.stdout.write(` ${colors.hint("brain repo:")} https://github.com/${owner}/${finalName}
@@ -16070,8 +16104,7 @@ var BrainLinkCommand = class extends M8tCommand {
16070
16104
  if (!health.ok) {
16071
16105
  throw new LocalCliError({
16072
16106
  code: "APP_HEALTH_FAILED",
16073
- message: "GitHub App is not configured correctly.",
16074
- hint: "Run `m8t brain check-app` for details, then re-run this command."
16107
+ ...appHealthFailureCopy(health)
16075
16108
  });
16076
16109
  }
16077
16110
  const account = await getAzAccount();
@@ -16112,7 +16145,7 @@ var BrainLinkCommand = class extends M8tCommand {
16112
16145
  this.context.stdout.write(` ${colors.success("\u2713")} installation detected (id: ${installationId})
16113
16146
  `);
16114
16147
  }
16115
- const result = await linkBrain({
16148
+ const result2 = await linkBrain({
16116
16149
  credential: credential2,
16117
16150
  kvUri,
16118
16151
  projectEndpoint: project.endpoint,
@@ -16140,18 +16173,18 @@ var BrainLinkCommand = class extends M8tCommand {
16140
16173
  worker: this.worker,
16141
16174
  repo,
16142
16175
  branch,
16143
- installationId: result.installationId,
16144
- connectionName: result.connectionName,
16145
- foundryVersion: result.foundryVersion,
16146
- alreadyLinked: result.alreadyLinked
16176
+ installationId: result2.installationId,
16177
+ connectionName: result2.connectionName,
16178
+ foundryVersion: result2.foundryVersion,
16179
+ alreadyLinked: result2.alreadyLinked
16147
16180
  }) + "\n");
16148
16181
  return 0;
16149
16182
  }
16150
- if (result.alreadyLinked) {
16183
+ if (result2.alreadyLinked) {
16151
16184
  this.context.stdout.write(`${colors.success("\u2713")} ${colors.field(this.worker)} already linked to ${colors.field(repo)} (token rotated). No-op.
16152
16185
  `);
16153
16186
  } else {
16154
- this.context.stdout.write(`${colors.success("\u2713")} linked ${colors.field(this.worker)} \u2194 ${colors.field(repo)} via App-mode (install ${installationId}, agent version ${result.foundryVersion ?? "unknown"}).
16187
+ this.context.stdout.write(`${colors.success("\u2713")} linked ${colors.field(this.worker)} \u2194 ${colors.field(repo)} via App-mode (install ${installationId}, agent version ${result2.foundryVersion ?? "unknown"}).
16155
16188
  `);
16156
16189
  this.context.stdout.write(` ${colors.hint("brain doctrine:")} https://github.com/${repo}/blob/${branch}/AGENTS.md
16157
16190
  `);
@@ -16171,16 +16204,16 @@ import { AIProjectClient as AIProjectClient2 } from "@azure/ai-projects";
16171
16204
  async function listM8tAgents(args) {
16172
16205
  const project = new AIProjectClient2(args.projectEndpoint, args.credential);
16173
16206
  const iter = project.agents.list();
16174
- const result = [];
16207
+ const result2 = [];
16175
16208
  for await (const raw of iter) {
16176
16209
  if (!raw.name && !raw.id) continue;
16177
16210
  const name = String(raw.name ?? raw.id);
16178
16211
  const md = raw.metadata ?? raw.versions?.latest?.metadata ?? {};
16179
16212
  if (md.source === "m8t") {
16180
- result.push({ name, metadata: md });
16213
+ result2.push({ name, metadata: md });
16181
16214
  }
16182
16215
  }
16183
- return result;
16216
+ return result2;
16184
16217
  }
16185
16218
 
16186
16219
  // src/commands/brain/list.ts
@@ -16230,9 +16263,9 @@ import { Command as Command21, Option as Option20 } from "clipanion";
16230
16263
 
16231
16264
  // src/lib/github-orgs.ts
16232
16265
  var ORG_QUERY = "{ viewer { organizations(first:50) { nodes { login viewerCanAdminister enterpriseOwners(first:1) { totalCount } } } } }";
16233
- function classifyOrgs(result) {
16234
- if (!result.ok) return { verdict: "undetermined", candidates: [], orgs: [] };
16235
- const orgs = result.orgs;
16266
+ function classifyOrgs(result2) {
16267
+ if (!result2.ok) return { verdict: "undetermined", candidates: [], orgs: [] };
16268
+ const orgs = result2.orgs;
16236
16269
  if (orgs.length === 0) return { verdict: "no-orgs", candidates: [], orgs };
16237
16270
  const enterprise = orgs.filter((o) => o.enterprise);
16238
16271
  if (enterprise.length === 1) {
@@ -16635,7 +16668,7 @@ var BrainUnlinkCommand = class extends M8tCommand {
16635
16668
  endpoint: typeof this.endpoint === "string" ? this.endpoint : void 0,
16636
16669
  agentEndpoint
16637
16670
  });
16638
- const result = await unlinkBrain({
16671
+ const result2 = await unlinkBrain({
16639
16672
  credential: credential2,
16640
16673
  kvUri,
16641
16674
  projectEndpoint: project.endpoint,
@@ -16649,9 +16682,9 @@ var BrainUnlinkCommand = class extends M8tCommand {
16649
16682
  renderJson({
16650
16683
  worker,
16651
16684
  repo,
16652
- foundryVersion: result.foundryVersion,
16653
- connectionDeleted: result.connectionDeleted,
16654
- appUninstalled: result.appUninstalled
16685
+ foundryVersion: result2.foundryVersion,
16686
+ connectionDeleted: result2.connectionDeleted,
16687
+ appUninstalled: result2.appUninstalled
16655
16688
  }) + "\n"
16656
16689
  );
16657
16690
  return 0;
@@ -16660,7 +16693,7 @@ var BrainUnlinkCommand = class extends M8tCommand {
16660
16693
  `${colors.success("\u2713")} unlinked ${colors.field(worker)} from ${colors.field(repo)}. Repo intact \u2014 run ${colors.field(`gh repo delete ${repo}`)} to remove it.
16661
16694
  `
16662
16695
  );
16663
- if (!keepAppInstall && result.appUninstalled) {
16696
+ if (!keepAppInstall && result2.appUninstalled) {
16664
16697
  this.context.stdout.write(
16665
16698
  ` ${colors.hint("app install removed:")} re-link anytime with \`m8t brain link ${worker} --repo ${repo}\` \u2014 no re-registration needed.
16666
16699
  `
@@ -17063,10 +17096,10 @@ async function enableA2a(args) {
17063
17096
  message: `Agent '${args.agentName}' is kind '${current.definition.kind}'. Agent-to-agent enablement supports prompt agents (callers) and hosted agents (callees); '${current.definition.kind}' is neither.`
17064
17097
  });
17065
17098
  }
17066
- const bearer = `a2a_${randomBytes2(32).toString("base64url")}`;
17067
- const bearerHash = createHash("sha256").update(bearer).digest("hex");
17099
+ const bearer2 = `a2a_${randomBytes2(32).toString("base64url")}`;
17100
+ const bearerHash = createHash("sha256").update(bearer2).digest("hex");
17068
17101
  progress("Provisioning the A2A connection\u2026");
17069
- await putA2aConnection({ credential: args.credential, projectArmId: args.projectArmId, connectionName, target: args.bridgeUrl, bearer });
17102
+ await putA2aConnection({ credential: args.credential, projectArmId: args.projectArmId, connectionName, target: args.bridgeUrl, bearer: bearer2 });
17070
17103
  progress("Deploying the a2a-enabled agent version\u2026");
17071
17104
  const definition = {
17072
17105
  ...current.definition,
@@ -17313,9 +17346,9 @@ function defaultRemoveAgentDeps(args) {
17313
17346
  await deleteAgent({ credential: credential2, endpoint: projectEndpoint, name: agentName });
17314
17347
  },
17315
17348
  deleteBrainRepo(repo) {
17316
- const result = spawnSync2("gh", ["repo", "delete", repo, "--yes"], { encoding: "utf8" });
17317
- if (result.status !== 0) {
17318
- return Promise.reject(new Error(result.stderr?.trim() || `gh repo delete exited with code ${String(result.status)}`));
17349
+ const result2 = spawnSync2("gh", ["repo", "delete", repo, "--yes"], { encoding: "utf8" });
17350
+ if (result2.status !== 0) {
17351
+ return Promise.reject(new Error(result2.stderr?.trim() || `gh repo delete exited with code ${String(result2.status)}`));
17319
17352
  }
17320
17353
  return Promise.resolve();
17321
17354
  },
@@ -17537,7 +17570,7 @@ var A2aEnableCommand = class extends M8tCommand {
17537
17570
  });
17538
17571
  const personaPath = this.resolvePersonaPath();
17539
17572
  const bridgeUrl = resolveBridgeUrl({ flag: typeof this.gatewayUrl === "string" ? this.gatewayUrl : void 0, env });
17540
- const result = await enableA2a({
17573
+ const result2 = await enableA2a({
17541
17574
  credential: credential2,
17542
17575
  projectEndpoint: project.endpoint,
17543
17576
  projectArmId: `${project.accountScope}/projects/${project.projectName}`,
@@ -17547,17 +17580,17 @@ var A2aEnableCommand = class extends M8tCommand {
17547
17580
  onProgress: progress
17548
17581
  });
17549
17582
  if (mode === "json") {
17550
- this.context.stdout.write(renderJson({ worker: this.worker, mode: result.mode, connectionName: result.connectionName, foundryVersion: result.foundryVersion, bridgeUrl }) + "\n");
17583
+ this.context.stdout.write(renderJson({ worker: this.worker, mode: result2.mode, connectionName: result2.connectionName, foundryVersion: result2.foundryVersion, bridgeUrl }) + "\n");
17551
17584
  return 0;
17552
17585
  }
17553
- if (result.mode === "target") {
17554
- this.context.stdout.write(`${colors.success("\u2713")} ${colors.field(this.worker)} is a discoverable a2a target (agent version ${result.foundryVersion}).
17586
+ if (result2.mode === "target") {
17587
+ this.context.stdout.write(`${colors.success("\u2713")} ${colors.field(this.worker)} is a discoverable a2a target (agent version ${result2.foundryVersion}).
17555
17588
  `);
17556
17589
  this.context.stdout.write(` ${colors.hint("directory:")} it now appears in every a2a caller's discover_workers as a callee. Hosted agents are callees only \u2014 no caller connection.
17557
17590
  `);
17558
17591
  return 0;
17559
17592
  }
17560
- this.context.stdout.write(`${colors.success("\u2713")} ${colors.field(this.worker)} is a2a-enabled (connection ${colors.field(result.connectionName ?? "")}, agent version ${result.foundryVersion}).
17593
+ this.context.stdout.write(`${colors.success("\u2713")} ${colors.field(this.worker)} is a2a-enabled (connection ${colors.field(result2.connectionName ?? "")}, agent version ${result2.foundryVersion}).
17561
17594
  `);
17562
17595
  this.context.stdout.write(` ${colors.hint("directory:")} it now appears in every a2a worker's discover_workers, and can delegate via invoke_worker.
17563
17596
  `);
@@ -17604,7 +17637,7 @@ var A2aDisableCommand = class extends M8tCommand {
17604
17637
  endpoint: typeof this.endpoint === "string" ? this.endpoint : void 0,
17605
17638
  agentEndpoint: readAgentYaml(this.worker)?.projectEndpoint
17606
17639
  });
17607
- const result = await disableA2a({
17640
+ const result2 = await disableA2a({
17608
17641
  credential: credential2,
17609
17642
  projectEndpoint: project.endpoint,
17610
17643
  projectArmId: `${project.accountScope}/projects/${project.projectName}`,
@@ -17612,10 +17645,10 @@ var A2aDisableCommand = class extends M8tCommand {
17612
17645
  onProgress: progress
17613
17646
  });
17614
17647
  if (mode === "json") {
17615
- this.context.stdout.write(renderJson({ worker: this.worker, ...result }) + "\n");
17648
+ this.context.stdout.write(renderJson({ worker: this.worker, ...result2 }) + "\n");
17616
17649
  return 0;
17617
17650
  }
17618
- this.context.stdout.write(`${colors.success("\u2713")} ${colors.field(this.worker)} a2a-disabled (connection ${colors.field(result.connectionName)} removed, agent version ${result.foundryVersion ?? ""}).
17651
+ this.context.stdout.write(`${colors.success("\u2713")} ${colors.field(this.worker)} a2a-disabled (connection ${colors.field(result2.connectionName)} removed, agent version ${result2.foundryVersion ?? ""}).
17619
17652
  `);
17620
17653
  return 0;
17621
17654
  }
@@ -18188,6 +18221,20 @@ async function stagePublicImageIfNeeded(args) {
18188
18221
  return { image: acrRef, staged: true, notes };
18189
18222
  }
18190
18223
 
18224
+ // src/lib/warnings.ts
18225
+ function makeWarningSink(echo) {
18226
+ const items = [];
18227
+ return {
18228
+ add(msg) {
18229
+ items.push(msg);
18230
+ echo?.(msg);
18231
+ },
18232
+ list() {
18233
+ return items.slice();
18234
+ }
18235
+ };
18236
+ }
18237
+
18191
18238
  // src/commands/coder/deploy.ts
18192
18239
  init_esm();
18193
18240
  var SIZE_PRESETS = {
@@ -18253,6 +18300,10 @@ var CoderDeployCommand = class extends M8tCommand {
18253
18300
  this.context.stdout
18254
18301
  );
18255
18302
  const interactive = this.context.stdout.isTTY === true;
18303
+ const warnings = makeWarningSink((m) => {
18304
+ if (mode !== "json") this.context.stdout.write(`${colors.hint("note:")} ${m}
18305
+ `);
18306
+ });
18256
18307
  const credential2 = new DefaultAzureCredential13();
18257
18308
  const account = await getAzAccount();
18258
18309
  const subscriptionId = this.subscription ?? account.subscriptionId;
@@ -18290,20 +18341,18 @@ var CoderDeployCommand = class extends M8tCommand {
18290
18341
  }
18291
18342
  const staged = await stagePublicImageIfNeeded({ image: requestedImage, project });
18292
18343
  for (const n of staged.notes) {
18293
- this.context.stderr.write(`${colors.hint("note:")} ${n}
18294
- `);
18344
+ warnings.add(n);
18295
18345
  }
18296
18346
  const image = staged.image;
18297
- const { warnings } = await checkPreconditions({
18347
+ const { warnings: preconditionWarnings } = await checkPreconditions({
18298
18348
  credential: credential2,
18299
18349
  subscriptionId,
18300
18350
  project,
18301
18351
  image,
18302
18352
  callerObjectId
18303
18353
  });
18304
- for (const w of warnings) {
18305
- this.context.stderr.write(`${colors.hint("note:")} ${w}
18306
- `);
18354
+ for (const w of preconditionWarnings) {
18355
+ warnings.add(w);
18307
18356
  }
18308
18357
  if (this.skipQuotaCheck !== true) {
18309
18358
  const usages = await listModelQuota(project.region);
@@ -18316,14 +18365,13 @@ var CoderDeployCommand = class extends M8tCommand {
18316
18365
  });
18317
18366
  }
18318
18367
  if (verdict === "unknown") {
18319
- this.context.stderr.write(`${colors.hint("note:")} could not confirm quota for '${env.MODEL_DEPLOYMENT_NAME}' in ${project.region}; proceeding.
18320
- `);
18368
+ warnings.add(`could not confirm quota for '${env.MODEL_DEPLOYMENT_NAME}' in ${project.region}; proceeding.`);
18321
18369
  }
18322
18370
  }
18323
18371
  const { persona: personaName, personaVersion } = resolvePersona(persona);
18324
18372
  const onProgress = mode === "pretty" ? (m) => this.context.stderr.write(` ${colors.dim(m)}
18325
18373
  `) : void 0;
18326
- const result = await deployHostedWorker({
18374
+ const result2 = await deployHostedWorker({
18327
18375
  credential: credential2,
18328
18376
  subscriptionId,
18329
18377
  project,
@@ -18354,10 +18402,7 @@ var CoderDeployCommand = class extends M8tCommand {
18354
18402
  if (!health.ok) {
18355
18403
  throw new LocalCliError({
18356
18404
  code: "APP_HEALTH_FAILED",
18357
- message: health.transient ? "GitHub App health check failed transiently (Key Vault / GitHub API blip)." : "GitHub App is not configured correctly.",
18358
- hint: health.transient ? "This is usually a transient Azure/GitHub blip \u2014 retrying. If it persists, run `m8t brain check-app`." : "Run `m8t brain check-app` for details.",
18359
- category: health.transient ? "transient" : "unknown",
18360
- retryable: health.transient === true
18405
+ ...appHealthFailureCopy(health)
18361
18406
  });
18362
18407
  }
18363
18408
  const { slug, appId, privateKeyPem } = await readAppSecrets({ credential: credential2, kvUri });
@@ -18397,7 +18442,7 @@ var CoderDeployCommand = class extends M8tCommand {
18397
18442
  await ensureDeliveryGrant({
18398
18443
  credential: credential2,
18399
18444
  subscriptionId,
18400
- principalId: result.principalId,
18445
+ principalId: result2.principalId,
18401
18446
  kvUri
18402
18447
  });
18403
18448
  this.context.stdout.write(
@@ -18432,19 +18477,20 @@ var CoderDeployCommand = class extends M8tCommand {
18432
18477
  this.context.stdout.write(
18433
18478
  renderJson({
18434
18479
  name: this.name,
18435
- version: result.version,
18436
- status: result.status,
18480
+ version: result2.version,
18481
+ status: result2.status,
18437
18482
  persona: personaName,
18438
18483
  image,
18439
18484
  size,
18440
18485
  endpoint: project.endpoint,
18441
- agentPrincipalId: result.principalId
18486
+ agentPrincipalId: result2.principalId,
18487
+ warnings: warnings.list()
18442
18488
  }) + "\n"
18443
18489
  );
18444
18490
  return 0;
18445
18491
  }
18446
18492
  this.context.stdout.write(
18447
- `${colors.success("\u2713")} deployed hosted coder ${colors.field(this.name)} (version ${result.version}, ${result.status}, ${size}, persona ${personaName}).
18493
+ `${colors.success("\u2713")} deployed hosted coder ${colors.field(this.name)} (version ${result2.version}, ${result2.status}, ${size}, persona ${personaName}).
18448
18494
  `
18449
18495
  );
18450
18496
  this.context.stdout.write(` image: ${image}
@@ -18662,6 +18708,10 @@ var AzureExecDeployCommand = class extends M8tCommand {
18662
18708
  const interactive = this.context.stdout.isTTY === true;
18663
18709
  const onProgress = mode === "pretty" ? (m) => this.context.stderr.write(` ${colors.dim(m)}
18664
18710
  `) : void 0;
18711
+ const warnings = makeWarningSink((m) => {
18712
+ if (mode !== "json") this.context.stdout.write(`${colors.hint("note:")} ${m}
18713
+ `);
18714
+ });
18665
18715
  const credential2 = new DefaultAzureCredential15();
18666
18716
  const account = await getAzAccount();
18667
18717
  const subscriptionId = this.subscription ?? account.subscriptionId;
@@ -18702,8 +18752,7 @@ var AzureExecDeployCommand = class extends M8tCommand {
18702
18752
  }
18703
18753
  const staged = await stagePublicImageIfNeeded({ image: requestedImage, project });
18704
18754
  for (const n of staged.notes) {
18705
- this.context.stderr.write(`${colors.hint("note:")} ${n}
18706
- `);
18755
+ warnings.add(n);
18707
18756
  }
18708
18757
  const image = staged.image;
18709
18758
  const processEnv = this.context.env;
@@ -18713,10 +18762,7 @@ var AzureExecDeployCommand = class extends M8tCommand {
18713
18762
  if (!health.ok) {
18714
18763
  throw new LocalCliError({
18715
18764
  code: "APP_HEALTH_FAILED",
18716
- message: health.transient ? "GitHub App health check failed transiently (Key Vault / GitHub API blip)." : "GitHub App is not configured correctly.",
18717
- hint: health.transient ? "This is usually a transient Azure/GitHub blip \u2014 retrying. If it persists, run `m8t brain check-app`." : "Run `m8t brain check-app` for details.",
18718
- category: health.transient ? "transient" : "unknown",
18719
- retryable: health.transient === true
18765
+ ...appHealthFailureCopy(health)
18720
18766
  });
18721
18767
  }
18722
18768
  const { slug, appId, privateKeyPem } = await readAppSecrets({ credential: credential2, kvUri });
@@ -18761,16 +18807,15 @@ var AzureExecDeployCommand = class extends M8tCommand {
18761
18807
  }
18762
18808
  env[pair.slice(0, eq)] = pair.slice(eq + 1);
18763
18809
  }
18764
- const { warnings } = await checkPreconditions({
18810
+ const { warnings: preconditionWarnings } = await checkPreconditions({
18765
18811
  credential: credential2,
18766
18812
  subscriptionId,
18767
18813
  project,
18768
18814
  image,
18769
18815
  callerObjectId
18770
18816
  });
18771
- for (const w of warnings) {
18772
- this.context.stderr.write(`${colors.hint("note:")} ${w}
18773
- `);
18817
+ for (const w of preconditionWarnings) {
18818
+ warnings.add(w);
18774
18819
  }
18775
18820
  if (this.skipQuotaCheck !== true) {
18776
18821
  const usages = await listModelQuota(project.region);
@@ -18784,7 +18829,7 @@ var AzureExecDeployCommand = class extends M8tCommand {
18784
18829
  }
18785
18830
  }
18786
18831
  const { persona: personaName, personaVersion } = resolvePersona("azure-executor");
18787
- const result = await deployHostedWorker({
18832
+ const result2 = await deployHostedWorker({
18788
18833
  credential: credential2,
18789
18834
  subscriptionId,
18790
18835
  project,
@@ -18801,7 +18846,7 @@ var AzureExecDeployCommand = class extends M8tCommand {
18801
18846
  credential: credential2,
18802
18847
  subscriptionId,
18803
18848
  scope: grantScope,
18804
- principalId: result.principalId
18849
+ principalId: result2.principalId
18805
18850
  });
18806
18851
  if (this.grantAccessAdmin === true) {
18807
18852
  onProgress?.(`granting User Access Administrator at ${grantScope}\u2026`);
@@ -18809,14 +18854,14 @@ var AzureExecDeployCommand = class extends M8tCommand {
18809
18854
  credential: credential2,
18810
18855
  subscriptionId,
18811
18856
  scope: grantScope,
18812
- principalId: result.principalId
18857
+ principalId: result2.principalId
18813
18858
  });
18814
18859
  }
18815
18860
  onProgress?.("granting Key Vault Secrets User for delivery\u2026");
18816
18861
  await ensureDeliveryGrant({
18817
18862
  credential: credential2,
18818
18863
  subscriptionId,
18819
- principalId: result.principalId,
18864
+ principalId: result2.principalId,
18820
18865
  kvUri
18821
18866
  });
18822
18867
  onProgress?.("a2a-enabling as a target\u2026");
@@ -18838,19 +18883,20 @@ var AzureExecDeployCommand = class extends M8tCommand {
18838
18883
  this.context.stdout.write(
18839
18884
  renderJson({
18840
18885
  name: this.name,
18841
- version: result.version,
18842
- status: result.status,
18886
+ version: result2.version,
18887
+ status: result2.status,
18843
18888
  persona: personaName,
18844
18889
  image,
18845
18890
  scope: grantScope,
18846
18891
  endpoint: project.endpoint,
18847
- agentPrincipalId: result.principalId
18892
+ agentPrincipalId: result2.principalId,
18893
+ warnings: warnings.list()
18848
18894
  }) + "\n"
18849
18895
  );
18850
18896
  return 0;
18851
18897
  }
18852
18898
  this.context.stdout.write(
18853
- `${colors.success("\u2713")} deployed Azure executor ${colors.field(this.name)} (version ${result.version}, ${result.status}, ${size}).
18899
+ `${colors.success("\u2713")} deployed Azure executor ${colors.field(this.name)} (version ${result2.version}, ${result2.status}, ${size}).
18854
18900
  `
18855
18901
  );
18856
18902
  this.context.stdout.write(` scope: ${grantScope}
@@ -19456,8 +19502,8 @@ async function readStampOutcome(opts) {
19456
19502
  }
19457
19503
  }
19458
19504
  async function readStamp(opts) {
19459
- const result = await readStampOutcome(opts);
19460
- return result.source === "explicit" ? result.stamp : null;
19505
+ const result2 = await readStampOutcome(opts);
19506
+ return result2.source === "explicit" ? result2.stamp : null;
19461
19507
  }
19462
19508
 
19463
19509
  // src/lib/platform-converge.ts
@@ -19606,9 +19652,18 @@ async function ensureResourceGroup(name, location) {
19606
19652
  function resolveAcrPullIdentity(opts) {
19607
19653
  return opts.explicit ?? "";
19608
19654
  }
19655
+ function buildDeployResult(args) {
19656
+ return {
19657
+ webapp: args.webappUrl,
19658
+ resourceGroup: args.resourceGroup,
19659
+ clientId: args.clientId,
19660
+ resources: args.resourceNames,
19661
+ warnings: args.warnings
19662
+ };
19663
+ }
19609
19664
  async function runBicepDeployment(opts) {
19610
19665
  const bicepPath = path20.join(opts.repoRoot, "deploy", "main.bicep");
19611
- const result = JSON.parse(
19666
+ const result2 = JSON.parse(
19612
19667
  await runAz([
19613
19668
  "deployment",
19614
19669
  "group",
@@ -19625,7 +19680,7 @@ async function runBicepDeployment(opts) {
19625
19680
  "json"
19626
19681
  ])
19627
19682
  );
19628
- const outputs = result.properties?.outputs ?? {};
19683
+ const outputs = result2.properties?.outputs ?? {};
19629
19684
  const fqdn = outputs.containerAppFqdn?.value;
19630
19685
  if (typeof fqdn !== "string" || !fqdn) {
19631
19686
  throw new LocalCliError({
@@ -21154,8 +21209,8 @@ async function runHealthGateWithRollback(args) {
21154
21209
  const target = args.plan.targetVersion;
21155
21210
  let applied = [];
21156
21211
  try {
21157
- const result = await applyPlan(args.plan, args.deps, args.ctx);
21158
- applied = result.applied;
21212
+ const result2 = await applyPlan(args.plan, args.deps, args.ctx);
21213
+ applied = result2.applied;
21159
21214
  const outcomes = await runGate(args, applied, budgetMs);
21160
21215
  if (!gatePassed(outcomes)) {
21161
21216
  const detail = outcomes.find((o) => !o.ok)?.detail ?? "a post-apply health probe failed";
@@ -21664,7 +21719,7 @@ var PlatformSeedStampCommand = class extends M8tCommand {
21664
21719
  `);
21665
21720
  };
21666
21721
  const credential2 = typeof this.miClientId === "string" && this.miClientId.trim().length > 0 ? new ManagedIdentityCredential2({ clientId: this.miClientId.trim() }) : new DefaultAzureCredential18();
21667
- const result = await seedStamp2({
21722
+ const result2 = await seedStamp2({
21668
21723
  descriptorPath: need(this.descriptor, "--descriptor"),
21669
21724
  credential: credential2,
21670
21725
  subscriptionId: need(this.subscription, "--subscription"),
@@ -21672,9 +21727,9 @@ var PlatformSeedStampCommand = class extends M8tCommand {
21672
21727
  onProgress
21673
21728
  });
21674
21729
  if (mode === "json") {
21675
- this.context.stdout.write(renderJson(result) + "\n");
21730
+ this.context.stdout.write(renderJson(result2) + "\n");
21676
21731
  } else {
21677
- this.context.stdout.write(`${result.outcome}: ${result.platformVersion} (storage ${result.storageAccount})
21732
+ this.context.stdout.write(`${result2.outcome}: ${result2.platformVersion} (storage ${result2.storageAccount})
21678
21733
  `);
21679
21734
  }
21680
21735
  return 0;
@@ -21768,9 +21823,9 @@ var PlatformStampBuildCommand = class extends M8tCommand {
21768
21823
  ...priorComponents?.brainSeeds ? { brainSeeds: priorComponents.brainSeeds } : {}
21769
21824
  });
21770
21825
  await writeStamp({ ...ctx, stamp, onProgress });
21771
- const result = { platformVersion: stamp.platformVersion, previous: previousPlatformVersion, storageAccount: accountName };
21826
+ const result2 = { platformVersion: stamp.platformVersion, previous: previousPlatformVersion, storageAccount: accountName };
21772
21827
  this.context.stdout.write(
21773
- mode === "json" ? renderJson(result) + "\n" : `platform stamp: ${result.platformVersion} (previous ${result.previous ?? "none"}, storage ${result.storageAccount})
21828
+ mode === "json" ? renderJson(result2) + "\n" : `platform stamp: ${result2.platformVersion} (previous ${result2.previous ?? "none"}, storage ${result2.storageAccount})
21774
21829
  `
21775
21830
  );
21776
21831
  return 0;
@@ -21865,23 +21920,23 @@ var PlatformPolicyCommand = class extends M8tCommand {
21865
21920
  if (typeof this.set === "string" && this.set.length > 0) {
21866
21921
  const policy = buildPolicy(this.set, (/* @__PURE__ */ new Date()).toISOString(), "cli");
21867
21922
  await writePolicy({ ...ctx, policy, onProgress });
21868
- const result2 = { mode: policy.mode, updatedAt: policy.updatedAt };
21869
- this.context.stdout.write(mode === "json" ? renderJson(result2) + "\n" : `update policy: ${policy.mode}
21923
+ const result3 = { mode: policy.mode, updatedAt: policy.updatedAt };
21924
+ this.context.stdout.write(mode === "json" ? renderJson(result3) + "\n" : `update policy: ${policy.mode}
21870
21925
  `);
21871
21926
  return 0;
21872
21927
  }
21873
21928
  const current = await readPolicy(ctx);
21874
21929
  if (current.source === "unreadable") {
21875
- const result2 = { outcome: "unreadable" };
21930
+ const result3 = { outcome: "unreadable" };
21876
21931
  this.context.stdout.write(
21877
- mode === "json" ? renderJson(result2) + "\n" : `update policy: could not be read \u2014 the current setting is unknown; do not assume it is on the default (${DEFAULT_POLICY_MODE}).
21932
+ mode === "json" ? renderJson(result3) + "\n" : `update policy: could not be read \u2014 the current setting is unknown; do not assume it is on the default (${DEFAULT_POLICY_MODE}).
21878
21933
  `
21879
21934
  );
21880
21935
  return 0;
21881
21936
  }
21882
- const result = current.source === "explicit" ? { mode: current.policy.mode, outcome: "explicit" } : { mode: DEFAULT_POLICY_MODE, outcome: "absent" };
21937
+ const result2 = current.source === "explicit" ? { mode: current.policy.mode, outcome: "explicit" } : { mode: DEFAULT_POLICY_MODE, outcome: "absent" };
21883
21938
  this.context.stdout.write(
21884
- mode === "json" ? renderJson(result) + "\n" : `update policy: ${result.mode}${result.outcome === "absent" ? " (default \u2014 never set)" : ""}
21939
+ mode === "json" ? renderJson(result2) + "\n" : `update policy: ${result2.mode}${result2.outcome === "absent" ? " (default \u2014 never set)" : ""}
21885
21940
  `
21886
21941
  );
21887
21942
  return 0;
@@ -22545,8 +22600,8 @@ async function runWhatIf(opts) {
22545
22600
  "--output",
22546
22601
  "json"
22547
22602
  ]);
22548
- const result = JSON.parse(out);
22549
- return result.changes ?? [];
22603
+ const result2 = JSON.parse(out);
22604
+ return result2.changes ?? [];
22550
22605
  }
22551
22606
  var fmt = (c) => ` \u2022 ${c.resourceType} ${c.path}: ${JSON.stringify(c.before ?? null)} \u2192 ${JSON.stringify(c.after ?? null)}${c.reason ? ` [${c.reason}]` : ""}`;
22552
22607
  function reportWhatIf(r, opts) {
@@ -22734,6 +22789,9 @@ var DeployCommand = class extends M8tCommand {
22734
22789
  const log = (msg) => {
22735
22790
  if (mode !== "json") this.context.stdout.write(msg + "\n");
22736
22791
  };
22792
+ const warnings = makeWarningSink((m) => {
22793
+ log(colors.dim(m));
22794
+ });
22737
22795
  if (this.whatIf) {
22738
22796
  try {
22739
22797
  if (this.subscription) {
@@ -22811,6 +22869,11 @@ var DeployCommand = class extends M8tCommand {
22811
22869
  }
22812
22870
  log(this.clientId ? `using existing app reg ${this.clientId}` : "ensuring Entra app reg\u2026");
22813
22871
  const app = await ensureAppReg({ tenantId: account.tenantId, clientId: this.clientId });
22872
+ if (app.appObjectId === null && this.clientId) {
22873
+ warnings.add(
22874
+ "BYO app reg - Expose-an-API was not verified or configured (Graph writes are skipped for a supplied --client-id); if sign-in fails, configure identifierUris + a user_impersonation scope + Azure CLI pre-authorization per deploy/operator-app-registration-setup.md."
22875
+ );
22876
+ }
22814
22877
  await writeFoundryConfig({ tenantId: app.tenantId, clientId: app.clientId, projectEndpoint: foundryEndpoint });
22815
22878
  log(`ensuring resource group ${this.resourceGroup}\u2026`);
22816
22879
  await ensureResourceGroup(this.resourceGroup, this.location);
@@ -22863,14 +22926,20 @@ var DeployCommand = class extends M8tCommand {
22863
22926
  if (writableAppObjectId) {
22864
22927
  await patchRedirectUris(writableAppObjectId, outputs.containerAppFqdn);
22865
22928
  } else {
22866
- log(colors.dim(
22867
- ` (could not register the sign-in redirect URI automatically \u2014 add https://${outputs.containerAppFqdn} as an SPA redirect URI on app registration ${app.clientId}, or sign-in will fail with AADSTS50011)`
22868
- ));
22929
+ warnings.add(
22930
+ `could not register the sign-in redirect URI automatically \u2014 add https://${outputs.containerAppFqdn} as an SPA redirect URI on app registration ${app.clientId}, or sign-in will fail with AADSTS50011`
22931
+ );
22869
22932
  }
22870
22933
  const webappUrl = `https://${outputs.containerAppFqdn}`;
22871
22934
  if (mode === "json") {
22872
22935
  this.context.stdout.write(
22873
- renderJson({ webapp: webappUrl, resourceGroup: this.resourceGroup, clientId: app.clientId, resources: outputs.resourceNames }) + "\n"
22936
+ renderJson(buildDeployResult({
22937
+ webappUrl,
22938
+ resourceGroup: this.resourceGroup,
22939
+ clientId: app.clientId,
22940
+ resourceNames: outputs.resourceNames,
22941
+ warnings: warnings.list()
22942
+ })) + "\n"
22874
22943
  );
22875
22944
  return 0;
22876
22945
  }
@@ -23624,8 +23693,12 @@ function checkDataPlane(probe) {
23624
23693
  return {
23625
23694
  name: "foundry data-plane",
23626
23695
  status: "FAIL",
23627
- detail: `HTTP ${probe.status.toString()} \u2014 your identity lacks a data-plane role on the Foundry account`,
23628
- remediation: `az role assignment create --assignee-object-id ${oid} --assignee-principal-type User --role "Cognitive Services User" --scope ${scope}`
23696
+ // Subscription Owner is the natural assumption and it is wrong: reading and
23697
+ // invoking agents are dataActions, and control-plane roles carry none.
23698
+ detail: `HTTP ${probe.status.toString()} \u2014 your identity lacks a data-plane role on the Foundry account (subscription Owner does not grant one)`,
23699
+ // "Foundry User", not a Cognitive Services role: Microsoft's guidance is that
23700
+ // Cognitive Services roles are for AI Services resources, not Foundry projects.
23701
+ remediation: `m8t prereqs --fix (or by hand: az role assignment create --assignee-object-id ${oid} --assignee-principal-type User --role "Foundry User" --scope ${scope})`
23629
23702
  };
23630
23703
  }
23631
23704
  return {
@@ -23634,6 +23707,24 @@ function checkDataPlane(probe) {
23634
23707
  detail: `HTTP ${probe.status !== 0 ? probe.status.toString() : "(unreachable)"} \u2014 unexpected; re-run with --verbose`
23635
23708
  };
23636
23709
  }
23710
+ function checkKeyVaultSecrets(probe) {
23711
+ if (probe.status >= 200 && probe.status < 300) {
23712
+ return { name: "key vault secrets", status: "PASS", detail: `HTTP ${probe.status.toString()}` };
23713
+ }
23714
+ if (probe.status === 401 || probe.status === 403) {
23715
+ return {
23716
+ name: "key vault secrets",
23717
+ status: "FAIL",
23718
+ detail: `HTTP ${probe.status.toString()} \u2014 you cannot read the platform Key Vault, so worker deploys will fail. This is a permanent permission gap, not a transient blip.`,
23719
+ remediation: `m8t prereqs --fix (or by hand: az role assignment create --assignee-object-id ${probe.oid ?? "<your-object-id>"} --assignee-principal-type User --role "Key Vault Secrets User" --scope ${probe.kvScope ?? "<key-vault-resource-id>"})`
23720
+ };
23721
+ }
23722
+ return {
23723
+ name: "key vault secrets",
23724
+ status: "INFO",
23725
+ detail: probe.status === 0 ? "skipped \u2014 could not reach the platform Key Vault" : `HTTP ${probe.status.toString()} \u2014 unexpected`
23726
+ };
23727
+ }
23637
23728
  function checkDeliveryGrant(probes) {
23638
23729
  const withDelivery = probes.filter((p) => p.hasDeliveryEnv);
23639
23730
  if (withDelivery.length === 0) {
@@ -23756,7 +23847,180 @@ function checkLegacyStateDir(probe) {
23756
23847
  init_foundry_agent_get();
23757
23848
  init_foundry_agents();
23758
23849
  init_rbac();
23759
- async function resolveFoundryAccountId(endpoint) {
23850
+
23851
+ // src/lib/prereq-probes.ts
23852
+ init_rbac();
23853
+ var FOUNDRY_SCOPE4 = "https://ai.azure.com/.default";
23854
+ var KV_SCOPE = "https://vault.azure.net/.default";
23855
+ var AGENTS_API = "v1";
23856
+ var COGNITIVE_SERVICES_USER_ROLE_ID = "a97b65f3-24c7-4388-baec-2e87135dc908";
23857
+ var FOUNDRY_DATA_PLANE_ROLE_IDS = [FOUNDRY_USER_ROLE_ID, COGNITIVE_SERVICES_USER_ROLE_ID];
23858
+ async function probeProviders(namespaces, subscriptionId) {
23859
+ return Promise.all(
23860
+ namespaces.map(async (namespace) => {
23861
+ try {
23862
+ const args = ["provider", "show", "--namespace", namespace, "--query", "registrationState", "-o", "tsv"];
23863
+ if (subscriptionId) args.push("--subscription", subscriptionId);
23864
+ const out = (await runAz(args)).trim();
23865
+ return { namespace, state: out && out !== "None" ? out : null };
23866
+ } catch {
23867
+ return { namespace, state: null };
23868
+ }
23869
+ })
23870
+ );
23871
+ }
23872
+ async function registerProviders(namespaces, opts = {}) {
23873
+ const attempts = opts.attempts ?? 20;
23874
+ const sleepMs = opts.sleepMs ?? 5e3;
23875
+ for (const namespace of namespaces) {
23876
+ try {
23877
+ const args = ["provider", "register", "--namespace", namespace, "--only-show-errors"];
23878
+ if (opts.subscriptionId) args.push("--subscription", opts.subscriptionId);
23879
+ await runAz(args);
23880
+ } catch {
23881
+ continue;
23882
+ }
23883
+ for (let i = 1; i <= attempts; i++) {
23884
+ const [state] = await probeProviders([namespace], opts.subscriptionId);
23885
+ if (state.state?.toLowerCase() === "registered") break;
23886
+ opts.onProgress?.(`registering ${namespace} (${i.toString()}/${attempts.toString()})\u2026`);
23887
+ if (sleepMs > 0) await new Promise((r) => setTimeout(r, sleepMs));
23888
+ }
23889
+ }
23890
+ }
23891
+ async function probeModelQuota(region, model) {
23892
+ const usages = await listModelQuota(region);
23893
+ const { verdict, quotad } = modelQuotaVerdict(usages, model);
23894
+ return { verdict, quotad, model, region, usagesReadable: usages.length > 0 };
23895
+ }
23896
+ async function bearer(credential2, scope) {
23897
+ try {
23898
+ const t = await credential2.getToken(scope);
23899
+ return t?.token ?? null;
23900
+ } catch {
23901
+ return null;
23902
+ }
23903
+ }
23904
+ async function hasRoleAtScope(credential2, scope, principalId, roleIds) {
23905
+ try {
23906
+ const token = await bearer(credential2, "https://management.azure.com/.default");
23907
+ if (!token) return null;
23908
+ const url = `https://management.azure.com${scope}/providers/Microsoft.Authorization/roleAssignments?api-version=2022-04-01&$filter=${encodeURIComponent(`principalId eq '${principalId}'`)}`;
23909
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
23910
+ if (!res.ok) return null;
23911
+ const body = await res.json();
23912
+ return (body.value ?? []).some(
23913
+ (a) => roleIds.some((id) => (a.properties?.roleDefinitionId ?? "").toLowerCase().endsWith(id.toLowerCase()))
23914
+ );
23915
+ } catch {
23916
+ return null;
23917
+ }
23918
+ }
23919
+ async function probeFoundryAccess(args) {
23920
+ const base = {
23921
+ scope: args.accountId,
23922
+ principalId: args.principalId,
23923
+ isSelf: args.isSelf
23924
+ };
23925
+ if (args.isSelf) {
23926
+ const token = await bearer(args.credential, FOUNDRY_SCOPE4);
23927
+ if (!token) return { ...base, evidence: "unavailable" };
23928
+ try {
23929
+ const res = await fetch(`${args.projectEndpoint}/agents?api-version=${AGENTS_API}`, {
23930
+ headers: { Authorization: `Bearer ${token}` }
23931
+ });
23932
+ return { ...base, evidence: "probe", status: res.status };
23933
+ } catch {
23934
+ return { ...base, evidence: "unavailable" };
23935
+ }
23936
+ }
23937
+ if (!args.accountId || !args.principalId) return { ...base, evidence: "unavailable" };
23938
+ const granted = await hasRoleAtScope(args.credential, args.accountId, args.principalId, FOUNDRY_DATA_PLANE_ROLE_IDS);
23939
+ if (granted === null) return { ...base, evidence: "unavailable" };
23940
+ return { ...base, evidence: "assignment", granted };
23941
+ }
23942
+ async function probeKeyVaultAccess(args) {
23943
+ const base = {
23944
+ scope: args.kvResourceId ?? args.kvUri,
23945
+ principalId: args.principalId,
23946
+ isSelf: args.isSelf
23947
+ };
23948
+ if (args.isSelf) {
23949
+ if (!args.kvUri) return { ...base, evidence: "unavailable" };
23950
+ const token = await bearer(args.credential, KV_SCOPE);
23951
+ if (!token) return { ...base, evidence: "unavailable" };
23952
+ try {
23953
+ const url = `${args.kvUri.replace(/\/$/, "")}/secrets?api-version=7.4&maxresults=1`;
23954
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
23955
+ return { ...base, evidence: "probe", status: res.status };
23956
+ } catch {
23957
+ return { ...base, evidence: "unavailable" };
23958
+ }
23959
+ }
23960
+ if (!args.kvResourceId || !args.principalId) return { ...base, evidence: "unavailable" };
23961
+ const granted = await hasRoleAtScope(args.credential, args.kvResourceId, args.principalId, [KV_SECRETS_USER_ROLE_ID]);
23962
+ if (granted === null) return { ...base, evidence: "unavailable" };
23963
+ return { ...base, evidence: "assignment", granted };
23964
+ }
23965
+ async function probeRedirectUri(clientId, gatewayUrl) {
23966
+ if (!clientId) return { registeredUris: null, gatewayUrl, clientId };
23967
+ try {
23968
+ const out = await runAz(["ad", "app", "show", "--id", clientId, "--query", "spa.redirectUris", "-o", "json"]);
23969
+ return { registeredUris: JSON.parse(out), gatewayUrl, clientId };
23970
+ } catch {
23971
+ return { registeredUris: null, gatewayUrl, clientId };
23972
+ }
23973
+ }
23974
+ async function resolveFoundryAccountId(projectEndpoint) {
23975
+ const m = /^https:\/\/([^.]+)\./.exec(projectEndpoint);
23976
+ if (!m) return null;
23977
+ try {
23978
+ const out = await runAz(["cognitiveservices", "account", "list", "--query", `[?name=='${m[1]}'].id`, "-o", "json"]);
23979
+ return JSON.parse(out)[0] ?? null;
23980
+ } catch {
23981
+ return null;
23982
+ }
23983
+ }
23984
+ async function resolvePlatformKeyVault(resourceGroup, subscriptionId) {
23985
+ try {
23986
+ const args = ["keyvault", "list", "-g", resourceGroup, "--query", "[0].{uri:properties.vaultUri,id:id}", "-o", "json"];
23987
+ if (subscriptionId) args.push("--subscription", subscriptionId);
23988
+ const parsed = JSON.parse(await runAz(args));
23989
+ return parsed?.uri && parsed.id ? { uri: parsed.uri, id: parsed.id } : null;
23990
+ } catch {
23991
+ return null;
23992
+ }
23993
+ }
23994
+ async function resolvePrincipalObjectId(who) {
23995
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(who)) {
23996
+ const asUser = await runAz(["ad", "user", "show", "--id", who, "--query", "id", "-o", "tsv"]).catch(() => "");
23997
+ if (asUser.trim()) return asUser.trim();
23998
+ return who;
23999
+ }
24000
+ const out = await runAz(["ad", "user", "show", "--id", who, "--query", "id", "-o", "tsv"]).catch(() => "");
24001
+ return out.trim() || null;
24002
+ }
24003
+ async function fixFoundryAccess(args) {
24004
+ await grantFoundryUser({
24005
+ credential: args.credential,
24006
+ subscriptionId: args.subscriptionId,
24007
+ accountScope: args.accountId,
24008
+ principalId: args.principalId,
24009
+ principalType: "User"
24010
+ });
24011
+ }
24012
+ async function fixKeyVaultAccess(args) {
24013
+ await grantKeyVaultSecretsUser({
24014
+ credential: args.credential,
24015
+ subscriptionId: args.subscriptionId,
24016
+ kvScope: args.kvResourceId,
24017
+ principalId: args.principalId,
24018
+ principalType: "User"
24019
+ });
24020
+ }
24021
+
24022
+ // src/commands/doctor.ts
24023
+ async function resolveFoundryAccountId2(endpoint) {
23760
24024
  const m = /^https:\/\/([^.]+)\.services\.ai\.azure\.com/.exec(endpoint);
23761
24025
  if (!m) return null;
23762
24026
  try {
@@ -23887,8 +24151,12 @@ var DoctorCommand = class extends M8tCommand {
23887
24151
  emit(checkLegacyStateDir(probeLegacyStateDir()));
23888
24152
  checking("gateway");
23889
24153
  let gw = { ok: false, detail: "not probed" };
24154
+ let platformRg = null;
24155
+ let platformSub = null;
23890
24156
  try {
23891
24157
  const ctx = await resolveGatewayContext({ interactive: false, resourceGroup: this.resourceGroup });
24158
+ platformRg = ctx.containerAppResourceId.split("/resourceGroups/")[1]?.split("/")[0] ?? null;
24159
+ platformSub = ctx.subscriptionId;
23892
24160
  const token = await getBearerToken(ctx.gatewayClientId);
23893
24161
  const res = await fetch(`${ctx.gatewayUrl}/api/me`, {
23894
24162
  headers: { Authorization: `Bearer ${token}` }
@@ -23910,9 +24178,24 @@ var DoctorCommand = class extends M8tCommand {
23910
24178
  } catch {
23911
24179
  status = 0;
23912
24180
  }
23913
- const foundryAccountId = await resolveFoundryAccountId(foundry.projectEndpoint);
24181
+ const foundryAccountId = await resolveFoundryAccountId2(foundry.projectEndpoint);
23914
24182
  const oid = await getCallerObjectId().catch(() => null);
23915
24183
  emit(checkDataPlane({ status, foundryAccountId, oid }));
24184
+ checking("key vault secrets");
24185
+ const kv = platformRg ? await resolvePlatformKeyVault(platformRg, platformSub ?? void 0) : null;
24186
+ let kvStatus = 0;
24187
+ if (kv) {
24188
+ try {
24189
+ const token = await new DefaultAzureCredential22().getToken("https://vault.azure.net/.default");
24190
+ const res = await fetch(`${kv.uri.replace(/\/$/, "")}/secrets?api-version=7.4&maxresults=1`, {
24191
+ headers: { Authorization: `Bearer ${token.token}` }
24192
+ });
24193
+ kvStatus = res.status;
24194
+ } catch {
24195
+ kvStatus = 0;
24196
+ }
24197
+ }
24198
+ emit(checkKeyVaultSecrets({ status: kvStatus, kvScope: kv?.id ?? null, oid }));
23916
24199
  checking("model capacity");
23917
24200
  const deployments = foundryAccountId ? await listModelDeployments(foundryAccountId) : [];
23918
24201
  emit(checkReasoningCapacity(deployments));
@@ -23965,66 +24248,773 @@ var DoctorCommand = class extends M8tCommand {
23965
24248
  }
23966
24249
  };
23967
24250
 
23968
- // src/commands/switch.ts
24251
+ // src/commands/prereqs.ts
23969
24252
  import { Command as Command47, Option as Option44 } from "clipanion";
24253
+ init_errors();
23970
24254
 
23971
- // src/lib/profiles.ts
23972
- import * as fs27 from "fs/promises";
23973
- import * as path29 from "path";
23974
- import { parse as parseYaml11, stringify as stringifyYaml4 } from "yaml";
23975
- function profilesDir() {
23976
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
23977
- return path29.join(home, ".m8t", "profiles");
23978
- }
23979
- function getProfilePath(name) {
23980
- return path29.join(profilesDir(), `${path29.basename(name)}.yaml`);
23981
- }
23982
- function slugifyTenant(domainOrId) {
23983
- const base = domainOrId.split(".")[0].toLowerCase();
23984
- const slug = base.replace(/[^a-z0-9-]/g, "");
23985
- return slug.length > 0 ? slug : "profile";
23986
- }
23987
- async function listProfiles() {
23988
- try {
23989
- const entries = await fs27.readdir(profilesDir());
23990
- return entries.filter((e) => e.endsWith(".yaml")).map((e) => e.replace(/\.yaml$/, "")).sort();
23991
- } catch (e) {
23992
- if (e.code === "ENOENT") return [];
23993
- throw e;
23994
- }
23995
- }
23996
- async function readProfile(name) {
23997
- try {
23998
- const raw = await fs27.readFile(getProfilePath(name), "utf8");
23999
- const parsed = parseYaml11(raw);
24000
- if (!parsed || typeof parsed !== "object") return null;
24001
- return parsed;
24002
- } catch (e) {
24003
- if (e.code === "ENOENT") return null;
24004
- throw e;
24005
- }
24255
+ // src/lib/prereq-deps.ts
24256
+ import { DefaultAzureCredential as DefaultAzureCredential23 } from "@azure/identity";
24257
+
24258
+ // src/lib/bootstrap-preflight.ts
24259
+ var ADMIN_DIRECTORY_ROLES = ["Global Administrator", "Application Administrator", "Cloud Application Administrator"];
24260
+ async function checkSubScopeAdmin(callerObjectId, subscriptionId) {
24261
+ const rows = JSON.parse(await runAz([
24262
+ "role",
24263
+ "assignment",
24264
+ "list",
24265
+ "--assignee",
24266
+ callerObjectId,
24267
+ "--scope",
24268
+ `/subscriptions/${subscriptionId}`,
24269
+ "--include-inherited",
24270
+ "--query",
24271
+ "[].{roleDefinitionName: roleDefinitionName}",
24272
+ "-o",
24273
+ "json"
24274
+ ]));
24275
+ const hit = rows.find((r) => r.roleDefinitionName === "Owner" || r.roleDefinitionName === "User Access Administrator");
24276
+ return { ok: Boolean(hit), role: hit?.roleDefinitionName };
24006
24277
  }
24007
- async function exists(p) {
24278
+ async function checkAppRegCapability(callerObjectId) {
24279
+ let roles;
24008
24280
  try {
24009
- await fs27.stat(p);
24010
- return true;
24281
+ roles = JSON.parse(await runAz([
24282
+ "rest",
24283
+ "--method",
24284
+ "GET",
24285
+ "--url",
24286
+ `https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?$filter=principalId eq '${callerObjectId}'&$expand=roleDefinition`,
24287
+ "--query",
24288
+ "value[].roleDefinition.displayName",
24289
+ "-o",
24290
+ "json"
24291
+ ]));
24011
24292
  } catch {
24012
- return false;
24293
+ roles = null;
24013
24294
  }
24295
+ if (roles === null) return { ok: false, inconclusive: true };
24296
+ return { ok: roles.some((r) => ADMIN_DIRECTORY_ROLES.includes(r)), inconclusive: false };
24014
24297
  }
24015
- async function saveProfile(p) {
24016
- await fs27.mkdir(profilesDir(), { recursive: true });
24017
- let name = p.name;
24018
- let n = 2;
24019
- while (await exists(getProfilePath(name))) {
24020
- name = `${p.name}-${n.toString()}`;
24021
- n++;
24022
- }
24023
- const target = getProfilePath(name);
24024
- const tmp = `${target}.tmp`;
24025
- await fs27.writeFile(tmp, stringifyYaml4({ ...p, name }, { indent: 2 }), "utf8");
24026
- await fs27.rename(tmp, target);
24027
- return name;
24298
+ async function registerContainerInstance(subscriptionId) {
24299
+ await runAz(["provider", "register", "--namespace", "Microsoft.ContainerInstance", "--subscription", subscriptionId, "--only-show-errors"]);
24300
+ }
24301
+ function buildPreflightBanner(who) {
24302
+ return [
24303
+ "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
24304
+ "\u2551 \u26D4 STOP \u2014 READ THIS BEFORE CONTINUING \u2551",
24305
+ "\u2551 \u2551",
24306
+ "\u2551 Installing m8t creates real Azure resources in YOUR \u2551",
24307
+ "\u2551 subscription and assigns roles. To do that, the account you just \u2551",
24308
+ "\u2551 signed in with MUST be: \u2551",
24309
+ "\u2551 \u2551",
24310
+ "\u2551 \u2022 OWNER or USER ACCESS ADMINISTRATOR at the subscription scope \u2551",
24311
+ "\u2551 (it has to create a managed identity and assign it roles), AND \u2551",
24312
+ "\u2551 \u2022 a directory admin able to register one Entra app \u2551",
24313
+ "\u2551 (Application / Cloud Application / Global Administrator) \u2551",
24314
+ "\u2551 \u2014 OR you supply a ready app registration with --client-id. \u2551",
24315
+ "\u2551 \u2551",
24316
+ "\u2551 If you are NOT this, this install CANNOT proceed and will STOP NOW. \u2551",
24317
+ "\u2551 Nothing has been created yet. Get an admin to run this, or ask one \u2551",
24318
+ "\u2551 for an app registration id (--client-id <appId>) and try again. \u2551",
24319
+ "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
24320
+ "",
24321
+ `Signed in as: ${who.upn} (tenant ${who.tenantId}, sub ${who.subscriptionId})`,
24322
+ ""
24323
+ ].join("\n");
24324
+ }
24325
+
24326
+ // src/lib/prereq-deps.ts
24327
+ var INSTALL_REASONING_MODEL = "gpt-5.4";
24328
+ function buildPrereqDeps(opts = {}) {
24329
+ let credentialSingleton2 = null;
24330
+ return {
24331
+ getAzAccount: () => getAzAccount().catch(() => null),
24332
+ getCallerObjectId: () => getCallerObjectId().catch(() => null),
24333
+ checkSubScopeAdmin,
24334
+ checkAppRegCapability,
24335
+ probeProviders,
24336
+ registerProviders,
24337
+ probeModelQuota,
24338
+ discoverPlatform: async () => {
24339
+ const cfg = await readFoundryConfig();
24340
+ const d = await discoverGateway({
24341
+ interactive: false,
24342
+ ...opts.resourceGroup ? { resourceGroup: opts.resourceGroup } : {},
24343
+ ...opts.subscription ? { subscriptionId: opts.subscription } : {}
24344
+ });
24345
+ return {
24346
+ gatewayUrl: d.gatewayUrl,
24347
+ gatewayClientId: d.gatewayClientId,
24348
+ subscriptionId: d.subscriptionId,
24349
+ resourceGroup: d.containerAppResourceId.split("/resourceGroups/")[1]?.split("/")[0] ?? "",
24350
+ // The DEPLOYMENT is the authority, local config only a fallback. A
24351
+ // teammate joining a platform someone else installed has no
24352
+ // ~/.m8t/config.yaml at all — reading it first left the single most
24353
+ // important check reporting "could not determine" for exactly the person
24354
+ // this feature exists to serve.
24355
+ projectEndpoint: d.projectEndpoint ?? cfg?.projectEndpoint ?? null
24356
+ };
24357
+ },
24358
+ isNoPlatformError: (e) => e.code === "GATEWAY_DISCOVERY_ZERO",
24359
+ resolveFoundryAccountId,
24360
+ resolvePlatformKeyVault,
24361
+ resolvePrincipalObjectId,
24362
+ probeFoundryAccess,
24363
+ probeKeyVaultAccess,
24364
+ probeRedirectUri,
24365
+ fixFoundryAccess,
24366
+ fixKeyVaultAccess,
24367
+ credential: () => credentialSingleton2 ??= new DefaultAzureCredential23()
24368
+ };
24369
+ }
24370
+
24371
+ // src/lib/prereqs.ts
24372
+ var PREREQS = [
24373
+ // ── install phase ────────────────────────────────────────────────────────
24374
+ {
24375
+ slug: "local-tooling",
24376
+ phase: "install",
24377
+ title: "git, Azure CLI, Node 20+, and the m8t CLI on your machine",
24378
+ breaks: "Nothing can run at all.",
24379
+ coverage: "auto",
24380
+ checkedBy: "runbook",
24381
+ fixable: false
24382
+ },
24383
+ {
24384
+ slug: "az-signed-in",
24385
+ phase: "install",
24386
+ title: "Signed in to Azure with an active subscription",
24387
+ breaks: "Every Azure call fails immediately.",
24388
+ coverage: "refuses",
24389
+ checkedBy: "prereqs",
24390
+ fixable: false
24391
+ },
24392
+ {
24393
+ slug: "subscription-admin",
24394
+ phase: "install",
24395
+ title: "Owner or User Access Administrator at subscription scope",
24396
+ breaks: "The install cannot create its managed identity or assign it roles.",
24397
+ coverage: "refuses",
24398
+ checkedBy: "prereqs",
24399
+ fixable: false
24400
+ },
24401
+ {
24402
+ slug: "app-registration",
24403
+ phase: "install",
24404
+ title: "Directory rights to register one Entra app (or a ready --client-id)",
24405
+ breaks: "There is no app registration, so nobody can ever sign in to the webapp.",
24406
+ coverage: "refuses",
24407
+ checkedBy: "prereqs",
24408
+ fixable: false
24409
+ },
24410
+ {
24411
+ slug: "resource-providers",
24412
+ phase: "install",
24413
+ title: "Every Azure resource provider the install uses is registered",
24414
+ breaks: "The installer dies partway through creating resources, with an error that names a provider rather than anything actionable.",
24415
+ coverage: "refuses",
24416
+ checkedBy: "prereqs",
24417
+ fixable: true
24418
+ },
24419
+ {
24420
+ slug: "model-quota",
24421
+ phase: "install",
24422
+ title: "Model quota for the reasoning model in the target region",
24423
+ breaks: "The install spends money creating resources and then cannot deploy the model the workers need.",
24424
+ coverage: "refuses",
24425
+ checkedBy: "prereqs",
24426
+ fixable: false
24427
+ },
24428
+ {
24429
+ slug: "github-org",
24430
+ phase: "install",
24431
+ title: "A GitHub organization and the rights to install an App on it",
24432
+ breaks: "The brain-backed workers have nowhere to keep their memory.",
24433
+ coverage: "refuses",
24434
+ checkedBy: "runbook",
24435
+ fixable: false
24436
+ },
24437
+ {
24438
+ slug: "azure-policy",
24439
+ phase: "install",
24440
+ title: "No Azure Policy or deny assignment blocking resource creation",
24441
+ breaks: "The installer is refused by governance partway through, with a policy error.",
24442
+ coverage: "unhandled",
24443
+ checkedBy: "none",
24444
+ fixable: false
24445
+ },
24446
+ {
24447
+ slug: "region-eligibility",
24448
+ phase: "install",
24449
+ title: "The target region can host every service the platform uses",
24450
+ breaks: "A resource fails to create in that region partway through the install.",
24451
+ coverage: "unhandled",
24452
+ checkedBy: "none",
24453
+ fixable: false
24454
+ },
24455
+ {
24456
+ slug: "soft-deleted-account",
24457
+ phase: "install",
24458
+ title: "No soft-deleted Cognitive Services account still holding the model quota",
24459
+ breaks: "Quota looks free but the model deployment fails; the quota is held by an account you already deleted.",
24460
+ coverage: "unhandled",
24461
+ checkedBy: "none",
24462
+ fixable: false
24463
+ },
24464
+ {
24465
+ slug: "subscription-billing",
24466
+ phase: "install",
24467
+ title: "The subscription is active and can be billed",
24468
+ breaks: "Resource creation is refused for billing reasons.",
24469
+ coverage: "unhandled",
24470
+ checkedBy: "none",
24471
+ fixable: false
24472
+ },
24473
+ // ── usage phase ──────────────────────────────────────────────────────────
24474
+ {
24475
+ slug: "platform-discoverable",
24476
+ phase: "usage",
24477
+ title: "The m8t CLI can find the platform you are pointed at",
24478
+ breaks: "Every CLI command that talks to the platform fails to find it.",
24479
+ coverage: "refuses",
24480
+ checkedBy: "prereqs",
24481
+ fixable: false
24482
+ },
24483
+ {
24484
+ slug: "signin-redirect-uri",
24485
+ phase: "usage",
24486
+ title: "The webapp's sign-in redirect URI is registered on the app",
24487
+ breaks: "Sign-in fails with AADSTS50011 on a platform that is otherwise perfectly healthy.",
24488
+ coverage: "auto",
24489
+ checkedBy: "prereqs",
24490
+ // Reported, not repaired. The app registration is shared across every
24491
+ // deployment in the tenant; writing it belongs to the install.
24492
+ fixable: false
24493
+ },
24494
+ {
24495
+ slug: "foundry-data-plane",
24496
+ phase: "usage",
24497
+ title: "Your account can reach the Foundry data plane",
24498
+ breaks: "Your AI team will not load, chat does not work, and the CLI and coding-agent plugin cannot reach your workers. Subscription Owner does NOT cover this \u2014 it is a data action, and control-plane roles carry none.",
24499
+ coverage: "auto",
24500
+ checkedBy: "prereqs",
24501
+ fixable: true
24502
+ },
24503
+ {
24504
+ slug: "keyvault-secrets",
24505
+ phase: "usage",
24506
+ title: "Your account can read the platform's Key Vault secrets",
24507
+ breaks: "Deploying a worker fails \u2014 the coding agent and the Azure executor both read the vault as you.",
24508
+ coverage: "auto",
24509
+ checkedBy: "prereqs",
24510
+ fixable: true
24511
+ },
24512
+ {
24513
+ slug: "local-state",
24514
+ phase: "usage",
24515
+ title: "Local state the coding-agent plugin needs (~/.m8t/repo-root, config)",
24516
+ breaks: "The m8t plugin cannot list or reach your workers from an agent session.",
24517
+ coverage: "warns",
24518
+ checkedBy: "doctor",
24519
+ fixable: false
24520
+ }
24521
+ ];
24522
+ function prereqsForPhase(phase) {
24523
+ return PREREQS.filter((p) => p.phase === phase);
24524
+ }
24525
+ function prereqBySlug(slug) {
24526
+ return PREREQS.find((p) => p.slug === slug);
24527
+ }
24528
+ function spec(slug) {
24529
+ const s = prereqBySlug(slug);
24530
+ if (!s) throw new Error(`unknown prereq slug: ${slug}`);
24531
+ return s;
24532
+ }
24533
+ function result(slug, status, detail, remedy) {
24534
+ const s = spec(slug);
24535
+ return { slug: s.slug, phase: s.phase, title: s.title, status, detail, ...remedy ? { remedy } : {} };
24536
+ }
24537
+ var REQUIRED_PROVIDERS = [
24538
+ "Microsoft.ContainerInstance",
24539
+ // the installer job itself
24540
+ "Microsoft.ManagedIdentity",
24541
+ // installer MI + gateway user-assigned identity
24542
+ "Microsoft.CognitiveServices",
24543
+ // Foundry account, project, model deployments
24544
+ "Microsoft.App",
24545
+ // Container Apps environment, gateway, updater job
24546
+ "Microsoft.OperationalInsights",
24547
+ // Log Analytics workspace
24548
+ "Microsoft.Storage",
24549
+ // status blob + platform tables/blobs
24550
+ "Microsoft.KeyVault",
24551
+ // platform secrets
24552
+ "Microsoft.ServiceBus",
24553
+ // gateway queues
24554
+ "Microsoft.Insights",
24555
+ // Application Insights
24556
+ "Microsoft.ContainerRegistry"
24557
+ // hosted-agent image staging
24558
+ ];
24559
+ function evaluateProviders(states) {
24560
+ if (states.length === 0) {
24561
+ return result("resource-providers", "skipped", "no provider registration state could be read \u2014 proceeding unverified");
24562
+ }
24563
+ const unreadable = states.filter((s) => s.state === null).map((s) => s.namespace);
24564
+ const unregistered = states.filter((s) => s.state !== null && s.state.toLowerCase() !== "registered" && s.state.toLowerCase() !== "registering").map((s) => s.namespace);
24565
+ const registering = states.filter((s) => s.state?.toLowerCase() === "registering").map((s) => s.namespace);
24566
+ if (unregistered.length > 0) {
24567
+ return result(
24568
+ "resource-providers",
24569
+ "fail",
24570
+ `not registered on this subscription: ${unregistered.join(", ")}`,
24571
+ unregistered.map((ns) => `az provider register --namespace ${ns}`).join("\n ")
24572
+ );
24573
+ }
24574
+ if (registering.length > 0) {
24575
+ return result("resource-providers", "warn", `still registering: ${registering.join(", ")} \u2014 this resolves on its own, usually within a minute`);
24576
+ }
24577
+ if (unreadable.length > 0) {
24578
+ return result("resource-providers", "skipped", `could not read registration state for: ${unreadable.join(", ")} \u2014 proceeding unverified`);
24579
+ }
24580
+ return result("resource-providers", "pass", `all ${states.length.toString()} required providers registered`);
24581
+ }
24582
+ function evaluateModelQuota(e) {
24583
+ if (!e.usagesReadable) {
24584
+ return result("model-quota", "skipped", `could not read model quota in ${e.region} \u2014 proceeding unverified`);
24585
+ }
24586
+ if (e.verdict === "no_quota") {
24587
+ const alt = e.quotad.length > 0 ? ` Models with quota there: ${e.quotad.join(", ")}.` : "";
24588
+ return result(
24589
+ "model-quota",
24590
+ "fail",
24591
+ `${e.model} has zero quota in ${e.region}.${alt}`,
24592
+ `Request quota for ${e.model} in ${e.region} in the Azure portal (Foundry \u2192 Quotas), or install into a region that already has it.`
24593
+ );
24594
+ }
24595
+ if (e.verdict === "unknown") {
24596
+ return result("model-quota", "warn", `no quota entry for ${e.model} in ${e.region} \u2014 cannot confirm; the install will report honestly if it is missing`);
24597
+ }
24598
+ return result("model-quota", "pass", `${e.model} has quota in ${e.region}`);
24599
+ }
24600
+ function evaluateAzSignedIn(account) {
24601
+ if (!account) return result("az-signed-in", "fail", "not signed in to Azure", "az login");
24602
+ return result("az-signed-in", "pass", `${account.upn} (tenant ${account.tenantId}, subscription ${account.subscriptionId})`);
24603
+ }
24604
+ function evaluateSubscriptionAdmin(probe) {
24605
+ if (!probe.ok) {
24606
+ return result(
24607
+ "subscription-admin",
24608
+ "fail",
24609
+ "you are not Owner or User Access Administrator at subscription scope",
24610
+ "Ask a subscription Owner to run the install \u2014 creating and granting the installer's managed identity is unavoidable."
24611
+ );
24612
+ }
24613
+ return result("subscription-admin", "pass", `${probe.role ?? "Owner/User Access Administrator"} at subscription scope`);
24614
+ }
24615
+ function evaluateAppRegistration(probe, clientId) {
24616
+ if (clientId) return result("app-registration", "pass", `using your app registration ${clientId}`);
24617
+ if (probe.inconclusive) {
24618
+ return result(
24619
+ "app-registration",
24620
+ "fail",
24621
+ "could not verify your directory admin role (Microsoft Graph denied the read \u2014 common for guest accounts)",
24622
+ "Re-run with --client-id <appId> using an app registration an admin prepared for you."
24623
+ );
24624
+ }
24625
+ if (!probe.ok) {
24626
+ return result(
24627
+ "app-registration",
24628
+ "fail",
24629
+ "you do not hold a directory admin role (Application / Cloud Application / Global Administrator)",
24630
+ "Re-run with --client-id <appId> using an app registration an admin prepared for you."
24631
+ );
24632
+ }
24633
+ return result("app-registration", "pass", "you can register the m8t app in this directory");
24634
+ }
24635
+ function grantRemedy(slug, p) {
24636
+ const target = p.isSelf ? "m8t prereqs --fix" : `m8t prereqs --fix --for ${p.principalId ?? "<user>"}`;
24637
+ const role = slug === "foundry-data-plane" ? "Foundry User" : "Key Vault Secrets User";
24638
+ const scope = p.scope ?? `<${slug === "foundry-data-plane" ? "foundry-account" : "key-vault"}-resource-id>`;
24639
+ return `${target}
24640
+ or by hand: az role assignment create --assignee-object-id ${p.principalId ?? "<object-id>"} --assignee-principal-type User --role "${role}" --scope ${scope}`;
24641
+ }
24642
+ function evaluateFoundryAccess(p) {
24643
+ const who = p.isSelf ? "your account" : "that account";
24644
+ if (p.evidence === "unavailable") {
24645
+ return result("foundry-data-plane", "skipped", `could not determine whether ${who} can reach the Foundry data plane`);
24646
+ }
24647
+ if (p.evidence === "probe") {
24648
+ if (p.status !== void 0 && p.status >= 200 && p.status < 300) {
24649
+ return result("foundry-data-plane", "pass", `${who} can reach the Foundry data plane (HTTP ${p.status.toString()})`);
24650
+ }
24651
+ if (p.status === 401 || p.status === 403) {
24652
+ return result(
24653
+ "foundry-data-plane",
24654
+ "fail",
24655
+ `${who} is missing a Foundry data-plane role on ${p.scope ?? "the Foundry account"} (HTTP ${p.status.toString()}). Subscription Owner does not cover this \u2014 reading and invoking agents are data actions, and control-plane roles carry none.`,
24656
+ grantRemedy("foundry-data-plane", p)
24657
+ );
24658
+ }
24659
+ return result("foundry-data-plane", "warn", `unexpected response from the Foundry data plane (HTTP ${(p.status ?? 0).toString()})`);
24660
+ }
24661
+ if (p.granted === true) {
24662
+ return result("foundry-data-plane", "pass", `${who} holds a Foundry data-plane role on ${p.scope ?? "the Foundry account"}`);
24663
+ }
24664
+ return result(
24665
+ "foundry-data-plane",
24666
+ "fail",
24667
+ `${who} holds no known Foundry data-plane role on ${p.scope ?? "the Foundry account"}. (Checked by role assignment, not by calling as them \u2014 a custom role granting the same data action would not be recognised here.)`,
24668
+ grantRemedy("foundry-data-plane", p)
24669
+ );
24670
+ }
24671
+ function evaluateKeyVaultAccess(p) {
24672
+ const who = p.isSelf ? "your account" : "that account";
24673
+ if (p.evidence === "unavailable") {
24674
+ return result("keyvault-secrets", "skipped", `could not determine whether ${who} can read the platform Key Vault`);
24675
+ }
24676
+ if (p.evidence === "probe") {
24677
+ if (p.status !== void 0 && p.status >= 200 && p.status < 300) {
24678
+ return result("keyvault-secrets", "pass", `${who} can read secrets from ${p.scope ?? "the platform Key Vault"}`);
24679
+ }
24680
+ if (p.status === 401 || p.status === 403) {
24681
+ return result(
24682
+ "keyvault-secrets",
24683
+ "fail",
24684
+ `${who} cannot read secrets from ${p.scope ?? "the platform Key Vault"} (HTTP ${p.status.toString()}). Deploying a worker will fail \u2014 this is a permanent permission gap, not a transient blip.`,
24685
+ grantRemedy("keyvault-secrets", p)
24686
+ );
24687
+ }
24688
+ return result("keyvault-secrets", "warn", `unexpected response from the platform Key Vault (HTTP ${(p.status ?? 0).toString()})`);
24689
+ }
24690
+ if (p.granted === true) {
24691
+ return result("keyvault-secrets", "pass", `${who} holds Key Vault Secrets User on ${p.scope ?? "the platform Key Vault"}`);
24692
+ }
24693
+ return result(
24694
+ "keyvault-secrets",
24695
+ "fail",
24696
+ `${who} holds no secrets-read role on ${p.scope ?? "the platform Key Vault"}. (Checked by role assignment, not by calling as them.)`,
24697
+ grantRemedy("keyvault-secrets", p)
24698
+ );
24699
+ }
24700
+ function evaluateRedirectUri(p) {
24701
+ if (!p.gatewayUrl) return result("signin-redirect-uri", "skipped", "no gateway URL resolved");
24702
+ if (p.registeredUris === null) {
24703
+ return result("signin-redirect-uri", "skipped", `could not read app registration ${p.clientId ?? "(unknown)"} \u2014 needs directory rights`);
24704
+ }
24705
+ const want = p.gatewayUrl.replace(/\/$/, "");
24706
+ if (p.registeredUris.some((u) => u.replace(/\/$/, "") === want)) {
24707
+ return result("signin-redirect-uri", "pass", `${want} is registered on the sign-in app`);
24708
+ }
24709
+ return result(
24710
+ "signin-redirect-uri",
24711
+ "fail",
24712
+ `${want} is not a registered redirect URI \u2014 sign-in will fail with AADSTS50011`,
24713
+ // Deliberately NOT "m8t prereqs --fix": this command reports it and does not
24714
+ // write it. The app registration is shared across every deployment in the
24715
+ // tenant, so the write belongs to the install, not to whoever is diagnosing.
24716
+ `Re-run 'm8t bootstrap finish' (it registers this), or ask a directory admin to add it \u2014 MERGE, never overwrite:
24717
+ az ad app update --id ${p.clientId ?? "<clientId>"} --set spa.redirectUris="['${want}']"`
24718
+ );
24719
+ }
24720
+ function evaluatePlatformDiscoverable(p) {
24721
+ if (p.gatewayUrl) return result("platform-discoverable", "pass", p.gatewayUrl);
24722
+ return result(
24723
+ "platform-discoverable",
24724
+ "fail",
24725
+ p.detail,
24726
+ "m8t switch (point this machine at the right tenant/subscription), or pass --resource-group if the subscription holds several deployments"
24727
+ );
24728
+ }
24729
+ function summarize(phase, results) {
24730
+ return {
24731
+ phase,
24732
+ ok: !results.some((r) => r.status === "fail"),
24733
+ results,
24734
+ unchecked: prereqsForPhase(phase).filter((p) => p.checkedBy === "none").map((p) => p.slug)
24735
+ };
24736
+ }
24737
+
24738
+ // src/lib/prereq-run.ts
24739
+ async function detectPhase(deps) {
24740
+ try {
24741
+ return await deps.discoverPlatform() ? "usage" : "install";
24742
+ } catch (e) {
24743
+ if (deps.isNoPlatformError(e)) return "install";
24744
+ throw e;
24745
+ }
24746
+ }
24747
+ async function runInstallPhase(deps, opts) {
24748
+ const results = [];
24749
+ const account = await deps.getAzAccount().catch(() => null);
24750
+ results.push(evaluateAzSignedIn(account));
24751
+ if (!account) return summarize("install", results);
24752
+ const subscriptionId = opts.subscription ?? account.subscriptionId;
24753
+ const oid = await deps.getCallerObjectId().catch(() => null);
24754
+ if (oid) {
24755
+ results.push(evaluateSubscriptionAdmin(await deps.checkSubScopeAdmin(oid, subscriptionId).catch(() => ({ ok: false }))));
24756
+ results.push(
24757
+ opts.clientId ? evaluateAppRegistration({ ok: true, inconclusive: false }, opts.clientId) : evaluateAppRegistration(await deps.checkAppRegCapability(oid).catch(() => ({ ok: false, inconclusive: true })))
24758
+ );
24759
+ }
24760
+ if (opts.fix) {
24761
+ const before = await deps.probeProviders(REQUIRED_PROVIDERS, subscriptionId);
24762
+ const missing = before.filter((s) => s.state !== null && s.state.toLowerCase() !== "registered").map((s) => s.namespace);
24763
+ if (missing.length > 0) {
24764
+ opts.onProgress?.(`registering ${missing.length.toString()} resource provider(s)\u2026`);
24765
+ await deps.registerProviders(missing, { subscriptionId, onProgress: opts.onProgress });
24766
+ }
24767
+ }
24768
+ results.push(evaluateProviders(await deps.probeProviders(REQUIRED_PROVIDERS, subscriptionId)));
24769
+ if (opts.region && opts.model) {
24770
+ results.push(evaluateModelQuota(await deps.probeModelQuota(opts.region, opts.model)));
24771
+ }
24772
+ return summarize("install", results);
24773
+ }
24774
+ async function runUsagePhase(deps, opts) {
24775
+ const results = [];
24776
+ let platform = null;
24777
+ try {
24778
+ platform = await deps.discoverPlatform();
24779
+ } catch (e) {
24780
+ if (!deps.isNoPlatformError(e)) throw e;
24781
+ }
24782
+ results.push(
24783
+ evaluatePlatformDiscoverable({
24784
+ gatewayUrl: platform?.gatewayUrl ?? null,
24785
+ detail: platform ? "" : "no m8t deployment found for this Azure session"
24786
+ })
24787
+ );
24788
+ if (!platform) return summarize("usage", results);
24789
+ const credential2 = deps.credential();
24790
+ const selfOid = await deps.getCallerObjectId().catch(() => null);
24791
+ const isSelf = !opts.forPrincipal;
24792
+ const principalId = isSelf ? selfOid : await deps.resolvePrincipalObjectId(opts.forPrincipal ?? "").catch(() => null);
24793
+ if (!isSelf && !principalId) {
24794
+ throw new UnknownPrincipalError(opts.forPrincipal ?? "");
24795
+ }
24796
+ if (isSelf) {
24797
+ results.push(evaluateRedirectUri(await deps.probeRedirectUri(platform.gatewayClientId, platform.gatewayUrl)));
24798
+ }
24799
+ const accountId = platform.projectEndpoint ? await deps.resolveFoundryAccountId(platform.projectEndpoint).catch(() => null) : null;
24800
+ const kv = await deps.resolvePlatformKeyVault(platform.resourceGroup, platform.subscriptionId).catch(() => null);
24801
+ const foundryProbe = async () => platform.projectEndpoint ? deps.probeFoundryAccess({ credential: credential2, projectEndpoint: platform.projectEndpoint, accountId, principalId, isSelf }) : null;
24802
+ if (!platform.projectEndpoint) {
24803
+ results.push(
24804
+ evaluateFoundryAccess({ evidence: "unavailable", scope: null, principalId, isSelf })
24805
+ );
24806
+ }
24807
+ let foundry = await foundryProbe();
24808
+ if (opts.fix && foundry && needsGrant(foundry) && accountId && principalId) {
24809
+ opts.onProgress?.("granting Foundry data-plane access\u2026");
24810
+ await deps.fixFoundryAccess({ credential: credential2, subscriptionId: platform.subscriptionId, accountId, principalId });
24811
+ results.push({ ...evaluateFoundryAccess(foundry), status: "fixed", detail: "granted Foundry User \u2014 allow up to a minute to take effect" });
24812
+ foundry = null;
24813
+ }
24814
+ if (foundry) results.push(evaluateFoundryAccess(foundry));
24815
+ let keyvault = await deps.probeKeyVaultAccess({
24816
+ credential: credential2,
24817
+ kvUri: kv?.uri ?? null,
24818
+ kvResourceId: kv?.id ?? null,
24819
+ principalId,
24820
+ isSelf
24821
+ });
24822
+ if (opts.fix && needsGrant(keyvault) && kv && principalId) {
24823
+ opts.onProgress?.("granting Key Vault secrets access\u2026");
24824
+ await deps.fixKeyVaultAccess({ credential: credential2, subscriptionId: platform.subscriptionId, kvResourceId: kv.id, principalId });
24825
+ results.push({ ...evaluateKeyVaultAccess(keyvault), status: "fixed", detail: "granted Key Vault Secrets User \u2014 allow up to a minute to take effect" });
24826
+ keyvault = null;
24827
+ }
24828
+ if (keyvault) results.push(evaluateKeyVaultAccess(keyvault));
24829
+ return summarize("usage", results);
24830
+ }
24831
+ var UnknownPrincipalError = class extends Error {
24832
+ constructor(who) {
24833
+ super(
24834
+ `Could not find '${who}' in this directory. Pass the exact user principal name (e.g. alice@contoso.com) or their object id.`
24835
+ );
24836
+ this.who = who;
24837
+ this.name = "UnknownPrincipalError";
24838
+ }
24839
+ who;
24840
+ };
24841
+ function needsGrant(p) {
24842
+ if (p.evidence === "probe") return p.status === 401 || p.status === 403;
24843
+ if (p.evidence === "assignment") return p.granted === false;
24844
+ return false;
24845
+ }
24846
+ async function runPrereqs(deps, opts) {
24847
+ const phase = opts.phase ?? await detectPhase(deps);
24848
+ return phase === "install" ? runInstallPhase(deps, opts) : runUsagePhase(deps, opts);
24849
+ }
24850
+
24851
+ // src/commands/prereqs.ts
24852
+ var GLYPH = {
24853
+ pass: colors.success("PASS"),
24854
+ fixed: colors.success("FIXED"),
24855
+ warn: colors.hint("WARN"),
24856
+ fail: colors.error("FAIL"),
24857
+ skipped: colors.dim("SKIP")
24858
+ };
24859
+ function renderVerdict(v) {
24860
+ const lines = [];
24861
+ lines.push(
24862
+ v.phase === "install" ? colors.field("Checking whether this subscription can install m8t.") : colors.field("Checking whether you can use this m8t platform.")
24863
+ );
24864
+ lines.push("");
24865
+ for (const r of v.results) {
24866
+ lines.push(`[${GLYPH[r.status]}] ${r.title}`);
24867
+ lines.push(` ${colors.dim(r.detail)}`);
24868
+ if (r.remedy) lines.push(colors.hint(` fix: ${r.remedy}`));
24869
+ }
24870
+ if (v.unchecked.length > 0) {
24871
+ lines.push("");
24872
+ lines.push(
24873
+ colors.dim(
24874
+ `Not checked by this command (you may still hit them): ${v.unchecked.join(", ")}. See guides/prerequisites.md.`
24875
+ )
24876
+ );
24877
+ }
24878
+ lines.push("");
24879
+ lines.push(v.ok ? colors.success("\u2705 Nothing is blocking you.") : colors.error("\u26D4 Blocked \u2014 fix the FAIL rows above, then re-run."));
24880
+ return lines.join("\n");
24881
+ }
24882
+ var PrereqsCommand = class extends M8tCommand {
24883
+ static paths = [["prereqs"]];
24884
+ static usage = Command47.Usage({
24885
+ description: "Check (and optionally fix) everything m8t needs \u2014 to install, or for you to use it.",
24886
+ details: "Runs one of two phases. With no platform discoverable it checks INSTALL prerequisites: your Azure sign-in, subscription and directory rights, resource-provider registrations, and model quota. With a platform live it checks USAGE prerequisites for the signed-in account: the sign-in redirect URI, Foundry data-plane access, and Key Vault secrets access. Pass --fix to repair what is repairable, and --fix --for <upn> to set a teammate up (usage phase only). Read-only without --fix.",
24887
+ examples: [
24888
+ ["Before installing", "$0 prereqs"],
24889
+ ["Register anything missing, then install", "$0 prereqs --fix"],
24890
+ ["After joining a platform someone else installed", "$0 prereqs"],
24891
+ ["Grant yourself what you are missing", "$0 prereqs --fix"],
24892
+ ["Set a teammate up (needs role-assignment rights)", "$0 prereqs --fix --for alice@contoso.com"]
24893
+ ]
24894
+ });
24895
+ fix = Option44.Boolean("--fix", false, { description: "Repair what is repairable. Without it the command only reports." });
24896
+ for_ = Option44.String("--for", { description: "UPN or object id of another person. Usage phase only." });
24897
+ phase = Option44.String("--phase", { description: "Force 'install' or 'usage' instead of auto-detecting." });
24898
+ region = Option44.String("--region", { description: "Target region for the install-phase quota check." });
24899
+ model = Option44.String("--model", { description: `Model whose quota the install needs (default ${INSTALL_REASONING_MODEL}).` });
24900
+ clientId = Option44.String("--client-id", { description: "A ready app registration, in place of directory-admin rights." });
24901
+ subscription = Option44.String("--subscription");
24902
+ resourceGroup = Option44.String("--resource-group");
24903
+ output = Option44.String("--output");
24904
+ async executeCommand() {
24905
+ const mode = resolveOutputMode(this.output, this.context.stdout);
24906
+ const json = mode === "json";
24907
+ const phase = this.resolvePhase();
24908
+ const forPrincipal = typeof this.for_ === "string" ? this.for_ : void 0;
24909
+ if (forPrincipal && phase === "install") {
24910
+ throw new LocalCliError({
24911
+ code: "PREREQS_FOR_INSTALL_PHASE",
24912
+ message: "--for applies to the usage phase only.",
24913
+ hint: "Install prerequisites belong to the subscription and the person running the install, not to a named teammate. Run 'm8t prereqs --fix --for <upn>' once the platform is live."
24914
+ });
24915
+ }
24916
+ if (forPrincipal && !this.fix) {
24917
+ this.context.stderr.write(
24918
+ colors.dim(" (checking by role assignment \u2014 we cannot call the data plane as someone else)\n")
24919
+ );
24920
+ }
24921
+ const deps = buildPrereqDeps({
24922
+ resourceGroup: typeof this.resourceGroup === "string" ? this.resourceGroup : void 0,
24923
+ subscription: typeof this.subscription === "string" ? this.subscription : void 0
24924
+ });
24925
+ const opts = {
24926
+ fix: this.fix === true,
24927
+ ...forPrincipal ? { forPrincipal } : {},
24928
+ ...phase ? { phase } : {},
24929
+ region: typeof this.region === "string" ? this.region : void 0,
24930
+ model: typeof this.model === "string" ? this.model : INSTALL_REASONING_MODEL,
24931
+ clientId: typeof this.clientId === "string" ? this.clientId : void 0,
24932
+ subscription: typeof this.subscription === "string" ? this.subscription : void 0,
24933
+ resourceGroup: typeof this.resourceGroup === "string" ? this.resourceGroup : void 0,
24934
+ onProgress: json ? void 0 : (m) => this.context.stderr.write(colors.dim(` ${m}
24935
+ `))
24936
+ };
24937
+ const verdict = await runPrereqs(deps, opts);
24938
+ if (json) {
24939
+ this.context.stdout.write(renderJson(verdict) + "\n");
24940
+ } else {
24941
+ this.context.stdout.write(renderVerdict(verdict) + "\n");
24942
+ }
24943
+ return verdict.ok ? 0 : 1;
24944
+ }
24945
+ resolvePhase() {
24946
+ if (typeof this.phase !== "string") return void 0;
24947
+ if (this.phase !== "install" && this.phase !== "usage") {
24948
+ throw new LocalCliError({
24949
+ code: "PREREQS_BAD_PHASE",
24950
+ message: `Unknown phase '${this.phase}'.`,
24951
+ hint: `Valid phases: install, usage. Omit --phase to auto-detect (install prerequisites are ${prereqsForPhase("install").length.toString()} checks; usage are ${prereqsForPhase("usage").length.toString()}).`
24952
+ });
24953
+ }
24954
+ return this.phase;
24955
+ }
24956
+ };
24957
+
24958
+ // src/commands/switch.ts
24959
+ import { Command as Command48, Option as Option45 } from "clipanion";
24960
+
24961
+ // src/lib/profiles.ts
24962
+ import * as fs27 from "fs/promises";
24963
+ import * as path29 from "path";
24964
+ import { parse as parseYaml11, stringify as stringifyYaml4 } from "yaml";
24965
+ function profilesDir() {
24966
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
24967
+ return path29.join(home, ".m8t", "profiles");
24968
+ }
24969
+ function getProfilePath(name) {
24970
+ return path29.join(profilesDir(), `${path29.basename(name)}.yaml`);
24971
+ }
24972
+ function slugifyTenant(domainOrId) {
24973
+ const base = domainOrId.split(".")[0].toLowerCase();
24974
+ const slug = base.replace(/[^a-z0-9-]/g, "");
24975
+ return slug.length > 0 ? slug : "profile";
24976
+ }
24977
+ async function listProfiles() {
24978
+ try {
24979
+ const entries = await fs27.readdir(profilesDir());
24980
+ return entries.filter((e) => e.endsWith(".yaml")).map((e) => e.replace(/\.yaml$/, "")).sort();
24981
+ } catch (e) {
24982
+ if (e.code === "ENOENT") return [];
24983
+ throw e;
24984
+ }
24985
+ }
24986
+ async function readProfile(name) {
24987
+ try {
24988
+ const raw = await fs27.readFile(getProfilePath(name), "utf8");
24989
+ const parsed = parseYaml11(raw);
24990
+ if (!parsed || typeof parsed !== "object") return null;
24991
+ return parsed;
24992
+ } catch (e) {
24993
+ if (e.code === "ENOENT") return null;
24994
+ throw e;
24995
+ }
24996
+ }
24997
+ async function exists(p) {
24998
+ try {
24999
+ await fs27.stat(p);
25000
+ return true;
25001
+ } catch {
25002
+ return false;
25003
+ }
25004
+ }
25005
+ async function saveProfile(p) {
25006
+ await fs27.mkdir(profilesDir(), { recursive: true });
25007
+ let name = p.name;
25008
+ let n = 2;
25009
+ while (await exists(getProfilePath(name))) {
25010
+ name = `${p.name}-${n.toString()}`;
25011
+ n++;
25012
+ }
25013
+ const target = getProfilePath(name);
25014
+ const tmp = `${target}.tmp`;
25015
+ await fs27.writeFile(tmp, stringifyYaml4({ ...p, name }, { indent: 2 }), "utf8");
25016
+ await fs27.rename(tmp, target);
25017
+ return name;
24028
25018
  }
24029
25019
 
24030
25020
  // src/lib/switch.ts
@@ -24106,14 +25096,14 @@ async function profileSwitch(name, asName) {
24106
25096
  init_errors();
24107
25097
  var SwitchCommand = class extends M8tCommand {
24108
25098
  static paths = [["switch"]];
24109
- static usage = Command47.Usage({
25099
+ static usage = Command48.Usage({
24110
25100
  description: "Re-point local config at another deployment: --subscription <id|name> (discovery) or <profile>."
24111
25101
  });
24112
- profile = Option44.String({ required: false });
24113
- subscription = Option44.String("--subscription");
24114
- list = Option44.Boolean("--list", false);
24115
- as = Option44.String("--as");
24116
- output = Option44.String("--output");
25102
+ profile = Option45.String({ required: false });
25103
+ subscription = Option45.String("--subscription");
25104
+ list = Option45.Boolean("--list", false);
25105
+ as = Option45.String("--as");
25106
+ output = Option45.String("--output");
24117
25107
  async executeCommand() {
24118
25108
  const mode = resolveOutputMode(
24119
25109
  this.output,
@@ -24136,11 +25126,11 @@ var SwitchCommand = class extends M8tCommand {
24136
25126
  }
24137
25127
  return 0;
24138
25128
  }
24139
- let result;
25129
+ let result2;
24140
25130
  if (this.subscription) {
24141
- result = await discoverySwitch(this.subscription, this.as);
25131
+ result2 = await discoverySwitch(this.subscription, this.as);
24142
25132
  } else if (this.profile) {
24143
- result = await profileSwitch(this.profile, this.as);
25133
+ result2 = await profileSwitch(this.profile, this.as);
24144
25134
  } else {
24145
25135
  throw new LocalCliError({
24146
25136
  code: "SWITCH_NO_TARGET",
@@ -24149,16 +25139,16 @@ var SwitchCommand = class extends M8tCommand {
24149
25139
  });
24150
25140
  }
24151
25141
  if (mode === "json") {
24152
- this.context.stdout.write(renderJson(result) + "\n");
25142
+ this.context.stdout.write(renderJson(result2) + "\n");
24153
25143
  return 0;
24154
25144
  }
24155
25145
  this.context.stdout.write(
24156
25146
  renderKeyValueBlock([
24157
- { key: "tenant", value: result.tenantId },
24158
- { key: "clientId", value: result.clientId },
24159
- { key: "endpoint", value: result.projectEndpoint },
24160
- { key: "subscription", value: result.subscriptionId },
24161
- { key: "saved profile", value: result.snapshot ?? colors.dim("(nothing to snapshot)") }
25147
+ { key: "tenant", value: result2.tenantId },
25148
+ { key: "clientId", value: result2.clientId },
25149
+ { key: "endpoint", value: result2.projectEndpoint },
25150
+ { key: "subscription", value: result2.subscriptionId },
25151
+ { key: "saved profile", value: result2.snapshot ?? colors.dim("(nothing to snapshot)") }
24162
25152
  ]) + "\n"
24163
25153
  );
24164
25154
  this.context.stdout.write(
@@ -24170,7 +25160,7 @@ var SwitchCommand = class extends M8tCommand {
24170
25160
 
24171
25161
  // src/commands/open.ts
24172
25162
  import { spawn as spawn5 } from "child_process";
24173
- import { Command as Command48, Option as Option45 } from "clipanion";
25163
+ import { Command as Command49, Option as Option46 } from "clipanion";
24174
25164
 
24175
25165
  // src/lib/open-targets.ts
24176
25166
  init_errors();
@@ -24216,14 +25206,14 @@ function openUrl(url) {
24216
25206
  }
24217
25207
  var OpenCommand = class extends M8tCommand {
24218
25208
  static paths = [["open"]];
24219
- static usage = Command48.Usage({
25209
+ static usage = Command49.Usage({
24220
25210
  description: "Open the deployed webapp (default), the Foundry portal, or the resource group.",
24221
25211
  details: "Targets: webapp (deployed app, default) | foundry (ai.azure.com) | portal (resource group in the Azure portal). Pass --print to emit the URL instead of launching a browser (also the default when stdout isn't a TTY)."
24222
25212
  });
24223
- target = Option45.String({ required: false });
24224
- print = Option45.Boolean("--print", false);
24225
- output = Option45.String("--output");
24226
- resourceGroup = Option45.String("--resource-group", {
25213
+ target = Option46.String({ required: false });
25214
+ print = Option46.Boolean("--print", false);
25215
+ output = Option46.String("--output");
25216
+ resourceGroup = Option46.String("--resource-group", {
24227
25217
  description: "m8t resource group to disambiguate the gateway (multi-deployment subscriptions)."
24228
25218
  });
24229
25219
  async executeCommand() {
@@ -24267,7 +25257,7 @@ var OpenCommand = class extends M8tCommand {
24267
25257
  };
24268
25258
 
24269
25259
  // src/commands/dream/run.ts
24270
- import { Command as Command49, Option as Option46 } from "clipanion";
25260
+ import { Command as Command50, Option as Option47 } from "clipanion";
24271
25261
  import { AzureCliCredential } from "@azure/identity";
24272
25262
  import { TableClient as TableClient7 } from "@azure/data-tables";
24273
25263
  import { AIProjectClient as AIProjectClient3 } from "@azure/ai-projects";
@@ -24472,11 +25462,11 @@ function extractQuery(args) {
24472
25462
  return null;
24473
25463
  }
24474
25464
  }
24475
- function extractSources(result) {
25465
+ function extractSources(result2) {
24476
25466
  const out = [];
24477
25467
  const re = /\[\d+\]\s*([^\n[]+)/g;
24478
25468
  let m;
24479
- while ((m = re.exec(result)) !== null) {
25469
+ while ((m = re.exec(result2)) !== null) {
24480
25470
  const t = m[1].trim();
24481
25471
  if (t)
24482
25472
  out.push(t);
@@ -24547,10 +25537,10 @@ async function fetchEnrichment(credential2, workspaceId, respIds, from, to) {
24547
25537
  if (ids.length === 0)
24548
25538
  return /* @__PURE__ */ new Map();
24549
25539
  const client = new LogsQueryClient(credential2);
24550
- const result = await client.queryWorkspace(workspaceId, buildEnrichKql(ids), { startTime: new Date(from), endTime: new Date(to) });
24551
- if (result.status !== LogsQueryResultStatus.Success || result.tables.length === 0)
25540
+ const result2 = await client.queryWorkspace(workspaceId, buildEnrichKql(ids), { startTime: new Date(from), endTime: new Date(to) });
25541
+ if (result2.status !== LogsQueryResultStatus.Success || result2.tables.length === 0)
24552
25542
  return /* @__PURE__ */ new Map();
24553
- return parseEnrichment(tableToSpans(result.tables[0]));
25543
+ return parseEnrichment(tableToSpans(result2.tables[0]));
24554
25544
  }
24555
25545
 
24556
25546
  // ../../packages/agent-ledger/dist/esm/read/brain-reads-client.js
@@ -26310,7 +27300,7 @@ async function runDream(input, deps, seams = defaultDreamSeams()) {
26310
27300
  return { worker: input.worker, applied: [], digest, advanced: true };
26311
27301
  }
26312
27302
  const message = applied.digest.length ? `dream: ${input.worker} consolidation` : `dream: ${input.worker}`;
26313
- const result = await commitWithRebase({
27303
+ const result2 = await commitWithRebase({
26314
27304
  writer: deps.brain,
26315
27305
  message,
26316
27306
  changes: applied.changes,
@@ -26318,13 +27308,13 @@ async function runDream(input, deps, seams = defaultDreamSeams()) {
26318
27308
  maxAttempts: MAX_WRITE_ATTEMPTS,
26319
27309
  verify: async () => verifyEndState(deps.brain, applied.changes)
26320
27310
  });
26321
- if (result.outcome !== "committed") {
26322
- deps.logger.error({ at: "runDream", worker: input.worker, terminal: result.outcome, attempts: result.attempts });
26323
- digest.push({ kind: "failure", detail: `write ${result.outcome} after ${String(result.attempts)} attempt(s)`, evidence: [] });
27311
+ if (result2.outcome !== "committed") {
27312
+ deps.logger.error({ at: "runDream", worker: input.worker, terminal: result2.outcome, attempts: result2.attempts });
27313
+ digest.push({ kind: "failure", detail: `write ${result2.outcome} after ${String(result2.attempts)} attempt(s)`, evidence: [] });
26324
27314
  return { worker: input.worker, applied: [], digest, advanced: false };
26325
27315
  }
26326
27316
  await commitCursorAdvance(input.advance, deps.cursor);
26327
- deps.logger.info({ at: "runDream", worker: input.worker, committed: result.newSha, advanced: true });
27317
+ deps.logger.info({ at: "runDream", worker: input.worker, committed: result2.newSha, advanced: true });
26328
27318
  await emitDigest(input, deps, digest);
26329
27319
  const appliedChanges = applied.digest.filter((e) => e.kind === "changed");
26330
27320
  return { worker: input.worker, applied: appliedChanges, digest, advanced: true };
@@ -26494,20 +27484,20 @@ function redactTranscripts(input) {
26494
27484
  }
26495
27485
  var DreamRunCommand = class extends M8tCommand {
26496
27486
  static paths = [["dream", "run"]];
26497
- static usage = Command49.Usage({
27487
+ static usage = Command50.Usage({
26498
27488
  description: "Dry-run the brain consumption pipeline for one worker (no model call, no writes).",
26499
27489
  details: "Builds AzureCliCredential + resolves the Foundry project, ledger table, and Log Analytics workspace, runs the consumption pipeline, and prints the skip-ledger, the partition invariant, and harvest stats. Transcripts are metadata-only unless --show-transcripts is passed."
26500
27490
  });
26501
- worker = Option46.String("--worker", { description: "Worker (canonical name) to harvest. Required." });
26502
- dryRun = Option46.Boolean("--dry-run", false, { description: "Read-only harvest; no model call, no writes." });
26503
- since = Option46.String("--since", { description: "ISO-8601 start override (rejected if malformed or future)." });
26504
- reset = Option46.Boolean("--reset", false, { description: "Ignore the stored cursor; read from the beginning." });
26505
- showTranscripts = Option46.Boolean("--show-transcripts", false, {
27491
+ worker = Option47.String("--worker", { description: "Worker (canonical name) to harvest. Required." });
27492
+ dryRun = Option47.Boolean("--dry-run", false, { description: "Read-only harvest; no model call, no writes." });
27493
+ since = Option47.String("--since", { description: "ISO-8601 start override (rejected if malformed or future)." });
27494
+ reset = Option47.Boolean("--reset", false, { description: "Ignore the stored cursor; read from the beginning." });
27495
+ showTranscripts = Option47.Boolean("--show-transcripts", false, {
26506
27496
  description: "Print transcript bodies (default: metadata only)."
26507
27497
  });
26508
- subscription = Option46.String("--subscription");
26509
- endpoint = Option46.String("--endpoint");
26510
- output = Option46.String("--output");
27498
+ subscription = Option47.String("--subscription");
27499
+ endpoint = Option47.String("--endpoint");
27500
+ output = Option47.String("--output");
26511
27501
  // Resolution seam — overridden by tests; built lazily at runtime otherwise.
26512
27502
  deps;
26513
27503
  async executeCommand() {
@@ -26527,7 +27517,7 @@ var DreamRunCommand = class extends M8tCommand {
26527
27517
  endpoint: typeof this.endpoint === "string" ? this.endpoint : void 0
26528
27518
  });
26529
27519
  const liveCursor = await liveDeps.readCursor(liveContext.worker);
26530
- const result = await liveDeps.runDream({
27520
+ const result2 = await liveDeps.runDream({
26531
27521
  worker: liveContext.worker,
26532
27522
  physicalPks: liveContext.physicalPks,
26533
27523
  since,
@@ -26537,10 +27527,10 @@ var DreamRunCommand = class extends M8tCommand {
26537
27527
  ...buildSources(liveContext, liveDeps)
26538
27528
  });
26539
27529
  this.context.stdout.write(
26540
- `${colors.field("dream run (live)")} worker=${result.worker} applied ${String(result.applied.length)} ${result.advanced ? colors.success("advanced") : colors.dim("not advanced")}
27530
+ `${colors.field("dream run (live)")} worker=${result2.worker} applied ${String(result2.applied.length)} ${result2.advanced ? colors.success("advanced") : colors.dim("not advanced")}
26541
27531
  `
26542
27532
  );
26543
- const changed = result.applied.filter(
27533
+ const changed = result2.applied.filter(
26544
27534
  (e) => e.kind === "changed"
26545
27535
  );
26546
27536
  for (const e of changed) {
@@ -26549,7 +27539,7 @@ var DreamRunCommand = class extends M8tCommand {
26549
27539
  `
26550
27540
  );
26551
27541
  }
26552
- const cost = result.digest.find(
27542
+ const cost = result2.digest.find(
26553
27543
  (d) => d.kind === "cost"
26554
27544
  );
26555
27545
  if (cost) {
@@ -26558,7 +27548,7 @@ var DreamRunCommand = class extends M8tCommand {
26558
27548
  `
26559
27549
  );
26560
27550
  }
26561
- const failures = result.digest.filter(
27551
+ const failures = result2.digest.filter(
26562
27552
  (d) => d.kind === "failure"
26563
27553
  );
26564
27554
  for (const f of failures) {
@@ -26687,12 +27677,12 @@ AppDependencies
26687
27677
  | where isnotempty(conv)
26688
27678
  | project conv
26689
27679
  | take 1`;
26690
- const result = await client.queryWorkspace(workspaceId, kql, {
27680
+ const result2 = await client.queryWorkspace(workspaceId, kql, {
26691
27681
  startTime: new Date(from),
26692
27682
  endTime: new Date(to)
26693
27683
  });
26694
- if (result.status !== LogsQueryResultStatus3.Success || result.tables.length === 0) return null;
26695
- const table = result.tables[0];
27684
+ if (result2.status !== LogsQueryResultStatus3.Success || result2.tables.length === 0) return null;
27685
+ const table = result2.tables[0];
26696
27686
  if (table.rows.length === 0) return null;
26697
27687
  const row = table.rows[0];
26698
27688
  const idx = table.columnDescriptors.findIndex((c) => c.name === "conv");
@@ -26855,7 +27845,7 @@ function defaultDeps(overrides) {
26855
27845
  }
26856
27846
 
26857
27847
  // src/commands/foundry/create.ts
26858
- import { Command as Command50, Option as Option47 } from "clipanion";
27848
+ import { Command as Command51, Option as Option48 } from "clipanion";
26859
27849
 
26860
27850
  // src/lib/foundry-create.ts
26861
27851
  init_errors();
@@ -27093,7 +28083,7 @@ async function createFoundryProject(args) {
27093
28083
  init_errors();
27094
28084
  var FoundryCreateCommand = class extends M8tCommand {
27095
28085
  static paths = [["foundry", "create"]];
27096
- static usage = Command50.Usage({
28086
+ static usage = Command51.Usage({
27097
28087
  description: "Create an AI Foundry (AIServices) account + project + model deployment from scratch.",
27098
28088
  details: "Non-interactive and idempotent. Creates the AIServices account (custom subdomain + project management), a project, and a model deployment (default gpt-4.1-mini @ capacity 50). Region must be hosted-agent-eligible. Emits the project endpoint as structured output. Re-run is a clean no-op (account/project skipped if present; deployment capacity converges UP, never down).",
27099
28089
  examples: [
@@ -27102,16 +28092,16 @@ var FoundryCreateCommand = class extends M8tCommand {
27102
28092
  ["Higher capacity for a reasoning model", "$0 foundry create --resource-group rg-m8t-stack --location eastus2 --model gpt-5-mini --capacity 250"]
27103
28093
  ]
27104
28094
  });
27105
- resourceGroup = Option47.String("--resource-group");
27106
- location = Option47.String("--location");
27107
- account = Option47.String("--account");
27108
- project = Option47.String("--project", "m8t");
27109
- model = Option47.String("--model", "gpt-4.1-mini");
27110
- modelVersion = Option47.String("--model-version", "2025-04-14");
27111
- capacity = Option47.String("--capacity", "50");
27112
- subscription = Option47.String("--subscription");
27113
- skipQuotaCheck = Option47.Boolean("--skip-quota-check", false);
27114
- output = Option47.String("--output");
28095
+ resourceGroup = Option48.String("--resource-group");
28096
+ location = Option48.String("--location");
28097
+ account = Option48.String("--account");
28098
+ project = Option48.String("--project", "m8t");
28099
+ model = Option48.String("--model", "gpt-4.1-mini");
28100
+ modelVersion = Option48.String("--model-version", "2025-04-14");
28101
+ capacity = Option48.String("--capacity", "50");
28102
+ subscription = Option48.String("--subscription");
28103
+ skipQuotaCheck = Option48.Boolean("--skip-quota-check", false);
28104
+ output = Option48.String("--output");
27115
28105
  async executeCommand() {
27116
28106
  const mode = resolveOutputMode(
27117
28107
  this.output,
@@ -27136,7 +28126,7 @@ var FoundryCreateCommand = class extends M8tCommand {
27136
28126
  const capacity = typeof this.capacity === "string" ? Number(this.capacity) : 50;
27137
28127
  const onProgress = mode === "pretty" ? (m) => this.context.stderr.write(` ${colors.dim(m)}
27138
28128
  `) : void 0;
27139
- const result = await createFoundryProject({
28129
+ const result2 = await createFoundryProject({
27140
28130
  resourceGroup,
27141
28131
  location,
27142
28132
  account: accountName,
@@ -27152,53 +28142,53 @@ var FoundryCreateCommand = class extends M8tCommand {
27152
28142
  if (mode === "json") {
27153
28143
  this.context.stdout.write(
27154
28144
  renderJson({
27155
- endpoint: result.endpoint,
27156
- accountName: result.accountName,
27157
- accountResourceId: result.accountScope,
27158
- projectName: result.projectName,
27159
- region: result.region,
27160
- model: result.model,
27161
- capacity: result.capacity,
27162
- created: result.created
28145
+ endpoint: result2.endpoint,
28146
+ accountName: result2.accountName,
28147
+ accountResourceId: result2.accountScope,
28148
+ projectName: result2.projectName,
28149
+ region: result2.region,
28150
+ model: result2.model,
28151
+ capacity: result2.capacity,
28152
+ created: result2.created
27163
28153
  }) + "\n"
27164
28154
  );
27165
28155
  return 0;
27166
28156
  }
27167
28157
  this.context.stdout.write(
27168
28158
  renderKeyValueBlock([
27169
- { key: "endpoint", value: result.endpoint },
27170
- { key: "account", value: result.accountName },
27171
- { key: "project", value: result.projectName },
27172
- { key: "region", value: result.region },
27173
- { key: "model", value: `${result.model} (capacity ${String(result.capacity)})` }
28159
+ { key: "endpoint", value: result2.endpoint },
28160
+ { key: "account", value: result2.accountName },
28161
+ { key: "project", value: result2.projectName },
28162
+ { key: "region", value: result2.region },
28163
+ { key: "model", value: `${result2.model} (capacity ${String(result2.capacity)})` }
27174
28164
  ]) + "\n"
27175
28165
  );
27176
- const made = Object.entries(result.created).filter(([, v]) => v).map(([k]) => k);
28166
+ const made = Object.entries(result2.created).filter(([, v]) => v).map(([k]) => k);
27177
28167
  this.context.stdout.write(colors.dim(`created: ${made.length ? made.join(", ") : "nothing (all present)"}
27178
28168
  `));
27179
- this.context.stdout.write(colors.dim(`next: 'm8t deploy --foundry-endpoint ${result.endpoint}' to deploy the gateway.
28169
+ this.context.stdout.write(colors.dim(`next: 'm8t deploy --foundry-endpoint ${result2.endpoint}' to deploy the gateway.
27180
28170
  `));
27181
28171
  return 0;
27182
28172
  }
27183
28173
  };
27184
28174
 
27185
28175
  // src/commands/foundry/await-ready.ts
27186
- import { Command as Command51, Option as Option48 } from "clipanion";
28176
+ import { Command as Command52, Option as Option49 } from "clipanion";
27187
28177
  import { AzureCliCredential as AzureCliCredential2 } from "@azure/identity";
27188
28178
  init_errors();
27189
28179
  var FoundryAwaitReadyCommand = class extends M8tCommand {
27190
28180
  static paths = [["foundry", "await-ready"]];
27191
- static usage = Command51.Usage({
28181
+ static usage = Command52.Usage({
27192
28182
  description: "Wait until a freshly-created Foundry project's data plane reliably serves it.",
27193
28183
  details: "Probes the project (GET /agents) until it returns 200 on a few consecutive tries, or fails clearly after a bounded budget. A newly-created account can serve intermittent 404 'Project not found' for minutes; run this after 'foundry create' and before deploying agents so the worker phase doesn't catch the unstable window.",
27194
28184
  examples: [["Wait for a project to be ready", "$0 foundry await-ready --endpoint https://acc.services.ai.azure.com/api/projects/m8t"]]
27195
28185
  });
27196
- endpoint = Option48.String("--endpoint");
27197
- consecutive = Option48.String("--consecutive", "3");
27198
- attempts = Option48.String("--attempts", "60");
27199
- interval = Option48.String("--interval", "5");
27200
- subscription = Option48.String("--subscription");
27201
- output = Option48.String("--output");
28186
+ endpoint = Option49.String("--endpoint");
28187
+ consecutive = Option49.String("--consecutive", "3");
28188
+ attempts = Option49.String("--attempts", "60");
28189
+ interval = Option49.String("--interval", "5");
28190
+ subscription = Option49.String("--subscription");
28191
+ output = Option49.String("--output");
27202
28192
  async executeCommand() {
27203
28193
  const mode = resolveOutputMode(this.output, this.context.stdout);
27204
28194
  const endpoint = typeof this.endpoint === "string" ? this.endpoint : void 0;
@@ -27232,7 +28222,7 @@ var FoundryAwaitReadyCommand = class extends M8tCommand {
27232
28222
  };
27233
28223
 
27234
28224
  // src/commands/bootstrap/preflight.ts
27235
- import { Command as Command52, Option as Option49 } from "clipanion";
28225
+ import { Command as Command53, Option as Option50 } from "clipanion";
27236
28226
 
27237
28227
  // ../../packages/telemetry-contract/artifact/tier-map.ts
27238
28228
  var EVENT_TIERS = {
@@ -27243,7 +28233,8 @@ var EVENT_TIERS = {
27243
28233
  "heartbeat": 1,
27244
28234
  "update-applied": 1,
27245
28235
  "update-failed": 1,
27246
- "consent-changed": 1
28236
+ "consent-changed": 1,
28237
+ "identity-mismatch": 1
27247
28238
  // extended additively for tier-2 usage events: "usage-rollup": 2
27248
28239
  };
27249
28240
  var EVENT_ENUM = Object.keys(EVENT_TIERS);
@@ -27276,87 +28267,23 @@ var SPEND_DISCLOSURE = [
27276
28267
  " - You can remove it later - see guides/uninstall.md."
27277
28268
  ].join("\n");
27278
28269
 
27279
- // src/lib/bootstrap-preflight.ts
27280
- var ADMIN_DIRECTORY_ROLES = ["Global Administrator", "Application Administrator", "Cloud Application Administrator"];
27281
- async function checkSubScopeAdmin(callerObjectId, subscriptionId) {
27282
- const rows = JSON.parse(await runAz([
27283
- "role",
27284
- "assignment",
27285
- "list",
27286
- "--assignee",
27287
- callerObjectId,
27288
- "--scope",
27289
- `/subscriptions/${subscriptionId}`,
27290
- "--include-inherited",
27291
- "--query",
27292
- "[].{roleDefinitionName: roleDefinitionName}",
27293
- "-o",
27294
- "json"
27295
- ]));
27296
- const hit = rows.find((r) => r.roleDefinitionName === "Owner" || r.roleDefinitionName === "User Access Administrator");
27297
- return { ok: Boolean(hit), role: hit?.roleDefinitionName };
27298
- }
27299
- async function checkAppRegCapability(callerObjectId) {
27300
- let roles;
27301
- try {
27302
- roles = JSON.parse(await runAz([
27303
- "rest",
27304
- "--method",
27305
- "GET",
27306
- "--url",
27307
- `https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?$filter=principalId eq '${callerObjectId}'&$expand=roleDefinition`,
27308
- "--query",
27309
- "value[].roleDefinition.displayName",
27310
- "-o",
27311
- "json"
27312
- ]));
27313
- } catch {
27314
- roles = null;
27315
- }
27316
- if (roles === null) return { ok: false, inconclusive: true };
27317
- return { ok: roles.some((r) => ADMIN_DIRECTORY_ROLES.includes(r)), inconclusive: false };
27318
- }
27319
- async function registerContainerInstance(subscriptionId) {
27320
- await runAz(["provider", "register", "--namespace", "Microsoft.ContainerInstance", "--subscription", subscriptionId, "--only-show-errors"]);
27321
- }
27322
- function buildPreflightBanner(who) {
27323
- return [
27324
- "\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
27325
- "\u2551 \u26D4 STOP \u2014 READ THIS BEFORE CONTINUING \u2551",
27326
- "\u2551 \u2551",
27327
- "\u2551 Installing m8t creates real Azure resources in YOUR \u2551",
27328
- "\u2551 subscription and assigns roles. To do that, the account you just \u2551",
27329
- "\u2551 signed in with MUST be: \u2551",
27330
- "\u2551 \u2551",
27331
- "\u2551 \u2022 OWNER or USER ACCESS ADMINISTRATOR at the subscription scope \u2551",
27332
- "\u2551 (it has to create a managed identity and assign it roles), AND \u2551",
27333
- "\u2551 \u2022 a directory admin able to register one Entra app \u2551",
27334
- "\u2551 (Application / Cloud Application / Global Administrator) \u2551",
27335
- "\u2551 \u2014 OR you supply a ready app registration with --client-id. \u2551",
27336
- "\u2551 \u2551",
27337
- "\u2551 If you are NOT this, this install CANNOT proceed and will STOP NOW. \u2551",
27338
- "\u2551 Nothing has been created yet. Get an admin to run this, or ask one \u2551",
27339
- "\u2551 for an app registration id (--client-id <appId>) and try again. \u2551",
27340
- "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
27341
- "",
27342
- `Signed in as: ${who.upn} (tenant ${who.tenantId}, sub ${who.subscriptionId})`,
27343
- ""
27344
- ].join("\n");
27345
- }
27346
-
27347
28270
  // src/commands/bootstrap/preflight.ts
27348
28271
  var BootstrapPreflightCommand = class extends M8tCommand {
27349
28272
  static paths = [["bootstrap", "preflight"]];
27350
- static usage = Command52.Usage({
28273
+ static usage = Command53.Usage({
27351
28274
  description: "Loudly verify you can install m8t (Owner/UAA + directory admin) and hard-stop if not.",
27352
- details: "Step 1 of `m8t bootstrap`. Prints an unmissable admin-credentials notice, then checks: Owner or User Access Administrator at subscription scope (hard requirement), directory-admin capability to register the app (or pass --client-id), and registers Microsoft.ContainerInstance. Exits non-zero with the exact failing check + remedy.",
28275
+ details: "Step 1 of `m8t bootstrap`. Prints an unmissable admin-credentials notice, then checks: Owner or User Access Administrator at subscription scope (hard requirement), directory-admin capability to register the app (or pass --client-id), every Azure resource provider the install uses (registering any that are missing), and \u2014 with --location \u2014 model quota in the target region. Exits non-zero with the exact failing check + remedy. `m8t prereqs` runs the same substrate checks on their own.",
27353
28276
  examples: [
27354
28277
  ["Run the preflight", "$0 bootstrap preflight"],
28278
+ ["Check quota for the region you will install into", "$0 bootstrap preflight --location eastus2"],
27355
28279
  ["BYO app registration (directory guests)", "$0 bootstrap preflight --client-id <appId>"]
27356
28280
  ]
27357
28281
  });
27358
- clientId = Option49.String("--client-id");
27359
- subscription = Option49.String("--subscription");
28282
+ clientId = Option50.String("--client-id");
28283
+ subscription = Option50.String("--subscription");
28284
+ location = Option50.String("--location", {
28285
+ description: "Target region. Enables the model-quota check \u2014 without it, quota is not verified."
28286
+ });
27360
28287
  async executeCommand() {
27361
28288
  const clientId = typeof this.clientId === "string" ? this.clientId : void 0;
27362
28289
  const account = await getAzAccount();
@@ -27387,8 +28314,33 @@ ${colors.dim(DISCLOSURE_TIER1)}
27387
28314
  } else {
27388
28315
  this.context.stdout.write(line(true, `Using your app registration ${clientId}`, "--client-id supplied; skipping the directory-admin check"));
27389
28316
  }
27390
- await registerContainerInstance(subscriptionId);
27391
- this.context.stdout.write(line(true, "Microsoft.ContainerInstance registered", "installer substrate"));
28317
+ const region = typeof this.location === "string" ? this.location : void 0;
28318
+ const verdict = await runInstallPhase(buildPrereqDeps({ subscription: subscriptionId }), {
28319
+ fix: true,
28320
+ subscription: subscriptionId,
28321
+ // BOTH region and model are required for the quota check to run at all.
28322
+ // Passing only the region silently skipped it while --location implied it
28323
+ // had happened.
28324
+ ...region ? { region, model: INSTALL_REASONING_MODEL } : {},
28325
+ ...clientId ? { clientId } : {},
28326
+ onProgress: (m) => {
28327
+ this.context.stderr.write(colors.dim(` ${m}
28328
+ `));
28329
+ }
28330
+ });
28331
+ for (const r of verdict.results.filter((x) => x.slug === "resource-providers" || x.slug === "model-quota")) {
28332
+ this.context.stdout.write(line(r.status !== "fail", r.title, r.detail));
28333
+ if (r.status === "fail") {
28334
+ this.context.stderr.write(failBlock(r.detail, r.remedy ?? "See guides/prerequisites.md."));
28335
+ throw new LocalCliError({ code: `BOOTSTRAP_PREREQ_${r.slug.toUpperCase().replace(/-/g, "_")}`, message: r.detail, hint: r.remedy });
28336
+ }
28337
+ }
28338
+ if (verdict.unchecked.length > 0) {
28339
+ this.context.stdout.write(
28340
+ colors.dim(` Not checked (you may still hit them): ${verdict.unchecked.join(", ")} \u2014 see guides/prerequisites.md
28341
+ `)
28342
+ );
28343
+ }
27392
28344
  this.context.stdout.write(
27393
28345
  `
27394
28346
  ${colors.success("\u2705 Preflight passed.")} Here is the ONE consent step you'll clear, then walk away:
@@ -27419,7 +28371,7 @@ ${colors.error(" " + why)}
27419
28371
  import * as fs30 from "fs";
27420
28372
  import * as os14 from "os";
27421
28373
  import * as path32 from "path";
27422
- import { Command as Command53, Option as Option50 } from "clipanion";
28374
+ import { Command as Command54, Option as Option51 } from "clipanion";
27423
28375
  init_errors();
27424
28376
 
27425
28377
  // src/lib/bootstrap-mi.ts
@@ -27783,12 +28735,12 @@ ${colors.error("\u2717")} The GitHub App on disk is installed on ${colors.field(
27783
28735
  // src/commands/bootstrap/launch.ts
27784
28736
  var DEFAULT_RG = "rg-m8t-stack";
27785
28737
  var DEFAULT_INSTALLER = "ghcr.io/m8t-labs/m8t-installer";
27786
- var DEFAULT_INSTALLER_TAG = "v0.1.44";
28738
+ var DEFAULT_INSTALLER_TAG = "v0.1.46";
27787
28739
  var ACI_NAME = "m8t-installer";
27788
28740
  var MI_NAME = "m8t-installer-mi";
27789
28741
  var BootstrapLaunchCommand = class extends M8tCommand {
27790
28742
  static paths = [["bootstrap", "launch"]];
27791
- static usage = Command53.Usage({
28743
+ static usage = Command54.Usage({
27792
28744
  description: "Create + authorize the installer managed identity, then kick the cloud installer.",
27793
28745
  details: "Step 2 of `m8t bootstrap` (run after `preflight`). Ensures the resource group, creates the m8t app registration (or uses --client-id), creates a user-assigned managed identity granted Owner at subscription scope, and launches the published m8t-installer image as an ACI run-to-completion job under that identity. Writes ~/.m8t/bootstrap.json for `status`/`reap`/`finish`.",
27794
28746
  examples: [
@@ -27799,26 +28751,26 @@ var BootstrapLaunchCommand = class extends M8tCommand {
27799
28751
  ["Share a support contact", "$0 bootstrap launch --location eastus2 --contact-email you@example.com --company 'Acme'"]
27800
28752
  ]
27801
28753
  });
27802
- location = Option50.String("--location");
27803
- resourceGroup = Option50.String("--resource-group");
27804
- clientId = Option50.String("--client-id");
27805
- subscription = Option50.String("--subscription");
27806
- installerTag = Option50.String("--installer-tag");
28754
+ location = Option51.String("--location");
28755
+ resourceGroup = Option51.String("--resource-group");
28756
+ clientId = Option51.String("--client-id");
28757
+ subscription = Option51.String("--subscription");
28758
+ installerTag = Option51.String("--installer-tag");
27807
28759
  // Full image ref override (registry + repo + tag) — an escape hatch when the
27808
28760
  // default org/tag is wrong for the current CLI (e.g. a stale published build).
27809
28761
  // Wins over --installer-tag / the pinned default.
27810
- installerImage = Option50.String("--installer-image");
27811
- gatewayImageRef = Option50.String("--gateway-image-ref");
27812
- githubAppCreds = Option50.String("--github-app-creds");
27813
- contactEmail = Option50.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
27814
- company = Option50.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
28762
+ installerImage = Option51.String("--installer-image");
28763
+ gatewayImageRef = Option51.String("--gateway-image-ref");
28764
+ githubAppCreds = Option51.String("--github-app-creds");
28765
+ contactEmail = Option51.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
28766
+ company = Option51.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
27815
28767
  // Value-carrying on purpose: a bare --force would be cargo-culted into
27816
28768
  // runbooks and harness prompts and erode the protection, whereas a faithful
27817
28769
  // paste can never accidentally carry the victim group's name. It AUTHORIZES
27818
28770
  // the target; --resource-group is what CHOOSES it.
27819
- reinstallInto = Option50.String("--reinstall-into", { description: "Consent to installing into --resource-group even though it already holds resources. Must match the target group's name." });
27820
- org = Option50.String("--org", { description: "Assert the GitHub App on disk is installed on this org; refuses on a mismatch." });
27821
- noBrains = Option50.Boolean("--no-brains", false, { description: "Install without brain-backed workers. For test and CI rigs; the supported install creates brains." });
28771
+ reinstallInto = Option51.String("--reinstall-into", { description: "Consent to installing into --resource-group even though it already holds resources. Must match the target group's name." });
28772
+ org = Option51.String("--org", { description: "Assert the GitHub App on disk is installed on this org; refuses on a mismatch." });
28773
+ noBrains = Option51.Boolean("--no-brains", false, { description: "Install without brain-backed workers. For test and CI rigs; the supported install creates brains." });
27822
28774
  async executeCommand() {
27823
28775
  const location = typeof this.location === "string" ? this.location : void 0;
27824
28776
  if (!location) {
@@ -27972,7 +28924,7 @@ var BootstrapLaunchCommand = class extends M8tCommand {
27972
28924
  };
27973
28925
 
27974
28926
  // src/commands/bootstrap/status.ts
27975
- import { Command as Command54, Option as Option51 } from "clipanion";
28927
+ import { Command as Command55, Option as Option52 } from "clipanion";
27976
28928
  init_errors();
27977
28929
 
27978
28930
  // src/lib/bootstrap-aci-state.ts
@@ -28000,13 +28952,13 @@ async function getAciState(opts) {
28000
28952
  // src/commands/bootstrap/status.ts
28001
28953
  var BootstrapStatusCommand = class extends M8tCommand {
28002
28954
  static paths = [["bootstrap", "status"]];
28003
- static usage = Command54.Usage({
28955
+ static usage = Command55.Usage({
28004
28956
  description: "Show the cloud installer's live status (phase, progress, result).",
28005
28957
  details: "Reads the durable status blob written by the installer. --watch polls until the install reaches done or failed.",
28006
28958
  examples: [["One read", "$0 bootstrap status"], ["Watch to completion", "$0 bootstrap status --watch"]]
28007
28959
  });
28008
- watch = Option51.Boolean("--watch", false);
28009
- output = Option51.String("--output");
28960
+ watch = Option52.Boolean("--watch", false);
28961
+ output = Option52.String("--output");
28010
28962
  async executeCommand() {
28011
28963
  const state = await readBootstrapState();
28012
28964
  if (!state) {
@@ -28091,7 +29043,7 @@ function formatStatus(d) {
28091
29043
  }
28092
29044
 
28093
29045
  // src/commands/bootstrap/reap.ts
28094
- import { Command as Command55, Option as Option52 } from "clipanion";
29046
+ import { Command as Command56, Option as Option53 } from "clipanion";
28095
29047
  init_errors();
28096
29048
 
28097
29049
  // src/lib/bootstrap-reap.ts
@@ -28185,14 +29137,14 @@ async function reapInstaller(opts) {
28185
29137
  // src/commands/bootstrap/reap.ts
28186
29138
  var BootstrapReapCommand = class extends M8tCommand {
28187
29139
  static paths = [["bootstrap", "reap"]];
28188
- static usage = Command55.Usage({
29140
+ static usage = Command56.Usage({
28189
29141
  description: "Tear down the installer scaffolding (ACI \u2192 MI \u2192 its role assignments) after a successful install.",
28190
29142
  details: "Runs locally on the 'done' signal (the installer can't delete its own identity). The platform RG and the gateway's own assignments persist. A failed install is left intact for diagnosis unless --force.",
28191
29143
  examples: [["Reap after done", "$0 bootstrap reap"]]
28192
29144
  });
28193
- force = Option52.Boolean("--force", false);
28194
- sweepOrphans = Option52.Boolean("--sweep-orphans", false);
28195
- yes = Option52.Boolean("--yes", false);
29145
+ force = Option53.Boolean("--force", false);
29146
+ sweepOrphans = Option53.Boolean("--sweep-orphans", false);
29147
+ yes = Option53.Boolean("--yes", false);
28196
29148
  async executeCommand() {
28197
29149
  if (this.sweepOrphans === true) {
28198
29150
  const { subscriptionId: sub } = await getAzAccount();
@@ -28289,7 +29241,7 @@ Found ${String(total)} orphaned assignment(s) (${breakdown}) (dry-run). ${colors
28289
29241
  import * as fs31 from "fs/promises";
28290
29242
  import * as os16 from "os";
28291
29243
  import * as path34 from "path";
28292
- import { Command as Command56, Option as Option53 } from "clipanion";
29244
+ import { Command as Command57, Option as Option54 } from "clipanion";
28293
29245
  init_errors();
28294
29246
 
28295
29247
  // src/lib/company-profile-seed.ts
@@ -28693,9 +29645,9 @@ async function findOnboardingProfile(args) {
28693
29645
  if (typeof part.transcript === "string") return [part.transcript];
28694
29646
  return [];
28695
29647
  }).join("\n");
28696
- const result = parseOnboardingArtifactResult(machineText);
28697
- if (result.ok) artifacts.push(result.artifact);
28698
- else if (machineText.includes("m8t_onboarding")) rejection ??= result.reason;
29648
+ const result2 = parseOnboardingArtifactResult(machineText);
29649
+ if (result2.ok) artifacts.push(result2.artifact);
29650
+ else if (machineText.includes("m8t_onboarding")) rejection ??= result2.reason;
28699
29651
  }
28700
29652
  if (artifacts.length === 0) {
28701
29653
  return {
@@ -28922,10 +29874,10 @@ var CERTAIN_UNAVAILABLE = /* @__PURE__ */ new Set([
28922
29874
  "deploy-rejected-region"
28923
29875
  ]);
28924
29876
  var QUOTA_BLOCKED = /* @__PURE__ */ new Set(["no-quota", "deploy-rejected-quota"]);
28925
- function decideNote(whitelist, result) {
28926
- const chosenIdx = result.chosen ? whitelist.findIndex((r) => r.model === result.chosen?.model) : whitelist.length;
28927
- const better = result.trace.slice(0, Math.max(chosenIdx, 0));
28928
- const runningModel = result.chosen?.model ?? null;
29877
+ function decideNote(whitelist, result2) {
29878
+ const chosenIdx = result2.chosen ? whitelist.findIndex((r) => r.model === result2.chosen?.model) : whitelist.length;
29879
+ const better = result2.trace.slice(0, Math.max(chosenIdx, 0));
29880
+ const runningModel = result2.chosen?.model ?? null;
28929
29881
  if (chosenIdx === 0) {
28930
29882
  return { status: "top", runningModel, pitchModel: null, unavailableAbovePitch: [] };
28931
29883
  }
@@ -29160,13 +30112,13 @@ async function applyProfileToBrains(args) {
29160
30112
  const verified = [];
29161
30113
  const failed = [];
29162
30114
  const causes = [];
29163
- settled.forEach((result, index) => {
30115
+ settled.forEach((result2, index) => {
29164
30116
  const brainRepo = brainRepos[index];
29165
- if (result.status === "fulfilled") {
30117
+ if (result2.status === "fulfilled") {
29166
30118
  verified.push(brainRepo);
29167
30119
  } else {
29168
30120
  failed.push(brainRepo);
29169
- causes.push(result.reason);
30121
+ causes.push(result2.reason);
29170
30122
  }
29171
30123
  });
29172
30124
  if (failed.length > 0 || verified.length !== brainRepos.length) {
@@ -29254,14 +30206,14 @@ function renderInstallSummary(args) {
29254
30206
  // src/commands/bootstrap/finish.ts
29255
30207
  var BootstrapFinishCommand = class extends M8tCommand {
29256
30208
  static paths = [["bootstrap", "finish"]];
29257
- static usage = Command56.Usage({
30209
+ static usage = Command57.Usage({
29258
30210
  description: "Point your local tools at the now-live platform (repo-root marker, discovery cache, next steps).",
29259
30211
  details: "Final step of `m8t bootstrap` (after the install reaches done). Writes ~/.m8t/repo-root, points your local tools at the live gateway, and \u2014 if you completed the onboarding intake \u2014 seeds your brain-backed advisor's brain with your company profile (best-effort; never blocks finish). Run `m8t bootstrap seed-profile` to seed manually later.",
29260
30212
  examples: [["Finish local", "$0 bootstrap finish --repo-root /path/to/m8t"]]
29261
30213
  });
29262
- repoRoot = Option53.String("--repo-root");
29263
- subscription = Option53.String("--subscription");
29264
- resourceGroup = Option53.String("--resource-group");
30214
+ repoRoot = Option54.String("--repo-root");
30215
+ subscription = Option54.String("--subscription");
30216
+ resourceGroup = Option54.String("--resource-group");
29265
30217
  async executeCommand() {
29266
30218
  const state = await readBootstrapState();
29267
30219
  if (!state) {
@@ -29312,6 +30264,36 @@ var BootstrapFinishCommand = class extends M8tCommand {
29312
30264
  ${colors.hint(` m8t bootstrap finish --repo-root ${repoRoot} # (as an admin), or by hand:`)}
29313
30265
  ${colors.hint(` az ad app update --id ${d.gatewayClientId} --set spa.redirectUris="['${d.gatewayUrl}']" # (merge, do not overwrite)`)}
29314
30266
 
30267
+ `
30268
+ );
30269
+ }
30270
+ try {
30271
+ const verdict = await runUsagePhase(
30272
+ buildPrereqDeps({ subscription: subscriptionId, resourceGroup }),
30273
+ { fix: true, onProgress: (m) => {
30274
+ this.context.stdout.write(colors.dim(` ${m}
30275
+ `));
30276
+ } }
30277
+ );
30278
+ const rows = verdict.results.filter((r) => r.slug !== "signin-redirect-uri");
30279
+ const unresolved = rows.filter((r) => r.status === "fail");
30280
+ if (unresolved.length === 0) {
30281
+ this.context.stdout.write(` ${colors.success("\u2713 your account can reach Foundry and read the platform Key Vault")}
30282
+
30283
+ `);
30284
+ }
30285
+ for (const r of unresolved) {
30286
+ this.context.stderr.write(
30287
+ ` ${colors.error("\u26A0 could not grant you access:")} ${r.detail}
30288
+ ` + (r.remedy ? ` ${colors.hint(`Fix it with: ${r.remedy}`)}
30289
+ ` : "") + "\n"
30290
+ );
30291
+ }
30292
+ } catch (e) {
30293
+ this.context.stderr.write(
30294
+ ` ${colors.error("\u26A0 could not check or grant your platform access:")} ${e.message}
30295
+ ${colors.hint("Run 'm8t prereqs --fix' when you can \u2014 until then your AI team will not load and worker deploys will fail.")}
30296
+
29315
30297
  `
29316
30298
  );
29317
30299
  }
@@ -29364,8 +30346,8 @@ ${colors.field("Then open a brand new chat/session")} \u2014 new skills + MCP se
29364
30346
  import * as fs33 from "fs";
29365
30347
  import * as os18 from "os";
29366
30348
  import * as path36 from "path";
29367
- import { Command as Command57, Option as Option54 } from "clipanion";
29368
- import { DefaultAzureCredential as DefaultAzureCredential23 } from "@azure/identity";
30349
+ import { Command as Command58, Option as Option55 } from "clipanion";
30350
+ import { DefaultAzureCredential as DefaultAzureCredential24 } from "@azure/identity";
29369
30351
  init_errors();
29370
30352
 
29371
30353
  // src/lib/bootstrap-ui.ts
@@ -29817,9 +30799,9 @@ async function serveOnboardingRelayDetached(args) {
29817
30799
  }
29818
30800
  function autoOpenOnboardingUi(url, open = tryOpenUrl) {
29819
30801
  try {
29820
- const result = open(url);
29821
- if (result instanceof Promise) {
29822
- result.catch(() => {
30802
+ const result2 = open(url);
30803
+ if (result2 instanceof Promise) {
30804
+ result2.catch(() => {
29823
30805
  });
29824
30806
  }
29825
30807
  } catch {
@@ -29951,15 +30933,15 @@ async function resolveIntakeModel(args) {
29951
30933
  });
29952
30934
  };
29953
30935
  const start = Date.now();
29954
- const result = await walkCascade(WHITELIST, plan, deploy, {
30936
+ const result2 = await walkCascade(WHITELIST, plan, deploy, {
29955
30937
  now: () => Date.now(),
29956
30938
  deadlineAt: start + (args.deadlineMs ?? DEFAULT_DEADLINE_MS),
29957
30939
  onRung: (model, outcome) => args.onNarrate?.(narrate(model, outcome, region))
29958
30940
  });
29959
30941
  return {
29960
- model: result.chosen?.model,
29961
- note: renderChosenModelNote(decideNote(WHITELIST, result)),
29962
- trace: result.trace
30942
+ model: result2.chosen?.model,
30943
+ note: renderChosenModelNote(decideNote(WHITELIST, result2)),
30944
+ trace: result2.trace
29963
30945
  };
29964
30946
  } catch (e) {
29965
30947
  return degradeToFloor(e instanceof Error ? e.message : String(e));
@@ -30172,7 +31154,7 @@ function renderDeployFailure(error) {
30172
31154
  }
30173
31155
  var BootstrapUiCommand = class extends M8tCommand {
30174
31156
  static paths = [["bootstrap", "ui"]];
30175
- static usage = Command57.Usage({
31157
+ static usage = Command58.Usage({
30176
31158
  description: "Deploy Azzy + start the local onboarding chat UI in the background (returns immediately).",
30177
31159
  details: [
30178
31160
  "Run after `m8t bootstrap launch`, in parallel with `status --watch`. Waits for the cloud",
@@ -30193,16 +31175,16 @@ var BootstrapUiCommand = class extends M8tCommand {
30193
31175
  ["Experimental: start the voice relay (no effect on the text-only intake)", "$0 bootstrap ui --repo-root /path/to/m8t --voice"]
30194
31176
  ]
30195
31177
  });
30196
- repoRoot = Option54.String("--repo-root");
30197
- port = Option54.String("--port", "3000");
30198
- endpoint = Option54.String("--endpoint", {
31178
+ repoRoot = Option55.String("--repo-root");
31179
+ port = Option55.String("--port", "3000");
31180
+ endpoint = Option55.String("--endpoint", {
30199
31181
  description: "Foundry project endpoint to target \u2014 disambiguates when the subscription has multiple projects."
30200
31182
  });
30201
- prepOnly = Option54.Boolean("--prep-only", false);
30202
- skipInstall = Option54.Boolean("--skip-install", false);
30203
- stop = Option54.Boolean("--stop", false);
30204
- foreground = Option54.Boolean("--foreground", false);
30205
- voice = Option54.Boolean("--voice", false, {
31183
+ prepOnly = Option55.Boolean("--prep-only", false);
31184
+ skipInstall = Option55.Boolean("--skip-install", false);
31185
+ stop = Option55.Boolean("--stop", false);
31186
+ foreground = Option55.Boolean("--foreground", false);
31187
+ voice = Option55.Boolean("--voice", false, {
30206
31188
  description: "Experimental: when serving, starts the voice relay and writes the intake voice env var. The onboarding intake is text-only and is unaffected by this flag \u2014 no voice worker is registered for it."
30207
31189
  });
30208
31190
  async executeCommand() {
@@ -30240,7 +31222,7 @@ var BootstrapUiCommand = class extends M8tCommand {
30240
31222
  if (fs33.existsSync(path36.join(os18.homedir(), ".m8t", "config.yaml"))) {
30241
31223
  out(colors.error("\u26A0 an existing ~/.m8t/config.yaml will override the onboarding app registration in apps/web \u2014 if Microsoft sign-in fails, move it aside and retry."));
30242
31224
  }
30243
- const credential2 = new DefaultAzureCredential23();
31225
+ const credential2 = new DefaultAzureCredential24();
30244
31226
  const account = await getAzAccount();
30245
31227
  assertNodeVersion();
30246
31228
  out("waiting for Foundry (the installer's foundry-create phase)\u2026");
@@ -30364,10 +31346,10 @@ var BootstrapUiCommand = class extends M8tCommand {
30364
31346
  };
30365
31347
 
30366
31348
  // src/commands/bootstrap/seed-profile.ts
30367
- import { Command as Command58, Option as Option55 } from "clipanion";
31349
+ import { Command as Command59, Option as Option56 } from "clipanion";
30368
31350
  var BootstrapSeedProfileCommand = class extends M8tCommand {
30369
31351
  static paths = [["bootstrap", "seed-profile"]];
30370
- static usage = Command58.Usage({
31352
+ static usage = Command59.Usage({
30371
31353
  description: "Seed your advisors' brains with the founder + company profile from the onboarding intake.",
30372
31354
  details: "Reads the latest onboarding conversation, renders memory/founder.md + memory/company-profile.md (+ their MEMORY.md index lines), and commits them to both <org>/stacey-brain and <org>/azzy-brain via the GitHub App. Idempotent. --watch polls until the founder finishes the intake.",
30373
31355
  examples: [
@@ -30375,11 +31357,11 @@ var BootstrapSeedProfileCommand = class extends M8tCommand {
30375
31357
  ["Wait for the founder to finish", "$0 bootstrap seed-profile --watch"]
30376
31358
  ]
30377
31359
  });
30378
- endpoint = Option55.String("--endpoint", { description: "Override the Foundry endpoint (else read from the install status)." });
30379
- brain = Option55.String("--brain", { description: "Seed only this one brain repo, instead of both <org>/stacey-brain and <org>/azzy-brain." });
30380
- watch = Option55.Boolean("--watch", false, { description: "Poll until the intake completes (or --timeout)." });
30381
- timeout = Option55.String("--timeout", { description: "Watch timeout in minutes (default 20)." });
30382
- githubAppCreds = Option55.String("--github-app-creds");
31360
+ endpoint = Option56.String("--endpoint", { description: "Override the Foundry endpoint (else read from the install status)." });
31361
+ brain = Option56.String("--brain", { description: "Seed only this one brain repo, instead of both <org>/stacey-brain and <org>/azzy-brain." });
31362
+ watch = Option56.Boolean("--watch", false, { description: "Poll until the intake completes (or --timeout)." });
31363
+ timeout = Option56.String("--timeout", { description: "Watch timeout in minutes (default 20)." });
31364
+ githubAppCreds = Option56.String("--github-app-creds");
30383
31365
  async executeCommand() {
30384
31366
  const ctx = await resolveSeedContext({
30385
31367
  endpointOverride: typeof this.endpoint === "string" ? this.endpoint : void 0,
@@ -30442,7 +31424,7 @@ var BootstrapSeedProfileCommand = class extends M8tCommand {
30442
31424
  import * as fs34 from "fs";
30443
31425
  import * as os19 from "os";
30444
31426
  import * as path37 from "path";
30445
- import { Command as Command59, Option as Option56 } from "clipanion";
31427
+ import { Command as Command60, Option as Option57 } from "clipanion";
30446
31428
  init_errors();
30447
31429
 
30448
31430
  // src/lib/telemetry-enroll.ts
@@ -30524,7 +31506,7 @@ async function resolveKeyVaultName(containerAppResourceId) {
30524
31506
  }
30525
31507
  var TelemetryEnrollCommand = class extends M8tCommand {
30526
31508
  static paths = [["telemetry", "enroll"]];
30527
- static usage = Command59.Usage({
31509
+ static usage = Command60.Usage({
30528
31510
  description: "Enroll this installation for operational telemetry (pre-existing installs).",
30529
31511
  details: "Generates an instance id + ingest key from the m8t telemetry ingest and stores the key in your platform Key Vault, the same place the installer writes it on a fresh install. Your installation is identified only by that random instance id \u2014 no contact, company, or subscription details are sent unless you pass them explicitly. Idempotent: refuses if a key already exists.",
30530
31512
  examples: [
@@ -30532,11 +31514,11 @@ var TelemetryEnrollCommand = class extends M8tCommand {
30532
31514
  ["Enroll and share a support contact", "$0 telemetry enroll --contact-email you@example.com --company 'Acme'"]
30533
31515
  ]
30534
31516
  });
30535
- company = Option56.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
30536
- contactEmail = Option56.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
30537
- subscription = Option56.String("--subscription", { description: "Azure subscription id used to find your deployment. Never sent to m8t." });
30538
- resourceGroup = Option56.String("--resource-group", { description: "Resource group to disambiguate discovery, if you have more than one m8t deployment." });
30539
- force = Option56.Boolean("--force", false, { description: "Enroll even when a key is already stored. Use only when m8t support asks you to." });
31517
+ company = Option57.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
31518
+ contactEmail = Option57.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
31519
+ subscription = Option57.String("--subscription", { description: "Azure subscription id used to find your deployment. Never sent to m8t." });
31520
+ resourceGroup = Option57.String("--resource-group", { description: "Resource group to disambiguate discovery, if you have more than one m8t deployment." });
31521
+ force = Option57.Boolean("--force", false, { description: "Enroll even when a key is already stored. Use only when m8t support asks you to." });
30540
31522
  async executeCommand() {
30541
31523
  const account = await getAzAccount();
30542
31524
  const subscriptionId = (typeof this.subscription === "string" ? this.subscription : void 0) ?? account.subscriptionId;
@@ -30596,6 +31578,7 @@ cli.register(VersionCommand);
30596
31578
  cli.register(WhoamiCommand);
30597
31579
  cli.register(StatusCommand);
30598
31580
  cli.register(DoctorCommand);
31581
+ cli.register(PrereqsCommand);
30599
31582
  cli.register(SwitchCommand);
30600
31583
  cli.register(OpenCommand);
30601
31584
  cli.register(ConfigShowCommand);