@m8t-stack/cli 0.2.30 → 0.2.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1247,7 +1247,7 @@ var init_enable_hosted_brain = __esm({
1247
1247
  import { Builtins, Cli } from "clipanion";
1248
1248
 
1249
1249
  // src/lib/package-version.ts
1250
- var CLI_VERSION = "0.2.30";
1250
+ var CLI_VERSION = "0.2.32";
1251
1251
 
1252
1252
  // src/lib/render-error.ts
1253
1253
  init_errors();
@@ -10645,9 +10645,9 @@ var Webhooks = class extends APIResource {
10645
10645
  __classPrivateFieldGet(this, _Webhooks_instances, "m", _Webhooks_validateSecret).call(this, secret);
10646
10646
  const headersObj = buildHeaders([headers]).values;
10647
10647
  const signatureHeader = __classPrivateFieldGet(this, _Webhooks_instances, "m", _Webhooks_getRequiredHeader).call(this, headersObj, "webhook-signature");
10648
- const timestamp = __classPrivateFieldGet(this, _Webhooks_instances, "m", _Webhooks_getRequiredHeader).call(this, headersObj, "webhook-timestamp");
10648
+ const timestamp2 = __classPrivateFieldGet(this, _Webhooks_instances, "m", _Webhooks_getRequiredHeader).call(this, headersObj, "webhook-timestamp");
10649
10649
  const webhookId = __classPrivateFieldGet(this, _Webhooks_instances, "m", _Webhooks_getRequiredHeader).call(this, headersObj, "webhook-id");
10650
- const timestampSeconds = parseInt(timestamp, 10);
10650
+ const timestampSeconds = parseInt(timestamp2, 10);
10651
10651
  if (isNaN(timestampSeconds)) {
10652
10652
  throw new InvalidWebhookSignatureError("Invalid webhook timestamp format");
10653
10653
  }
@@ -10660,7 +10660,7 @@ var Webhooks = class extends APIResource {
10660
10660
  }
10661
10661
  const signatures = signatureHeader.split(" ").map((part) => part.startsWith("v1,") ? part.substring(3) : part);
10662
10662
  const decodedSecret = secret.startsWith("whsec_") ? Buffer.from(secret.replace("whsec_", ""), "base64") : Buffer.from(secret, "utf-8");
10663
- const signedPayload = webhookId ? `${webhookId}.${timestamp}.${payload}` : `${timestamp}.${payload}`;
10663
+ const signedPayload = webhookId ? `${webhookId}.${timestamp2}.${payload}` : `${timestamp2}.${payload}`;
10664
10664
  const key2 = await crypto.subtle.importKey("raw", decodedSecret, { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
10665
10665
  for (const signature of signatures) {
10666
10666
  try {
@@ -17000,7 +17000,7 @@ var SIZE_PRESETS = {
17000
17000
  };
17001
17001
  var DEFAULT_REGISTRY = "ghcr.io/m8t-labs";
17002
17002
  var DEFAULT_IMAGE = "m8t-coding-agent";
17003
- var DEFAULT_TAG = "v0.1.0";
17003
+ var DEFAULT_TAG = "v0.1.2";
17004
17004
  var DEFAULT_MODEL = "gpt-4.1-mini";
17005
17005
  var NAME_RE = /^[a-z0-9-]+$/;
17006
17006
  var CoderDeployCommand = class extends M8tCommand {
@@ -18088,9 +18088,7 @@ function extractTarGz(tgz, into) {
18088
18088
  }
18089
18089
 
18090
18090
  // src/lib/platform-stamp.ts
18091
- import { TableClient, AzureNamedKeyCredential } from "@azure/data-tables";
18092
- init_errors();
18093
- init_rbac();
18091
+ import { TableClient as TableClient2 } from "@azure/data-tables";
18094
18092
 
18095
18093
  // src/lib/platform-storage-discovery.ts
18096
18094
  init_http();
@@ -18118,7 +18116,19 @@ async function discoverStampStorage(opts) {
18118
18116
  return { tableEndpoint, accountResourceId: chosen.id, accountName: chosen.name };
18119
18117
  }
18120
18118
 
18121
- // src/lib/platform-stamp.ts
18119
+ // src/lib/metadata-table-write.ts
18120
+ import { TableClient, AzureNamedKeyCredential } from "@azure/data-tables";
18121
+ init_errors();
18122
+ init_rbac();
18123
+ function is403(e) {
18124
+ return e.statusCode === 403;
18125
+ }
18126
+ function is404(e) {
18127
+ return e.statusCode === 404;
18128
+ }
18129
+ function sleep2(ms) {
18130
+ return new Promise((r) => setTimeout(r, ms));
18131
+ }
18122
18132
  async function makeSharedKeyClient(opts) {
18123
18133
  const rg = /\/resourceGroups\/([^/]+)\//i.exec(opts.accountResourceId)?.[1];
18124
18134
  if (!rg) return null;
@@ -18126,22 +18136,29 @@ async function makeSharedKeyClient(opts) {
18126
18136
  if (!key2) return null;
18127
18137
  return new TableClient(opts.tableEndpoint, TABLE, new AzureNamedKeyCredential(opts.accountName, key2));
18128
18138
  }
18129
- async function writeStamp(opts) {
18130
- const { tableEndpoint, accountResourceId, accountName } = await discoverStampStorage(opts);
18131
- const entity = stampToEntity(opts.stamp);
18132
- const aad = new TableClient(tableEndpoint, TABLE, opts.credential);
18139
+ async function upsertMetadataEntity(opts) {
18140
+ const discover = opts.discoverImpl ?? discoverStampStorage;
18141
+ const makeAad = opts.aadClientImpl ?? ((endpoint, credential2) => new TableClient(endpoint, TABLE, credential2));
18142
+ const makeShared = opts.sharedClientImpl ?? makeSharedKeyClient;
18143
+ const nap = opts.sleepImpl ?? sleep2;
18144
+ const { tableEndpoint, accountResourceId, accountName } = await discover({
18145
+ credential: opts.credential,
18146
+ subscriptionId: opts.subscriptionId,
18147
+ resourceGroup: opts.resourceGroup
18148
+ });
18149
+ const aad = makeAad(tableEndpoint, opts.credential);
18133
18150
  await aad.createTable().catch(() => void 0);
18134
18151
  try {
18135
- await aad.upsertEntity(entity, "Replace");
18152
+ await aad.upsertEntity(opts.entity, "Replace");
18136
18153
  return;
18137
18154
  } catch (e) {
18138
18155
  if (!is403(e)) throw e;
18139
18156
  }
18140
- opts.onProgress?.("writing the platform stamp with the account key \u2014 your identity lacks the Storage Table data role.");
18141
- const shared = await makeSharedKeyClient({ tableEndpoint, accountResourceId, accountName });
18157
+ opts.onProgress?.(`writing the ${opts.label} with the account key \u2014 your identity lacks the Storage Table data role.`);
18158
+ const shared = await makeShared({ tableEndpoint, accountResourceId, accountName });
18142
18159
  if (shared) {
18143
18160
  await shared.createTable().catch(() => void 0);
18144
- await shared.upsertEntity(entity, "Replace");
18161
+ await shared.upsertEntity(opts.entity, "Replace");
18145
18162
  return;
18146
18163
  }
18147
18164
  opts.onProgress?.("shared-key access is disabled \u2014 granting Storage Table Data Contributor to your identity (one-time)\u2026");
@@ -18155,30 +18172,45 @@ async function writeStamp(opts) {
18155
18172
  });
18156
18173
  } catch (e) {
18157
18174
  throw new LocalCliError({
18158
- code: "PLATFORM_STAMP_NO_ACCESS",
18159
- message: `Cannot write the platform stamp: AAD lacks the Storage Table data role for ${accountName}, shared-key access is disabled, and self-granting the role failed.`,
18175
+ code: opts.errorCode,
18176
+ 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.`,
18160
18177
  hint: "Grant yourself 'Storage Table Data Contributor' on the storage account and retry.",
18161
18178
  cause: e
18162
18179
  });
18163
18180
  }
18164
18181
  for (let i = 0; i < 6; i++) {
18165
- await sleep2(1e4);
18182
+ await nap(1e4);
18166
18183
  try {
18167
- await aad.upsertEntity(entity, "Replace");
18184
+ await aad.upsertEntity(opts.entity, "Replace");
18168
18185
  return;
18169
18186
  } catch (e) {
18170
18187
  if (!is403(e)) throw e;
18171
18188
  }
18172
18189
  }
18173
18190
  throw new LocalCliError({
18174
- code: "PLATFORM_STAMP_NO_ACCESS",
18175
- message: `Cannot write the platform stamp: granted 'Storage Table Data Contributor' on ${accountName}, but the data-plane role has not propagated yet.`,
18176
- hint: "The role assignment persists \u2014 retry 'm8t platform update' in a few minutes."
18191
+ code: opts.errorCode,
18192
+ message: `Cannot write the ${opts.label}: granted 'Storage Table Data Contributor' on ${accountName}, but the data-plane role has not propagated yet.`,
18193
+ hint: opts.retryHint
18194
+ });
18195
+ }
18196
+
18197
+ // src/lib/platform-stamp.ts
18198
+ async function writeStamp(opts) {
18199
+ const entity = stampToEntity(opts.stamp);
18200
+ await upsertMetadataEntity({
18201
+ credential: opts.credential,
18202
+ subscriptionId: opts.subscriptionId,
18203
+ resourceGroup: opts.resourceGroup,
18204
+ entity,
18205
+ label: "platform stamp",
18206
+ errorCode: "PLATFORM_STAMP_NO_ACCESS",
18207
+ retryHint: "The role assignment persists \u2014 retry 'm8t platform update' in a few minutes.",
18208
+ ...opts.onProgress ? { onProgress: opts.onProgress } : {}
18177
18209
  });
18178
18210
  }
18179
18211
  async function readStamp(opts) {
18180
18212
  const { tableEndpoint, accountResourceId, accountName } = await discoverStampStorage(opts);
18181
- const aad = new TableClient(tableEndpoint, TABLE, opts.credential);
18213
+ const aad = new TableClient2(tableEndpoint, TABLE, opts.credential);
18182
18214
  try {
18183
18215
  const row = await aad.getEntity(PK, RK);
18184
18216
  return entityToStamp(row);
@@ -18195,15 +18227,6 @@ async function readStamp(opts) {
18195
18227
  return null;
18196
18228
  }
18197
18229
  }
18198
- function is403(e) {
18199
- return e.statusCode === 403;
18200
- }
18201
- function is404(e) {
18202
- return e.statusCode === 404;
18203
- }
18204
- function sleep2(ms) {
18205
- return new Promise((r) => setTimeout(r, ms));
18206
- }
18207
18230
 
18208
18231
  // src/lib/platform-converge.ts
18209
18232
  import * as fs22 from "fs";
@@ -18245,6 +18268,7 @@ function buildBicepParams(p) {
18245
18268
  if (p.installerImage !== void 0) params.push(`installerImage=${p.installerImage}`);
18246
18269
  if (p.updateCron !== void 0) params.push(`updateCron=${p.updateCron}`);
18247
18270
  if (p.channelUrl !== void 0) params.push(`channelUrl=${p.channelUrl}`);
18271
+ if (p.voiceInternalSecret) params.push(`voiceInternalSecret=${p.voiceInternalSecret}`);
18248
18272
  if (p.gatewayCpu) params.push(`gatewayCpu=${p.gatewayCpu}`);
18249
18273
  if (p.gatewayMemory) params.push(`gatewayMemory=${p.gatewayMemory}`);
18250
18274
  if (p.refereeEnabled) params.push(`refereeEnabled=${p.refereeEnabled}`);
@@ -18664,7 +18688,7 @@ async function resolveBicepParamsForConverge(ctx, opts = {}) {
18664
18688
  hint: "Pass --suffix <existing-suffix> (from the deployed resource names) to avoid provisioning duplicate resources."
18665
18689
  });
18666
18690
  }
18667
- const location = (await runAz(["resource", "list", "-g", ctx.resourceGroup, "--query", "[0].location", "-o", "tsv"])).trim();
18691
+ const location = opts.location ?? (await runAz(["resource", "list", "-g", ctx.resourceGroup, "--query", "[0].location", "-o", "tsv"])).trim();
18668
18692
  const foundryResourceId = await resolveFoundryResourceId(opts.foundryEndpoint ?? "", opts.foundryResourceId);
18669
18693
  const acrPullIdentityResourceId = resolveAcrPullIdentity({ explicit: opts.acrPullIdentity });
18670
18694
  const imageRef = `${ctx.manifest.components.gateway.ref}:${ctx.manifest.components.gateway.tag}`;
@@ -18680,7 +18704,9 @@ async function resolveBicepParamsForConverge(ctx, opts = {}) {
18680
18704
  acrPullIdentityResourceId,
18681
18705
  acrResourceId,
18682
18706
  foundryTracingMode: opts.foundryTracingMode,
18683
- ...opts.assignSubscriptionRoles !== void 0 ? { assignSubscriptionRoles: opts.assignSubscriptionRoles } : {}
18707
+ ...opts.assignSubscriptionRoles !== void 0 ? { assignSubscriptionRoles: opts.assignSubscriptionRoles } : {},
18708
+ ...opts.voiceInternalSecret ? { voiceInternalSecret: opts.voiceInternalSecret } : {},
18709
+ ...opts.channelUrl ? { channelUrl: opts.channelUrl } : {}
18684
18710
  };
18685
18711
  }
18686
18712
  function swapHostedImage(def, newImage) {
@@ -18949,10 +18975,10 @@ import { DefaultAzureCredential as DefaultAzureCredential17 } from "@azure/ident
18949
18975
  import * as path25 from "path";
18950
18976
 
18951
18977
  // src/lib/platform-infra-params.ts
18952
- import { TableClient as TableClient2 } from "@azure/data-tables";
18978
+ import { TableClient as TableClient3 } from "@azure/data-tables";
18953
18979
  async function openInfraParamsTable(opts) {
18954
18980
  const { tableEndpoint } = await discoverStampStorage(opts);
18955
- return new TableClient2(tableEndpoint, "Metadata", opts.credential);
18981
+ return new TableClient3(tableEndpoint, "Metadata", opts.credential);
18956
18982
  }
18957
18983
  async function readInfraParams(client) {
18958
18984
  try {
@@ -18961,8 +18987,17 @@ async function readInfraParams(client) {
18961
18987
  return null;
18962
18988
  }
18963
18989
  }
18964
- async function writeInfraParams(client, p) {
18965
- await client.upsertEntity(infraParamsToEntity(p), "Replace");
18990
+ async function writeInfraParams(ctx, p) {
18991
+ await upsertMetadataEntity({
18992
+ credential: ctx.credential,
18993
+ subscriptionId: ctx.subscriptionId,
18994
+ resourceGroup: ctx.resourceGroup,
18995
+ entity: infraParamsToEntity(p),
18996
+ label: "infra-params row",
18997
+ errorCode: "PLATFORM_INFRA_PARAMS_NO_ACCESS",
18998
+ retryHint: ctx.retryHint,
18999
+ ...ctx.onProgress ? { onProgress: ctx.onProgress } : {}
19000
+ });
18966
19001
  }
18967
19002
  async function deriveSuffixVerified(opts) {
18968
19003
  const list = JSON.parse(
@@ -18999,6 +19034,24 @@ async function deriveSuffixVerified(opts) {
18999
19034
  return cae.length === 1 ? suffix : null;
19000
19035
  }
19001
19036
 
19037
+ // src/lib/gateway-live-params.ts
19038
+ async function readLiveGatewayEnvVar(opts) {
19039
+ const args = ["containerapp", "show", "-n", opts.gatewayName, "-g", opts.resourceGroup];
19040
+ if (opts.subscriptionId) args.push("--subscription", opts.subscriptionId);
19041
+ args.push("--query", `properties.template.containers[0].env[?name=='${opts.name}'].value | [0]`, "-o", "tsv");
19042
+ let raw;
19043
+ try {
19044
+ raw = await runAz(args);
19045
+ } catch {
19046
+ return "";
19047
+ }
19048
+ const v = raw.trim();
19049
+ return v === "None" ? "" : v;
19050
+ }
19051
+ function readLiveVoiceInternalSecret(opts) {
19052
+ return readLiveGatewayEnvVar({ ...opts, name: "M8T_INTERNAL_SECRET" });
19053
+ }
19054
+
19002
19055
  // src/lib/platform-converge-cli.ts
19003
19056
  init_errors();
19004
19057
 
@@ -19412,6 +19465,17 @@ async function buildConvergeDeps(args) {
19412
19465
  const infraTable = await openInfraParamsTable({ credential: args.credential, subscriptionId: args.subscriptionId, resourceGroup: args.resourceGroup });
19413
19466
  const stamped = (await readInfraParams(infraTable))?.bicep;
19414
19467
  args.onProgress?.("subscription-scoped modules skipped: updater is RG-scoped; sub-level changes require a founder-run CLI converge.");
19468
+ const gatewayName = args.suffix ? `m8t-gateway-${args.suffix}` : void 0;
19469
+ const voiceInternalSecret = gatewayName ? await readLiveVoiceInternalSecret({
19470
+ gatewayName,
19471
+ resourceGroup: args.resourceGroup,
19472
+ subscriptionId: args.subscriptionId
19473
+ }) : "";
19474
+ if (gatewayName && !voiceInternalSecret) {
19475
+ args.onProgress?.(
19476
+ `could not read the live voice-relay secret (M8T_INTERNAL_SECRET) off gateway '${gatewayName}' \u2014 the read failed or the variable is missing. This infra converge will re-mint the secret, rolling BOTH the gateway and the voice relay.`
19477
+ );
19478
+ }
19415
19479
  const opts = {
19416
19480
  suffix: args.suffix,
19417
19481
  assignSubscriptionRoles: false,
@@ -19422,8 +19486,11 @@ async function buildConvergeDeps(args) {
19422
19486
  foundryResourceId: stamped.foundryResourceId,
19423
19487
  acrPullIdentity: stamped.acrPullIdentityResourceId,
19424
19488
  acrResourceId: stamped.acrResourceId,
19425
- ...stamped.foundryTracingMode ? { foundryTracingMode: stamped.foundryTracingMode } : {}
19426
- } : {}
19489
+ ...stamped.foundryTracingMode ? { foundryTracingMode: stamped.foundryTracingMode } : {},
19490
+ ...stamped.channelUrl ? { channelUrl: stamped.channelUrl } : {},
19491
+ ...stamped.location ? { location: stamped.location } : {}
19492
+ } : {},
19493
+ ...voiceInternalSecret ? { voiceInternalSecret } : {}
19427
19494
  };
19428
19495
  return applyInfraDeploy(a, ctx, target, opts);
19429
19496
  },
@@ -19471,17 +19538,26 @@ async function buildConvergeDeps(args) {
19471
19538
  const table = await openInfraParamsTable({ credential: args.credential, subscriptionId: args.subscriptionId, resourceGroup: args.resourceGroup });
19472
19539
  const existing = await readInfraParams(table);
19473
19540
  if (!existing) {
19474
- warn("infra-params re-stamp skipped: no existing row to refresh (deploy/enable-auto-update writes the authoritative row).");
19541
+ warn("infra-params re-stamp skipped: no existing row to refresh (enable-auto-update writes the authoritative row).");
19475
19542
  return;
19476
19543
  }
19477
- await writeInfraParams(table, {
19478
- ...existing,
19479
- bicep: {
19480
- ...existing.bicep,
19481
- imageRef: `${ctx.manifest.components.gateway.ref}:${ctx.manifest.components.gateway.tag}`
19544
+ await writeInfraParams(
19545
+ {
19546
+ credential: args.credential,
19547
+ subscriptionId: args.subscriptionId,
19548
+ resourceGroup: args.resourceGroup,
19549
+ retryHint: "The role assignment persists \u2014 retry 'm8t platform update' in a few minutes.",
19550
+ onProgress: args.onProgress
19482
19551
  },
19483
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
19484
- });
19552
+ {
19553
+ ...existing,
19554
+ bicep: {
19555
+ ...existing.bicep,
19556
+ imageRef: `${ctx.manifest.components.gateway.ref}:${ctx.manifest.components.gateway.tag}`
19557
+ },
19558
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
19559
+ }
19560
+ );
19485
19561
  },
19486
19562
  async applySeedRefresh(_a3, ctx) {
19487
19563
  let kvUri;
@@ -19718,10 +19794,10 @@ function resolveHeadlessContextFromEnv(env) {
19718
19794
  }
19719
19795
 
19720
19796
  // src/lib/apply-request-store.ts
19721
- import { TableClient as TableClient3 } from "@azure/data-tables";
19797
+ import { TableClient as TableClient4 } from "@azure/data-tables";
19722
19798
  async function openApplyTable(opts) {
19723
19799
  const { tableEndpoint } = await discoverStampStorage(opts);
19724
- return new TableClient3(tableEndpoint, "Metadata", opts.credential);
19800
+ return new TableClient4(tableEndpoint, "Metadata", opts.credential);
19725
19801
  }
19726
19802
  function hasStatus(e, n) {
19727
19803
  return e.statusCode === n;
@@ -20111,10 +20187,11 @@ var PlatformConvergeCommand = class extends M8tCommand {
20111
20187
  }
20112
20188
  /**
20113
20189
  * Recover the real bicep suffix WITHOUT guessing: prefer the stamped
20114
- * `system/infra-params` row (written by deploy + every full converge), and
20115
- * fall back to deriving it from the live gateway container-app name — but only
20116
- * when that derivation VERIFIES (exactly one gateway + a matching CAE). If
20117
- * both fail, throw rather than provision a duplicate stack.
20190
+ * `system/infra-params` row (written by enable-auto-update, and re-stamped
20191
+ * by every full converge), and fall back to deriving it from the live
20192
+ * gateway container-app name but only when that derivation VERIFIES
20193
+ * (exactly one gateway + a matching CAE). If both fail, throw rather than
20194
+ * provision a duplicate stack.
20118
20195
  */
20119
20196
  async recoverSuffix(ctx) {
20120
20197
  const infraTable = await openInfraParamsTable({
@@ -20510,6 +20587,12 @@ var PlatformEnableAutoUpdateCommand = class extends M8tCommand {
20510
20587
  channelUrl = Option33.String("--channel-url", {
20511
20588
  description: "Release-channel URL the updater job polls (bicep default applies when omitted)."
20512
20589
  });
20590
+ location = Option33.String("--location", {
20591
+ description: "Region for the updater identity + job. Defaults to the resource group's existing resources."
20592
+ });
20593
+ foundryTracing = Option33.String("--foundry-tracing", {
20594
+ description: "project | account | skip. Pass the value the install was deployed with \u2014 omitting it lets the bicep default (project) switch tracing on."
20595
+ });
20513
20596
  endpoint = Option33.String("--endpoint", {
20514
20597
  description: "Foundry project endpoint URL. Disambiguates the project in a multi-project subscription."
20515
20598
  });
@@ -20574,7 +20657,12 @@ var PlatformEnableAutoUpdateCommand = class extends M8tCommand {
20574
20657
  message: `Could not read the live image for gateway '${gatewayName}' in resource group ${resourceGroup}.`
20575
20658
  });
20576
20659
  }
20577
- const location = (await runAz(["resource", "list", "-g", resourceGroup, "--subscription", subscriptionId, "--query", "[0].location", "-o", "tsv"])).trim();
20660
+ const voiceInternalSecret = await readLiveVoiceInternalSecret({
20661
+ gatewayName,
20662
+ resourceGroup,
20663
+ subscriptionId
20664
+ });
20665
+ const location = typeof this.location === "string" && this.location.length > 0 ? this.location : (await runAz(["resource", "list", "-g", resourceGroup, "--subscription", subscriptionId, "--query", "[0].location", "-o", "tsv"])).trim();
20578
20666
  if (!location) {
20579
20667
  throw new LocalCliError({
20580
20668
  code: "PLATFORM_ENABLE_AUTO_UPDATE_NO_LOCATION",
@@ -20593,7 +20681,9 @@ var PlatformEnableAutoUpdateCommand = class extends M8tCommand {
20593
20681
  foundryResourceId,
20594
20682
  foundryProjectEndpoint: project.endpoint,
20595
20683
  acrPullIdentityResourceId,
20596
- acrResourceId
20684
+ acrResourceId,
20685
+ ...parseFoundryTracingMode(this.foundryTracing) ? { foundryTracingMode: parseFoundryTracingMode(this.foundryTracing) } : {},
20686
+ ...typeof this.channelUrl === "string" && this.channelUrl.length > 0 ? { channelUrl: this.channelUrl } : {}
20597
20687
  };
20598
20688
  onProgress("running Bicep deployment to provision the updater (~2-4 min)\u2026");
20599
20689
  const repoRoot = await resolveRepoRoot2();
@@ -20604,18 +20694,27 @@ var PlatformEnableAutoUpdateCommand = class extends M8tCommand {
20604
20694
  ...recoveredParams,
20605
20695
  provisionUpdater: true,
20606
20696
  installerImage: this.installerImage,
20607
- ...typeof this.updateCron === "string" ? { updateCron: this.updateCron } : {},
20608
- ...typeof this.channelUrl === "string" ? { channelUrl: this.channelUrl } : {}
20697
+ ...voiceInternalSecret ? { voiceInternalSecret } : {},
20698
+ ...typeof this.updateCron === "string" ? { updateCron: this.updateCron } : {}
20609
20699
  }),
20610
20700
  deploymentName: `m8t-enable-auto-update-${suffix.slice(0, 12)}`
20611
20701
  });
20612
20702
  onProgress("backfilling system/infra-params\u2026");
20613
- await writeInfraParams(infraTable, {
20614
- schemaVersion: 1,
20615
- suffix,
20616
- bicep: recoveredParams,
20617
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
20618
- });
20703
+ await writeInfraParams(
20704
+ {
20705
+ credential: credential2,
20706
+ subscriptionId,
20707
+ resourceGroup,
20708
+ retryHint: "The role assignment persists \u2014 retry 'm8t platform enable-auto-update' in a few minutes.",
20709
+ onProgress
20710
+ },
20711
+ {
20712
+ schemaVersion: 1,
20713
+ suffix,
20714
+ bicep: recoveredParams,
20715
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
20716
+ }
20717
+ );
20619
20718
  if (mode === "json") {
20620
20719
  this.context.stdout.write(renderJson({ suffix, resourceGroup, installerImage: this.installerImage, enabled: true }) + "\n");
20621
20720
  return 0;
@@ -22589,7 +22688,7 @@ var OpenCommand = class extends M8tCommand {
22589
22688
  // src/commands/dream/run.ts
22590
22689
  import { Command as Command46, Option as Option43 } from "clipanion";
22591
22690
  import { AzureCliCredential } from "@azure/identity";
22592
- import { TableClient as TableClient5 } from "@azure/data-tables";
22691
+ import { TableClient as TableClient6 } from "@azure/data-tables";
22593
22692
  import { AIProjectClient as AIProjectClient3 } from "@azure/ai-projects";
22594
22693
  import { LogsQueryClient as LogsQueryClient3, LogsQueryResultStatus as LogsQueryResultStatus3 } from "@azure/monitor-query";
22595
22694
 
@@ -22898,7 +22997,7 @@ var ConvFetchError = class extends Error {
22898
22997
  };
22899
22998
 
22900
22999
  // ../../packages/brain/engine/dist/esm/cursor.js
22901
- import { TableClient as TableClient4 } from "@azure/data-tables";
23000
+ import { TableClient as TableClient5 } from "@azure/data-tables";
22902
23001
 
22903
23002
  // ../../packages/agent-ledger/dist/esm/row-key.js
22904
23003
  var MAX_MS = 864e13;
@@ -23038,7 +23137,7 @@ async function commitCursorAdvance(plan, cursor) {
23038
23137
  }
23039
23138
  var CURSOR_TABLE_NAME = "BrainDreamCursor";
23040
23139
  function makeTableCursor(credential2, tableEndpoint) {
23041
- const client = new TableClient4(tableEndpoint, CURSOR_TABLE_NAME, credential2);
23140
+ const client = new TableClient5(tableEndpoint, CURSOR_TABLE_NAME, credential2);
23042
23141
  return new TableCursor(client);
23043
23142
  }
23044
23143
  function formatCursorPreview(cursor) {
@@ -25022,7 +25121,7 @@ AppDependencies
25022
25121
  };
25023
25122
  }
25024
25123
  function buildLiveSources(context, credential2) {
25025
- const ledgerClient = new TableClient5(context.ledgerTableEndpoint, LEDGER_TABLE_NAME, credential2);
25124
+ const ledgerClient = new TableClient6(context.ledgerTableEndpoint, LEDGER_TABLE_NAME, credential2);
25026
25125
  const ledger = makeLedgerSource(
25027
25126
  async (pk, sinceTs) => fetchLedgerRows(
25028
25127
  { workers: [pk], source: "all", from: sinceTs, to: "9999-12-31T23:59:59.999Z" },
@@ -25818,10 +25917,16 @@ function buildAciCreateArgs(s) {
25818
25917
  `RESOURCE_GROUP=${s.resourceGroup}`,
25819
25918
  `LOCATION=${s.location}`,
25820
25919
  `MI_CLIENT_ID=${s.miClientId}`,
25821
- `APP_REG_CLIENT_ID=${s.appRegClientId}`
25920
+ `APP_REG_CLIENT_ID=${s.appRegClientId}`,
25921
+ // The installer's own image ref, so the install can provision the updater job
25922
+ // to run THIS engine. Derived from the launched image rather than threaded
25923
+ // separately, so the two can never disagree — and never read from the release
25924
+ // pin, which can name an installer several versions behind the running one.
25925
+ `INSTALLER_IMAGE_REF=${s.image}`
25822
25926
  ];
25823
25927
  if (s.gatewayImageRef) env.push(`GATEWAY_IMAGE_REF=${s.gatewayImageRef}`);
25824
25928
  if (s.foundryTracing) env.push(`FOUNDRY_TRACING=${s.foundryTracing}`);
25929
+ if (s.updateChannelUrl) env.push(`M8T_UPDATE_CHANNEL_URL=${s.updateChannelUrl}`);
25825
25930
  if (s.enrollContactEmail) env.push(`M8T_ENROLL_CONTACT_EMAIL=${s.enrollContactEmail}`);
25826
25931
  if (s.githubApp) {
25827
25932
  const g = s.githubApp;
@@ -25973,7 +26078,7 @@ async function writeBootstrapState(state, home = os13.homedir()) {
25973
26078
  // src/commands/bootstrap/launch.ts
25974
26079
  var DEFAULT_RG = "rg-m8t-stack";
25975
26080
  var DEFAULT_INSTALLER = "ghcr.io/m8t-labs/m8t-installer";
25976
- var DEFAULT_INSTALLER_TAG = "v0.1.36";
26081
+ var DEFAULT_INSTALLER_TAG = "v0.1.38";
25977
26082
  var ACI_NAME = "m8t-installer";
25978
26083
  var MI_NAME = "m8t-installer-mi";
25979
26084
  var BootstrapLaunchCommand = class extends M8tCommand {
@@ -26090,7 +26195,8 @@ var BootstrapLaunchCommand = class extends M8tCommand {
26090
26195
  gatewayImageRef,
26091
26196
  foundryTracing: "skip",
26092
26197
  githubApp,
26093
- enrollContactEmail: account.upn
26198
+ enrollContactEmail: account.upn,
26199
+ ...process.env.M8T_UPDATE_CHANNEL_URL ? { updateChannelUrl: process.env.M8T_UPDATE_CHANNEL_URL } : {}
26094
26200
  });
26095
26201
  this.context.stdout.write(
26096
26202
  `${colors.success("\u2713")} installer launched in ${colors.field(resourceGroup)} (${location}).
@@ -26423,42 +26529,301 @@ var DEFAULT_MEMORY_INDEX_HEADER = [
26423
26529
  `> Your memories, newest first. The summary on each line is usually enough to answer \u2014 open the linked file only when you need full detail. Each path is repo-root; copy it verbatim, never invent one.`,
26424
26530
  ``
26425
26531
  ].join("\n");
26426
- function parseOnboardingBlock(text) {
26427
- const candidates = [
26428
- ...[...text.matchAll(/```(?:json)?\s*([\s\S]*?)```/g)].map((m) => m[1]),
26429
- text
26430
- ].reverse();
26431
- for (const c of candidates) {
26532
+ var ONBOARDING_BLOCK_KEYS = [
26533
+ "schema_version",
26534
+ "company_stage",
26535
+ "icp",
26536
+ "industry",
26537
+ "team_size",
26538
+ "context",
26539
+ "founder_name",
26540
+ "founder_email",
26541
+ "advisor_name",
26542
+ "advisor_email"
26543
+ ];
26544
+ function isRecord(value) {
26545
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26546
+ }
26547
+ function hasValidUniqueJsonKeys(source) {
26548
+ let offset = 0;
26549
+ const skipWhitespace = () => {
26550
+ while (/\s/.test(source[offset] ?? "")) offset += 1;
26551
+ };
26552
+ const parseString = () => {
26553
+ if (source[offset] !== '"') return null;
26554
+ const start = offset;
26555
+ offset += 1;
26556
+ let escaped = false;
26557
+ while (offset < source.length) {
26558
+ const char = source[offset];
26559
+ if (escaped) {
26560
+ escaped = false;
26561
+ offset += 1;
26562
+ continue;
26563
+ }
26564
+ if (char === "\\") {
26565
+ escaped = true;
26566
+ offset += 1;
26567
+ continue;
26568
+ }
26569
+ if (char === '"') {
26570
+ offset += 1;
26571
+ try {
26572
+ const parsed = JSON.parse(source.slice(start, offset));
26573
+ return typeof parsed === "string" ? parsed : null;
26574
+ } catch {
26575
+ return null;
26576
+ }
26577
+ }
26578
+ offset += 1;
26579
+ }
26580
+ return null;
26581
+ };
26582
+ const parseValue = (depth) => {
26583
+ if (depth > 32) return false;
26584
+ skipWhitespace();
26585
+ const char = source[offset];
26586
+ if (char === '"') return parseString() !== null;
26587
+ if (char === "{") {
26588
+ offset += 1;
26589
+ skipWhitespace();
26590
+ const keys = /* @__PURE__ */ new Set();
26591
+ if (source[offset] === "}") {
26592
+ offset += 1;
26593
+ return true;
26594
+ }
26595
+ for (; ; ) {
26596
+ skipWhitespace();
26597
+ const key2 = parseString();
26598
+ if (key2 === null) return false;
26599
+ if (keys.has(key2)) return false;
26600
+ keys.add(key2);
26601
+ skipWhitespace();
26602
+ if (source[offset] !== ":") return false;
26603
+ offset += 1;
26604
+ if (!parseValue(depth + 1)) return false;
26605
+ skipWhitespace();
26606
+ if (source[offset] === "}") {
26607
+ offset += 1;
26608
+ return true;
26609
+ }
26610
+ if (source[offset] !== ",") return false;
26611
+ offset += 1;
26612
+ }
26613
+ }
26614
+ if (char === "[") {
26615
+ offset += 1;
26616
+ skipWhitespace();
26617
+ if (source[offset] === "]") {
26618
+ offset += 1;
26619
+ return true;
26620
+ }
26621
+ for (; ; ) {
26622
+ if (!parseValue(depth + 1)) return false;
26623
+ skipWhitespace();
26624
+ if (source[offset] === "]") {
26625
+ offset += 1;
26626
+ return true;
26627
+ }
26628
+ if (source[offset] !== ",") return false;
26629
+ offset += 1;
26630
+ }
26631
+ }
26632
+ const primitiveStart = offset;
26633
+ while (offset < source.length && !/[\s,}\]]/.test(source[offset] ?? "")) offset += 1;
26634
+ return offset > primitiveStart;
26635
+ };
26636
+ if (!parseValue(0)) return false;
26637
+ skipWhitespace();
26638
+ if (offset !== source.length) return false;
26639
+ try {
26640
+ JSON.parse(source);
26641
+ return true;
26642
+ } catch {
26643
+ return false;
26644
+ }
26645
+ }
26646
+ function canonicalBlock(value) {
26647
+ if (!isRecord(value)) return null;
26648
+ const keys = Object.keys(value).sort();
26649
+ const expected = [...ONBOARDING_BLOCK_KEYS].sort();
26650
+ if (keys.length !== expected.length || keys.some((key2, index) => key2 !== expected[index])) return null;
26651
+ if (value.schema_version !== "2") return null;
26652
+ for (const key2 of ONBOARDING_BLOCK_KEYS) {
26653
+ if (typeof value[key2] !== "string") return null;
26654
+ }
26655
+ return value;
26656
+ }
26657
+ function parseOnboardingArtifact(machineText) {
26658
+ const fencePattern = /^```([^\r\n]*)\r?\n([\s\S]*?)^```[ \t]*(?=\r?$)/gm;
26659
+ const fences = [...machineText.matchAll(fencePattern)];
26660
+ const jsonFenceStarts = [...machineText.matchAll(/^```[ \t]*json[ \t]*\r?$/gm)];
26661
+ const matchedJsonFences = fences.filter((match) => match[1].trim() === "json");
26662
+ if (jsonFenceStarts.length !== matchedJsonFences.length) return null;
26663
+ if (fences.some((match) => match[1].trim() !== "json" && /"m8t_onboarding"\s*:/.test(match[2]))) return null;
26664
+ let outsideFences = "";
26665
+ let previousEnd = 0;
26666
+ for (const fence of fences) {
26667
+ const start = fence.index;
26668
+ outsideFences += machineText.slice(previousEnd, start);
26669
+ previousEnd = start + fence[0].length;
26670
+ }
26671
+ outsideFences += machineText.slice(previousEnd);
26672
+ if (/"m8t_onboarding"\s*:/.test(outsideFences)) return null;
26673
+ const artifacts = [];
26674
+ for (const fence of matchedJsonFences) {
26675
+ const json = fence[2].trim();
26676
+ let parsed;
26677
+ if (!hasValidUniqueJsonKeys(json)) {
26678
+ if (json.includes("m8t_onboarding")) return null;
26679
+ continue;
26680
+ }
26681
+ try {
26682
+ parsed = JSON.parse(json);
26683
+ } catch {
26684
+ if (json.includes("m8t_onboarding")) return null;
26685
+ continue;
26686
+ }
26687
+ if (!isRecord(parsed) || !Object.hasOwn(parsed, "m8t_onboarding")) continue;
26688
+ if (Object.keys(parsed).length !== 1) return null;
26689
+ const block = canonicalBlock(parsed.m8t_onboarding);
26690
+ if (!block) return null;
26691
+ const start = fence.index;
26692
+ artifacts.push({ block, start, end: start + fence[0].length });
26693
+ }
26694
+ if (artifacts.length !== 1) return null;
26695
+ const artifact = artifacts[0];
26696
+ const before = machineText.slice(0, artifact.start).trimEnd();
26697
+ const after = machineText.slice(artifact.end).trimStart();
26698
+ return {
26699
+ block: artifact.block,
26700
+ machineText,
26701
+ speechText: [before, after].filter((part) => part.length > 0).join("\n").trim()
26702
+ };
26703
+ }
26704
+ var EMPTY_PROFILE_RESULT = {
26705
+ hadIntake: false,
26706
+ block: null,
26707
+ machineText: null,
26708
+ speechText: null
26709
+ };
26710
+ async function readCursorPages(args) {
26711
+ const all = [];
26712
+ const cursors = /* @__PURE__ */ new Set();
26713
+ let url = args.url;
26714
+ for (let pageNumber = 0; pageNumber < 100; pageNumber += 1) {
26715
+ let response;
26716
+ try {
26717
+ response = await args.fetchImpl(url, { headers: args.headers });
26718
+ } catch {
26719
+ return null;
26720
+ }
26721
+ if (!response.ok) return null;
26722
+ let body;
26432
26723
  try {
26433
- const o = JSON.parse(c.trim()).m8t_onboarding;
26434
- if (o?.company_stage && o.context) return o;
26724
+ body = JSON.parse(await response.text());
26435
26725
  } catch {
26726
+ return null;
26727
+ }
26728
+ if (!isRecord(body) || !Array.isArray(body.data)) return null;
26729
+ const page = body;
26730
+ all.push(...page.data);
26731
+ if (page.has_more !== true) {
26732
+ if (page.has_more === void 0 || page.has_more === false) return all;
26733
+ return null;
26436
26734
  }
26735
+ if (typeof page.last_id !== "string" || page.last_id.trim().length === 0) return null;
26736
+ if (cursors.has(page.last_id)) return null;
26737
+ cursors.add(page.last_id);
26738
+ const next = new URL(url);
26739
+ next.searchParams.set("after", page.last_id);
26740
+ url = next.toString();
26437
26741
  }
26438
26742
  return null;
26439
26743
  }
26440
- async function safeJson(res) {
26744
+ function decodeFoundryUser(token) {
26745
+ const parts = token.split(".");
26746
+ if (parts.length !== 3 || parts.some((part) => part.length === 0) || !/^[A-Za-z0-9_-]+$/.test(parts[1] ?? "")) return null;
26441
26747
  try {
26442
- const t = await res.text();
26443
- return t ? JSON.parse(t) : null;
26748
+ const payload = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8"));
26749
+ if (!isRecord(payload)) return null;
26750
+ const claim = payload.upn ?? payload.preferred_username ?? payload.email;
26751
+ return typeof claim === "string" && claim.trim().length > 0 ? claim : null;
26444
26752
  } catch {
26445
26753
  return null;
26446
26754
  }
26447
26755
  }
26756
+ function timestamp(value) {
26757
+ if (typeof value === "number" && Number.isFinite(value)) return value < 1e12 ? value * 1e3 : value;
26758
+ if (typeof value === "string") {
26759
+ const parsed = Date.parse(value);
26760
+ return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
26761
+ }
26762
+ return Number.NEGATIVE_INFINITY;
26763
+ }
26764
+ function orderNewest(values) {
26765
+ if (!values.every((value) => Number.isFinite(value.createdAt))) return values;
26766
+ return values.sort((a, b) => b.createdAt - a.createdAt || a.ordinal - b.ordinal || b.id.localeCompare(a.id));
26767
+ }
26448
26768
  async function findOnboardingProfile(args) {
26769
+ const userId = decodeFoundryUser(args.token);
26770
+ if (!userId) return { ...EMPTY_PROFILE_RESULT };
26449
26771
  const doFetch = args.fetchImpl ?? fetch;
26450
26772
  const H = { Authorization: `Bearer ${args.token}` };
26451
- const listRes = await doFetch(`${args.endpoint}/conversations?api-version=v1&order=desc&limit=20`, { headers: H });
26452
- const list = await safeJson(listRes);
26453
- const convs = (list?.data ?? []).filter((c) => c.metadata?.agent === "stacey-intake");
26454
- for (const conv of convs) {
26455
- const itemsRes = await doFetch(`${args.endpoint}/conversations/${conv.id}/items?api-version=v1`, { headers: H });
26456
- const items = await safeJson(itemsRes);
26457
- const text = (items?.data ?? []).flatMap((m) => (m.content ?? []).map((c) => c.text ?? "")).join("\n");
26458
- const block = parseOnboardingBlock(text);
26459
- if (block) return { hadIntake: true, block };
26460
- }
26461
- return { hadIntake: convs.length > 0, block: null };
26773
+ const base = args.endpoint.replace(/\/+$/, "");
26774
+ const listed = await readCursorPages({
26775
+ url: `${base}/openai/v1/conversations?order=desc&limit=100`,
26776
+ headers: H,
26777
+ fetchImpl: doFetch
26778
+ });
26779
+ if (!listed) return { ...EMPTY_PROFILE_RESULT };
26780
+ const conversations = orderNewest(listed.flatMap((value, ordinal) => {
26781
+ if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0 || !isRecord(value.metadata)) return [];
26782
+ const metadata = value.metadata;
26783
+ if (metadata.app !== "m8t-webapp" || metadata.agent !== "stacey-intake" || metadata.userId !== userId) return [];
26784
+ return [{
26785
+ id: value.id,
26786
+ createdAt: timestamp(value.created_at) !== Number.NEGATIVE_INFINITY ? timestamp(value.created_at) : timestamp(metadata.createdAt),
26787
+ ordinal
26788
+ }];
26789
+ }));
26790
+ const conversation = conversations.at(0);
26791
+ if (conversation === void 0) return { ...EMPTY_PROFILE_RESULT };
26792
+ const items = await readCursorPages({
26793
+ url: `${base}/openai/v1/conversations/${encodeURIComponent(conversation.id)}/items?order=desc&limit=100`,
26794
+ headers: H,
26795
+ fetchImpl: doFetch
26796
+ });
26797
+ if (!items) return { hadIntake: true, block: null, machineText: null, speechText: null };
26798
+ const assistantItems = orderNewest(items.flatMap((value, ordinal) => {
26799
+ if (!isRecord(value) || value.type !== "message" || value.role !== "assistant" || !Array.isArray(value.content)) return [];
26800
+ const id = typeof value.id === "string" ? value.id : "";
26801
+ return [{ value, id, createdAt: timestamp(value.created_at), ordinal }];
26802
+ }));
26803
+ const artifacts = [];
26804
+ let malformedArtifact = false;
26805
+ for (const item of assistantItems) {
26806
+ const content = item.value.content;
26807
+ const machineText = content.flatMap((part) => {
26808
+ if (!isRecord(part)) return [];
26809
+ if (typeof part.text === "string" && part.text.length > 0) return [part.text];
26810
+ if (typeof part.transcript === "string") return [part.transcript];
26811
+ return [];
26812
+ }).join("\n");
26813
+ const artifact2 = parseOnboardingArtifact(machineText);
26814
+ if (artifact2) artifacts.push(artifact2);
26815
+ else if (machineText.includes("m8t_onboarding")) malformedArtifact = true;
26816
+ }
26817
+ if (malformedArtifact || artifacts.length !== 1) {
26818
+ return { hadIntake: true, block: null, machineText: null, speechText: null };
26819
+ }
26820
+ const artifact = artifacts[0];
26821
+ return {
26822
+ hadIntake: true,
26823
+ block: artifact.block,
26824
+ machineText: artifact.machineText,
26825
+ speechText: artifact.speechText
26826
+ };
26462
26827
  }
26463
26828
  function renderCompanyProfile(block, now = (/* @__PURE__ */ new Date()).toISOString()) {
26464
26829
  const dash = (v) => v?.trim() ? v.trim() : "\u2014";
@@ -26490,24 +26855,6 @@ function renderCompanyProfile(block, now = (/* @__PURE__ */ new Date()).toISOStr
26490
26855
  const memoryIndexLine = `- \`${COMPANY_PROFILE_PATH}\` \u2014 **Company profile**: ${bits}. (seeded from onboarding)`;
26491
26856
  return { profileMd, memoryIndexLine };
26492
26857
  }
26493
- function companyProfileBodyEquals(a, b) {
26494
- const strip = (s) => s.replace(/^(created|updated):.*$/gm, "").trim();
26495
- return strip(a) === strip(b);
26496
- }
26497
- function upsertMemoryIndex(existing, line2, targetPath2 = COMPANY_PROFILE_PATH) {
26498
- const lines = existing.split("\n");
26499
- const existingIdx = lines.findIndex((l) => l.includes(`\`${targetPath2}\``));
26500
- if (existingIdx >= 0) {
26501
- lines[existingIdx] = line2;
26502
- return lines.join("\n");
26503
- }
26504
- const firstItemIdx = lines.findIndex((l) => /^-\s+`memory\//.test(l));
26505
- if (firstItemIdx >= 0) {
26506
- lines.splice(firstItemIdx, 0, line2);
26507
- return lines.join("\n");
26508
- }
26509
- return existing.replace(/\s*$/, "\n\n") + line2 + "\n";
26510
- }
26511
26858
  var FOUNDER_RECORD_PATH = "memory/founder.md";
26512
26859
  var NOT_CAPTURED = "_not captured yet \u2014 add it anytime (run `m8t bootstrap seed-profile`, or just tell me)_";
26513
26860
  function renderFounderRecord(block, inputs, now = (/* @__PURE__ */ new Date()).toISOString()) {
@@ -26545,10 +26892,6 @@ function renderFounderRecord(block, inputs, now = (/* @__PURE__ */ new Date()).t
26545
26892
  const memoryIndexLine = `- \`${FOUNDER_RECORD_PATH}\` \u2014 **Founder & install context**: ${founderName || "founder"} \xB7 advisor ${idxAdvisor} \xB7 sub ${idxSub}. (seeded from onboarding)`;
26546
26893
  return { founderMd, memoryIndexLine };
26547
26894
  }
26548
- function founderRecordBodyEquals(a, b) {
26549
- const strip = (s) => s.replace(/^(created|updated):.*$/gm, "").trim();
26550
- return strip(a) === strip(b);
26551
- }
26552
26895
 
26553
26896
  // src/lib/founder-identity.ts
26554
26897
  function deriveEmailCandidate(raw) {
@@ -26620,6 +26963,44 @@ async function resolveSeedContext(opts) {
26620
26963
  const brainRepos = opts.brainOverride ? [opts.brainOverride] : [`${appCreds.org}/stacey-brain`, `${appCreds.org}/azzy-brain`];
26621
26964
  return { endpoint, brainRepos, appCreds, subscriptionId };
26622
26965
  }
26966
+ var MEMORY_INDEX_PATH = "memory/MEMORY.md";
26967
+ function normalizeFrontmatterTimestamps(markdown) {
26968
+ const lines = markdown.split("\n");
26969
+ if (lines[0]?.replace(/\r$/, "") !== "---") return markdown;
26970
+ const end = lines.findIndex((line2, index) => index > 0 && line2.replace(/\r$/, "") === "---");
26971
+ if (end < 0) return markdown;
26972
+ return lines.map((line2, index) => {
26973
+ if (index <= 0 || index >= end) return line2;
26974
+ return /^(created|updated):/.test(line2) ? line2.replace(/^(created|updated):.*$/, "$1: <generated>") : line2;
26975
+ }).join("\n");
26976
+ }
26977
+ function seededDocumentMatches(actual, expected) {
26978
+ return actual != null && normalizeFrontmatterTimestamps(actual) === normalizeFrontmatterTimestamps(expected);
26979
+ }
26980
+ function upsertMemoryIndexOnce(existing, line2, targetPath2) {
26981
+ const lines = existing.split("\n");
26982
+ const kept = [];
26983
+ let insertionIndex;
26984
+ for (const existingLine of lines) {
26985
+ if (existingLine.includes(`\`${targetPath2}\``)) {
26986
+ insertionIndex ??= kept.length;
26987
+ } else {
26988
+ kept.push(existingLine);
26989
+ }
26990
+ }
26991
+ if (insertionIndex === void 0) {
26992
+ const firstMemoryItem = kept.findIndex((existingLine) => /^-\s+`memory\//.test(existingLine));
26993
+ if (firstMemoryItem >= 0) {
26994
+ insertionIndex = firstMemoryItem;
26995
+ } else {
26996
+ const base = kept.join("\n").replace(/\s*$/, "\n\n");
26997
+ return `${base}${line2}
26998
+ `;
26999
+ }
27000
+ }
27001
+ kept.splice(insertionIndex, 0, line2);
27002
+ return kept.join("\n");
27003
+ }
26623
27004
  async function applyProfileToBrain(args) {
26624
27005
  const token = await mintInstallationTokenFromPem({
26625
27006
  appId: args.appCreds.appId,
@@ -26633,29 +27014,84 @@ async function applyProfileToBrain(args) {
26633
27014
  { subscriptionId: args.subscriptionId, azIdentity: args.azIdentity },
26634
27015
  args.now
26635
27016
  );
26636
- const read = (p) => readRepoFileViaApp({ token, repo: args.brainRepo, path: p, fetchImpl: args.fetchImpl });
26637
- const existingProfile = await read(COMPANY_PROFILE_PATH);
26638
- const existingFounder = await read(FOUNDER_RECORD_PATH);
26639
- const existingIndex = await read("memory/MEMORY.md") ?? DEFAULT_MEMORY_INDEX_HEADER;
26640
- let nextIndex = upsertMemoryIndex(existingIndex, companyLine, COMPANY_PROFILE_PATH);
26641
- nextIndex = upsertMemoryIndex(nextIndex, founderLine, FOUNDER_RECORD_PATH);
26642
- const profileSame = existingProfile != null && companyProfileBodyEquals(existingProfile, profileMd);
26643
- const founderSame = existingFounder != null && founderRecordBodyEquals(existingFounder, founderMd);
26644
- if (profileSame && founderSame && nextIndex === existingIndex) {
26645
- return;
26646
- }
26647
- await commitFilesViaApp({
27017
+ const read = (p) => readRepoFileViaApp({
26648
27018
  token,
26649
27019
  repo: args.brainRepo,
26650
- branch: args.branch,
26651
- message: "seed(brain): founder + company profile from onboarding",
26652
- files: [
26653
- { path: COMPANY_PROFILE_PATH, content: profileMd },
26654
- { path: FOUNDER_RECORD_PATH, content: founderMd },
26655
- { path: "memory/MEMORY.md", content: nextIndex }
26656
- ],
27020
+ path: p,
27021
+ ref: args.branch,
26657
27022
  fetchImpl: args.fetchImpl
26658
27023
  });
27024
+ const existingProfile = await read(COMPANY_PROFILE_PATH);
27025
+ const existingFounder = await read(FOUNDER_RECORD_PATH);
27026
+ const existingIndex = await read(MEMORY_INDEX_PATH) ?? DEFAULT_MEMORY_INDEX_HEADER;
27027
+ let nextIndex = upsertMemoryIndexOnce(existingIndex, companyLine, COMPANY_PROFILE_PATH);
27028
+ nextIndex = upsertMemoryIndexOnce(nextIndex, founderLine, FOUNDER_RECORD_PATH);
27029
+ const files = [
27030
+ ...!seededDocumentMatches(existingProfile, profileMd) ? [{ path: COMPANY_PROFILE_PATH, content: profileMd }] : [],
27031
+ ...!seededDocumentMatches(existingFounder, founderMd) ? [{ path: FOUNDER_RECORD_PATH, content: founderMd }] : [],
27032
+ ...nextIndex !== existingIndex ? [{ path: MEMORY_INDEX_PATH, content: nextIndex }] : []
27033
+ ];
27034
+ if (files.length > 0) {
27035
+ await commitFilesViaApp({
27036
+ token,
27037
+ repo: args.brainRepo,
27038
+ branch: args.branch,
27039
+ message: "seed(brain): founder + company profile from onboarding",
27040
+ files,
27041
+ fetchImpl: args.fetchImpl
27042
+ });
27043
+ const [verifiedProfile, verifiedFounder, verifiedIndex] = await Promise.all([
27044
+ read(COMPANY_PROFILE_PATH),
27045
+ read(FOUNDER_RECORD_PATH),
27046
+ read(MEMORY_INDEX_PATH)
27047
+ ]);
27048
+ const mismatches = [
27049
+ ...!seededDocumentMatches(verifiedProfile, profileMd) ? [COMPANY_PROFILE_PATH] : [],
27050
+ ...!seededDocumentMatches(verifiedFounder, founderMd) ? [FOUNDER_RECORD_PATH] : [],
27051
+ ...verifiedIndex !== nextIndex ? [MEMORY_INDEX_PATH] : []
27052
+ ];
27053
+ if (mismatches.length > 0) {
27054
+ throw new LocalCliError({
27055
+ code: "SEED_PROFILE_READBACK_FAILED",
27056
+ message: `Profile write could not be verified in ${args.brainRepo}: ${mismatches.join(", ")}.`,
27057
+ hint: "Re-run 'm8t bootstrap seed-profile'; verified files will be left unchanged."
27058
+ });
27059
+ }
27060
+ }
27061
+ }
27062
+ async function applyProfileToBrains(args) {
27063
+ const brainRepos = [...new Set(args.brainRepos)];
27064
+ if (brainRepos.length === 0) {
27065
+ throw new LocalCliError({
27066
+ code: "SEED_PROFILE_INCOMPLETE",
27067
+ message: "Profile seeding is incomplete: no brain repositories were resolved.",
27068
+ hint: "Re-run 'm8t bootstrap seed-profile' after the Stacey and Azzy brains are provisioned."
27069
+ });
27070
+ }
27071
+ const settled = await Promise.allSettled(brainRepos.map(async (brainRepo) => {
27072
+ await applyProfileToBrain({ ...args, brainRepo });
27073
+ return brainRepo;
27074
+ }));
27075
+ const verified = [];
27076
+ const failed = [];
27077
+ const causes = [];
27078
+ settled.forEach((result, index) => {
27079
+ const brainRepo = brainRepos[index];
27080
+ if (result.status === "fulfilled") {
27081
+ verified.push(brainRepo);
27082
+ } else {
27083
+ failed.push(brainRepo);
27084
+ causes.push(result.reason);
27085
+ }
27086
+ });
27087
+ if (failed.length > 0 || verified.length !== brainRepos.length) {
27088
+ throw new LocalCliError({
27089
+ code: "SEED_PROFILE_INCOMPLETE",
27090
+ message: `Profile seeding is incomplete: verified ${verified.join(", ") || "none"}; failed ${failed.join(", ") || "none"}.`,
27091
+ hint: "Re-run 'm8t bootstrap seed-profile'; verified brain files will be skipped and missing files repaired.",
27092
+ cause: new AggregateError(causes, "One or more brain profile writes failed verification.")
27093
+ });
27094
+ }
26659
27095
  }
26660
27096
  function spawnDetachedSeedWatch() {
26661
27097
  const logPath = path33.join(os15.homedir(), ".m8t", "seed-profile.log");
@@ -26677,17 +27113,15 @@ async function reactiveSeedOnFinish(args) {
26677
27113
  const { hadIntake, block } = await findOnboardingProfile({ endpoint: ctx.endpoint, token, fetchImpl: args.fetchImpl });
26678
27114
  if (block) {
26679
27115
  const azIdentity = args.azIdentity ?? await (args.getSignedInUserIdentityImpl ?? getSignedInUserIdentity)();
26680
- for (const brainRepo of ctx.brainRepos) {
26681
- await applyProfileToBrain({
26682
- block,
26683
- brainRepo,
26684
- branch: "main",
26685
- appCreds: ctx.appCreds,
26686
- subscriptionId: ctx.subscriptionId,
26687
- azIdentity,
26688
- fetchImpl: args.fetchImpl
26689
- });
26690
- }
27116
+ await applyProfileToBrains({
27117
+ block,
27118
+ brainRepos: ctx.brainRepos,
27119
+ branch: "main",
27120
+ appCreds: ctx.appCreds,
27121
+ subscriptionId: ctx.subscriptionId,
27122
+ azIdentity,
27123
+ fetchImpl: args.fetchImpl
27124
+ });
26691
27125
  args.stdout(`${colors.success("\u2713")} Your advisors now know your company + how to reach you (seeded ${ctx.brainRepos.join(", ")}).
26692
27126
  `);
26693
27127
  return;
@@ -26854,6 +27288,7 @@ import * as fs32 from "fs";
26854
27288
  import * as net from "net";
26855
27289
  import * as os17 from "os";
26856
27290
  import * as path35 from "path";
27291
+ import { randomBytes as randomBytes3 } from "crypto";
26857
27292
  import { spawn as spawn7, spawnSync as spawnSync6 } from "child_process";
26858
27293
  init_errors();
26859
27294
  init_rbac();
@@ -27018,9 +27453,19 @@ async function ensureFounderFoundryRole(args) {
27018
27453
  }
27019
27454
  function writeWebEnvLocal(args) {
27020
27455
  const envPath = path35.join(args.repoRoot, "apps", "web", ".env.local");
27456
+ let prior = "";
27021
27457
  if (fs32.existsSync(envPath)) {
27458
+ prior = fs32.readFileSync(envPath, "utf8");
27022
27459
  fs32.copyFileSync(envPath, envPath + ".bak");
27023
27460
  }
27461
+ const priorSecret = readValidInternalSecret(prior);
27462
+ const internalSecret = priorSecret ?? (args.randomBytesImpl ?? randomBytes3)(32).toString("base64url");
27463
+ if (!isValidInternalSecret(internalSecret)) {
27464
+ throw new LocalCliError({
27465
+ code: "BOOTSTRAP_UI_INTERNAL_SECRET_GENERATION_FAILED",
27466
+ message: "Could not generate a valid local internal service secret."
27467
+ });
27468
+ }
27024
27469
  const body = [
27025
27470
  "# Written by `m8t bootstrap ui` \u2014 onboarding chat-only run (Simple Stacey).",
27026
27471
  "# STORAGE unset \u2192 gateway boot skipped; the webapp forwards your MSAL token to Foundry.",
@@ -27029,14 +27474,47 @@ function writeWebEnvLocal(args) {
27029
27474
  `FOUNDRY_PROJECT_ENDPOINT=${args.foundryEndpoint}`,
27030
27475
  "VOICE_RELAY_PUBLIC_URL=ws://localhost:8790",
27031
27476
  "STORAGE_ACCOUNT_NAME=",
27477
+ `M8T_INTERNAL_SECRET=${internalSecret}`,
27478
+ "M8T_SINGLE_GATEWAY_PROCESS=1",
27032
27479
  "M8T_DISABLE_WORKER=1",
27033
27480
  "M8T_ONBOARDING=1",
27034
27481
  "NEXT_PUBLIC_M8T_ONBOARDING=1",
27035
27482
  ""
27036
27483
  ].join("\n");
27037
- fs32.writeFileSync(envPath, body, "utf8");
27484
+ fs32.writeFileSync(envPath, body, { encoding: "utf8", mode: 384 });
27485
+ try {
27486
+ fs32.chmodSync(envPath, 384);
27487
+ } catch {
27488
+ }
27038
27489
  return envPath;
27039
27490
  }
27491
+ function isValidInternalSecret(value) {
27492
+ if (!value || !/^[A-Za-z0-9_-]{43}$/.test(value)) return false;
27493
+ const decoded = Buffer.from(value, "base64url");
27494
+ return decoded.length === 32 && decoded.toString("base64url") === value;
27495
+ }
27496
+ function readValidInternalSecret(envText) {
27497
+ const values = envText.split(/\r?\n/).filter((line2) => line2.startsWith("M8T_INTERNAL_SECRET=")).map((line2) => line2.slice("M8T_INTERNAL_SECRET=".length));
27498
+ if (values.length !== 1) return void 0;
27499
+ return isValidInternalSecret(values[0]) ? values[0] : void 0;
27500
+ }
27501
+ function readWebInternalSecret(repoRoot) {
27502
+ const envPath = path35.join(repoRoot, "apps", "web", ".env.local");
27503
+ let secret;
27504
+ try {
27505
+ secret = readValidInternalSecret(fs32.readFileSync(envPath, "utf8"));
27506
+ } catch {
27507
+ secret = void 0;
27508
+ }
27509
+ if (!secret) {
27510
+ throw new LocalCliError({
27511
+ code: "BOOTSTRAP_UI_INTERNAL_SECRET_MISSING",
27512
+ message: "The local web environment has no valid internal service secret.",
27513
+ hint: "Re-run 'm8t bootstrap ui' so the web app and voice relay receive the same secret."
27514
+ });
27515
+ }
27516
+ return secret;
27517
+ }
27040
27518
  function assertNodeVersion(versionString = process.version) {
27041
27519
  const major = Number(/^v?(\d+)\./.exec(versionString)?.[1] ?? "0");
27042
27520
  if (major < 20) {
@@ -27106,20 +27584,67 @@ function isLocalPortOpen(port) {
27106
27584
  });
27107
27585
  });
27108
27586
  }
27587
+ function removePidFile(pidPath) {
27588
+ try {
27589
+ fs32.unlinkSync(pidPath);
27590
+ } catch {
27591
+ }
27592
+ }
27593
+ function parseManagedPid(text) {
27594
+ if (!/^[1-9]\d*$/.test(text)) return null;
27595
+ const pid = Number(text);
27596
+ if (!Number.isSafeInteger(pid) || pid < 2 || pid === process.pid || pid === process.ppid) return null;
27597
+ return pid;
27598
+ }
27599
+ function readLiveManagedPid(pidPath) {
27600
+ let text;
27601
+ try {
27602
+ text = fs32.readFileSync(pidPath, "utf8").trim();
27603
+ } catch {
27604
+ return null;
27605
+ }
27606
+ const pid = parseManagedPid(text);
27607
+ if (pid == null) {
27608
+ removePidFile(pidPath);
27609
+ return null;
27610
+ }
27611
+ try {
27612
+ process.kill(pid, 0);
27613
+ return pid;
27614
+ } catch (error) {
27615
+ const code = error.code;
27616
+ if (code === "ESRCH") {
27617
+ removePidFile(pidPath);
27618
+ return null;
27619
+ }
27620
+ if (code === "EPERM") return pid;
27621
+ throw error;
27622
+ }
27623
+ }
27109
27624
  async function serveOnboardingUiDetached(args) {
27110
27625
  const { logPath, pidPath } = onboardingUiPaths();
27111
27626
  const portNum = Number(args.port);
27112
27627
  const isPortOpen = () => isLocalPortOpen(portNum);
27113
- if (await isPortOpen()) {
27628
+ const portOpen = await isPortOpen();
27629
+ const managedPid = readLiveManagedPid(pidPath);
27630
+ if (portOpen && managedPid != null) {
27114
27631
  return { alreadyRunning: true, logPath };
27115
27632
  }
27633
+ if (portOpen) {
27634
+ throw new LocalCliError({
27635
+ code: "BOOTSTRAP_UI_PORT_IN_USE",
27636
+ message: `Port ${args.port} is occupied by a process not started by m8t bootstrap.`,
27637
+ hint: `Stop the process using port ${args.port}, then re-run 'm8t bootstrap ui'.`
27638
+ });
27639
+ }
27640
+ if (managedPid != null) return { alreadyRunning: true, logPath };
27116
27641
  fs32.mkdirSync(path35.dirname(logPath), { recursive: true });
27117
27642
  const fd = fs32.openSync(logPath, "a");
27118
27643
  const child = spawn7("pnpm", ["--filter", "web", "dev"], {
27119
27644
  cwd: args.repoRoot,
27120
27645
  detached: true,
27121
27646
  stdio: ["ignore", fd, fd],
27122
- env: { ...process.env, PORT: args.port },
27647
+ env: { ...process.env, PORT: args.port, M8T_SINGLE_GATEWAY_PROCESS: "1" },
27123
27648
  // pnpm resolves to pnpm.cmd on Windows, which needs a shell to launch.
27124
27649
  shell: process.platform === "win32"
27125
27650
  });
@@ -27144,9 +27669,26 @@ async function serveOnboardingUiDetached(args) {
27144
27669
  async function serveOnboardingRelayDetached(args) {
27145
27670
  const { logPath, pidPath } = onboardingRelayPaths();
27146
27671
  const portNum = 8790;
27147
- if (await isLocalPortOpen(portNum)) {
27672
+ if (args.webInternalBaseUrl !== "http://localhost:3000") {
27673
+ throw new LocalCliError({
27674
+ code: "BOOTSTRAP_UI_INTERNAL_BASE_URL_INVALID",
27675
+ message: "The local voice relay internal base URL must be http://localhost:3000."
27676
+ });
27677
+ }
27678
+ const internalSecret = readWebInternalSecret(args.repoRoot);
27679
+ const portOpen = await isLocalPortOpen(portNum);
27680
+ const managedPid = readLiveManagedPid(pidPath);
27681
+ if (portOpen && managedPid != null) {
27148
27682
  return { alreadyRunning: true, logPath };
27149
27683
  }
27684
+ if (portOpen) {
27685
+ throw new LocalCliError({
27686
+ code: "BOOTSTRAP_UI_RELAY_PORT_IN_USE",
27687
+ message: "Port 8790 is occupied by a process not started by m8t bootstrap.",
27688
+ hint: "Stop the process using port 8790, then re-run 'm8t bootstrap ui'."
27689
+ });
27690
+ }
27691
+ if (managedPid != null) return { alreadyRunning: true, logPath };
27150
27692
  fs32.mkdirSync(path35.dirname(logPath), { recursive: true });
27151
27693
  const fd = fs32.openSync(logPath, "a");
27152
27694
  const child = spawn7("pnpm", ["--filter", "web", "exec", "tsx", "voice-relay-entry.ts"], {
@@ -27159,7 +27701,9 @@ async function serveOnboardingRelayDetached(args) {
27159
27701
  AZURE_TENANT_ID: args.tenantId,
27160
27702
  AZURE_CLIENT_ID: args.clientId,
27161
27703
  RELAY_ALLOWED_ORIGINS: "http://localhost:3000",
27162
- RELAY_PORT: "8790"
27704
+ RELAY_PORT: "8790",
27705
+ M8T_INTERNAL_SECRET: internalSecret,
27706
+ WEB_INTERNAL_BASE_URL: args.webInternalBaseUrl
27163
27707
  },
27164
27708
  // pnpm resolves to pnpm.cmd on Windows, which needs a shell to launch.
27165
27709
  shell: process.platform === "win32"
@@ -27199,8 +27743,8 @@ function stopPidFile(pidPath) {
27199
27743
  } catch {
27200
27744
  return false;
27201
27745
  }
27202
- const pid = Number(pidStr);
27203
- if (!Number.isNaN(pid) && pid > 0) {
27746
+ const pid = parseManagedPid(pidStr);
27747
+ if (pid != null) {
27204
27748
  if (process.platform === "win32") {
27205
27749
  spawnSync6("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
27206
27750
  } else {
@@ -27353,7 +27897,8 @@ var BootstrapUiCommand = class extends M8tCommand {
27353
27897
  repoRoot,
27354
27898
  foundryEndpoint: endpoint,
27355
27899
  tenantId: account.tenantId,
27356
- clientId: state.appRegClientId
27900
+ clientId: state.appRegClientId,
27901
+ webInternalBaseUrl: "http://localhost:3000"
27357
27902
  });
27358
27903
  out("voice relay on :8790");
27359
27904
  const outcome2 = await deployOutcome;
@@ -27371,7 +27916,8 @@ var BootstrapUiCommand = class extends M8tCommand {
27371
27916
  repoRoot,
27372
27917
  foundryEndpoint: endpoint,
27373
27918
  tenantId: account.tenantId,
27374
- clientId: state.appRegClientId
27919
+ clientId: state.appRegClientId,
27920
+ webInternalBaseUrl: "http://localhost:3000"
27375
27921
  });
27376
27922
  out("voice relay on :8790");
27377
27923
  out("starting the webapp on :3000 in the background\u2026");
@@ -27438,9 +27984,14 @@ var BootstrapSeedProfileCommand = class extends M8tCommand {
27438
27984
  const { hadIntake, block } = await findOnboardingProfile({ endpoint: ctx.endpoint, token });
27439
27985
  if (block) {
27440
27986
  const azIdentity = await getSignedInUserIdentity();
27441
- for (const brainRepo of ctx.brainRepos) {
27442
- await applyProfileToBrain({ block, brainRepo, branch: "main", appCreds: ctx.appCreds, subscriptionId: ctx.subscriptionId, azIdentity });
27443
- }
27987
+ await applyProfileToBrains({
27988
+ block,
27989
+ brainRepos: ctx.brainRepos,
27990
+ branch: "main",
27991
+ appCreds: ctx.appCreds,
27992
+ subscriptionId: ctx.subscriptionId,
27993
+ azIdentity
27994
+ });
27444
27995
  this.context.stdout.write(`${colors.success("\u2713")} seeded your advisors' brains (${ctx.brainRepos.join(", ")}) from your questionnaire.
27445
27996
  `);
27446
27997
  return 0;