@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/README.md +1 -1
- package/dist/cli.js +1412 -429
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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 {
|
|
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.
|
|
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
|
|
2058
|
-
if (
|
|
2092
|
+
const result2 = await reader.read();
|
|
2093
|
+
if (result2?.done)
|
|
2059
2094
|
reader.releaseLock();
|
|
2060
|
-
return
|
|
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
|
|
2803
|
-
left.push(
|
|
2804
|
-
right.push(
|
|
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
|
|
4388
|
-
if (
|
|
4389
|
-
this._addMessage(
|
|
4390
|
-
if (singleFunctionToCall &&
|
|
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
|
|
4398
|
-
if (
|
|
4399
|
-
throw
|
|
4432
|
+
for (const result2 of results) {
|
|
4433
|
+
if (result2.status === "rejected")
|
|
4434
|
+
throw result2.reason;
|
|
4400
4435
|
}
|
|
4401
|
-
for (const
|
|
4402
|
-
if (
|
|
4403
|
-
this._addMessage(
|
|
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((
|
|
11251
|
+
const rejected = results.filter((result2) => result2.status === "rejected");
|
|
11217
11252
|
if (rejected.length) {
|
|
11218
|
-
for (const
|
|
11219
|
-
console.error(
|
|
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
|
|
11225
|
-
if (
|
|
11226
|
-
values.push(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
14755
|
+
const result2 = await checkAppHealth({ credential: credential2, kvUri });
|
|
14721
14756
|
if (mode === "json") {
|
|
14722
|
-
this.context.stdout.write(renderJson(
|
|
14723
|
-
return
|
|
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",
|
|
14735
|
-
row("App ID numeric",
|
|
14736
|
-
row("Private key valid",
|
|
14737
|
-
row("App JWT accepted",
|
|
14738
|
-
row("Installations listable",
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
15979
|
-
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 ${
|
|
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
|
-
|
|
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
|
|
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:
|
|
16144
|
-
connectionName:
|
|
16145
|
-
foundryVersion:
|
|
16146
|
-
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 (
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
16213
|
+
result2.push({ name, metadata: md });
|
|
16181
16214
|
}
|
|
16182
16215
|
}
|
|
16183
|
-
return
|
|
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(
|
|
16234
|
-
if (!
|
|
16235
|
-
const 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
|
|
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:
|
|
16653
|
-
connectionDeleted:
|
|
16654
|
-
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 &&
|
|
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
|
|
17067
|
-
const bearerHash = createHash("sha256").update(
|
|
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
|
|
17317
|
-
if (
|
|
17318
|
-
return Promise.reject(new Error(
|
|
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
|
|
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:
|
|
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 (
|
|
17554
|
-
this.context.stdout.write(`${colors.success("\u2713")} ${colors.field(this.worker)} is a discoverable a2a target (agent version ${
|
|
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(
|
|
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
|
|
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, ...
|
|
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(
|
|
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
|
-
|
|
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
|
|
18305
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
18436
|
-
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:
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
18772
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
18842
|
-
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:
|
|
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 ${
|
|
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
|
|
19460
|
-
return
|
|
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
|
|
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 =
|
|
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
|
|
21158
|
-
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
|
|
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(
|
|
21730
|
+
this.context.stdout.write(renderJson(result2) + "\n");
|
|
21676
21731
|
} else {
|
|
21677
|
-
this.context.stdout.write(`${
|
|
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
|
|
21826
|
+
const result2 = { platformVersion: stamp.platformVersion, previous: previousPlatformVersion, storageAccount: accountName };
|
|
21772
21827
|
this.context.stdout.write(
|
|
21773
|
-
mode === "json" ? renderJson(
|
|
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
|
|
21869
|
-
this.context.stdout.write(mode === "json" ? renderJson(
|
|
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
|
|
21930
|
+
const result3 = { outcome: "unreadable" };
|
|
21876
21931
|
this.context.stdout.write(
|
|
21877
|
-
mode === "json" ? renderJson(
|
|
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
|
|
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(
|
|
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
|
|
22549
|
-
return
|
|
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
|
-
|
|
22867
|
-
`
|
|
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({
|
|
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
|
-
|
|
23628
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
24251
|
+
// src/commands/prereqs.ts
|
|
23969
24252
|
import { Command as Command47, Option as Option44 } from "clipanion";
|
|
24253
|
+
init_errors();
|
|
23970
24254
|
|
|
23971
|
-
// src/lib/
|
|
23972
|
-
import
|
|
23973
|
-
|
|
23974
|
-
|
|
23975
|
-
|
|
23976
|
-
|
|
23977
|
-
|
|
23978
|
-
|
|
23979
|
-
|
|
23980
|
-
|
|
23981
|
-
|
|
23982
|
-
|
|
23983
|
-
|
|
23984
|
-
|
|
23985
|
-
|
|
23986
|
-
|
|
23987
|
-
|
|
23988
|
-
|
|
23989
|
-
|
|
23990
|
-
|
|
23991
|
-
|
|
23992
|
-
|
|
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
|
|
24278
|
+
async function checkAppRegCapability(callerObjectId) {
|
|
24279
|
+
let roles;
|
|
24008
24280
|
try {
|
|
24009
|
-
|
|
24010
|
-
|
|
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
|
-
|
|
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
|
|
24016
|
-
await
|
|
24017
|
-
|
|
24018
|
-
|
|
24019
|
-
|
|
24020
|
-
|
|
24021
|
-
|
|
24022
|
-
|
|
24023
|
-
|
|
24024
|
-
|
|
24025
|
-
|
|
24026
|
-
|
|
24027
|
-
|
|
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 =
|
|
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 =
|
|
24113
|
-
subscription =
|
|
24114
|
-
list =
|
|
24115
|
-
as =
|
|
24116
|
-
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
|
|
25129
|
+
let result2;
|
|
24140
25130
|
if (this.subscription) {
|
|
24141
|
-
|
|
25131
|
+
result2 = await discoverySwitch(this.subscription, this.as);
|
|
24142
25132
|
} else if (this.profile) {
|
|
24143
|
-
|
|
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(
|
|
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:
|
|
24158
|
-
{ key: "clientId", value:
|
|
24159
|
-
{ key: "endpoint", value:
|
|
24160
|
-
{ key: "subscription", value:
|
|
24161
|
-
{ key: "saved profile", value:
|
|
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
|
|
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 =
|
|
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 =
|
|
24224
|
-
print =
|
|
24225
|
-
output =
|
|
24226
|
-
resourceGroup =
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
24551
|
-
if (
|
|
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(
|
|
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
|
|
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 (
|
|
26322
|
-
deps.logger.error({ at: "runDream", worker: input.worker, terminal:
|
|
26323
|
-
digest.push({ kind: "failure", detail: `write ${
|
|
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:
|
|
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 =
|
|
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 =
|
|
26502
|
-
dryRun =
|
|
26503
|
-
since =
|
|
26504
|
-
reset =
|
|
26505
|
-
showTranscripts =
|
|
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 =
|
|
26509
|
-
endpoint =
|
|
26510
|
-
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
|
|
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=${
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
27680
|
+
const result2 = await client.queryWorkspace(workspaceId, kql, {
|
|
26691
27681
|
startTime: new Date(from),
|
|
26692
27682
|
endTime: new Date(to)
|
|
26693
27683
|
});
|
|
26694
|
-
if (
|
|
26695
|
-
const table =
|
|
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
|
|
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 =
|
|
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 =
|
|
27106
|
-
location =
|
|
27107
|
-
account =
|
|
27108
|
-
project =
|
|
27109
|
-
model =
|
|
27110
|
-
modelVersion =
|
|
27111
|
-
capacity =
|
|
27112
|
-
subscription =
|
|
27113
|
-
skipQuotaCheck =
|
|
27114
|
-
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
|
|
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:
|
|
27156
|
-
accountName:
|
|
27157
|
-
accountResourceId:
|
|
27158
|
-
projectName:
|
|
27159
|
-
region:
|
|
27160
|
-
model:
|
|
27161
|
-
capacity:
|
|
27162
|
-
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:
|
|
27170
|
-
{ key: "account", value:
|
|
27171
|
-
{ key: "project", value:
|
|
27172
|
-
{ key: "region", value:
|
|
27173
|
-
{ key: "model", value: `${
|
|
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(
|
|
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 ${
|
|
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
|
|
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 =
|
|
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 =
|
|
27197
|
-
consecutive =
|
|
27198
|
-
attempts =
|
|
27199
|
-
interval =
|
|
27200
|
-
subscription =
|
|
27201
|
-
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
|
|
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 =
|
|
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
|
|
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 =
|
|
27359
|
-
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
|
-
|
|
27391
|
-
|
|
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
|
|
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.
|
|
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 =
|
|
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 =
|
|
27803
|
-
resourceGroup =
|
|
27804
|
-
clientId =
|
|
27805
|
-
subscription =
|
|
27806
|
-
installerTag =
|
|
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 =
|
|
27811
|
-
gatewayImageRef =
|
|
27812
|
-
githubAppCreds =
|
|
27813
|
-
contactEmail =
|
|
27814
|
-
company =
|
|
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 =
|
|
27820
|
-
org =
|
|
27821
|
-
noBrains =
|
|
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
|
|
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 =
|
|
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 =
|
|
28009
|
-
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
|
|
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 =
|
|
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 =
|
|
28194
|
-
sweepOrphans =
|
|
28195
|
-
yes =
|
|
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
|
|
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
|
|
28697
|
-
if (
|
|
28698
|
-
else if (machineText.includes("m8t_onboarding")) rejection ??=
|
|
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,
|
|
28926
|
-
const chosenIdx =
|
|
28927
|
-
const better =
|
|
28928
|
-
const runningModel =
|
|
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((
|
|
30115
|
+
settled.forEach((result2, index) => {
|
|
29164
30116
|
const brainRepo = brainRepos[index];
|
|
29165
|
-
if (
|
|
30117
|
+
if (result2.status === "fulfilled") {
|
|
29166
30118
|
verified.push(brainRepo);
|
|
29167
30119
|
} else {
|
|
29168
30120
|
failed.push(brainRepo);
|
|
29169
|
-
causes.push(
|
|
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 =
|
|
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 =
|
|
29263
|
-
subscription =
|
|
29264
|
-
resourceGroup =
|
|
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
|
|
29368
|
-
import { DefaultAzureCredential as
|
|
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
|
|
29821
|
-
if (
|
|
29822
|
-
|
|
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
|
|
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:
|
|
29961
|
-
note: renderChosenModelNote(decideNote(WHITELIST,
|
|
29962
|
-
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 =
|
|
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 =
|
|
30197
|
-
port =
|
|
30198
|
-
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 =
|
|
30202
|
-
skipInstall =
|
|
30203
|
-
stop =
|
|
30204
|
-
foreground =
|
|
30205
|
-
voice =
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
30379
|
-
brain =
|
|
30380
|
-
watch =
|
|
30381
|
-
timeout =
|
|
30382
|
-
githubAppCreds =
|
|
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
|
|
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 =
|
|
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 =
|
|
30536
|
-
contactEmail =
|
|
30537
|
-
subscription =
|
|
30538
|
-
resourceGroup =
|
|
30539
|
-
force =
|
|
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);
|