@m8t-stack/cli 0.2.88 → 0.2.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +1357 -675
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1461,8 +1461,33 @@ var init_enable_hosted_brain = __esm({
1461
1461
  // src/cli.ts
1462
1462
  import { Builtins, Cli } from "clipanion";
1463
1463
 
1464
+ // src/lib/dns-shim.ts
1465
+ import dns from "dns";
1466
+ var installed = false;
1467
+ function installFoundryDnsShim() {
1468
+ if (installed) return;
1469
+ installed = true;
1470
+ const origLookup = dns.lookup.bind(dns);
1471
+ dns.lookup = (hostname, options, cb) => {
1472
+ const opts = typeof options === "function" ? {} : options;
1473
+ const callback = typeof options === "function" ? options : cb;
1474
+ if (typeof hostname === "string" && hostname.endsWith("services.ai.azure.com")) {
1475
+ dns.resolve4(hostname, (err, addrs) => {
1476
+ if (err || !addrs?.length) {
1477
+ origLookup(hostname, options, cb);
1478
+ return;
1479
+ }
1480
+ if (opts?.all) callback(null, addrs.map((a) => ({ address: a, family: 4 })));
1481
+ else callback(null, addrs[0], 4);
1482
+ });
1483
+ return;
1484
+ }
1485
+ origLookup(hostname, options, cb);
1486
+ };
1487
+ }
1488
+
1464
1489
  // src/lib/package-version.ts
1465
- var CLI_VERSION = "0.2.88";
1490
+ var CLI_VERSION = "0.2.90";
1466
1491
 
1467
1492
  // src/lib/render-error.ts
1468
1493
  init_errors();
@@ -19667,19 +19692,20 @@ init_rbac();
19667
19692
  // src/lib/acs-provision.ts
19668
19693
  var MANAGED_DOMAIN = "AzureManagedDomain";
19669
19694
  async function provisionAcsEmail(args) {
19670
- const { client, subscriptionId, resourceGroup, name, dataLocation, onProgress } = args;
19671
- const acsName = `acs-${name}`;
19672
- const emailName = `${name}-email`;
19695
+ const { client, subscriptionId, resourceGroup, name, dataLocation, tags, onProgress } = args;
19696
+ const acsName = args.acsName ?? `acs-${name}`;
19697
+ const emailName = args.emailName ?? `${name}-email`;
19673
19698
  onProgress?.(`creating ACS resource ${acsName}\u2026`);
19674
19699
  const acs = await client.communicationServices.beginCreateOrUpdateAndWait(
19675
19700
  resourceGroup,
19676
19701
  acsName,
19677
- { location: "global", dataLocation }
19702
+ { location: "global", dataLocation, ...tags ? { tags } : {} }
19678
19703
  );
19679
19704
  onProgress?.(`creating Email Communication Service ${emailName}\u2026`);
19680
19705
  await client.emailServices.beginCreateOrUpdateAndWait(resourceGroup, emailName, {
19681
19706
  location: "global",
19682
- dataLocation
19707
+ dataLocation,
19708
+ ...tags ? { tags } : {}
19683
19709
  });
19684
19710
  onProgress?.("provisioning the Azure-managed domain\u2026");
19685
19711
  const domain = await client.domains.beginCreateOrUpdateAndWait(
@@ -19693,7 +19719,8 @@ async function provisionAcsEmail(args) {
19693
19719
  await client.communicationServices.beginCreateOrUpdateAndWait(resourceGroup, acsName, {
19694
19720
  location: "global",
19695
19721
  dataLocation,
19696
- linkedDomains: [domainResourceId]
19722
+ linkedDomains: [domainResourceId],
19723
+ ...tags ? { tags } : {}
19697
19724
  });
19698
19725
  const hostName = acs.hostName ?? `${acsName}.communication.azure.com`;
19699
19726
  const fromSenderDomain = domain.fromSenderDomain;
@@ -19706,6 +19733,515 @@ async function provisionAcsEmail(args) {
19706
19733
  };
19707
19734
  }
19708
19735
 
19736
+ // src/lib/acs-resolve.ts
19737
+ init_errors();
19738
+ var ACS_PLATFORM_TAG = "m8t:email";
19739
+ var ACS_PLATFORM_TAG_VALUE = "platform";
19740
+ var VERIFIED = "Verified";
19741
+ function emailFeatureFrom(acs, enabled) {
19742
+ return { enabled, acsEndpoint: acs.endpoint, acsSender: acs.sender };
19743
+ }
19744
+ function platformSuffixFromKvUri(kvUri) {
19745
+ const host = /^https?:\/\/([^./]+)\./i.exec(kvUri.trim())?.[1];
19746
+ if (!host) return null;
19747
+ return /^m8t-kv-(.+)$/i.exec(host)?.[1] ?? host;
19748
+ }
19749
+ function resourceGroupFromArmId(armId) {
19750
+ return /\/resourceGroups\/([^/]+)/i.exec(armId)?.[1] ?? null;
19751
+ }
19752
+ function classifyGrantScope(grantScope, expectedSubscriptionId) {
19753
+ const unusable = (reason) => ({ kind: "unusable", reason });
19754
+ const parts = grantScope.trim().replace(/^\/+/, "").replace(/\/+$/, "").split("/");
19755
+ if (parts.length < 2 || parts[0]?.toLowerCase() !== "subscriptions") {
19756
+ return unusable(`'${grantScope}' is not a recognisable Azure scope`);
19757
+ }
19758
+ const subscriptionId = parts[1] ?? "";
19759
+ if (subscriptionId === "") return unusable(`'${grantScope}' names no subscription`);
19760
+ if (subscriptionId.toLowerCase() !== expectedSubscriptionId.toLowerCase()) {
19761
+ return unusable(
19762
+ `the Contributor scope is in subscription ${subscriptionId}, but this deploy targets ${expectedSubscriptionId} \u2014 an ACS created here would not be covered by that grant`
19763
+ );
19764
+ }
19765
+ if (parts.length === 2) return { kind: "subscription", subscriptionId };
19766
+ if (parts.length < 4 || parts[2]?.toLowerCase() !== "resourcegroups") {
19767
+ return unusable(`'${grantScope}' is neither a subscription nor a resource-group scope`);
19768
+ }
19769
+ const resourceGroup = parts[3] ?? "";
19770
+ if (resourceGroup === "") return unusable(`'${grantScope}' names no resource group`);
19771
+ if (parts.length > 4) {
19772
+ return unusable(
19773
+ `the Contributor scope is a single resource (${grantScope}), so no resource group is authorised for an ACS data-plane send`
19774
+ );
19775
+ }
19776
+ return { kind: "resource-group", subscriptionId, resourceGroup };
19777
+ }
19778
+ function parseDomainId(id) {
19779
+ const m = /\/emailServices\/([^/]+)\/domains\/([^/]+)/i.exec(id);
19780
+ if (!m?.[1] || !m[2]) return null;
19781
+ return { emailServiceName: m[1], domainName: m[2] };
19782
+ }
19783
+ async function resolveAcs(args) {
19784
+ const { client, stamp, subscriptionId, resourceGroup, suffix, dataLocation, onProgress } = args;
19785
+ const recorded = stamp?.features?.email;
19786
+ if (recorded?.acsEndpoint && recorded.acsSender) {
19787
+ onProgress?.("using the ACS sender recorded in the platform stamp.");
19788
+ return { source: "stamp", acs: { endpoint: recorded.acsEndpoint, sender: recorded.acsSender } };
19789
+ }
19790
+ onProgress?.(`looking for an existing email-capable ACS in ${resourceGroup}\u2026`);
19791
+ const candidates = [];
19792
+ for await (const r of client.communicationServices.listByResourceGroup(resourceGroup)) {
19793
+ const linked = r.linkedDomains;
19794
+ if (!linked || linked.length === 0) continue;
19795
+ if (!r.name || !r.hostName) continue;
19796
+ candidates.push({
19797
+ name: r.name,
19798
+ hostName: r.hostName,
19799
+ linkedDomains: linked,
19800
+ tagged: r.tags?.[ACS_PLATFORM_TAG] === ACS_PLATFORM_TAG_VALUE
19801
+ });
19802
+ }
19803
+ candidates.sort((a, b) => a.tagged === b.tagged ? a.name.localeCompare(b.name) : a.tagged ? -1 : 1);
19804
+ for (const c of candidates) {
19805
+ const sender = await verifiedSenderFor(client, resourceGroup, c);
19806
+ if (sender === null) continue;
19807
+ onProgress?.(`adopting the existing ACS ${c.name} (verified managed domain).`);
19808
+ return { source: "discovered", acsName: c.name, acs: { endpoint: `https://${c.hostName}/`, sender } };
19809
+ }
19810
+ if (suffix === null || suffix === "") {
19811
+ throw new LocalCliError({
19812
+ code: "NO_PLATFORM_SUFFIX",
19813
+ message: "No email-capable ACS exists here and one cannot be named: this install's resource suffix could not be worked out.",
19814
+ hint: "Pass --kv-uri <install vault uri> (or set AZURE_KEYVAULT_URI) so the ACS can be named consistently with the rest of the platform."
19815
+ });
19816
+ }
19817
+ const acsName = `m8t-acs-${suffix}`;
19818
+ const emailName = `m8t-email-${suffix}`;
19819
+ onProgress?.(`no email-capable ACS found \u2014 provisioning ${acsName}\u2026`);
19820
+ const provisioned = await provisionAcsEmail({
19821
+ client,
19822
+ subscriptionId,
19823
+ resourceGroup,
19824
+ name: `m8t-${suffix}`,
19825
+ acsName,
19826
+ emailName,
19827
+ dataLocation,
19828
+ tags: { [ACS_PLATFORM_TAG]: ACS_PLATFORM_TAG_VALUE },
19829
+ onProgress
19830
+ });
19831
+ return { source: "provisioned", acsName, acs: { endpoint: provisioned.endpoint, sender: provisioned.sender } };
19832
+ }
19833
+ async function verifiedSenderFor(client, resourceGroup, c) {
19834
+ for (const id of c.linkedDomains) {
19835
+ const parsed = parseDomainId(id);
19836
+ if (!parsed) continue;
19837
+ try {
19838
+ const d = await client.domains.get(resourceGroup, parsed.emailServiceName, parsed.domainName);
19839
+ if (d.verificationStates?.domain?.status !== VERIFIED) continue;
19840
+ if (!d.fromSenderDomain) continue;
19841
+ return `DoNotReply@${d.fromSenderDomain}`;
19842
+ } catch {
19843
+ continue;
19844
+ }
19845
+ }
19846
+ return null;
19847
+ }
19848
+
19849
+ // src/lib/platform-stamp.ts
19850
+ import { TableClient as TableClient2 } from "@azure/data-tables";
19851
+
19852
+ // ../../packages/platform-release/dist/esm/manifest.js
19853
+ var SEVERITIES = ["critical", "recommended", "optional"];
19854
+ var IMAGE_KEYS = ["gateway", "codingAgent", "azureExecutor", "installer"];
19855
+ var COMPANION_TARGETS = [
19856
+ "darwin-arm64",
19857
+ "darwin-x64",
19858
+ "win32-arm64",
19859
+ "win32-x64"
19860
+ ];
19861
+ function companionTargetFor(platform, architecture) {
19862
+ const target = `${platform}-${architecture}`;
19863
+ return COMPANION_TARGETS.includes(target) ? target : null;
19864
+ }
19865
+ function isObj2(v) {
19866
+ return typeof v === "object" && v !== null && !Array.isArray(v);
19867
+ }
19868
+ function validateManifest(m) {
19869
+ const errors = [];
19870
+ if (!isObj2(m))
19871
+ return ["manifest is not an object"];
19872
+ if (m.schemaVersion !== 1)
19873
+ errors.push("schemaVersion must be the integer 1");
19874
+ const p = m.platform;
19875
+ if (!isObj2(p)) {
19876
+ errors.push("platform is required");
19877
+ } else {
19878
+ for (const f of ["version", "tag", "releasedAt", "commit", "notes", "notesUrl"]) {
19879
+ if (typeof p[f] !== "string" || p[f].length === 0)
19880
+ errors.push(`platform.${f} is required`);
19881
+ }
19882
+ if (!SEVERITIES.includes(p.severity))
19883
+ errors.push(`platform.severity must be one of ${SEVERITIES.join("|")}`);
19884
+ if (!(typeof p.previousVersion === "string" || p.previousVersion === null)) {
19885
+ errors.push("platform.previousVersion must be a string or null");
19886
+ }
19887
+ }
19888
+ const c = m.components;
19889
+ if (!isObj2(c)) {
19890
+ errors.push("components is required");
19891
+ return errors;
19892
+ }
19893
+ for (const key2 of IMAGE_KEYS) {
19894
+ const img = c[key2];
19895
+ if (!isObj2(img)) {
19896
+ errors.push(`components.${key2} is required`);
19897
+ continue;
19898
+ }
19899
+ if (img.kind !== "image")
19900
+ errors.push(`components.${key2}.kind must be "image"`);
19901
+ for (const f of ["ref", "tag", "version"]) {
19902
+ if (typeof img[f] !== "string" || img[f].length === 0)
19903
+ errors.push(`components.${key2}.${f} is required`);
19904
+ }
19905
+ if (typeof img.digest !== "string" || !/^sha256:[0-9a-f]+$/.test(img.digest)) {
19906
+ errors.push(`components.${key2}.digest must be a sha256:\u2026 digest`);
19907
+ }
19908
+ }
19909
+ const cli2 = c.cli;
19910
+ if (!isObj2(cli2) || cli2.kind !== "npm") {
19911
+ errors.push("components.cli (kind: npm) is required");
19912
+ } else {
19913
+ for (const f of ["package", "version", "min", "recommended"]) {
19914
+ if (typeof cli2[f] !== "string" || cli2[f].length === 0)
19915
+ errors.push(`components.cli.${f} is required`);
19916
+ }
19917
+ }
19918
+ const personas = c.personas;
19919
+ if (!isObj2(personas) || !isObj2(personas.items)) {
19920
+ errors.push("components.personas.items is required");
19921
+ } else {
19922
+ for (const [name, item] of Object.entries(personas.items)) {
19923
+ if (!isObj2(item) || typeof item.path !== "string" || item.path.length === 0) {
19924
+ errors.push(`components.personas.items.${name}.path is required`);
19925
+ }
19926
+ if (!isObj2(item) || typeof item.treeSha !== "string" || item.treeSha.length < 4) {
19927
+ errors.push(`components.personas.items.${name}.treeSha is required`);
19928
+ }
19929
+ }
19930
+ }
19931
+ const seeds = c.brainSeeds;
19932
+ if (!isObj2(seeds) || !isObj2(seeds.items)) {
19933
+ errors.push("components.brainSeeds.items is required");
19934
+ } else {
19935
+ for (const [name, item] of Object.entries(seeds.items)) {
19936
+ if (!isObj2(item) || typeof item.path !== "string" || item.path.length === 0) {
19937
+ errors.push(`components.brainSeeds.items.${name}.path is required`);
19938
+ }
19939
+ if (!isObj2(item) || !isObj2(item.subtrees)) {
19940
+ errors.push(`components.brainSeeds.items.${name}.subtrees is required`);
19941
+ }
19942
+ }
19943
+ }
19944
+ const infra = c.infra;
19945
+ if (infra !== void 0) {
19946
+ if (!isObj2(infra) || infra.kind !== "infra") {
19947
+ errors.push('components.infra.kind must be "infra"');
19948
+ }
19949
+ if (!isObj2(infra) || typeof infra.treeSha !== "string" || infra.treeSha.length < 4) {
19950
+ errors.push("components.infra.treeSha is required");
19951
+ }
19952
+ }
19953
+ const companion = c.companion;
19954
+ if (companion !== void 0) {
19955
+ if (!isObj2(companion) || companion.kind !== "desktop") {
19956
+ errors.push('components.companion.kind must be "desktop"');
19957
+ } else {
19958
+ for (const f of ["version", "tag"]) {
19959
+ if (typeof companion[f] !== "string" || companion[f].length === 0) {
19960
+ errors.push(`components.companion.${f} is required`);
19961
+ }
19962
+ }
19963
+ if (!isObj2(companion.targets) || Object.keys(companion.targets).length === 0) {
19964
+ errors.push("components.companion.targets must name at least one target");
19965
+ } else {
19966
+ for (const [name, target] of Object.entries(companion.targets)) {
19967
+ if (!COMPANION_TARGETS.includes(name)) {
19968
+ errors.push(`components.companion.targets.${name} is not a known target`);
19969
+ continue;
19970
+ }
19971
+ if (!isObj2(target) || typeof target.asset !== "string" || target.asset.length === 0) {
19972
+ errors.push(`components.companion.targets.${name}.asset is required`);
19973
+ }
19974
+ if (!isObj2(target) || typeof target.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(target.sha256)) {
19975
+ errors.push(`components.companion.targets.${name}.sha256 must be a sha256 hex digest`);
19976
+ }
19977
+ }
19978
+ }
19979
+ }
19980
+ }
19981
+ return errors;
19982
+ }
19983
+
19984
+ // ../../packages/platform-release/dist/esm/stamp.js
19985
+ var TABLE = "Metadata";
19986
+ var PK = "system";
19987
+ var RK = "platform";
19988
+ function isEmailFeatureUsable(f) {
19989
+ return f?.enabled === true && typeof f.acsEndpoint === "string" && f.acsEndpoint !== "" && typeof f.acsSender === "string" && f.acsSender !== "";
19990
+ }
19991
+ function stampToEntity(s) {
19992
+ return {
19993
+ partitionKey: PK,
19994
+ rowKey: RK,
19995
+ platformVersion: s.platformVersion,
19996
+ updatedAt: s.updatedAt,
19997
+ lastResult: s.lastResult,
19998
+ value: JSON.stringify(s)
19999
+ };
20000
+ }
20001
+ function entityToStamp(e) {
20002
+ const raw = e.value;
20003
+ if (typeof raw !== "string")
20004
+ return null;
20005
+ try {
20006
+ return JSON.parse(raw);
20007
+ } catch {
20008
+ return null;
20009
+ }
20010
+ }
20011
+
20012
+ // ../../packages/platform-release/dist/esm/channel-url.js
20013
+ var RELEASES_REPO = "m8t-labs/m8t-releases";
20014
+ var RELEASES_BASE = `https://github.com/${RELEASES_REPO}/releases`;
20015
+ var CHANNEL_LATEST_URL = `${RELEASES_BASE}/latest/download/manifest.json`;
20016
+ var CANARY_POINTER_TAG = "canary";
20017
+ var CHANNEL_CANARY_URL = `${RELEASES_BASE}/download/${CANARY_POINTER_TAG}/manifest.json`;
20018
+ function platformTag(version) {
20019
+ const bare = version.trim().replace(/^platform-/, "").replace(/^v/, "");
20020
+ return `platform-v${bare}`;
20021
+ }
20022
+ function channelUrlForVersion(version) {
20023
+ return `${RELEASES_BASE}/download/${platformTag(version)}/manifest.json`;
20024
+ }
20025
+ function releaseAssetUrl(tag, asset) {
20026
+ return `${RELEASES_BASE}/download/${tag}/${asset}`;
20027
+ }
20028
+
20029
+ // ../../packages/platform-release/dist/esm/canary-version.js
20030
+ var CANARY_IDENTIFIER = "canary";
20031
+ var STABLE_CORE = String.raw`\d+\.\d+\.\d+`;
20032
+ var CANARY_SUFFIX = String.raw`-${CANARY_IDENTIFIER}\.(?:0|[1-9]\d*)`;
20033
+ var CANARY_VERSION_PATTERN = `^${STABLE_CORE}${CANARY_SUFFIX}$`;
20034
+ var PLATFORM_VERSION_PATTERN = `^${STABLE_CORE}(?:${CANARY_SUFFIX})?$`;
20035
+ var PLATFORM_TAG_PATTERN = `^platform-v${STABLE_CORE}(?:${CANARY_SUFFIX})?$`;
20036
+ var CANARY_VERSION_RE = new RegExp(CANARY_VERSION_PATTERN);
20037
+ var STABLE_CORE_RE = new RegExp(`^${STABLE_CORE}$`);
20038
+
20039
+ // ../../packages/platform-release/dist/esm/apply-request.js
20040
+ var APPLY_REQUEST_PK = "system";
20041
+ var APPLY_REQUEST_RK = "apply-request";
20042
+ var IN_FLIGHT = ["claimed", "applying"];
20043
+ var LEASE_MS = 30 * 60 * 1e3;
20044
+ var MAX_LEASE_TAKEOVERS = 2;
20045
+ var MAX_HEALTHY_IN_FLIGHT_MS = LEASE_MS * (MAX_LEASE_TAKEOVERS + 1);
20046
+ function isInFlight(status) {
20047
+ return IN_FLIGHT.includes(status);
20048
+ }
20049
+ var BLOCKS_NEW_INTENT = ["pending", "claimed", "applying", "awaiting-engine-update"];
20050
+ function blocksNewIntent(status) {
20051
+ return BLOCKS_NEW_INTENT.includes(status);
20052
+ }
20053
+ function newIntent(target, provenance, nowIso) {
20054
+ return {
20055
+ schemaVersion: 1,
20056
+ target,
20057
+ provenance,
20058
+ status: "pending",
20059
+ phase: null,
20060
+ claimedBy: null,
20061
+ leaseUntil: null,
20062
+ attempt: 0,
20063
+ breaker: "none",
20064
+ result: null,
20065
+ createdAt: nowIso,
20066
+ updatedAt: nowIso
20067
+ };
20068
+ }
20069
+ function applyRequestToEntity(r) {
20070
+ return { partitionKey: APPLY_REQUEST_PK, rowKey: APPLY_REQUEST_RK, value: JSON.stringify(r), updatedAt: r.updatedAt };
20071
+ }
20072
+ function entityToApplyRequest(e) {
20073
+ return JSON.parse(e.value);
20074
+ }
20075
+
20076
+ // ../../packages/platform-release/dist/esm/update-policy.js
20077
+ var DEFAULT_POLICY_MODE = "auto-critical";
20078
+ var UPDATE_POLICY_PK = "system";
20079
+ var UPDATE_POLICY_RK = "update-policy";
20080
+ var MODES = ["notify-only", "auto-critical", "auto-all"];
20081
+ function isPolicyMode(x) {
20082
+ return typeof x === "string" && MODES.includes(x);
20083
+ }
20084
+ function updatePolicyToEntity(p) {
20085
+ return { partitionKey: UPDATE_POLICY_PK, rowKey: UPDATE_POLICY_RK, value: JSON.stringify(p) };
20086
+ }
20087
+ function entityToUpdatePolicy(e) {
20088
+ return JSON.parse(e.value);
20089
+ }
20090
+
20091
+ // ../../packages/platform-release/dist/esm/infra-params.js
20092
+ var INFRA_PARAMS_PK = "system";
20093
+ var INFRA_PARAMS_RK = "infra-params";
20094
+ function infraParamsToEntity(p) {
20095
+ return { partitionKey: INFRA_PARAMS_PK, rowKey: INFRA_PARAMS_RK, value: JSON.stringify(p) };
20096
+ }
20097
+ function entityToInfraParams(e) {
20098
+ return JSON.parse(e.value);
20099
+ }
20100
+
20101
+ // src/lib/platform-storage-discovery.ts
20102
+ init_http();
20103
+ init_errors();
20104
+ var ARM3 = "https://management.azure.com";
20105
+ var ARM_SCOPE7 = "https://management.azure.com/.default";
20106
+ var STORAGE_API = "2023-05-01";
20107
+ async function discoverStampStorage(opts) {
20108
+ const list = await authedJson({
20109
+ credential: opts.credential,
20110
+ scope: ARM_SCOPE7,
20111
+ method: "GET",
20112
+ url: `${ARM3}/subscriptions/${opts.subscriptionId}/resourceGroups/${opts.resourceGroup}/providers/Microsoft.Storage/storageAccounts?api-version=${STORAGE_API}`
20113
+ }) ?? {};
20114
+ const accounts = (list.value ?? []).filter((a) => a.properties?.primaryEndpoints?.table);
20115
+ const chosen = accounts.find((a) => a.tags?.m8t === "storage") ?? accounts.find((a) => a.tags?.["m8t:role"] === "gateway" || a.tags?.["m8t:role"] === "ledger") ?? (accounts.length === 1 ? accounts[0] : void 0);
20116
+ const tableEndpoint = chosen?.properties?.primaryEndpoints?.table?.replace(/\/$/, "");
20117
+ if (!tableEndpoint || !chosen?.id || !chosen.name) {
20118
+ throw new LocalCliError({
20119
+ code: "PLATFORM_STAMP_STORAGE_NOT_FOUND",
20120
+ message: `No table-capable storage account found in resource group ${opts.resourceGroup}.`,
20121
+ hint: `Found: ${accounts.map((a) => a.name).join(", ") || "(none)"}.`
20122
+ });
20123
+ }
20124
+ return { tableEndpoint, accountResourceId: chosen.id, accountName: chosen.name };
20125
+ }
20126
+
20127
+ // src/lib/metadata-table-write.ts
20128
+ import { TableClient, AzureNamedKeyCredential } from "@azure/data-tables";
20129
+ init_errors();
20130
+ init_rbac();
20131
+ function is403(e) {
20132
+ return e.statusCode === 403;
20133
+ }
20134
+ function is404(e) {
20135
+ return e.statusCode === 404;
20136
+ }
20137
+ function sleep2(ms) {
20138
+ return new Promise((r) => setTimeout(r, ms));
20139
+ }
20140
+ async function makeSharedKeyClient(opts) {
20141
+ const rg = /\/resourceGroups\/([^/]+)\//i.exec(opts.accountResourceId)?.[1];
20142
+ if (!rg) return null;
20143
+ const key2 = (await runAz(["storage", "account", "keys", "list", "--account-name", opts.accountName, "-g", rg, "--query", "[0].value", "-o", "tsv"])).trim();
20144
+ if (!key2) return null;
20145
+ return new TableClient(opts.tableEndpoint, TABLE, new AzureNamedKeyCredential(opts.accountName, key2));
20146
+ }
20147
+ async function upsertMetadataEntity(opts) {
20148
+ const discover = opts.discoverImpl ?? discoverStampStorage;
20149
+ const makeAad = opts.aadClientImpl ?? ((endpoint, credential2) => new TableClient(endpoint, TABLE, credential2));
20150
+ const makeShared = opts.sharedClientImpl ?? makeSharedKeyClient;
20151
+ const nap = opts.sleepImpl ?? sleep2;
20152
+ const { tableEndpoint, accountResourceId, accountName } = await discover({
20153
+ credential: opts.credential,
20154
+ subscriptionId: opts.subscriptionId,
20155
+ resourceGroup: opts.resourceGroup
20156
+ });
20157
+ const aad = makeAad(tableEndpoint, opts.credential);
20158
+ await aad.createTable().catch(() => void 0);
20159
+ try {
20160
+ await aad.upsertEntity(opts.entity, "Replace");
20161
+ return;
20162
+ } catch (e) {
20163
+ if (!is403(e)) throw e;
20164
+ }
20165
+ opts.onProgress?.(`writing the ${opts.label} with the account key \u2014 your identity lacks the Storage Table data role.`);
20166
+ const shared = await makeShared({ tableEndpoint, accountResourceId, accountName });
20167
+ if (shared) {
20168
+ await shared.createTable().catch(() => void 0);
20169
+ await shared.upsertEntity(opts.entity, "Replace");
20170
+ return;
20171
+ }
20172
+ opts.onProgress?.("shared-key access is disabled \u2014 granting Storage Table Data Contributor to your identity (one-time)\u2026");
20173
+ try {
20174
+ const oid = await getCallerObjectId();
20175
+ await grantStorageTableDataContributor({
20176
+ credential: opts.credential,
20177
+ subscriptionId: opts.subscriptionId,
20178
+ scope: accountResourceId,
20179
+ principalId: oid
20180
+ });
20181
+ } catch (e) {
20182
+ throw new LocalCliError({
20183
+ code: opts.errorCode,
20184
+ message: `Cannot write the ${opts.label}: AAD lacks the Storage Table data role for ${accountName}, shared-key access is disabled, and self-granting the role failed.`,
20185
+ hint: "Grant yourself 'Storage Table Data Contributor' on the storage account and retry.",
20186
+ cause: e
20187
+ });
20188
+ }
20189
+ for (let i = 0; i < 6; i++) {
20190
+ await nap(1e4);
20191
+ try {
20192
+ await aad.upsertEntity(opts.entity, "Replace");
20193
+ return;
20194
+ } catch (e) {
20195
+ if (!is403(e)) throw e;
20196
+ }
20197
+ }
20198
+ throw new LocalCliError({
20199
+ code: opts.errorCode,
20200
+ message: `Cannot write the ${opts.label}: granted 'Storage Table Data Contributor' on ${accountName}, but the data-plane role has not propagated yet.`,
20201
+ hint: opts.retryHint
20202
+ });
20203
+ }
20204
+
20205
+ // src/lib/platform-stamp.ts
20206
+ async function writeStamp(opts) {
20207
+ const entity = stampToEntity(opts.stamp);
20208
+ await upsertMetadataEntity({
20209
+ credential: opts.credential,
20210
+ subscriptionId: opts.subscriptionId,
20211
+ resourceGroup: opts.resourceGroup,
20212
+ entity,
20213
+ label: "platform stamp",
20214
+ errorCode: "PLATFORM_STAMP_NO_ACCESS",
20215
+ retryHint: "The role assignment persists \u2014 retry 'm8t platform update' in a few minutes.",
20216
+ ...opts.onProgress ? { onProgress: opts.onProgress } : {}
20217
+ });
20218
+ }
20219
+ async function readStampOutcome(opts) {
20220
+ const { tableEndpoint, accountResourceId, accountName } = await discoverStampStorage(opts);
20221
+ const aad = new TableClient2(tableEndpoint, TABLE, opts.credential);
20222
+ try {
20223
+ const row = await aad.getEntity(PK, RK);
20224
+ const stamp = entityToStamp(row);
20225
+ return stamp ? { source: "explicit", stamp } : { source: "unreadable" };
20226
+ } catch (e) {
20227
+ if (is404(e)) return { source: "absent" };
20228
+ if (!is403(e)) return { source: "unreadable" };
20229
+ }
20230
+ try {
20231
+ const shared = await makeSharedKeyClient({ tableEndpoint, accountResourceId, accountName });
20232
+ if (!shared) return { source: "unreadable" };
20233
+ const row = await shared.getEntity(PK, RK);
20234
+ const stamp = entityToStamp(row);
20235
+ return stamp ? { source: "explicit", stamp } : { source: "unreadable" };
20236
+ } catch {
20237
+ return { source: "unreadable" };
20238
+ }
20239
+ }
20240
+ async function readStamp(opts) {
20241
+ const result2 = await readStampOutcome(opts);
20242
+ return result2.source === "explicit" ? result2.stamp : null;
20243
+ }
20244
+
19709
20245
  // src/commands/azure-exec/deploy.ts
19710
20246
  import { CommunicationServiceManagementClient } from "@azure/arm-communication";
19711
20247
  var SIZE_PRESETS2 = {
@@ -19746,7 +20282,14 @@ var AzureExecDeployCommand = class extends M8tCommand {
19746
20282
  output = Option31.String("--output");
19747
20283
  skipQuotaCheck = Option31.Boolean("--skip-quota-check", false);
19748
20284
  grantAccessAdmin = Option31.Boolean("--grant-access-admin", false);
19749
- enableEmail = Option31.Boolean("--enable-email", false);
20285
+ /** Deprecated no-op: outbound email is provisioned by default now. Kept for one release
20286
+ * so runbooks and scripts that still pass it do not hard-fail. */
20287
+ enableEmail = Option31.Boolean("--enable-email", false, {
20288
+ description: "Deprecated no-op \u2014 outbound email is provisioned by default. Use --no-email to opt out."
20289
+ });
20290
+ noEmail = Option31.Boolean("--no-email", false, {
20291
+ description: "Deploy without outbound email. The executor renders advisor handoffs but cannot send them."
20292
+ });
19750
20293
  async executeCommand() {
19751
20294
  if (!NAME_RE2.test(this.name)) {
19752
20295
  throw new LocalCliError({
@@ -19796,12 +20339,8 @@ var AzureExecDeployCommand = class extends M8tCommand {
19796
20339
  });
19797
20340
  }
19798
20341
  }
19799
- if (this.enableEmail === true && (typeof this.resourceGroup !== "string" || this.resourceGroup.length === 0)) {
19800
- throw new LocalCliError({
19801
- code: "USAGE",
19802
- message: "--enable-email requires --resource-group (the ACS resource lands in the Contributor-granted RG so the executor's identity can send).",
19803
- hint: "Pass --resource-group <rg> (the same scope the executor is granted Contributor on)."
19804
- });
20342
+ if (this.enableEmail === true) {
20343
+ warnings.add("--enable-email is a no-op now: outbound email is provisioned by default. Pass --no-email to opt out.");
19805
20344
  }
19806
20345
  const imageInput = this.image ?? DEFAULT_IMAGE2;
19807
20346
  const repoRef = imageInput.includes("/") ? imageInput : `${DEFAULT_REGISTRY2}/${imageInput}`;
@@ -19843,20 +20382,62 @@ var AzureExecDeployCommand = class extends M8tCommand {
19843
20382
  privateKeyPem,
19844
20383
  write: (s) => this.context.stdout.write(s)
19845
20384
  });
20385
+ const wantEmail = this.noEmail !== true;
19846
20386
  let acsEnv = {};
19847
- if (this.enableEmail === true && typeof this.resourceGroup === "string") {
19848
- const resourceGroup = this.resourceGroup;
19849
- onProgress?.("provisioning ACS Email (managed domain)\u2026");
19850
- const acsClient = new CommunicationServiceManagementClient(credential2, subscriptionId);
19851
- const acs = await provisionAcsEmail({
19852
- client: acsClient,
20387
+ const stampRg = await this.resolvePlatformResourceGroup({ subscriptionId, interactive, warnings });
20388
+ const scopeClass = classifyGrantScope(grantScope, subscriptionId);
20389
+ if (wantEmail) {
20390
+ const suffix = platformSuffixFromKvUri(kvUri);
20391
+ const acsRg = scopeClass.kind === "resource-group" ? scopeClass.resourceGroup : scopeClass.kind === "subscription" ? stampRg : null;
20392
+ if (scopeClass.kind === "unusable") {
20393
+ warnings.add(`Outbound email was NOT provisioned: ${scopeClass.reason}. Re-run with --resource-group <rg> to place it explicitly.`);
20394
+ } else if (acsRg === null || suffix === null) {
20395
+ warnings.add(
20396
+ "Outbound email was NOT provisioned: could not work out which resource group it belongs in. Re-run with --resource-group <rg>, or turn it on later with `m8t platform email on`."
20397
+ );
20398
+ } else {
20399
+ try {
20400
+ onProgress?.("resolving the ACS email sender\u2026");
20401
+ const acsClient = new CommunicationServiceManagementClient(credential2, subscriptionId);
20402
+ const resolution = await resolveAcs({
20403
+ client: acsClient,
20404
+ stamp: stampRg === null ? null : await this.readStampQuietly({ credential: credential2, subscriptionId, resourceGroup: stampRg }),
20405
+ subscriptionId,
20406
+ resourceGroup: acsRg,
20407
+ suffix,
20408
+ dataLocation: "United States",
20409
+ onProgress
20410
+ });
20411
+ acsEnv = {
20412
+ M8T_ACS_ENDPOINT: resolution.acs.endpoint,
20413
+ M8T_ACS_SENDER: resolution.acs.sender,
20414
+ M8T_EMAIL_ENABLED: "true"
20415
+ };
20416
+ await this.recordEmailIntent({
20417
+ credential: credential2,
20418
+ subscriptionId,
20419
+ resourceGroup: stampRg,
20420
+ feature: emailFeatureFrom(resolution.acs, true),
20421
+ warnings,
20422
+ onProgress
20423
+ });
20424
+ } catch (e) {
20425
+ acsEnv = {};
20426
+ warnings.add(
20427
+ `Outbound email was NOT provisioned (${e instanceof Error ? e.message : String(e)}). The executor is deployed and usable; turn email on later with \`m8t platform email on\`.`
20428
+ );
20429
+ }
20430
+ }
20431
+ } else {
20432
+ onProgress?.("--no-email: skipping outbound email.");
20433
+ await this.recordEmailIntent({
20434
+ credential: credential2,
19853
20435
  subscriptionId,
19854
- resourceGroup,
19855
- name: this.name,
19856
- dataLocation: "United States",
20436
+ resourceGroup: stampRg,
20437
+ feature: { enabled: false, acsEndpoint: "", acsSender: "" },
20438
+ warnings,
19857
20439
  onProgress
19858
20440
  });
19859
- acsEnv = { M8T_ACS_ENDPOINT: acs.endpoint, M8T_ACS_SENDER: acs.sender };
19860
20441
  }
19861
20442
  const env = {
19862
20443
  MODEL_DEPLOYMENT_NAME: this.modelDeployment ?? DEFAULT_MODEL2,
@@ -19977,6 +20558,93 @@ var AzureExecDeployCommand = class extends M8tCommand {
19977
20558
  );
19978
20559
  return 0;
19979
20560
  }
20561
+ /** Resolve the PLATFORM resource group — the one holding the stamp's storage account.
20562
+ *
20563
+ * Deliberately independent of the ACS resource group: they are often the same, but when
20564
+ * they are not, reading the stamp from the wrong one throws and would read as "no stamp".
20565
+ * Returns null (with a warning) rather than throwing, because a worker deploy must not
20566
+ * fail over the stamp. */
20567
+ async resolvePlatformResourceGroup(opts) {
20568
+ const disambiguator = typeof this.resourceGroup === "string" && this.resourceGroup.length > 0 ? this.resourceGroup : void 0;
20569
+ try {
20570
+ return resourceGroupFromArmId(
20571
+ (await discoverGateway({
20572
+ subscriptionId: opts.subscriptionId,
20573
+ interactive: opts.interactive,
20574
+ ...disambiguator !== void 0 ? { resourceGroup: disambiguator } : {}
20575
+ })).containerAppResourceId
20576
+ );
20577
+ } catch (e) {
20578
+ if (e.code === "GATEWAY_DISCOVERY_RG_NOT_FOUND" && disambiguator !== void 0) {
20579
+ try {
20580
+ return resourceGroupFromArmId(
20581
+ (await discoverGateway({ subscriptionId: opts.subscriptionId, interactive: opts.interactive })).containerAppResourceId
20582
+ );
20583
+ } catch (retryErr) {
20584
+ opts.warnings.add(
20585
+ `Could not locate the platform resource group (${retryErr instanceof Error ? retryErr.message : String(retryErr)}), so the outbound-email decision could not be recorded. Run \`m8t platform email on|off\` once the platform resolves.`
20586
+ );
20587
+ return null;
20588
+ }
20589
+ }
20590
+ opts.warnings.add(
20591
+ `Could not locate the platform resource group (${e instanceof Error ? e.message : String(e)}), so the outbound-email decision could not be recorded. Run \`m8t platform email on|off\` once the platform resolves.`
20592
+ );
20593
+ return null;
20594
+ }
20595
+ }
20596
+ /** Read the platform stamp, distinguishing "genuinely absent" from "unreadable".
20597
+ *
20598
+ * 🔴 The distinction matters: on `absent` (a fresh install) falling through to ACS
20599
+ * discovery is correct, but on `unreadable` we cannot know whether an ACS is already on
20600
+ * record, and provisioning a duplicate billable resource on a failed read would be worse
20601
+ * than doing nothing. `unreadable` therefore throws, and the caller's catch turns it into
20602
+ * a loud skip. */
20603
+ async readStampQuietly(opts) {
20604
+ const outcome = await readStampOutcome(opts);
20605
+ if (outcome.source === "explicit") return outcome.stamp;
20606
+ if (outcome.source === "absent") return null;
20607
+ throw new Error(
20608
+ "the platform stamp exists but could not be read, so it is not safe to decide whether an ACS is already on record"
20609
+ );
20610
+ }
20611
+ /** Persist the outbound-email decision to the platform stamp.
20612
+ *
20613
+ * Recording intent is the durability fix: before this, email lived only in the argv of
20614
+ * one command. If the stamp cannot be read or written, the deploy still succeeds — but
20615
+ * it WARNS, because an intent that silently failed to persist is exactly the state that
20616
+ * hid this outage. */
20617
+ async recordEmailIntent(opts) {
20618
+ const { credential: credential2, subscriptionId, resourceGroup, feature, warnings, onProgress } = opts;
20619
+ if (resourceGroup === null) return;
20620
+ try {
20621
+ const current = await readStamp({ credential: credential2, subscriptionId, resourceGroup });
20622
+ if (current === null) {
20623
+ warnings.add(
20624
+ `Outbound email is ${feature.enabled ? "on" : "off"} for this executor, but there is no platform stamp to record it in \u2014 a later converge will not know about it. Run \`m8t platform email on\` once the stamp exists.`
20625
+ );
20626
+ return;
20627
+ }
20628
+ const prior = current.features?.email;
20629
+ const merged = {
20630
+ enabled: feature.enabled,
20631
+ acsEndpoint: feature.acsEndpoint || (prior?.acsEndpoint ?? ""),
20632
+ acsSender: feature.acsSender || (prior?.acsSender ?? "")
20633
+ };
20634
+ await writeStamp({
20635
+ credential: credential2,
20636
+ subscriptionId,
20637
+ resourceGroup,
20638
+ stamp: { ...current, features: { ...current.features, email: merged } },
20639
+ onProgress
20640
+ });
20641
+ onProgress?.(`recorded outbound email = ${merged.enabled ? "on" : "off"} in the platform stamp.`);
20642
+ } catch (e) {
20643
+ warnings.add(
20644
+ `Could not record the outbound-email decision in the platform stamp (${e instanceof Error ? e.message : String(e)}). The executor is wired correctly, but a later converge will not know the intent.`
20645
+ );
20646
+ }
20647
+ }
19980
20648
  resolveScope(subscriptionId) {
19981
20649
  if (typeof this.scope === "string" && this.scope.startsWith("/subscriptions/")) return this.scope;
19982
20650
  if (typeof this.resourceGroup === "string" && this.resourceGroup.length > 0) {
@@ -20144,254 +20812,6 @@ async function fetchPublicTags(repo, fetchImpl = fetch) {
20144
20812
 
20145
20813
  // src/lib/release-channel.ts
20146
20814
  import * as fs23 from "fs";
20147
-
20148
- // ../../packages/platform-release/dist/esm/manifest.js
20149
- var SEVERITIES = ["critical", "recommended", "optional"];
20150
- var IMAGE_KEYS = ["gateway", "codingAgent", "azureExecutor", "installer"];
20151
- var COMPANION_TARGETS = [
20152
- "darwin-arm64",
20153
- "darwin-x64",
20154
- "win32-arm64",
20155
- "win32-x64"
20156
- ];
20157
- function companionTargetFor(platform, architecture) {
20158
- const target = `${platform}-${architecture}`;
20159
- return COMPANION_TARGETS.includes(target) ? target : null;
20160
- }
20161
- function isObj2(v) {
20162
- return typeof v === "object" && v !== null && !Array.isArray(v);
20163
- }
20164
- function validateManifest(m) {
20165
- const errors = [];
20166
- if (!isObj2(m))
20167
- return ["manifest is not an object"];
20168
- if (m.schemaVersion !== 1)
20169
- errors.push("schemaVersion must be the integer 1");
20170
- const p = m.platform;
20171
- if (!isObj2(p)) {
20172
- errors.push("platform is required");
20173
- } else {
20174
- for (const f of ["version", "tag", "releasedAt", "commit", "notes", "notesUrl"]) {
20175
- if (typeof p[f] !== "string" || p[f].length === 0)
20176
- errors.push(`platform.${f} is required`);
20177
- }
20178
- if (!SEVERITIES.includes(p.severity))
20179
- errors.push(`platform.severity must be one of ${SEVERITIES.join("|")}`);
20180
- if (!(typeof p.previousVersion === "string" || p.previousVersion === null)) {
20181
- errors.push("platform.previousVersion must be a string or null");
20182
- }
20183
- }
20184
- const c = m.components;
20185
- if (!isObj2(c)) {
20186
- errors.push("components is required");
20187
- return errors;
20188
- }
20189
- for (const key2 of IMAGE_KEYS) {
20190
- const img = c[key2];
20191
- if (!isObj2(img)) {
20192
- errors.push(`components.${key2} is required`);
20193
- continue;
20194
- }
20195
- if (img.kind !== "image")
20196
- errors.push(`components.${key2}.kind must be "image"`);
20197
- for (const f of ["ref", "tag", "version"]) {
20198
- if (typeof img[f] !== "string" || img[f].length === 0)
20199
- errors.push(`components.${key2}.${f} is required`);
20200
- }
20201
- if (typeof img.digest !== "string" || !/^sha256:[0-9a-f]+$/.test(img.digest)) {
20202
- errors.push(`components.${key2}.digest must be a sha256:\u2026 digest`);
20203
- }
20204
- }
20205
- const cli2 = c.cli;
20206
- if (!isObj2(cli2) || cli2.kind !== "npm") {
20207
- errors.push("components.cli (kind: npm) is required");
20208
- } else {
20209
- for (const f of ["package", "version", "min", "recommended"]) {
20210
- if (typeof cli2[f] !== "string" || cli2[f].length === 0)
20211
- errors.push(`components.cli.${f} is required`);
20212
- }
20213
- }
20214
- const personas = c.personas;
20215
- if (!isObj2(personas) || !isObj2(personas.items)) {
20216
- errors.push("components.personas.items is required");
20217
- } else {
20218
- for (const [name, item] of Object.entries(personas.items)) {
20219
- if (!isObj2(item) || typeof item.path !== "string" || item.path.length === 0) {
20220
- errors.push(`components.personas.items.${name}.path is required`);
20221
- }
20222
- if (!isObj2(item) || typeof item.treeSha !== "string" || item.treeSha.length < 4) {
20223
- errors.push(`components.personas.items.${name}.treeSha is required`);
20224
- }
20225
- }
20226
- }
20227
- const seeds = c.brainSeeds;
20228
- if (!isObj2(seeds) || !isObj2(seeds.items)) {
20229
- errors.push("components.brainSeeds.items is required");
20230
- } else {
20231
- for (const [name, item] of Object.entries(seeds.items)) {
20232
- if (!isObj2(item) || typeof item.path !== "string" || item.path.length === 0) {
20233
- errors.push(`components.brainSeeds.items.${name}.path is required`);
20234
- }
20235
- if (!isObj2(item) || !isObj2(item.subtrees)) {
20236
- errors.push(`components.brainSeeds.items.${name}.subtrees is required`);
20237
- }
20238
- }
20239
- }
20240
- const infra = c.infra;
20241
- if (infra !== void 0) {
20242
- if (!isObj2(infra) || infra.kind !== "infra") {
20243
- errors.push('components.infra.kind must be "infra"');
20244
- }
20245
- if (!isObj2(infra) || typeof infra.treeSha !== "string" || infra.treeSha.length < 4) {
20246
- errors.push("components.infra.treeSha is required");
20247
- }
20248
- }
20249
- const companion = c.companion;
20250
- if (companion !== void 0) {
20251
- if (!isObj2(companion) || companion.kind !== "desktop") {
20252
- errors.push('components.companion.kind must be "desktop"');
20253
- } else {
20254
- for (const f of ["version", "tag"]) {
20255
- if (typeof companion[f] !== "string" || companion[f].length === 0) {
20256
- errors.push(`components.companion.${f} is required`);
20257
- }
20258
- }
20259
- if (!isObj2(companion.targets) || Object.keys(companion.targets).length === 0) {
20260
- errors.push("components.companion.targets must name at least one target");
20261
- } else {
20262
- for (const [name, target] of Object.entries(companion.targets)) {
20263
- if (!COMPANION_TARGETS.includes(name)) {
20264
- errors.push(`components.companion.targets.${name} is not a known target`);
20265
- continue;
20266
- }
20267
- if (!isObj2(target) || typeof target.asset !== "string" || target.asset.length === 0) {
20268
- errors.push(`components.companion.targets.${name}.asset is required`);
20269
- }
20270
- if (!isObj2(target) || typeof target.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(target.sha256)) {
20271
- errors.push(`components.companion.targets.${name}.sha256 must be a sha256 hex digest`);
20272
- }
20273
- }
20274
- }
20275
- }
20276
- }
20277
- return errors;
20278
- }
20279
-
20280
- // ../../packages/platform-release/dist/esm/stamp.js
20281
- var TABLE = "Metadata";
20282
- var PK = "system";
20283
- var RK = "platform";
20284
- function stampToEntity(s) {
20285
- return {
20286
- partitionKey: PK,
20287
- rowKey: RK,
20288
- platformVersion: s.platformVersion,
20289
- updatedAt: s.updatedAt,
20290
- lastResult: s.lastResult,
20291
- value: JSON.stringify(s)
20292
- };
20293
- }
20294
- function entityToStamp(e) {
20295
- const raw = e.value;
20296
- if (typeof raw !== "string")
20297
- return null;
20298
- try {
20299
- return JSON.parse(raw);
20300
- } catch {
20301
- return null;
20302
- }
20303
- }
20304
-
20305
- // ../../packages/platform-release/dist/esm/channel-url.js
20306
- var RELEASES_REPO = "m8t-labs/m8t-releases";
20307
- var RELEASES_BASE = `https://github.com/${RELEASES_REPO}/releases`;
20308
- var CHANNEL_LATEST_URL = `${RELEASES_BASE}/latest/download/manifest.json`;
20309
- var CANARY_POINTER_TAG = "canary";
20310
- var CHANNEL_CANARY_URL = `${RELEASES_BASE}/download/${CANARY_POINTER_TAG}/manifest.json`;
20311
- function platformTag(version) {
20312
- const bare = version.trim().replace(/^platform-/, "").replace(/^v/, "");
20313
- return `platform-v${bare}`;
20314
- }
20315
- function channelUrlForVersion(version) {
20316
- return `${RELEASES_BASE}/download/${platformTag(version)}/manifest.json`;
20317
- }
20318
- function releaseAssetUrl(tag, asset) {
20319
- return `${RELEASES_BASE}/download/${tag}/${asset}`;
20320
- }
20321
-
20322
- // ../../packages/platform-release/dist/esm/canary-version.js
20323
- var CANARY_IDENTIFIER = "canary";
20324
- var STABLE_CORE = String.raw`\d+\.\d+\.\d+`;
20325
- var CANARY_SUFFIX = String.raw`-${CANARY_IDENTIFIER}\.(?:0|[1-9]\d*)`;
20326
- var CANARY_VERSION_PATTERN = `^${STABLE_CORE}${CANARY_SUFFIX}$`;
20327
- var PLATFORM_VERSION_PATTERN = `^${STABLE_CORE}(?:${CANARY_SUFFIX})?$`;
20328
- var PLATFORM_TAG_PATTERN = `^platform-v${STABLE_CORE}(?:${CANARY_SUFFIX})?$`;
20329
- var CANARY_VERSION_RE = new RegExp(CANARY_VERSION_PATTERN);
20330
- var STABLE_CORE_RE = new RegExp(`^${STABLE_CORE}$`);
20331
-
20332
- // ../../packages/platform-release/dist/esm/apply-request.js
20333
- var APPLY_REQUEST_PK = "system";
20334
- var APPLY_REQUEST_RK = "apply-request";
20335
- var IN_FLIGHT = ["claimed", "applying"];
20336
- var LEASE_MS = 30 * 60 * 1e3;
20337
- var MAX_LEASE_TAKEOVERS = 2;
20338
- var MAX_HEALTHY_IN_FLIGHT_MS = LEASE_MS * (MAX_LEASE_TAKEOVERS + 1);
20339
- function isInFlight(status) {
20340
- return IN_FLIGHT.includes(status);
20341
- }
20342
- var BLOCKS_NEW_INTENT = ["pending", "claimed", "applying", "awaiting-engine-update"];
20343
- function blocksNewIntent(status) {
20344
- return BLOCKS_NEW_INTENT.includes(status);
20345
- }
20346
- function newIntent(target, provenance, nowIso) {
20347
- return {
20348
- schemaVersion: 1,
20349
- target,
20350
- provenance,
20351
- status: "pending",
20352
- phase: null,
20353
- claimedBy: null,
20354
- leaseUntil: null,
20355
- attempt: 0,
20356
- breaker: "none",
20357
- result: null,
20358
- createdAt: nowIso,
20359
- updatedAt: nowIso
20360
- };
20361
- }
20362
- function applyRequestToEntity(r) {
20363
- return { partitionKey: APPLY_REQUEST_PK, rowKey: APPLY_REQUEST_RK, value: JSON.stringify(r), updatedAt: r.updatedAt };
20364
- }
20365
- function entityToApplyRequest(e) {
20366
- return JSON.parse(e.value);
20367
- }
20368
-
20369
- // ../../packages/platform-release/dist/esm/update-policy.js
20370
- var DEFAULT_POLICY_MODE = "auto-critical";
20371
- var UPDATE_POLICY_PK = "system";
20372
- var UPDATE_POLICY_RK = "update-policy";
20373
- var MODES = ["notify-only", "auto-critical", "auto-all"];
20374
- function isPolicyMode(x) {
20375
- return typeof x === "string" && MODES.includes(x);
20376
- }
20377
- function updatePolicyToEntity(p) {
20378
- return { partitionKey: UPDATE_POLICY_PK, rowKey: UPDATE_POLICY_RK, value: JSON.stringify(p) };
20379
- }
20380
- function entityToUpdatePolicy(e) {
20381
- return JSON.parse(e.value);
20382
- }
20383
-
20384
- // ../../packages/platform-release/dist/esm/infra-params.js
20385
- var INFRA_PARAMS_PK = "system";
20386
- var INFRA_PARAMS_RK = "infra-params";
20387
- function infraParamsToEntity(p) {
20388
- return { partitionKey: INFRA_PARAMS_PK, rowKey: INFRA_PARAMS_RK, value: JSON.stringify(p) };
20389
- }
20390
- function entityToInfraParams(e) {
20391
- return JSON.parse(e.value);
20392
- }
20393
-
20394
- // src/lib/release-channel.ts
20395
20815
  init_errors();
20396
20816
  function manifestSourceForVersionTag(channelUrl, version) {
20397
20817
  const tag = platformTag(version);
@@ -20551,153 +20971,6 @@ async function resolveReleaseContentWithAgents(manifest, opts = {}) {
20551
20971
  return contentDir;
20552
20972
  }
20553
20973
 
20554
- // src/lib/platform-stamp.ts
20555
- import { TableClient as TableClient2 } from "@azure/data-tables";
20556
-
20557
- // src/lib/platform-storage-discovery.ts
20558
- init_http();
20559
- init_errors();
20560
- var ARM3 = "https://management.azure.com";
20561
- var ARM_SCOPE7 = "https://management.azure.com/.default";
20562
- var STORAGE_API = "2023-05-01";
20563
- async function discoverStampStorage(opts) {
20564
- const list = await authedJson({
20565
- credential: opts.credential,
20566
- scope: ARM_SCOPE7,
20567
- method: "GET",
20568
- url: `${ARM3}/subscriptions/${opts.subscriptionId}/resourceGroups/${opts.resourceGroup}/providers/Microsoft.Storage/storageAccounts?api-version=${STORAGE_API}`
20569
- }) ?? {};
20570
- const accounts = (list.value ?? []).filter((a) => a.properties?.primaryEndpoints?.table);
20571
- const chosen = accounts.find((a) => a.tags?.m8t === "storage") ?? accounts.find((a) => a.tags?.["m8t:role"] === "gateway" || a.tags?.["m8t:role"] === "ledger") ?? (accounts.length === 1 ? accounts[0] : void 0);
20572
- const tableEndpoint = chosen?.properties?.primaryEndpoints?.table?.replace(/\/$/, "");
20573
- if (!tableEndpoint || !chosen?.id || !chosen.name) {
20574
- throw new LocalCliError({
20575
- code: "PLATFORM_STAMP_STORAGE_NOT_FOUND",
20576
- message: `No table-capable storage account found in resource group ${opts.resourceGroup}.`,
20577
- hint: `Found: ${accounts.map((a) => a.name).join(", ") || "(none)"}.`
20578
- });
20579
- }
20580
- return { tableEndpoint, accountResourceId: chosen.id, accountName: chosen.name };
20581
- }
20582
-
20583
- // src/lib/metadata-table-write.ts
20584
- import { TableClient, AzureNamedKeyCredential } from "@azure/data-tables";
20585
- init_errors();
20586
- init_rbac();
20587
- function is403(e) {
20588
- return e.statusCode === 403;
20589
- }
20590
- function is404(e) {
20591
- return e.statusCode === 404;
20592
- }
20593
- function sleep2(ms) {
20594
- return new Promise((r) => setTimeout(r, ms));
20595
- }
20596
- async function makeSharedKeyClient(opts) {
20597
- const rg = /\/resourceGroups\/([^/]+)\//i.exec(opts.accountResourceId)?.[1];
20598
- if (!rg) return null;
20599
- const key2 = (await runAz(["storage", "account", "keys", "list", "--account-name", opts.accountName, "-g", rg, "--query", "[0].value", "-o", "tsv"])).trim();
20600
- if (!key2) return null;
20601
- return new TableClient(opts.tableEndpoint, TABLE, new AzureNamedKeyCredential(opts.accountName, key2));
20602
- }
20603
- async function upsertMetadataEntity(opts) {
20604
- const discover = opts.discoverImpl ?? discoverStampStorage;
20605
- const makeAad = opts.aadClientImpl ?? ((endpoint, credential2) => new TableClient(endpoint, TABLE, credential2));
20606
- const makeShared = opts.sharedClientImpl ?? makeSharedKeyClient;
20607
- const nap = opts.sleepImpl ?? sleep2;
20608
- const { tableEndpoint, accountResourceId, accountName } = await discover({
20609
- credential: opts.credential,
20610
- subscriptionId: opts.subscriptionId,
20611
- resourceGroup: opts.resourceGroup
20612
- });
20613
- const aad = makeAad(tableEndpoint, opts.credential);
20614
- await aad.createTable().catch(() => void 0);
20615
- try {
20616
- await aad.upsertEntity(opts.entity, "Replace");
20617
- return;
20618
- } catch (e) {
20619
- if (!is403(e)) throw e;
20620
- }
20621
- opts.onProgress?.(`writing the ${opts.label} with the account key \u2014 your identity lacks the Storage Table data role.`);
20622
- const shared = await makeShared({ tableEndpoint, accountResourceId, accountName });
20623
- if (shared) {
20624
- await shared.createTable().catch(() => void 0);
20625
- await shared.upsertEntity(opts.entity, "Replace");
20626
- return;
20627
- }
20628
- opts.onProgress?.("shared-key access is disabled \u2014 granting Storage Table Data Contributor to your identity (one-time)\u2026");
20629
- try {
20630
- const oid = await getCallerObjectId();
20631
- await grantStorageTableDataContributor({
20632
- credential: opts.credential,
20633
- subscriptionId: opts.subscriptionId,
20634
- scope: accountResourceId,
20635
- principalId: oid
20636
- });
20637
- } catch (e) {
20638
- throw new LocalCliError({
20639
- code: opts.errorCode,
20640
- message: `Cannot write the ${opts.label}: AAD lacks the Storage Table data role for ${accountName}, shared-key access is disabled, and self-granting the role failed.`,
20641
- hint: "Grant yourself 'Storage Table Data Contributor' on the storage account and retry.",
20642
- cause: e
20643
- });
20644
- }
20645
- for (let i = 0; i < 6; i++) {
20646
- await nap(1e4);
20647
- try {
20648
- await aad.upsertEntity(opts.entity, "Replace");
20649
- return;
20650
- } catch (e) {
20651
- if (!is403(e)) throw e;
20652
- }
20653
- }
20654
- throw new LocalCliError({
20655
- code: opts.errorCode,
20656
- message: `Cannot write the ${opts.label}: granted 'Storage Table Data Contributor' on ${accountName}, but the data-plane role has not propagated yet.`,
20657
- hint: opts.retryHint
20658
- });
20659
- }
20660
-
20661
- // src/lib/platform-stamp.ts
20662
- async function writeStamp(opts) {
20663
- const entity = stampToEntity(opts.stamp);
20664
- await upsertMetadataEntity({
20665
- credential: opts.credential,
20666
- subscriptionId: opts.subscriptionId,
20667
- resourceGroup: opts.resourceGroup,
20668
- entity,
20669
- label: "platform stamp",
20670
- errorCode: "PLATFORM_STAMP_NO_ACCESS",
20671
- retryHint: "The role assignment persists \u2014 retry 'm8t platform update' in a few minutes.",
20672
- ...opts.onProgress ? { onProgress: opts.onProgress } : {}
20673
- });
20674
- }
20675
- async function readStampOutcome(opts) {
20676
- const { tableEndpoint, accountResourceId, accountName } = await discoverStampStorage(opts);
20677
- const aad = new TableClient2(tableEndpoint, TABLE, opts.credential);
20678
- try {
20679
- const row = await aad.getEntity(PK, RK);
20680
- const stamp = entityToStamp(row);
20681
- return stamp ? { source: "explicit", stamp } : { source: "unreadable" };
20682
- } catch (e) {
20683
- if (is404(e)) return { source: "absent" };
20684
- if (!is403(e)) return { source: "unreadable" };
20685
- }
20686
- try {
20687
- const shared = await makeSharedKeyClient({ tableEndpoint, accountResourceId, accountName });
20688
- if (!shared) return { source: "unreadable" };
20689
- const row = await shared.getEntity(PK, RK);
20690
- const stamp = entityToStamp(row);
20691
- return stamp ? { source: "explicit", stamp } : { source: "unreadable" };
20692
- } catch {
20693
- return { source: "unreadable" };
20694
- }
20695
- }
20696
- async function readStamp(opts) {
20697
- const result2 = await readStampOutcome(opts);
20698
- return result2.source === "explicit" ? result2.stamp : null;
20699
- }
20700
-
20701
20974
  // src/lib/platform-converge.ts
20702
20975
  init_esm();
20703
20976
  import * as fs26 from "fs";
@@ -21115,6 +21388,9 @@ async function applyPlan(plan, deps, ctx) {
21115
21388
  stamp.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
21116
21389
  await deps.writeStamp(stamp);
21117
21390
  }
21391
+ if (deps.reconcileEmail) {
21392
+ await deps.reconcileEmail();
21393
+ }
21118
21394
  await deps.healthGate(ctx, applied);
21119
21395
  stamp.lastResult = "success";
21120
21396
  delete stamp.lastError;
@@ -21594,6 +21870,73 @@ import { DefaultAzureCredential as DefaultAzureCredential17 } from "@azure/ident
21594
21870
  // src/lib/platform-converge-cli.ts
21595
21871
  import * as path31 from "path";
21596
21872
 
21873
+ // src/lib/executor-email-env.ts
21874
+ init_foundry_agent_get();
21875
+ init_foundry_agent_version();
21876
+ var EMAIL_ENV_KEYS = ["M8T_ACS_ENDPOINT", "M8T_ACS_SENDER", "M8T_EMAIL_ENABLED"];
21877
+ var FOUNDRY_HOSTED_HEADER = { "Foundry-Features": "HostedAgents=V1Preview" };
21878
+ function applyEmailEnv(def, feature) {
21879
+ const live = def.environment_variables ?? {};
21880
+ const carried = Object.fromEntries(
21881
+ Object.entries(live).filter(([k]) => !EMAIL_ENV_KEYS.includes(k))
21882
+ );
21883
+ if (feature !== null && isEmailFeatureUsable(feature)) {
21884
+ return {
21885
+ ...def,
21886
+ environment_variables: {
21887
+ ...carried,
21888
+ M8T_ACS_ENDPOINT: feature.acsEndpoint,
21889
+ M8T_ACS_SENDER: feature.acsSender,
21890
+ M8T_EMAIL_ENABLED: "true"
21891
+ }
21892
+ };
21893
+ }
21894
+ return { ...def, environment_variables: carried };
21895
+ }
21896
+ async function reconcileExecutorEmailEnv(args) {
21897
+ const { credential: credential2, projectEndpoint, agentName, feature, onProgress } = args;
21898
+ let current;
21899
+ try {
21900
+ current = await getAgentVersion({ credential: credential2, projectEndpoint, agentName });
21901
+ } catch (e) {
21902
+ const code = e.code;
21903
+ if (code === "AGENT_NOT_FOUND") return { changed: false, absent: true };
21904
+ return { changed: false, absent: false, unreadable: e instanceof Error ? e.message : String(e) };
21905
+ }
21906
+ const next = applyEmailEnv(current.definition, feature);
21907
+ const before = current.definition.environment_variables ?? {};
21908
+ const after = next.environment_variables;
21909
+ if (EMAIL_ENV_KEYS.every((k) => before[k] === after[k])) {
21910
+ return { changed: false, absent: false };
21911
+ }
21912
+ onProgress?.(`reconciling outbound-email env on ${agentName}\u2026`);
21913
+ const version = await createAgentVersion({
21914
+ credential: credential2,
21915
+ projectEndpoint,
21916
+ agentName,
21917
+ definition: next,
21918
+ // Carry the live metadata through unchanged: it holds the a2a card and brain markers,
21919
+ // and dropping those has bitten this repo before.
21920
+ metadata: current.metadata ?? {},
21921
+ extraHeaders: FOUNDRY_HOSTED_HEADER
21922
+ });
21923
+ return { changed: true, absent: false, version };
21924
+ }
21925
+ var EXECUTOR_PERSONA = "azure-executor";
21926
+ var EXECUTOR_AGENT_FALLBACK = "ezra-executor";
21927
+ async function resolveExecutorAgentName(args) {
21928
+ if (typeof args.override === "string" && args.override.length > 0) {
21929
+ return { agentName: args.override, provenance: "override" };
21930
+ }
21931
+ try {
21932
+ const agents = await listM8tAgents({ credential: args.credential, projectEndpoint: args.projectEndpoint });
21933
+ const hit = agents.find((a) => a.metadata.persona === EXECUTOR_PERSONA);
21934
+ if (hit) return { agentName: hit.name, provenance: "discovered" };
21935
+ } catch {
21936
+ }
21937
+ return { agentName: EXECUTOR_AGENT_FALLBACK, provenance: "fallback" };
21938
+ }
21939
+
21597
21940
  // src/lib/platform-infra-params.ts
21598
21941
  import { TableClient as TableClient3 } from "@azure/data-tables";
21599
21942
  async function openInfraParamsTable(opts) {
@@ -22071,6 +22414,37 @@ async function buildConvergeDeps(args) {
22071
22414
  async applyGateway(a, ctx) {
22072
22415
  return applyGatewayImage(a, ctx, args.gatewayResourceId);
22073
22416
  },
22417
+ async reconcileEmail() {
22418
+ const dir2 = HOSTED_AGENT_PERSONA_DIR.azureExecutor;
22419
+ const agentName = dir2 ? discovered[dir2] : void 0;
22420
+ if (!agentName) return;
22421
+ const outcome = await readStampOutcome({
22422
+ credential: args.credential,
22423
+ subscriptionId: args.subscriptionId,
22424
+ resourceGroup: args.resourceGroup
22425
+ }).catch(() => ({ source: "unreadable" }));
22426
+ if (outcome.source === "unreadable") {
22427
+ warn("skipped the outbound-email reconcile: the platform stamp could not be read, so the recorded intent is unknown.");
22428
+ return;
22429
+ }
22430
+ const feature = outcome.source === "explicit" ? outcome.stamp.features?.email ?? null : null;
22431
+ try {
22432
+ const r = await reconcileExecutorEmailEnv({
22433
+ credential: args.credential,
22434
+ projectEndpoint: args.project.endpoint,
22435
+ agentName,
22436
+ feature,
22437
+ onProgress: args.onProgress
22438
+ });
22439
+ if (r.unreadable !== void 0) {
22440
+ warn(`could not read ${agentName} to reconcile outbound email (${r.unreadable}) \u2014 left untouched.`);
22441
+ } else if (r.changed) {
22442
+ args.onProgress?.(`outbound-email env reconciled on ${agentName} (version ${r.version ?? "?"}).`);
22443
+ }
22444
+ } catch (e) {
22445
+ warn(`could not reconcile outbound-email env on ${agentName}: ${e instanceof Error ? e.message : String(e)}`);
22446
+ }
22447
+ },
22074
22448
  async applyInfra(a, ctx) {
22075
22449
  const target = infraTargetSha(ctx.manifest, args.tree);
22076
22450
  if (a.reason !== "forced") return applyInfraDeploy(a, ctx, target);
@@ -22335,6 +22709,27 @@ var PlatformUpdateCommand = class extends M8tCommand {
22335
22709
  return 0;
22336
22710
  }
22337
22711
  if (plan.actions.length === 0) {
22712
+ if (only === void 0 || only === "azureExecutor") {
22713
+ try {
22714
+ const noDriftDeps = await buildConvergeDeps({
22715
+ credential: credential2,
22716
+ subscriptionId,
22717
+ resourceGroup,
22718
+ contentDir,
22719
+ manifest,
22720
+ tree,
22721
+ project,
22722
+ gatewayResourceId: gw.containerAppResourceId,
22723
+ gatewayUrl: gw.gatewayUrl,
22724
+ suffix,
22725
+ onProgress,
22726
+ onWarn
22727
+ });
22728
+ if (noDriftDeps.reconcileEmail) await noDriftDeps.reconcileEmail();
22729
+ } catch (e) {
22730
+ onWarn(`could not reconcile outbound email: ${e instanceof Error ? e.message : String(e)}`);
22731
+ }
22732
+ }
22338
22733
  if (mode === "json") this.context.stdout.write(renderJson({ ...plan, applied: false }) + "\n");
22339
22734
  else log(colors.success("already up to date \u2713"));
22340
22735
  return 0;
@@ -22741,9 +23136,9 @@ async function readBreaker(client) {
22741
23136
 
22742
23137
  // src/commands/platform/converge.ts
22743
23138
  init_errors();
22744
- function isForwardTarget(target, installed) {
22745
- if (!installed) return true;
22746
- return comparePlatformVersion(target, installed) >= 0;
23139
+ function isForwardTarget(target, installed2) {
23140
+ if (!installed2) return true;
23141
+ return comparePlatformVersion(target, installed2) >= 0;
22747
23142
  }
22748
23143
  async function failIfStillInFlight(client, now, error, opts = {}) {
22749
23144
  const cur = await readApplyRequest(client);
@@ -22812,15 +23207,15 @@ var PlatformConvergeCommand = class extends M8tCommand {
22812
23207
  subscriptionId: ctx.subscriptionId,
22813
23208
  resourceGroup: ctx.resourceGroup
22814
23209
  });
22815
- const installed = stamp ? stamp.platformVersion : null;
22816
- if (!isForwardTarget(claimed.target, installed)) {
23210
+ const installed2 = stamp ? stamp.platformVersion : null;
23211
+ if (!isForwardTarget(claimed.target, installed2)) {
22817
23212
  await patchApplyRequest(client, (r) => ({
22818
23213
  ...r,
22819
23214
  status: "failed",
22820
- result: { appliedVersion: null, error: `refused downgrade ${installed ?? "(none)"} \u2192 ${claimed.target}` },
23215
+ result: { appliedVersion: null, error: `refused downgrade ${installed2 ?? "(none)"} \u2192 ${claimed.target}` },
22821
23216
  updatedAt: now()
22822
23217
  }));
22823
- log(`refused: target ${claimed.target} is a downgrade from installed ${installed ?? "(none)"}.`);
23218
+ log(`refused: target ${claimed.target} is a downgrade from installed ${installed2 ?? "(none)"}.`);
22824
23219
  return 1;
22825
23220
  }
22826
23221
  const manifest = await fetchManifest(targetManifestSource(ctx, claimed.target));
@@ -23669,46 +24064,222 @@ var PlatformEnableCostReportCommand = class extends M8tCommand {
23669
24064
  }
23670
24065
  };
23671
24066
 
23672
- // src/commands/platform/enable-auto-update.ts
24067
+ // src/commands/platform/email.ts
23673
24068
  import { Command as Command42, Option as Option39 } from "clipanion";
23674
- import { confirm as confirm6 } from "@inquirer/prompts";
23675
24069
  import { DefaultAzureCredential as DefaultAzureCredential22 } from "@azure/identity";
24070
+ import { CommunicationServiceManagementClient as CommunicationServiceManagementClient2 } from "@azure/arm-communication";
24071
+ init_errors();
24072
+ var PlatformEmailCommand = class extends M8tCommand {
24073
+ static paths = [["platform", "email"]];
24074
+ static usage = Command42.Usage({
24075
+ description: "Turn outbound email (advisor handoffs) on or off for this install.",
24076
+ details: "`on` resolves the ACS sender (reusing an existing one, provisioning only if there is none), records the decision in the platform stamp, and reconciles the executor's environment. `off` records the decision and strips the executor's email wiring, leaving the ACS resource in place so turning it back on is a flag flip. Idempotent \u2014 re-running either direction is safe, and no new agent version is posted when nothing changes.",
24077
+ examples: [
24078
+ ["Turn outbound email on", "m8t platform email on"],
24079
+ ["Turn it off (a shared, public-facing deployment should stay off)", "m8t platform email off"]
24080
+ ]
24081
+ });
24082
+ state = Option39.String({ required: true, name: "on|off" });
24083
+ subscription = Option39.String("--subscription");
24084
+ resourceGroup = Option39.String("--resource-group", {
24085
+ description: "m8t resource group, to disambiguate in a multi-deployment subscription."
24086
+ });
24087
+ agent = Option39.String("--agent", { description: `Executor agent name. Resolved by persona when omitted (usually ${EXECUTOR_AGENT_FALLBACK}).` });
24088
+ kvUri = Option39.String("--kv-uri", {
24089
+ description: "Install's Key Vault URI. Only needed if no ACS is on record yet and one must be provisioned."
24090
+ });
24091
+ endpoint = Option39.String("--endpoint", {
24092
+ description: "Foundry project endpoint, to disambiguate a subscription holding several."
24093
+ });
24094
+ output = Option39.String("--output");
24095
+ async executeCommand() {
24096
+ const wanted = this.state.trim().toLowerCase();
24097
+ if (wanted !== "on" && wanted !== "off") {
24098
+ throw new LocalCliError({
24099
+ code: "USAGE",
24100
+ message: `Expected 'on' or 'off', got '${this.state}'.`,
24101
+ hint: "Run `m8t platform email on` or `m8t platform email off`."
24102
+ });
24103
+ }
24104
+ const enable = wanted === "on";
24105
+ const mode = resolveOutputMode(
24106
+ this.output,
24107
+ this.context.stdout
24108
+ );
24109
+ const onProgress = mode === "pretty" ? (m) => this.context.stderr.write(` ${colors.dim(m)}
24110
+ `) : void 0;
24111
+ const ctx = await resolveGatewayContext({
24112
+ interactive: mode !== "json",
24113
+ subscriptionId: this.subscription,
24114
+ resourceGroup: this.resourceGroup
24115
+ });
24116
+ const { resourceGroup } = parseContainerAppResourceId(ctx.containerAppResourceId);
24117
+ const subscriptionId = ctx.subscriptionId;
24118
+ const credential2 = new DefaultAzureCredential22();
24119
+ const stamp = await readStamp({ credential: credential2, subscriptionId, resourceGroup });
24120
+ if (stamp === null) {
24121
+ throw new LocalCliError({
24122
+ code: "NO_PLATFORM_STAMP",
24123
+ message: `No platform stamp found in ${resourceGroup} \u2014 there is nowhere to record the decision.`,
24124
+ hint: "This command needs an install written by the current CLI. Check you are pointed at the right deployment with `m8t status`."
24125
+ });
24126
+ }
24127
+ let feature;
24128
+ if (enable) {
24129
+ const suffix = this.resolveSuffix();
24130
+ const resolution = await resolveAcs({
24131
+ client: new CommunicationServiceManagementClient2(credential2, subscriptionId),
24132
+ stamp,
24133
+ subscriptionId,
24134
+ resourceGroup,
24135
+ suffix,
24136
+ dataLocation: "United States",
24137
+ onProgress
24138
+ });
24139
+ onProgress?.(`sender: ${resolution.acs.sender} (${resolution.source}).`);
24140
+ feature = emailFeatureFrom(resolution.acs, true);
24141
+ } else {
24142
+ const prior = stamp.features?.email;
24143
+ feature = { enabled: false, acsEndpoint: prior?.acsEndpoint ?? "", acsSender: prior?.acsSender ?? "" };
24144
+ }
24145
+ await writeStamp({
24146
+ credential: credential2,
24147
+ subscriptionId,
24148
+ resourceGroup,
24149
+ stamp: { ...stamp, features: { ...stamp.features, email: feature } },
24150
+ onProgress
24151
+ });
24152
+ const project = await resolveFoundryProject({
24153
+ credential: credential2,
24154
+ subscriptionId,
24155
+ interactive: mode !== "json",
24156
+ ...typeof this.endpoint === "string" && this.endpoint.length > 0 ? { endpoint: this.endpoint } : {},
24157
+ ...typeof this.resourceGroup === "string" && this.resourceGroup.length > 0 ? { resourceGroup: this.resourceGroup } : {}
24158
+ });
24159
+ const { agentName, provenance } = await resolveExecutorAgentName({
24160
+ credential: credential2,
24161
+ projectEndpoint: project.endpoint,
24162
+ ...typeof this.agent === "string" && this.agent.length > 0 ? { override: this.agent } : {}
24163
+ });
24164
+ const reconciled = await reconcileExecutorEmailEnv({
24165
+ credential: credential2,
24166
+ projectEndpoint: project.endpoint,
24167
+ agentName,
24168
+ feature,
24169
+ onProgress
24170
+ });
24171
+ if (mode === "json") {
24172
+ this.context.stdout.write(
24173
+ renderJson({
24174
+ email: enable ? "on" : "off",
24175
+ resourceGroup,
24176
+ acsEndpoint: feature.acsEndpoint,
24177
+ acsSender: feature.acsSender,
24178
+ executor: agentName,
24179
+ executorNameProvenance: provenance,
24180
+ executorReconciled: reconciled.changed,
24181
+ executorAbsent: reconciled.absent,
24182
+ ...reconciled.unreadable !== void 0 ? { executorUnreadable: reconciled.unreadable } : {},
24183
+ ...reconciled.version !== void 0 ? { executorVersion: reconciled.version } : {}
24184
+ }) + "\n"
24185
+ );
24186
+ return reconciled.unreadable === void 0 ? 0 : 1;
24187
+ }
24188
+ this.context.stdout.write(
24189
+ renderKeyValueBlock([
24190
+ { key: "outbound email", value: enable ? "on" : "off" },
24191
+ { key: "resource group", value: resourceGroup },
24192
+ { key: "sender", value: feature.acsSender === "" ? "(none on record)" : feature.acsSender },
24193
+ {
24194
+ key: "executor",
24195
+ // 🔴 `unreadable` MUST be its own branch. Falling through to "already correct" would
24196
+ // have this command — the one an operator runs to FIX email — affirmatively claim
24197
+ // success while having changed nothing, which is the exact conflation of broken with
24198
+ // fine that this whole change exists to eliminate.
24199
+ value: reconciled.unreadable !== void 0 ? `${agentName} (could NOT be read: ${reconciled.unreadable} \u2014 wiring NOT reconciled)` : reconciled.absent ? `${agentName} (not deployed \u2014 nothing to reconcile)` : reconciled.changed ? `${agentName} \u2192 version ${reconciled.version ?? "?"}` : `${agentName} (already correct)`
24200
+ }
24201
+ ]) + "\n"
24202
+ );
24203
+ if (reconciled.absent) {
24204
+ this.context.stdout.write(
24205
+ ` ${colors.hint("note:")} no executor is deployed here, so nothing can send yet. The decision is recorded and will apply when one is deployed.
24206
+ `
24207
+ );
24208
+ }
24209
+ if (provenance === "fallback" && reconciled.absent) {
24210
+ this.context.stdout.write(
24211
+ ` ${colors.hint("note:")} '${agentName}' is a fallback name \u2014 the executor could not be discovered by persona. If yours is named differently, re-run with --agent <name>.
24212
+ `
24213
+ );
24214
+ }
24215
+ if (reconciled.unreadable !== void 0) {
24216
+ this.context.stdout.write(
24217
+ colors.error(
24218
+ `the decision is recorded, but ${agentName} was NOT reconciled \u2014 it exists and could not be read. Nothing about sending has changed yet; fix access and re-run.`
24219
+ ) + "\n"
24220
+ );
24221
+ return 1;
24222
+ }
24223
+ this.context.stdout.write(
24224
+ colors.success(enable ? "outbound email is on \u2713" : "outbound email is off \u2713") + "\n"
24225
+ );
24226
+ return 0;
24227
+ }
24228
+ /** The install's resource suffix, or null when it cannot be worked out.
24229
+ *
24230
+ * Returns null rather than throwing: it is only needed if `resolveAcs` reaches its
24231
+ * provisioning leg, and failing the whole command over a missing AZURE_KEYVAULT_URI would
24232
+ * break the flip on every install that already has an ACS on record. */
24233
+ resolveSuffix() {
24234
+ try {
24235
+ const explicit = typeof this.kvUri === "string" && this.kvUri.length > 0 ? this.kvUri : void 0;
24236
+ return platformSuffixFromKvUri(discoverKvUri(process.env, explicit));
24237
+ } catch {
24238
+ return null;
24239
+ }
24240
+ }
24241
+ };
24242
+
24243
+ // src/commands/platform/enable-auto-update.ts
24244
+ import { Command as Command43, Option as Option40 } from "clipanion";
24245
+ import { confirm as confirm6 } from "@inquirer/prompts";
24246
+ import { DefaultAzureCredential as DefaultAzureCredential23 } from "@azure/identity";
23676
24247
  init_errors();
23677
24248
  var PlatformEnableAutoUpdateCommand = class extends M8tCommand {
23678
24249
  static paths = [["platform", "enable-auto-update"]];
23679
- static usage = Command42.Usage({
24250
+ static usage = Command43.Usage({
23680
24251
  category: "Platform",
23681
24252
  description: "Retrofit the auto-updater (MI + cron job + role assignments) onto an existing install.",
23682
24253
  details: "For an install that predates the Platform Update Framework: recovers the deployment's resource-name suffix (from the stamped system/infra-params row, or by verified derivation from the live gateway), recovers the other bicep params from live state (WITHOUT changing the deployed gateway image), and re-runs deploy/main.bicep with provisionUpdater=true \u2014 which provisions the updater managed identity, the cron Container Apps Job, and every role assignment (Owner@RG + Foundry User@account + Storage Table + Storage Blob + AcrPull), all scoped to the resource group. On success, backfills system/infra-params so future converges never have to re-derive the suffix. Idempotent \u2014 safe to re-run."
23683
24254
  });
23684
- subscription = Option39.String("--subscription");
23685
- resourceGroup = Option39.String("--resource-group", {
24255
+ subscription = Option40.String("--subscription");
24256
+ resourceGroup = Option40.String("--resource-group", {
23686
24257
  description: "m8t resource group to disambiguate the gateway (multi-deployment subscriptions)."
23687
24258
  });
23688
- suffix = Option39.String("--suffix", {
24259
+ suffix = Option40.String("--suffix", {
23689
24260
  description: "The existing deployment's resource-name suffix. Required when it cannot be recovered from system/infra-params or the live gateway."
23690
24261
  });
23691
- installerImage = Option39.String("--installer-image", {
24262
+ installerImage = Option40.String("--installer-image", {
23692
24263
  required: true,
23693
24264
  description: "Updater CA-Job image ref (the converge engine at the current release). Required \u2014 an empty image would skip provisioning."
23694
24265
  });
23695
- updateCron = Option39.String("--update-cron", {
24266
+ updateCron = Option40.String("--update-cron", {
23696
24267
  description: "Cron schedule for the updater job (bicep default applies when omitted)."
23697
24268
  });
23698
- channelUrl = Option39.String("--channel-url", {
24269
+ channelUrl = Option40.String("--channel-url", {
23699
24270
  description: "Release-channel URL the updater job polls (bicep default applies when omitted)."
23700
24271
  });
23701
- location = Option39.String("--location", {
24272
+ location = Option40.String("--location", {
23702
24273
  description: "Region for the updater identity + job. Defaults to the resource group's existing resources."
23703
24274
  });
23704
- foundryTracing = Option39.String("--foundry-tracing", {
24275
+ foundryTracing = Option40.String("--foundry-tracing", {
23705
24276
  description: "project | account | skip. Pass the value the install was deployed with \u2014 omitting it lets the bicep default (project) switch tracing on."
23706
24277
  });
23707
- endpoint = Option39.String("--endpoint", {
24278
+ endpoint = Option40.String("--endpoint", {
23708
24279
  description: "Foundry project endpoint URL. Disambiguates the project in a multi-project subscription."
23709
24280
  });
23710
- yes = Option39.Boolean("--yes", false);
23711
- output = Option39.String("--output");
24281
+ yes = Option40.Boolean("--yes", false);
24282
+ output = Option40.String("--output");
23712
24283
  async executeCommand() {
23713
24284
  const mode = resolveOutputMode(
23714
24285
  this.output,
@@ -23727,7 +24298,7 @@ var PlatformEnableAutoUpdateCommand = class extends M8tCommand {
23727
24298
  resourceGroup: this.resourceGroup
23728
24299
  });
23729
24300
  const { resourceGroup, name: gatewayName } = parseContainerAppResourceId(gw.containerAppResourceId);
23730
- const credential2 = new DefaultAzureCredential22();
24301
+ const credential2 = new DefaultAzureCredential23();
23731
24302
  const account = await getAzAccount();
23732
24303
  const subscriptionId = this.subscription ?? account.subscriptionId;
23733
24304
  const explicitSuffix = typeof this.suffix === "string" && this.suffix.length > 0 ? this.suffix : void 0;
@@ -23845,7 +24416,7 @@ var PlatformEnableAutoUpdateCommand = class extends M8tCommand {
23845
24416
  };
23846
24417
 
23847
24418
  // src/commands/deploy.ts
23848
- import { Command as Command43, Option as Option40 } from "clipanion";
24419
+ import { Command as Command44, Option as Option41 } from "clipanion";
23849
24420
 
23850
24421
  // src/lib/app-reg.ts
23851
24422
  init_esm();
@@ -24322,6 +24893,14 @@ var KNOWN_NOISE = [
24322
24893
  //
24323
24894
  // REMOVE THESE TWO RULES when the deployment path stops regenerating the value.
24324
24895
  // Their presence is the marker that it still does.
24896
+ // Storage RP defaults the bicep does not declare. what-if renders "the template
24897
+ // omits this, so it would be removed", but the RP re-stamps them on every write
24898
+ // and an apply is a verified no-op. Narrow on purpose: `deleteRetentionPolicy`
24899
+ // and the two encryption-scope fields only. An unanchored `properties` rule on
24900
+ // blobServices would swallow a real change to versioning, CORS or public access
24901
+ // on the account that holds every conversation.
24902
+ { resourceType: "Microsoft.Storage/storageAccounts/blobServices", pathPattern: /(^|\.)properties$/, reason: "RP-owned blob-service defaults (deleteRetentionPolicy); the bicep declares none" },
24903
+ { resourceType: "Microsoft.Storage/storageAccounts/blobServices/containers", pathPattern: /(^|\.)(defaultEncryptionScope|denyEncryptionScopeOverride)$/, reason: "RP default ($account-encryption-key); the bicep declares no scope" },
24325
24904
  { resourceType: "Microsoft.App/containerApps", pathPattern: /(^|\.)configuration\.secrets$/, reason: "shared relay secret is regenerated on every deployment \u2014 owned elsewhere" },
24326
24905
  { resourceType: "Microsoft.App/containerApps", pathPattern: /(^|\.)configuration\.secrets\[\d+\]\.value$/, reason: "shared relay secret is regenerated on every deployment \u2014 owned elsewhere" }
24327
24906
  ];
@@ -24385,36 +24964,44 @@ function classifyWhatIf(changes) {
24385
24964
  var DEFAULT_IMAGE_REF = "ghcr.io/m8t-labs/m8t:latest";
24386
24965
  var DeployCommand = class extends M8tCommand {
24387
24966
  static paths = [["deploy"]];
24388
- static usage = Command43.Usage({
24967
+ static usage = Command44.Usage({
24389
24968
  description: "Deploy (or update) the m8t gateway/webapp stack via Bicep.",
24390
24969
  details: "Ensures the Entra app reg (or pass --client-id to reuse an existing one \u2014 required if you can't create app regs), writes ~/.m8t/config.yaml, ensures the resource group, and runs deploy/main.bicep. The repo is located via ~/.m8t/repo-root."
24391
24970
  });
24392
- subscription = Option40.String("--subscription");
24393
- resourceGroup = Option40.String("--resource-group", "rg-m8t-stack");
24394
- location = Option40.String("--location", "eastus");
24395
- suffix = Option40.String("--suffix", "");
24396
- imageRef = Option40.String("--image-ref", DEFAULT_IMAGE_REF);
24397
- acrPullIdentity = Option40.String("--acrpull-identity");
24398
- acrResourceId = Option40.String("--acr-resource-id");
24399
- foundryEndpoint = Option40.String("--foundry-endpoint");
24400
- foundryResourceId = Option40.String("--foundry-resource-id");
24401
- foundryTracing = Option40.String("--foundry-tracing");
24971
+ subscription = Option41.String("--subscription");
24972
+ resourceGroup = Option41.String("--resource-group", "rg-m8t-stack");
24973
+ location = Option41.String("--location", "eastus");
24974
+ suffix = Option41.String("--suffix", "");
24975
+ imageRef = Option41.String("--image-ref", DEFAULT_IMAGE_REF);
24976
+ // Gateway-only override. Empty ⇒ the gateway uses --image-ref, which is the
24977
+ // from-zero case. It exists because the gateway and the voice relay do NOT
24978
+ // always run the same image: a converge preserves a BYOC gateway on its own
24979
+ // ACR while the relay tracks the release on the public repo. A comparison
24980
+ // (`--what-if`) that can only express one image therefore reports the other
24981
+ // app as drift on every single run, forever — which is exactly what the
24982
+ // infra-drift gate did from 2026-08-11.
24983
+ gatewayImageRef = Option41.String("--gateway-image-ref", "");
24984
+ acrPullIdentity = Option41.String("--acrpull-identity");
24985
+ acrResourceId = Option41.String("--acr-resource-id");
24986
+ foundryEndpoint = Option41.String("--foundry-endpoint");
24987
+ foundryResourceId = Option41.String("--foundry-resource-id");
24988
+ foundryTracing = Option41.String("--foundry-tracing");
24402
24989
  // project | account | skip (bicep default: project)
24403
- clientId = Option40.String("--client-id");
24404
- whatIf = Option40.Boolean("--what-if", false);
24990
+ clientId = Option41.String("--client-id");
24991
+ whatIf = Option41.Boolean("--what-if", false);
24405
24992
  // Only meaningful with --what-if. Routes the comparison through the
24406
24993
  // value-free renderers (see ./lib/whatif-redact.js) instead of the default
24407
24994
  // before/after renderer. Defaults false so a local, interactive run keeps
24408
24995
  // showing values — that is the whole diagnostic point of --what-if.
24409
24996
  // Automation that forwards this output anywhere non-private (a CI log, an
24410
24997
  // issue) MUST pass --redact.
24411
- redact = Option40.Boolean("--redact", false);
24412
- output = Option40.String("--output");
24998
+ redact = Option41.Boolean("--redact", false);
24999
+ output = Option41.String("--output");
24413
25000
  // Subscription-scoped role assignments. Omitted ⇒ the template default (true).
24414
25001
  // Pass false when deploying as a principal scoped to the resource group only:
24415
25002
  // it cannot deploy at subscription scope, and those assignments persist
24416
25003
  // idempotently from the initial deployment anyway.
24417
- assignSubscriptionRoles = Option40.String("--assign-subscription-roles");
25004
+ assignSubscriptionRoles = Option41.String("--assign-subscription-roles");
24418
25005
  /**
24419
25006
  * The installer image the updater Container-Apps Job runs.
24420
25007
  *
@@ -24425,23 +25012,23 @@ var DeployCommand = class extends M8tCommand {
24425
25012
  * than that — the comparison proposes REMOVING an updater job that exists and
24426
25013
  * should, and reports it as drift on every single run.
24427
25014
  */
24428
- installerImage = Option40.String("--installer-image");
25015
+ installerImage = Option41.String("--installer-image");
24429
25016
  // Referee — all optional, undefined by default ⇒ the bicep defaults
24430
25017
  // apply (dormant: gatewayCpu=0.25, gatewayMemory=0.5Gi, every referee/exam var
24431
25018
  // empty). Only pass these when explicitly enabling the referee exam stack.
24432
- gatewayCpu = Option40.String("--gateway-cpu");
24433
- gatewayMemory = Option40.String("--gateway-memory");
24434
- refereeEnabled = Option40.String("--referee-enabled");
24435
- refereeBrainRepos = Option40.String("--referee-brain-repos");
24436
- refereeFeedRepo = Option40.String("--referee-feed-repo");
24437
- refereeInstallationId = Option40.String("--referee-installation-id");
24438
- refereeWebhookHmacKvUri = Option40.String("--referee-webhook-hmac-kv-uri");
24439
- examKvUri = Option40.String("--exam-kv-uri");
24440
- examLaWorkspaceId = Option40.String("--exam-la-workspace-id");
24441
- brainEvalDeployment = Option40.String("--brain-eval-deployment");
24442
- brainAppLogin = Option40.String("--brain-app-login");
24443
- refereeCheckpointDir = Option40.String("--referee-checkpoint-dir");
24444
- examApiBase = Option40.String("--exam-api-base");
25019
+ gatewayCpu = Option41.String("--gateway-cpu");
25020
+ gatewayMemory = Option41.String("--gateway-memory");
25021
+ refereeEnabled = Option41.String("--referee-enabled");
25022
+ refereeBrainRepos = Option41.String("--referee-brain-repos");
25023
+ refereeFeedRepo = Option41.String("--referee-feed-repo");
25024
+ refereeInstallationId = Option41.String("--referee-installation-id");
25025
+ refereeWebhookHmacKvUri = Option41.String("--referee-webhook-hmac-kv-uri");
25026
+ examKvUri = Option41.String("--exam-kv-uri");
25027
+ examLaWorkspaceId = Option41.String("--exam-la-workspace-id");
25028
+ brainEvalDeployment = Option41.String("--brain-eval-deployment");
25029
+ brainAppLogin = Option41.String("--brain-app-login");
25030
+ refereeCheckpointDir = Option41.String("--referee-checkpoint-dir");
25031
+ examApiBase = Option41.String("--exam-api-base");
24445
25032
  async executeCommand() {
24446
25033
  const mode = resolveOutputMode(
24447
25034
  this.output,
@@ -24483,6 +25070,7 @@ var DeployCommand = class extends M8tCommand {
24483
25070
  location: this.location,
24484
25071
  suffix: this.suffix,
24485
25072
  imageRef: this.imageRef,
25073
+ ...this.gatewayImageRef ? { gatewayImageRef: this.gatewayImageRef } : {},
24486
25074
  tenantId: account2.tenantId,
24487
25075
  clientId,
24488
25076
  foundryResourceId: foundryResourceId2,
@@ -24552,6 +25140,7 @@ var DeployCommand = class extends M8tCommand {
24552
25140
  location: this.location,
24553
25141
  suffix: this.suffix,
24554
25142
  imageRef: this.imageRef,
25143
+ ...this.gatewayImageRef ? { gatewayImageRef: this.gatewayImageRef } : {},
24555
25144
  tenantId: app.tenantId,
24556
25145
  clientId: app.clientId,
24557
25146
  foundryResourceId,
@@ -24622,7 +25211,7 @@ var DeployCommand = class extends M8tCommand {
24622
25211
 
24623
25212
  // src/commands/eval/skill.ts
24624
25213
  import { spawnSync as spawnSync4 } from "child_process";
24625
- import { Command as Command44, Option as Option41 } from "clipanion";
25214
+ import { Command as Command45, Option as Option42 } from "clipanion";
24626
25215
  init_errors();
24627
25216
  var DECISIONS = /* @__PURE__ */ new Set(["promote", "reject", "needs_review"]);
24628
25217
  var JUDGE_STATUSES = /* @__PURE__ */ new Set(["ok", "skipped", "unavailable"]);
@@ -24647,14 +25236,14 @@ function parseVerdict(stdout) {
24647
25236
  }
24648
25237
  var EvalSkillCommand = class extends M8tCommand {
24649
25238
  static paths = [["eval", "skill"]];
24650
- static usage = Command44.Usage({
25239
+ static usage = Command45.Usage({
24651
25240
  description: "Vet one inbox skill candidate: promote / reject / needs_review. Shells out to the Python `brain-eval` core (override its path with $BRAIN_EVAL_BIN)."
24652
25241
  });
24653
- candidate = Option41.String();
24654
- skillsDir = Option41.String("--skills-dir");
24655
- noJudge = Option41.Boolean("--no-judge", false);
24656
- deployment = Option41.String("--deployment");
24657
- output = Option41.String("--output");
25242
+ candidate = Option42.String();
25243
+ skillsDir = Option42.String("--skills-dir");
25244
+ noJudge = Option42.Boolean("--no-judge", false);
25245
+ deployment = Option42.String("--deployment");
25246
+ output = Option42.String("--output");
24658
25247
  executeCommand() {
24659
25248
  return Promise.resolve(this._runCommand());
24660
25249
  }
@@ -24713,7 +25302,7 @@ var EvalSkillCommand = class extends M8tCommand {
24713
25302
  import { spawnSync as spawnSync5 } from "child_process";
24714
25303
  import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync21, existsSync as existsSync20, readdirSync as readdirSync3 } from "fs";
24715
25304
  import { join as join30 } from "path";
24716
- import { Command as Command45, Option as Option42 } from "clipanion";
25305
+ import { Command as Command46, Option as Option43 } from "clipanion";
24717
25306
  init_errors();
24718
25307
  init_esm();
24719
25308
  function parseArmToken(tok, opts) {
@@ -24943,24 +25532,24 @@ function buildPlan(args) {
24943
25532
  }
24944
25533
  var EvalExamCommand = class extends M8tCommand {
24945
25534
  static paths = [["eval", "exam"]];
24946
- static usage = Command45.Usage({
25535
+ static usage = Command46.Usage({
24947
25536
  description: "Run a brain exam: impact A/B + dream-delta. Resolves the plan, then shells out to the Python `brain-exam` orchestrator (override its path with $BRAIN_EXAM_BIN). Renders an ExamVerdict: three-valued verdict + power note + per-task flips."
24948
25537
  });
24949
- worker = Option42.String();
24950
- arms = Option42.String("--arms");
24951
- taskSet = Option42.String("--task-set");
24952
- examType = Option42.String("--exam-type");
24953
- skill = Option42.String("--skill");
24954
- reps = Option42.String("-n,--reps");
24955
- probes = Option42.String("--probes");
24956
- pool = Option42.String("--pool");
24957
- out = Option42.String("--out");
24958
- dryRun = Option42.Boolean("--dry-run", false);
24959
- keepArms = Option42.Boolean("--keep-arms", false);
24960
- allowStub = Option42.Boolean("--allow-stub", false);
24961
- deployment = Option42.String("--deployment");
24962
- output = Option42.String("--output");
24963
- observeWaitS = Option42.String("--observe-wait-s");
25538
+ worker = Option43.String();
25539
+ arms = Option43.String("--arms");
25540
+ taskSet = Option43.String("--task-set");
25541
+ examType = Option43.String("--exam-type");
25542
+ skill = Option43.String("--skill");
25543
+ reps = Option43.String("-n,--reps");
25544
+ probes = Option43.String("--probes");
25545
+ pool = Option43.String("--pool");
25546
+ out = Option43.String("--out");
25547
+ dryRun = Option43.Boolean("--dry-run", false);
25548
+ keepArms = Option43.Boolean("--keep-arms", false);
25549
+ allowStub = Option43.Boolean("--allow-stub", false);
25550
+ deployment = Option43.String("--deployment");
25551
+ output = Option43.String("--output");
25552
+ observeWaitS = Option43.String("--observe-wait-s");
24964
25553
  async executeCommand() {
24965
25554
  await Promise.resolve();
24966
25555
  const worker = typeof this.worker === "string" ? this.worker : void 0;
@@ -25075,10 +25664,10 @@ var EvalExamCommand = class extends M8tCommand {
25075
25664
  };
25076
25665
 
25077
25666
  // src/commands/version.ts
25078
- import { Command as Command46, Option as Option43 } from "clipanion";
25667
+ import { Command as Command47, Option as Option44 } from "clipanion";
25079
25668
  var VersionCommand = class extends M8tCommand {
25080
25669
  static paths = [["version"], ["--version"], ["-v"]];
25081
- static usage = Command46.Usage({
25670
+ static usage = Command47.Usage({
25082
25671
  description: "Print the CLI version.",
25083
25672
  details: "Prints the m8t CLI version. With --verbose, also prints Node version and platform.",
25084
25673
  examples: [
@@ -25086,8 +25675,8 @@ var VersionCommand = class extends M8tCommand {
25086
25675
  ["Print as JSON", "$0 version --output json"]
25087
25676
  ]
25088
25677
  });
25089
- output = Option43.String("--output", { description: "pretty | json | auto (default)" });
25090
- verbose = Option43.Boolean("--verbose", false);
25678
+ output = Option44.String("--output", { description: "pretty | json | auto (default)" });
25679
+ verbose = Option44.Boolean("--verbose", false);
25091
25680
  executeCommand() {
25092
25681
  const mode = resolveOutputMode(
25093
25682
  this.output ?? "auto",
@@ -25118,18 +25707,18 @@ var VersionCommand = class extends M8tCommand {
25118
25707
  };
25119
25708
 
25120
25709
  // src/commands/whoami.ts
25121
- import { Command as Command47, Option as Option44 } from "clipanion";
25710
+ import { Command as Command48, Option as Option45 } from "clipanion";
25122
25711
  var WhoamiCommand = class extends M8tCommand {
25123
25712
  static paths = [["whoami"]];
25124
- static usage = Command47.Usage({
25713
+ static usage = Command48.Usage({
25125
25714
  description: "Show your identity + the gateway you'll talk to. Probes the backend."
25126
25715
  });
25127
- output = Option44.String("--output");
25128
- verbose = Option44.Boolean("--verbose", false);
25129
- subscription = Option44.String("--subscription", {
25716
+ output = Option45.String("--output");
25717
+ verbose = Option45.Boolean("--verbose", false);
25718
+ subscription = Option45.String("--subscription", {
25130
25719
  description: "Azure subscription ID to discover gateway in (defaults to active az subscription)."
25131
25720
  });
25132
- resourceGroup = Option44.String("--resource-group", {
25721
+ resourceGroup = Option45.String("--resource-group", {
25133
25722
  description: "m8t resource group to disambiguate the gateway (multi-deployment subscriptions)."
25134
25723
  });
25135
25724
  async executeCommand() {
@@ -25194,7 +25783,7 @@ var WhoamiCommand = class extends M8tCommand {
25194
25783
  };
25195
25784
 
25196
25785
  // src/commands/status.ts
25197
- import { Command as Command48, Option as Option45 } from "clipanion";
25786
+ import { Command as Command49, Option as Option46 } from "clipanion";
25198
25787
 
25199
25788
  // src/lib/azd.ts
25200
25789
  init_errors();
@@ -25259,10 +25848,10 @@ async function resolveLocalContext() {
25259
25848
  // src/commands/status.ts
25260
25849
  var StatusCommand = class extends M8tCommand {
25261
25850
  static paths = [["status"]];
25262
- static usage = Command48.Usage({
25851
+ static usage = Command49.Usage({
25263
25852
  description: "Show the local m8t context: identity, config.yaml, gateway cache, azd mode."
25264
25853
  });
25265
- output = Option45.String("--output");
25854
+ output = Option46.String("--output");
25266
25855
  async executeCommand() {
25267
25856
  const mode = resolveOutputMode(
25268
25857
  this.output,
@@ -25300,8 +25889,8 @@ var StatusCommand = class extends M8tCommand {
25300
25889
  };
25301
25890
 
25302
25891
  // src/commands/doctor.ts
25303
- import { Command as Command49, Option as Option46 } from "clipanion";
25304
- import { DefaultAzureCredential as DefaultAzureCredential23 } from "@azure/identity";
25892
+ import { Command as Command50, Option as Option47 } from "clipanion";
25893
+ import { DefaultAzureCredential as DefaultAzureCredential24 } from "@azure/identity";
25305
25894
  import * as fs30 from "fs";
25306
25895
  import * as os12 from "os";
25307
25896
  import * as path33 from "path";
@@ -25505,6 +26094,43 @@ function checkLegacyStateDir(probe) {
25505
26094
  remediation: "review ~/.m8t-stack for anything you still need, then remove it"
25506
26095
  };
25507
26096
  }
26097
+ function checkOutboundEmail(probe) {
26098
+ const name = "outbound email";
26099
+ const intended = probe.stampFeature?.enabled === true;
26100
+ if (!intended) {
26101
+ return {
26102
+ name,
26103
+ status: "INFO",
26104
+ detail: "off \u2014 advisor handoffs render but do not send",
26105
+ remediation: "m8t platform email on"
26106
+ };
26107
+ }
26108
+ if (probe.executorEnv === null) {
26109
+ return {
26110
+ name,
26111
+ status: "WARN",
26112
+ detail: `enabled in the stamp, but no ${probe.executorName} is deployed \u2014 nothing can send`,
26113
+ remediation: `m8t azure-exec deploy ${probe.executorName} --resource-group <rg> --brain <owner/repo>`
26114
+ };
26115
+ }
26116
+ const endpoint = probe.executorEnv.M8T_ACS_ENDPOINT ?? "";
26117
+ const sender = probe.executorEnv.M8T_ACS_SENDER ?? "";
26118
+ const flag = probe.executorEnv.M8T_EMAIL_ENABLED ?? "";
26119
+ if (endpoint !== "" && sender !== "" && flag !== "") {
26120
+ return { name, status: "PASS", detail: `on \u2014 sending as ${sender}` };
26121
+ }
26122
+ const missing = [
26123
+ ...flag === "" ? ["M8T_EMAIL_ENABLED"] : [],
26124
+ ...endpoint === "" ? ["M8T_ACS_ENDPOINT"] : [],
26125
+ ...sender === "" ? ["M8T_ACS_SENDER"] : []
26126
+ ];
26127
+ return {
26128
+ name,
26129
+ status: "FAIL",
26130
+ detail: `MISCONFIGURED \u2014 the stamp says email is on, but ${probe.executorName} is missing ${missing.join(", ")}. Ezra will refuse to send and report a platform bug.`,
26131
+ remediation: "m8t platform email on"
26132
+ };
26133
+ }
25508
26134
 
25509
26135
  // src/commands/doctor.ts
25510
26136
  init_foundry_agent_get();
@@ -25811,12 +26437,12 @@ function probeLegacyStateDir() {
25811
26437
  }
25812
26438
  var DoctorCommand = class extends M8tCommand {
25813
26439
  static paths = [["doctor"]];
25814
- static usage = Command49.Usage({
26440
+ static usage = Command50.Usage({
25815
26441
  description: "Diagnose the local m8t setup: az login, config.yaml, gateway, Foundry data-plane."
25816
26442
  });
25817
- output = Option46.String("--output");
25818
- agent = Option46.String("--agent");
25819
- resourceGroup = Option46.String("--resource-group", {
26443
+ output = Option47.String("--output");
26444
+ agent = Option47.String("--agent");
26445
+ resourceGroup = Option47.String("--resource-group", {
25820
26446
  description: "m8t resource group to disambiguate the gateway (multi-deployment subscriptions)."
25821
26447
  });
25822
26448
  async executeCommand() {
@@ -25889,7 +26515,7 @@ var DoctorCommand = class extends M8tCommand {
25889
26515
  let kvStatus = 0;
25890
26516
  if (kv) {
25891
26517
  try {
25892
- const token = await new DefaultAzureCredential23().getToken("https://vault.azure.net/.default");
26518
+ const token = await new DefaultAzureCredential24().getToken("https://vault.azure.net/.default");
25893
26519
  const res = await fetch(`${kv.uri.replace(/\/$/, "")}/secrets?api-version=7.4&maxresults=1`, {
25894
26520
  headers: { Authorization: `Bearer ${token.token}` }
25895
26521
  });
@@ -25906,10 +26532,64 @@ var DoctorCommand = class extends M8tCommand {
25906
26532
  const region = foundryAccountId ? await resolveAccountLocation(foundryAccountId) : null;
25907
26533
  const usages = region ? await listModelQuota(region) : [];
25908
26534
  emit(checkModelQuota(deployments, usages));
26535
+ checking("outbound email");
26536
+ try {
26537
+ const credential2 = new DefaultAzureCredential24();
26538
+ const outcome = platformRg && platformSub ? await readStampOutcome({ credential: credential2, subscriptionId: platformSub, resourceGroup: platformRg }).catch(
26539
+ () => ({ source: "unreadable" })
26540
+ ) : { source: "unreadable" };
26541
+ if (outcome.source === "unreadable") {
26542
+ emit({
26543
+ name: "outbound email",
26544
+ status: "INFO",
26545
+ detail: "could not read the recorded intent (platform stamp unreadable) \u2014 state unknown, not necessarily off"
26546
+ });
26547
+ } else {
26548
+ const { agentName: executorName } = await resolveExecutorAgentName({
26549
+ credential: credential2,
26550
+ projectEndpoint: foundry.projectEndpoint
26551
+ });
26552
+ const stamp = outcome.source === "explicit" ? outcome.stamp : null;
26553
+ let executorEnv = null;
26554
+ let executorUnreadable = null;
26555
+ try {
26556
+ const cur = await getAgentVersion({
26557
+ credential: credential2,
26558
+ projectEndpoint: foundry.projectEndpoint,
26559
+ agentName: executorName
26560
+ });
26561
+ executorEnv = cur.definition.environment_variables ?? {};
26562
+ } catch (err) {
26563
+ if (err.code === "AGENT_NOT_FOUND") executorEnv = null;
26564
+ else executorUnreadable = err instanceof Error ? err.message : String(err);
26565
+ }
26566
+ if (executorUnreadable !== null) {
26567
+ emit({
26568
+ name: "outbound email",
26569
+ status: "INFO",
26570
+ detail: `could not read ${executorName} (${executorUnreadable}) \u2014 wiring unknown`
26571
+ });
26572
+ } else {
26573
+ emit(
26574
+ checkOutboundEmail({
26575
+ stampFeature: stamp?.features?.email ?? null,
26576
+ executorEnv,
26577
+ executorName
26578
+ })
26579
+ );
26580
+ }
26581
+ }
26582
+ } catch (e) {
26583
+ emit({
26584
+ name: "outbound email",
26585
+ status: "INFO",
26586
+ detail: `could not check: ${e instanceof Error ? e.message : String(e)}`
26587
+ });
26588
+ }
25909
26589
  if (typeof this.agent === "string" && this.agent) {
25910
26590
  checking(`delivery grant for ${this.agent}`);
25911
26591
  try {
25912
- const credential2 = new DefaultAzureCredential23();
26592
+ const credential2 = new DefaultAzureCredential24();
25913
26593
  const cur = await getAgentVersion({
25914
26594
  credential: credential2,
25915
26595
  projectEndpoint: foundry.projectEndpoint,
@@ -25952,11 +26632,11 @@ var DoctorCommand = class extends M8tCommand {
25952
26632
  };
25953
26633
 
25954
26634
  // src/commands/prereqs.ts
25955
- import { Command as Command50, Option as Option47 } from "clipanion";
26635
+ import { Command as Command51, Option as Option48 } from "clipanion";
25956
26636
  init_errors();
25957
26637
 
25958
26638
  // src/lib/prereq-deps.ts
25959
- import { DefaultAzureCredential as DefaultAzureCredential24 } from "@azure/identity";
26639
+ import { DefaultAzureCredential as DefaultAzureCredential25 } from "@azure/identity";
25960
26640
 
25961
26641
  // src/lib/bootstrap-preflight.ts
25962
26642
  var ADMIN_DIRECTORY_ROLES = ["Global Administrator", "Application Administrator", "Cloud Application Administrator"];
@@ -26068,7 +26748,7 @@ function buildPrereqDeps(opts = {}) {
26068
26748
  probeRedirectUri,
26069
26749
  fixFoundryAccess,
26070
26750
  fixKeyVaultAccess,
26071
- credential: () => credentialSingleton2 ??= new DefaultAzureCredential24()
26751
+ credential: () => credentialSingleton2 ??= new DefaultAzureCredential25()
26072
26752
  };
26073
26753
  }
26074
26754
 
@@ -26658,7 +27338,7 @@ function renderVerdict(v) {
26658
27338
  }
26659
27339
  var PrereqsCommand = class extends M8tCommand {
26660
27340
  static paths = [["prereqs"]];
26661
- static usage = Command50.Usage({
27341
+ static usage = Command51.Usage({
26662
27342
  description: "Check (and optionally fix) everything m8t needs \u2014 to install, or for you to use it.",
26663
27343
  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, soft-deleted Cognitive Services accounts (they hold their model quota until purged), 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.",
26664
27344
  examples: [
@@ -26669,15 +27349,15 @@ var PrereqsCommand = class extends M8tCommand {
26669
27349
  ["Set a teammate up (needs role-assignment rights)", "$0 prereqs --fix --for alice@contoso.com"]
26670
27350
  ]
26671
27351
  });
26672
- fix = Option47.Boolean("--fix", false, { description: "Repair what is repairable. Without it the command only reports." });
26673
- for_ = Option47.String("--for", { description: "UPN or object id of another person. Usage phase only." });
26674
- phase = Option47.String("--phase", { description: "Force 'install' or 'usage' instead of auto-detecting." });
26675
- region = Option47.String("--region", { description: "Target region. Enables the install-phase quota check, and judges soft-deleted accounts against the region you will install into (without it, they only warn)." });
26676
- model = Option47.String("--model", { description: `Model whose quota the install needs (default ${INSTALL_REASONING_MODEL}).` });
26677
- clientId = Option47.String("--client-id", { description: "A ready app registration, in place of directory-admin rights." });
26678
- subscription = Option47.String("--subscription");
26679
- resourceGroup = Option47.String("--resource-group");
26680
- output = Option47.String("--output");
27352
+ fix = Option48.Boolean("--fix", false, { description: "Repair what is repairable. Without it the command only reports." });
27353
+ for_ = Option48.String("--for", { description: "UPN or object id of another person. Usage phase only." });
27354
+ phase = Option48.String("--phase", { description: "Force 'install' or 'usage' instead of auto-detecting." });
27355
+ region = Option48.String("--region", { description: "Target region. Enables the install-phase quota check, and judges soft-deleted accounts against the region you will install into (without it, they only warn)." });
27356
+ model = Option48.String("--model", { description: `Model whose quota the install needs (default ${INSTALL_REASONING_MODEL}).` });
27357
+ clientId = Option48.String("--client-id", { description: "A ready app registration, in place of directory-admin rights." });
27358
+ subscription = Option48.String("--subscription");
27359
+ resourceGroup = Option48.String("--resource-group");
27360
+ output = Option48.String("--output");
26681
27361
  async executeCommand() {
26682
27362
  const mode = resolveOutputMode(this.output, this.context.stdout);
26683
27363
  const json = mode === "json";
@@ -26733,7 +27413,7 @@ var PrereqsCommand = class extends M8tCommand {
26733
27413
  };
26734
27414
 
26735
27415
  // src/commands/switch.ts
26736
- import { Command as Command51, Option as Option48 } from "clipanion";
27416
+ import { Command as Command52, Option as Option49 } from "clipanion";
26737
27417
 
26738
27418
  // src/lib/profiles.ts
26739
27419
  import * as fs31 from "fs/promises";
@@ -26873,14 +27553,14 @@ async function profileSwitch(name, asName) {
26873
27553
  init_errors();
26874
27554
  var SwitchCommand = class extends M8tCommand {
26875
27555
  static paths = [["switch"]];
26876
- static usage = Command51.Usage({
27556
+ static usage = Command52.Usage({
26877
27557
  description: "Re-point local config at another deployment: --subscription <id|name> (discovery) or <profile>."
26878
27558
  });
26879
- profile = Option48.String({ required: false });
26880
- subscription = Option48.String("--subscription");
26881
- list = Option48.Boolean("--list", false);
26882
- as = Option48.String("--as");
26883
- output = Option48.String("--output");
27559
+ profile = Option49.String({ required: false });
27560
+ subscription = Option49.String("--subscription");
27561
+ list = Option49.Boolean("--list", false);
27562
+ as = Option49.String("--as");
27563
+ output = Option49.String("--output");
26884
27564
  async executeCommand() {
26885
27565
  const mode = resolveOutputMode(
26886
27566
  this.output,
@@ -26937,7 +27617,7 @@ var SwitchCommand = class extends M8tCommand {
26937
27617
 
26938
27618
  // src/commands/open.ts
26939
27619
  import { spawn as spawn5 } from "child_process";
26940
- import { Command as Command52, Option as Option49 } from "clipanion";
27620
+ import { Command as Command53, Option as Option50 } from "clipanion";
26941
27621
 
26942
27622
  // src/lib/open-targets.ts
26943
27623
  init_errors();
@@ -26983,14 +27663,14 @@ function openUrl(url) {
26983
27663
  }
26984
27664
  var OpenCommand = class extends M8tCommand {
26985
27665
  static paths = [["open"]];
26986
- static usage = Command52.Usage({
27666
+ static usage = Command53.Usage({
26987
27667
  description: "Open the deployed webapp (default), the Foundry portal, or the resource group.",
26988
27668
  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)."
26989
27669
  });
26990
- target = Option49.String({ required: false });
26991
- print = Option49.Boolean("--print", false);
26992
- output = Option49.String("--output");
26993
- resourceGroup = Option49.String("--resource-group", {
27670
+ target = Option50.String({ required: false });
27671
+ print = Option50.Boolean("--print", false);
27672
+ output = Option50.String("--output");
27673
+ resourceGroup = Option50.String("--resource-group", {
26994
27674
  description: "m8t resource group to disambiguate the gateway (multi-deployment subscriptions)."
26995
27675
  });
26996
27676
  async executeCommand() {
@@ -27034,7 +27714,7 @@ var OpenCommand = class extends M8tCommand {
27034
27714
  };
27035
27715
 
27036
27716
  // src/commands/dream/run.ts
27037
- import { Command as Command53, Option as Option50 } from "clipanion";
27717
+ import { Command as Command54, Option as Option51 } from "clipanion";
27038
27718
  import { AzureCliCredential } from "@azure/identity";
27039
27719
  import { TableClient as TableClient7 } from "@azure/data-tables";
27040
27720
  import { AIProjectClient as AIProjectClient3 } from "@azure/ai-projects";
@@ -29328,28 +30008,28 @@ function redactTranscripts(input) {
29328
30008
  }
29329
30009
  var DreamRunCommand = class extends M8tCommand {
29330
30010
  static paths = [["dream", "run"]];
29331
- static usage = Command53.Usage({
30011
+ static usage = Command54.Usage({
29332
30012
  description: "Dry-run the brain consumption pipeline for one worker (no model call, no writes). Pass --live to actually run it.",
29333
30013
  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.\n\nThe bare command is READ-ONLY. --live takes the other branch: a real model call and a commit to the worker's brain repo through a minted GitHub App token."
29334
30014
  });
29335
- worker = Option50.String("--worker", { description: "Worker (canonical name) to harvest. Required." });
30015
+ worker = Option51.String("--worker", { description: "Worker (canonical name) to harvest. Required." });
29336
30016
  // Opting IN to the side effects, rather than opting out of them. This command's
29337
30017
  // own help has always described a dry run, but the bare invocation used to take
29338
30018
  // the live branch — a real model call and a commit to the brain repo — so anyone
29339
30019
  // acting on `--help` got the opposite of what they read.
29340
- live = Option50.Boolean("--live", false, { description: "Actually run it: model call + commit to the brain repo." });
29341
- dryRun = Option50.Boolean("--dry-run", false, { description: "Read-only harvest. This is the default; the flag is accepted for compatibility." });
29342
- since = Option50.String("--since", { description: "ISO-8601 start override (rejected if malformed or future)." });
29343
- reset = Option50.Boolean("--reset", false, { description: "Ignore the stored cursor; read from the beginning." });
29344
- showTranscripts = Option50.Boolean("--show-transcripts", false, {
30020
+ live = Option51.Boolean("--live", false, { description: "Actually run it: model call + commit to the brain repo." });
30021
+ dryRun = Option51.Boolean("--dry-run", false, { description: "Read-only harvest. This is the default; the flag is accepted for compatibility." });
30022
+ since = Option51.String("--since", { description: "ISO-8601 start override (rejected if malformed or future)." });
30023
+ reset = Option51.Boolean("--reset", false, { description: "Ignore the stored cursor; read from the beginning." });
30024
+ showTranscripts = Option51.Boolean("--show-transcripts", false, {
29345
30025
  description: "Print transcript bodies (default: metadata only)."
29346
30026
  });
29347
- subscription = Option50.String("--subscription");
29348
- endpoint = Option50.String("--endpoint");
29349
- storageAccount = Option50.String("--storage-account", {
30027
+ subscription = Option51.String("--subscription");
30028
+ endpoint = Option51.String("--endpoint");
30029
+ storageAccount = Option51.String("--storage-account", {
29350
30030
  description: "Ledger storage account name (skips tag-based discovery)"
29351
30031
  });
29352
- output = Option50.String("--output");
30032
+ output = Option51.String("--output");
29353
30033
  // Resolution seam — overridden by tests; built lazily at runtime otherwise.
29354
30034
  deps;
29355
30035
  async executeCommand() {
@@ -29700,7 +30380,7 @@ function defaultDeps(overrides) {
29700
30380
 
29701
30381
  // src/commands/conversations/sweep.ts
29702
30382
  import { createHash as createHash4 } from "crypto";
29703
- import { Command as Command54, Option as Option51 } from "clipanion";
30383
+ import { Command as Command55, Option as Option52 } from "clipanion";
29704
30384
  import { AzureCliCredential as AzureCliCredential2 } from "@azure/identity";
29705
30385
  import { TableClient as TableClient8 } from "@azure/data-tables";
29706
30386
  import { AIProjectClient as AIProjectClient4 } from "@azure/ai-projects";
@@ -29826,7 +30506,7 @@ function defaultDeps2() {
29826
30506
  }
29827
30507
  var ConversationsSweepCommand = class extends M8tCommand {
29828
30508
  static paths = [["conversations", "sweep"]];
29829
- static usage = Command54.Usage({
30509
+ static usage = Command55.Usage({
29830
30510
  category: "Conversations",
29831
30511
  description: "Delete expired public-visitor conversations (dry run by default)",
29832
30512
  details: `
@@ -29842,22 +30522,22 @@ var ConversationsSweepCommand = class extends M8tCommand {
29842
30522
  ["Delete, capped at 50", "m8t conversations sweep --delete --max 50"]
29843
30523
  ]
29844
30524
  });
29845
- doDelete = Option51.Boolean("--delete", false, {
30525
+ doDelete = Option52.Boolean("--delete", false, {
29846
30526
  description: "Perform deletions (without this flag the command only reports)"
29847
30527
  });
29848
- principal = Option51.String("--principal", {
30528
+ principal = Option52.String("--principal", {
29849
30529
  description: "Service-principal oid whose end-user keys are known-ephemeral (required with --delete; a dry run without it reports candidates grouped by principal)"
29850
30530
  });
29851
- max = Option51.String("--max", "200", { description: "Maximum deletions per run" });
29852
- graceDays = Option51.String("--grace-days", "14", {
30531
+ max = Option52.String("--max", "200", { description: "Maximum deletions per run" });
30532
+ graceDays = Option52.String("--grace-days", "14", {
29853
30533
  description: "Days past the 30-day key life before a conversation is eligible"
29854
30534
  });
29855
- lookbackDays = Option51.String("--lookback-days", "180", {
30535
+ lookbackDays = Option52.String("--lookback-days", "180", {
29856
30536
  description: "How far back to scan ledger activity for candidates"
29857
30537
  });
29858
- subscription = Option51.String("--subscription", { description: "Azure subscription id override" });
29859
- endpoint = Option51.String("--endpoint", { description: "Foundry project endpoint override" });
29860
- storageAccount = Option51.String("--storage-account", {
30538
+ subscription = Option52.String("--subscription", { description: "Azure subscription id override" });
30539
+ endpoint = Option52.String("--endpoint", { description: "Foundry project endpoint override" });
30540
+ storageAccount = Option52.String("--storage-account", {
29861
30541
  description: "Ledger storage account name (skips tag-based discovery)"
29862
30542
  });
29863
30543
  deps = defaultDeps2();
@@ -29985,7 +30665,7 @@ var ConversationsSweepCommand = class extends M8tCommand {
29985
30665
  };
29986
30666
 
29987
30667
  // src/commands/foundry/create.ts
29988
- import { Command as Command55, Option as Option52 } from "clipanion";
30668
+ import { Command as Command56, Option as Option53 } from "clipanion";
29989
30669
 
29990
30670
  // src/lib/foundry-create.ts
29991
30671
  init_errors();
@@ -30223,7 +30903,7 @@ async function createFoundryProject(args) {
30223
30903
  init_errors();
30224
30904
  var FoundryCreateCommand = class extends M8tCommand {
30225
30905
  static paths = [["foundry", "create"]];
30226
- static usage = Command55.Usage({
30906
+ static usage = Command56.Usage({
30227
30907
  description: "Create an AI Foundry (AIServices) account + project + model deployment from scratch.",
30228
30908
  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).",
30229
30909
  examples: [
@@ -30232,16 +30912,16 @@ var FoundryCreateCommand = class extends M8tCommand {
30232
30912
  ["Higher capacity for a reasoning model", "$0 foundry create --resource-group rg-m8t-stack --location eastus2 --model gpt-5-mini --capacity 250"]
30233
30913
  ]
30234
30914
  });
30235
- resourceGroup = Option52.String("--resource-group");
30236
- location = Option52.String("--location");
30237
- account = Option52.String("--account");
30238
- project = Option52.String("--project", "m8t");
30239
- model = Option52.String("--model", "gpt-4.1-mini");
30240
- modelVersion = Option52.String("--model-version", "2025-04-14");
30241
- capacity = Option52.String("--capacity", "50");
30242
- subscription = Option52.String("--subscription");
30243
- skipQuotaCheck = Option52.Boolean("--skip-quota-check", false);
30244
- output = Option52.String("--output");
30915
+ resourceGroup = Option53.String("--resource-group");
30916
+ location = Option53.String("--location");
30917
+ account = Option53.String("--account");
30918
+ project = Option53.String("--project", "m8t");
30919
+ model = Option53.String("--model", "gpt-4.1-mini");
30920
+ modelVersion = Option53.String("--model-version", "2025-04-14");
30921
+ capacity = Option53.String("--capacity", "50");
30922
+ subscription = Option53.String("--subscription");
30923
+ skipQuotaCheck = Option53.Boolean("--skip-quota-check", false);
30924
+ output = Option53.String("--output");
30245
30925
  async executeCommand() {
30246
30926
  const mode = resolveOutputMode(
30247
30927
  this.output,
@@ -30313,22 +30993,22 @@ var FoundryCreateCommand = class extends M8tCommand {
30313
30993
  };
30314
30994
 
30315
30995
  // src/commands/foundry/await-ready.ts
30316
- import { Command as Command56, Option as Option53 } from "clipanion";
30996
+ import { Command as Command57, Option as Option54 } from "clipanion";
30317
30997
  import { AzureCliCredential as AzureCliCredential3 } from "@azure/identity";
30318
30998
  init_errors();
30319
30999
  var FoundryAwaitReadyCommand = class extends M8tCommand {
30320
31000
  static paths = [["foundry", "await-ready"]];
30321
- static usage = Command56.Usage({
31001
+ static usage = Command57.Usage({
30322
31002
  description: "Wait until a freshly-created Foundry project's data plane reliably serves it.",
30323
31003
  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.",
30324
31004
  examples: [["Wait for a project to be ready", "$0 foundry await-ready --endpoint https://acc.services.ai.azure.com/api/projects/m8t"]]
30325
31005
  });
30326
- endpoint = Option53.String("--endpoint");
30327
- consecutive = Option53.String("--consecutive", "3");
30328
- attempts = Option53.String("--attempts", "60");
30329
- interval = Option53.String("--interval", "5");
30330
- subscription = Option53.String("--subscription");
30331
- output = Option53.String("--output");
31006
+ endpoint = Option54.String("--endpoint");
31007
+ consecutive = Option54.String("--consecutive", "3");
31008
+ attempts = Option54.String("--attempts", "60");
31009
+ interval = Option54.String("--interval", "5");
31010
+ subscription = Option54.String("--subscription");
31011
+ output = Option54.String("--output");
30332
31012
  async executeCommand() {
30333
31013
  const mode = resolveOutputMode(this.output, this.context.stdout);
30334
31014
  const endpoint = typeof this.endpoint === "string" ? this.endpoint : void 0;
@@ -30362,7 +31042,7 @@ var FoundryAwaitReadyCommand = class extends M8tCommand {
30362
31042
  };
30363
31043
 
30364
31044
  // src/commands/bootstrap/preflight.ts
30365
- import { Command as Command57, Option as Option54 } from "clipanion";
31045
+ import { Command as Command58, Option as Option55 } from "clipanion";
30366
31046
 
30367
31047
  // ../../packages/telemetry-contract/artifact/tier-map.ts
30368
31048
  var EVENT_TIERS = {
@@ -30414,7 +31094,7 @@ function preflightRenderable(results) {
30414
31094
  }
30415
31095
  var BootstrapPreflightCommand = class extends M8tCommand {
30416
31096
  static paths = [["bootstrap", "preflight"]];
30417
- static usage = Command57.Usage({
31097
+ static usage = Command58.Usage({
30418
31098
  description: "Loudly verify you can install m8t (Owner/UAA + directory admin) and hard-stop if not.",
30419
31099
  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), soft-deleted Cognitive Services accounts (they keep their model quota until purged, so quota can read free and the model deployment still fail), 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.",
30420
31100
  examples: [
@@ -30423,9 +31103,9 @@ var BootstrapPreflightCommand = class extends M8tCommand {
30423
31103
  ["BYO app registration (directory guests)", "$0 bootstrap preflight --client-id <appId>"]
30424
31104
  ]
30425
31105
  });
30426
- clientId = Option54.String("--client-id");
30427
- subscription = Option54.String("--subscription");
30428
- location = Option54.String("--location", {
31106
+ clientId = Option55.String("--client-id");
31107
+ subscription = Option55.String("--subscription");
31108
+ location = Option55.String("--location", {
30429
31109
  description: "Target region. Enables the model-quota check \u2014 without it, quota is not verified."
30430
31110
  });
30431
31111
  async executeCommand() {
@@ -30525,7 +31205,7 @@ ${colors.error(" " + why)}
30525
31205
  import * as fs34 from "fs";
30526
31206
  import * as os15 from "os";
30527
31207
  import * as path37 from "path";
30528
- import { Command as Command58, Option as Option55 } from "clipanion";
31208
+ import { Command as Command59, Option as Option56 } from "clipanion";
30529
31209
  init_errors();
30530
31210
 
30531
31211
  // src/lib/bootstrap-mi.ts
@@ -30895,7 +31575,7 @@ var ACI_NAME = "m8t-installer";
30895
31575
  var MI_NAME = "m8t-installer-mi";
30896
31576
  var BootstrapLaunchCommand = class extends M8tCommand {
30897
31577
  static paths = [["bootstrap", "launch"]];
30898
- static usage = Command58.Usage({
31578
+ static usage = Command59.Usage({
30899
31579
  description: "Create + authorize the installer managed identity, then kick the cloud installer.",
30900
31580
  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` and `reap`, and threads your Azure object id so the installer can grant you the platform's data-plane roles itself.",
30901
31581
  examples: [
@@ -30906,26 +31586,26 @@ var BootstrapLaunchCommand = class extends M8tCommand {
30906
31586
  ["Share a support contact", "$0 bootstrap launch --location eastus2 --contact-email you@example.com --company 'Acme'"]
30907
31587
  ]
30908
31588
  });
30909
- location = Option55.String("--location");
30910
- resourceGroup = Option55.String("--resource-group");
30911
- clientId = Option55.String("--client-id");
30912
- subscription = Option55.String("--subscription");
30913
- installerTag = Option55.String("--installer-tag");
31589
+ location = Option56.String("--location");
31590
+ resourceGroup = Option56.String("--resource-group");
31591
+ clientId = Option56.String("--client-id");
31592
+ subscription = Option56.String("--subscription");
31593
+ installerTag = Option56.String("--installer-tag");
30914
31594
  // Full image ref override (registry + repo + tag) — an escape hatch when the
30915
31595
  // default org/tag is wrong for the current CLI (e.g. a stale published build).
30916
31596
  // Wins over --installer-tag / the pinned default.
30917
- installerImage = Option55.String("--installer-image");
30918
- gatewayImageRef = Option55.String("--gateway-image-ref");
30919
- githubAppCreds = Option55.String("--github-app-creds");
30920
- contactEmail = Option55.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
30921
- company = Option55.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
31597
+ installerImage = Option56.String("--installer-image");
31598
+ gatewayImageRef = Option56.String("--gateway-image-ref");
31599
+ githubAppCreds = Option56.String("--github-app-creds");
31600
+ contactEmail = Option56.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
31601
+ company = Option56.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
30922
31602
  // Value-carrying on purpose: a bare --force would be cargo-culted into
30923
31603
  // runbooks and harness prompts and erode the protection, whereas a faithful
30924
31604
  // paste can never accidentally carry the victim group's name. It AUTHORIZES
30925
31605
  // the target; --resource-group is what CHOOSES it.
30926
- reinstallInto = Option55.String("--reinstall-into", { description: "Consent to installing into --resource-group even though it already holds resources. Must match the target group's name." });
30927
- org = Option55.String("--org", { description: "Assert the GitHub App on disk is installed on this org; refuses on a mismatch." });
30928
- noBrains = Option55.Boolean("--no-brains", false, { description: "Install without brain-backed workers. For test and CI rigs; the supported install creates brains." });
31606
+ reinstallInto = Option56.String("--reinstall-into", { description: "Consent to installing into --resource-group even though it already holds resources. Must match the target group's name." });
31607
+ org = Option56.String("--org", { description: "Assert the GitHub App on disk is installed on this org; refuses on a mismatch." });
31608
+ noBrains = Option56.Boolean("--no-brains", false, { description: "Install without brain-backed workers. For test and CI rigs; the supported install creates brains." });
30929
31609
  async executeCommand() {
30930
31610
  const location = typeof this.location === "string" ? this.location : void 0;
30931
31611
  if (!location) {
@@ -31086,7 +31766,7 @@ var BootstrapLaunchCommand = class extends M8tCommand {
31086
31766
  };
31087
31767
 
31088
31768
  // src/commands/bootstrap/status.ts
31089
- import { Command as Command60, Option as Option57 } from "clipanion";
31769
+ import { Command as Command61, Option as Option58 } from "clipanion";
31090
31770
  init_errors();
31091
31771
 
31092
31772
  // src/lib/bootstrap-aci-state.ts
@@ -32709,17 +33389,17 @@ async function statusCompanion(options) {
32709
33389
  reason: error instanceof Error ? error.message : "Unsafe owned directory"
32710
33390
  };
32711
33391
  }
32712
- let installed;
33392
+ let installed2;
32713
33393
  try {
32714
- installed = await readInstalled(options);
33394
+ installed2 = await readInstalled(options);
32715
33395
  } catch (error) {
32716
33396
  return {
32717
33397
  state: "needs-repair",
32718
33398
  reason: error instanceof Error ? error.message : "Invalid install manifest"
32719
33399
  };
32720
33400
  }
32721
- if (!installed) return { state: "not-installed" };
32722
- const { paths, install } = installed;
33401
+ if (!installed2) return { state: "not-installed" };
33402
+ const { paths, install } = installed2;
32723
33403
  if (install.platform !== options.platform || install.architecture !== options.architecture || install.ownedTargetRoot !== paths.targetRoot) {
32724
33404
  return {
32725
33405
  state: "needs-repair",
@@ -32960,9 +33640,9 @@ async function uninstallCompanion(options) {
32960
33640
  assertSupported(options);
32961
33641
  const expectedPaths = companionInstallPaths(options);
32962
33642
  await assertOwnedParents(options, expectedPaths);
32963
- const installed = await readInstalled(options);
32964
- if (!installed) return { state: "not-installed" };
32965
- const { paths, install } = installed;
33643
+ const installed2 = await readInstalled(options);
33644
+ if (!installed2) return { state: "not-installed" };
33645
+ const { paths, install } = installed2;
32966
33646
  if (install.ownedTargetRoot !== paths.targetRoot) {
32967
33647
  throw new Error("Refusing to remove a non-owned companion target");
32968
33648
  }
@@ -32985,7 +33665,7 @@ async function uninstallCompanion(options) {
32985
33665
  // src/commands/companion/install.ts
32986
33666
  import * as os19 from "os";
32987
33667
  import * as path43 from "path";
32988
- import { Command as Command59, Option as Option56 } from "clipanion";
33668
+ import { Command as Command60, Option as Option57 } from "clipanion";
32989
33669
 
32990
33670
  // src/lib/companion-channel.ts
32991
33671
  async function readCompanionRelease(source = { url: CHANNEL_LATEST_URL }, deps = {}) {
@@ -33143,7 +33823,7 @@ Version: ${state.version}
33143
33823
  }
33144
33824
  var CompanionInstallCommand = class extends M8tCommand {
33145
33825
  static paths = [["companion", "install"]];
33146
- static usage = Command59.Usage({
33826
+ static usage = Command60.Usage({
33147
33827
  description: "Install the desktop companions for this user from the release channel.",
33148
33828
  examples: [
33149
33829
  ["Install the released build", "$0 companion install"],
@@ -33153,10 +33833,10 @@ var CompanionInstallCommand = class extends M8tCommand {
33153
33833
  ]
33154
33834
  ]
33155
33835
  });
33156
- from = Option56.String("--from", {
33836
+ from = Option57.String("--from", {
33157
33837
  description: "A locally staged build directory instead of the released one."
33158
33838
  });
33159
- resourceGroup = Option56.String("--resource-group", {
33839
+ resourceGroup = Option57.String("--resource-group", {
33160
33840
  description: "Which deployment to bind to, when the subscription holds more than one."
33161
33841
  });
33162
33842
  async executeCommand() {
@@ -33372,7 +34052,7 @@ var BootstrapStatusCommand = class extends M8tCommand {
33372
34052
  // runbooks and shakedown recipes that a founder may already be part-way
33373
34053
  // through — it prints a deprecation notice and does the right thing.
33374
34054
  static paths = [["bootstrap", "status"], ["bootstrap", "finish"]];
33375
- static usage = Command60.Usage({
34055
+ static usage = Command61.Usage({
33376
34056
  description: "Show the cloud installer's live status (phase, progress, result) \u2014 and finish the local setup when it lands.",
33377
34057
  details: "Reads the durable status blob written by the installer. --watch polls until the install reaches done or failed.\n\nOn reaching done under --watch this also completes the local half of the install, which nothing else in the bootstrap path can do: it registers the webapp's sign-in redirect URI (the installer runs as a managed identity with no directory role, and at launch time the gateway FQDN did not exist yet), writes ~/.m8t/repo-root and the gateway discovery cache, and seeds your advisors' brains from the onboarding intake. Re-runnable \u2014 `m8t bootstrap finish` is a deprecated alias that does exactly this on an already-done install.",
33378
34058
  examples: [
@@ -33381,12 +34061,12 @@ var BootstrapStatusCommand = class extends M8tCommand {
33381
34061
  ["Redo the local setup on a finished install", "$0 bootstrap status --finalize --repo-root /path/to/m8t"]
33382
34062
  ]
33383
34063
  });
33384
- watch = Option57.Boolean("--watch", false);
33385
- output = Option57.String("--output");
33386
- repoRoot = Option57.String("--repo-root", { description: "The m8t clone to point local tools at (default: the current directory)." });
33387
- finalize = Option57.Boolean("--finalize", false, { description: "Complete the local setup against an already-done install, without watching." });
33388
- subscription = Option57.String("--subscription");
33389
- resourceGroup = Option57.String("--resource-group");
34064
+ watch = Option58.Boolean("--watch", false);
34065
+ output = Option58.String("--output");
34066
+ repoRoot = Option58.String("--repo-root", { description: "The m8t clone to point local tools at (default: the current directory)." });
34067
+ finalize = Option58.Boolean("--finalize", false, { description: "Complete the local setup against an already-done install, without watching." });
34068
+ subscription = Option58.String("--subscription");
34069
+ resourceGroup = Option58.String("--resource-group");
33390
34070
  async executeCommand() {
33391
34071
  const state = await readBootstrapState();
33392
34072
  if (!state) {
@@ -33519,7 +34199,7 @@ function formatStatus(d) {
33519
34199
  }
33520
34200
 
33521
34201
  // src/commands/bootstrap/reap.ts
33522
- import { Command as Command61, Option as Option58 } from "clipanion";
34202
+ import { Command as Command62, Option as Option59 } from "clipanion";
33523
34203
  init_errors();
33524
34204
 
33525
34205
  // src/lib/bootstrap-reap.ts
@@ -33613,7 +34293,7 @@ async function reapInstaller(opts) {
33613
34293
  // src/commands/bootstrap/reap.ts
33614
34294
  var BootstrapReapCommand = class extends M8tCommand {
33615
34295
  static paths = [["bootstrap", "reap"]];
33616
- static usage = Command61.Usage({
34296
+ static usage = Command62.Usage({
33617
34297
  description: "Tear down the installer scaffolding (ACI \u2192 MI \u2192 its role assignments) after a successful install.",
33618
34298
  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.\n\n--sweep-orphans is a DIFFERENT and much broader mode: instead of reaping this install, it scans the WHOLE SUBSCRIPTION for m8t role assignments left behind by installs whose resources are gone \u2014 orphaned installer Owner-at-subscription-scope grants and orphaned gateway subscription-scope roles. It is a dry run that only lists what it found unless you also pass --yes, which deletes them.",
33619
34299
  examples: [
@@ -33623,9 +34303,9 @@ var BootstrapReapCommand = class extends M8tCommand {
33623
34303
  ["\u2026and delete them", "$0 bootstrap reap --sweep-orphans --yes"]
33624
34304
  ]
33625
34305
  });
33626
- force = Option58.Boolean("--force", false, { description: "Reap even if the install failed or never reported a status." });
33627
- sweepOrphans = Option58.Boolean("--sweep-orphans", false, { description: "Subscription-wide: find m8t role assignments orphaned by earlier installs. Lists only, unless --yes." });
33628
- yes = Option58.Boolean("--yes", false, { description: "With --sweep-orphans, actually delete what the sweep found." });
34306
+ force = Option59.Boolean("--force", false, { description: "Reap even if the install failed or never reported a status." });
34307
+ sweepOrphans = Option59.Boolean("--sweep-orphans", false, { description: "Subscription-wide: find m8t role assignments orphaned by earlier installs. Lists only, unless --yes." });
34308
+ yes = Option59.Boolean("--yes", false, { description: "With --sweep-orphans, actually delete what the sweep found." });
33629
34309
  async executeCommand() {
33630
34310
  if (this.sweepOrphans === true) {
33631
34311
  const { subscriptionId: sub } = await getAzAccount();
@@ -33719,7 +34399,7 @@ Found ${String(total)} orphaned assignment(s) (${breakdown}) (dry-run). ${colors
33719
34399
  };
33720
34400
 
33721
34401
  // src/commands/bootstrap/ui.ts
33722
- import { Command as Command62, Option as Option59 } from "clipanion";
34402
+ import { Command as Command63, Option as Option60 } from "clipanion";
33723
34403
 
33724
34404
  // src/lib/bootstrap-ui.ts
33725
34405
  import * as fs40 from "fs";
@@ -33794,7 +34474,7 @@ function renderDeprecationNotice() {
33794
34474
  }
33795
34475
  var BootstrapUiCommand = class extends M8tCommand {
33796
34476
  static paths = [["bootstrap", "ui"]];
33797
- static usage = Command62.Usage({
34477
+ static usage = Command63.Usage({
33798
34478
  description: "DEPRECATED \u2014 does nothing. Use `m8t bootstrap profile` instead.",
33799
34479
  details: [
33800
34480
  "The local onboarding chat has been retired. Your details are collected by",
@@ -33814,14 +34494,14 @@ var BootstrapUiCommand = class extends M8tCommand {
33814
34494
  // Accepted and ignored, deliberately: removing them would turn an old script's
33815
34495
  // harmless no-op into an "unknown option" failure mid-install. CLI-rewrite: drop
33816
34496
  // the whole surface when the rewrite lands.
33817
- repoRoot = Option59.String("--repo-root", { description: "Ignored (deprecated)." });
33818
- port = Option59.String("--port", "3000", { description: "Ignored (deprecated)." });
33819
- endpoint = Option59.String("--endpoint", { description: "Ignored (deprecated)." });
33820
- prepOnly = Option59.Boolean("--prep-only", false, { description: "Ignored (deprecated)." });
33821
- skipInstall = Option59.Boolean("--skip-install", false, { description: "Ignored (deprecated)." });
33822
- foreground = Option59.Boolean("--foreground", false, { description: "Ignored (deprecated)." });
33823
- voice = Option59.Boolean("--voice", false, { description: "Ignored (deprecated)." });
33824
- stop = Option59.Boolean("--stop", false, {
34497
+ repoRoot = Option60.String("--repo-root", { description: "Ignored (deprecated)." });
34498
+ port = Option60.String("--port", "3000", { description: "Ignored (deprecated)." });
34499
+ endpoint = Option60.String("--endpoint", { description: "Ignored (deprecated)." });
34500
+ prepOnly = Option60.Boolean("--prep-only", false, { description: "Ignored (deprecated)." });
34501
+ skipInstall = Option60.Boolean("--skip-install", false, { description: "Ignored (deprecated)." });
34502
+ foreground = Option60.Boolean("--foreground", false, { description: "Ignored (deprecated)." });
34503
+ voice = Option60.Boolean("--voice", false, { description: "Ignored (deprecated)." });
34504
+ stop = Option60.Boolean("--stop", false, {
33825
34505
  description: "Shut down a local chat UI left running by an earlier version of this command."
33826
34506
  });
33827
34507
  // Not `async`: there is nothing left to await. Everything this command used to
@@ -33843,7 +34523,7 @@ var BootstrapUiCommand = class extends M8tCommand {
33843
34523
 
33844
34524
  // src/commands/bootstrap/profile.ts
33845
34525
  import * as readline3 from "readline/promises";
33846
- import { Command as Command63, Option as Option60 } from "clipanion";
34526
+ import { Command as Command64, Option as Option61 } from "clipanion";
33847
34527
 
33848
34528
  // src/lib/profile-collect.ts
33849
34529
  init_errors();
@@ -34012,7 +34692,7 @@ async function openChatInvite(deps = {}) {
34012
34692
  // src/commands/bootstrap/profile.ts
34013
34693
  var BootstrapProfileCommand = class extends M8tCommand {
34014
34694
  static paths = [["bootstrap", "profile"]];
34015
- static usage = Command63.Usage({
34695
+ static usage = Command64.Usage({
34016
34696
  description: "Confirm your email + your Microsoft startup advisor, and open Ezra to talk to while the install runs.",
34017
34697
  details: [
34018
34698
  "Run after `m8t bootstrap launch`, in parallel with `status --watch`. Records the two",
@@ -34032,12 +34712,12 @@ var BootstrapProfileCommand = class extends M8tCommand {
34032
34712
  ["Skip the browser hand-off", "$0 bootstrap profile --founder-email you@example.com --print"]
34033
34713
  ]
34034
34714
  });
34035
- founderEmail = Option60.String("--founder-email", { description: "The founder's contact address \u2014 Ezra copies them on its outbound mail." });
34036
- advisorName = Option60.String("--advisor-name", { description: "Their Microsoft startup advisor's name." });
34037
- advisorEmail = Option60.String("--advisor-email", { description: "Their Microsoft startup advisor's email." });
34038
- noAdvisor = Option60.Boolean("--no-advisor", false, { description: "Record that they have no startup advisor to add (or don't know it yet)." });
34039
- noChat = Option60.Boolean("--no-chat", false, { description: "Record the answers without opening the hosted Ezra." });
34040
- print = Option60.Boolean("--print", false, { description: "Print the chat link instead of opening a browser (headless / SSH)." });
34715
+ founderEmail = Option61.String("--founder-email", { description: "The founder's contact address \u2014 Ezra copies them on its outbound mail." });
34716
+ advisorName = Option61.String("--advisor-name", { description: "Their Microsoft startup advisor's name." });
34717
+ advisorEmail = Option61.String("--advisor-email", { description: "Their Microsoft startup advisor's email." });
34718
+ noAdvisor = Option61.Boolean("--no-advisor", false, { description: "Record that they have no startup advisor to add (or don't know it yet)." });
34719
+ noChat = Option61.Boolean("--no-chat", false, { description: "Record the answers without opening the hosted Ezra." });
34720
+ print = Option61.Boolean("--print", false, { description: "Print the chat link instead of opening a browser (headless / SSH)." });
34041
34721
  async executeCommand() {
34042
34722
  const stdin = this.context.stdin;
34043
34723
  const stdout = this.context.stdout;
@@ -34130,10 +34810,10 @@ var BootstrapProfileCommand = class extends M8tCommand {
34130
34810
  };
34131
34811
 
34132
34812
  // src/commands/bootstrap/seed-profile.ts
34133
- import { Command as Command64, Option as Option61 } from "clipanion";
34813
+ import { Command as Command65, Option as Option62 } from "clipanion";
34134
34814
  var BootstrapSeedProfileCommand = class extends M8tCommand {
34135
34815
  static paths = [["bootstrap", "seed-profile"]];
34136
- static usage = Command64.Usage({
34816
+ static usage = Command65.Usage({
34137
34817
  description: "Seed your advisors' brains with how to reach you, from what you confirmed at `m8t bootstrap profile`.",
34138
34818
  details: [
34139
34819
  "Renders memory/founder.md + memory/company-profile.md (+ their MEMORY.md index lines)",
@@ -34150,11 +34830,11 @@ var BootstrapSeedProfileCommand = class extends M8tCommand {
34150
34830
  ["Seed now (idempotent)", "$0 bootstrap seed-profile"]
34151
34831
  ]
34152
34832
  });
34153
- endpoint = Option61.String("--endpoint", { description: "Override the Foundry endpoint (else read from the install status)." });
34154
- brain = Option61.String("--brain", { description: "Seed only this one brain repo, instead of both <org>/stacey-brain and <org>/ezra-brain." });
34155
- watch = Option61.Boolean("--watch", false, { description: "Poll until the intake completes (or --timeout)." });
34156
- timeout = Option61.String("--timeout", { description: "Watch timeout in minutes (default 20)." });
34157
- githubAppCreds = Option61.String("--github-app-creds");
34833
+ endpoint = Option62.String("--endpoint", { description: "Override the Foundry endpoint (else read from the install status)." });
34834
+ brain = Option62.String("--brain", { description: "Seed only this one brain repo, instead of both <org>/stacey-brain and <org>/ezra-brain." });
34835
+ watch = Option62.Boolean("--watch", false, { description: "Poll until the intake completes (or --timeout)." });
34836
+ timeout = Option62.String("--timeout", { description: "Watch timeout in minutes (default 20)." });
34837
+ githubAppCreds = Option62.String("--github-app-creds");
34158
34838
  async executeCommand() {
34159
34839
  const ctx = await resolveSeedContext({
34160
34840
  endpointOverride: typeof this.endpoint === "string" ? this.endpoint : void 0,
@@ -34242,7 +34922,7 @@ var BootstrapSeedProfileCommand = class extends M8tCommand {
34242
34922
  import * as fs41 from "fs";
34243
34923
  import * as os22 from "os";
34244
34924
  import * as path46 from "path";
34245
- import { Command as Command65, Option as Option62 } from "clipanion";
34925
+ import { Command as Command66, Option as Option63 } from "clipanion";
34246
34926
  init_errors();
34247
34927
 
34248
34928
  // src/lib/telemetry-enroll.ts
@@ -34324,7 +35004,7 @@ async function resolveKeyVaultName(containerAppResourceId) {
34324
35004
  }
34325
35005
  var TelemetryEnrollCommand = class extends M8tCommand {
34326
35006
  static paths = [["telemetry", "enroll"]];
34327
- static usage = Command65.Usage({
35007
+ static usage = Command66.Usage({
34328
35008
  description: "Enroll this installation for operational telemetry (pre-existing installs).",
34329
35009
  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.",
34330
35010
  examples: [
@@ -34332,11 +35012,11 @@ var TelemetryEnrollCommand = class extends M8tCommand {
34332
35012
  ["Enroll and share a support contact", "$0 telemetry enroll --contact-email you@example.com --company 'Acme'"]
34333
35013
  ]
34334
35014
  });
34335
- company = Option62.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
34336
- contactEmail = Option62.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
34337
- subscription = Option62.String("--subscription", { description: "Azure subscription id used to find your deployment. Never sent to m8t." });
34338
- resourceGroup = Option62.String("--resource-group", { description: "Resource group to disambiguate discovery, if you have more than one m8t deployment." });
34339
- force = Option62.Boolean("--force", false, { description: "Enroll even when a key is already stored. Use only when m8t support asks you to." });
35015
+ company = Option63.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
35016
+ contactEmail = Option63.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
35017
+ subscription = Option63.String("--subscription", { description: "Azure subscription id used to find your deployment. Never sent to m8t." });
35018
+ resourceGroup = Option63.String("--resource-group", { description: "Resource group to disambiguate discovery, if you have more than one m8t deployment." });
35019
+ force = Option63.Boolean("--force", false, { description: "Enroll even when a key is already stored. Use only when m8t support asks you to." });
34340
35020
  async executeCommand() {
34341
35021
  const account = await getAzAccount();
34342
35022
  const subscriptionId = (typeof this.subscription === "string" ? this.subscription : void 0) ?? account.subscriptionId;
@@ -34386,7 +35066,7 @@ var TelemetryEnrollCommand = class extends M8tCommand {
34386
35066
  };
34387
35067
 
34388
35068
  // src/commands/companion/bridge.ts
34389
- import { Command as Command66, Option as Option63 } from "clipanion";
35069
+ import { Command as Command67, Option as Option64 } from "clipanion";
34390
35070
 
34391
35071
  // ../../packages/companion-bridge-contract/src/index.ts
34392
35072
  var COMPANION_MESSAGE_MAX_CODE_POINTS = 32768;
@@ -35148,9 +35828,9 @@ async function rosterCompanions(sink, deps = {}) {
35148
35828
  controller.signal
35149
35829
  );
35150
35830
  const now = (deps.now ?? Date.now)();
35151
- const installed = await (deps.installEpoch ?? readInstallEpoch)();
35831
+ const installed2 = await (deps.installEpoch ?? readInstallEpoch)();
35152
35832
  const activeSince = new Date(
35153
- installed === null || installed > now ? now : installed
35833
+ installed2 === null || installed2 > now ? now : installed2
35154
35834
  ).toISOString();
35155
35835
  const PRESENTABLE = [
35156
35836
  { personaKey: "startup-advisor", displayName: "Stacey", portraitKey: "stacey" },
@@ -35573,11 +36253,11 @@ async function fetchConversationTurns(request, sink, deps, decode) {
35573
36253
 
35574
36254
  // src/lib/companion-update-check.ts
35575
36255
  async function checkCompanionUpdate(options, deps = {}) {
35576
- const installed = await statusCompanion(options).then((state) => state.state === "installed" ? state.version : null).catch(() => null);
36256
+ const installed2 = await statusCompanion(options).then((state) => state.state === "installed" ? state.version : null).catch(() => null);
35577
36257
  const release = await (deps.readRelease ?? readCompanionRelease)().catch(() => null);
35578
36258
  return {
35579
36259
  type: "update",
35580
- installed,
36260
+ installed: installed2,
35581
36261
  available: release?.component.version ?? null,
35582
36262
  severity: release === null ? null : release.severity
35583
36263
  };
@@ -35842,14 +36522,14 @@ async function runCompanionBridge(stdin, stdout, stderr, deps = defaultDeps4) {
35842
36522
  return 3;
35843
36523
  }
35844
36524
  }
35845
- var CompanionBridgeCommand = class extends Command66 {
36525
+ var CompanionBridgeCommand = class extends Command67 {
35846
36526
  static paths = [["companion", "_bridge"]];
35847
36527
  /**
35848
36528
  * One process serving many requests instead of one per request, so the
35849
36529
  * session keeps its authenticated context between them. A CLI predating the
35850
36530
  * flag rejects it outright, which is how the app knows to fall back.
35851
36531
  */
35852
- serve = Option63.Boolean("--serve", false);
36532
+ serve = Option64.Boolean("--serve", false);
35853
36533
  async execute() {
35854
36534
  if (this.serve) {
35855
36535
  return runCompanionBridgeServe(
@@ -35867,7 +36547,7 @@ var CompanionBridgeCommand = class extends Command66 {
35867
36547
  };
35868
36548
 
35869
36549
  // src/commands/companion/status.ts
35870
- import { Command as Command67 } from "clipanion";
36550
+ import { Command as Command68 } from "clipanion";
35871
36551
  async function withTimeout(work, ms) {
35872
36552
  let timer;
35873
36553
  try {
@@ -35939,7 +36619,7 @@ Run: m8t companion install
35939
36619
  }
35940
36620
  var CompanionStatusCommand = class extends M8tCommand {
35941
36621
  static paths = [["companion", "status"]];
35942
- static usage = Command67.Usage({
36622
+ static usage = Command68.Usage({
35943
36623
  description: "Verify the installed desktop companion without launching it."
35944
36624
  });
35945
36625
  async executeCommand() {
@@ -35953,7 +36633,7 @@ var CompanionStatusCommand = class extends M8tCommand {
35953
36633
  };
35954
36634
 
35955
36635
  // src/commands/companion/repair.ts
35956
- import { Command as Command68, Option as Option64 } from "clipanion";
36636
+ import { Command as Command69, Option as Option65 } from "clipanion";
35957
36637
  async function runCompanionRepairCommand(stdout, repair) {
35958
36638
  const state = await repair();
35959
36639
  if (state.state === "not-released") {
@@ -35972,10 +36652,10 @@ async function runCompanionRepairCommand(stdout, repair) {
35972
36652
  }
35973
36653
  var CompanionRepairCommand = class extends M8tCommand {
35974
36654
  static paths = [["companion", "repair"]];
35975
- static usage = Command68.Usage({
36655
+ static usage = Command69.Usage({
35976
36656
  description: "Restore the desktop companions and start-at-login state."
35977
36657
  });
35978
- resourceGroup = Option64.String("--resource-group", {
36658
+ resourceGroup = Option65.String("--resource-group", {
35979
36659
  description: "Which deployment to bind to, when the subscription holds more than one."
35980
36660
  });
35981
36661
  async executeCommand() {
@@ -35989,7 +36669,7 @@ var CompanionRepairCommand = class extends M8tCommand {
35989
36669
  };
35990
36670
 
35991
36671
  // src/commands/companion/uninstall.ts
35992
- import { Command as Command69 } from "clipanion";
36672
+ import { Command as Command70 } from "clipanion";
35993
36673
  async function runCompanionUninstallCommand(stdout, uninstall) {
35994
36674
  const state = await uninstall();
35995
36675
  if (state.state !== "not-installed") {
@@ -36001,7 +36681,7 @@ async function runCompanionUninstallCommand(stdout, uninstall) {
36001
36681
  }
36002
36682
  var CompanionUninstallCommand = class extends M8tCommand {
36003
36683
  static paths = [["companion", "uninstall"]];
36004
- static usage = Command69.Usage({
36684
+ static usage = Command70.Usage({
36005
36685
  description: "Remove only this user's desktop companion installation."
36006
36686
  });
36007
36687
  async executeCommand() {
@@ -36014,6 +36694,7 @@ var CompanionUninstallCommand = class extends M8tCommand {
36014
36694
  };
36015
36695
 
36016
36696
  // src/cli.ts
36697
+ installFoundryDnsShim();
36017
36698
  var cli = new Cli({
36018
36699
  binaryName: "m8t",
36019
36700
  binaryLabel: "m8t CLI \u2014 manage m8t deployments",
@@ -36063,6 +36744,7 @@ cli.register(PlatformRequestUpdateCommand);
36063
36744
  cli.register(PlatformSeedStampCommand);
36064
36745
  cli.register(PlatformPolicyCommand);
36065
36746
  cli.register(PlatformEnableCostReportCommand);
36747
+ cli.register(PlatformEmailCommand);
36066
36748
  cli.register(PlatformEnableAutoUpdateCommand);
36067
36749
  cli.register(DeployCommand);
36068
36750
  cli.register(AgentDeployAdvisorCommand);