@mutmutco/codex-plugin 4.5.102 → 4.5.103

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mmi",
3
- "version": "4.5.102",
3
+ "version": "4.5.103",
4
4
  "mmiCompat": "4.x",
5
5
  "description": "MMI workflow skills and org gates delivery.",
6
6
  "author": {
package/cli/dist/main.cjs CHANGED
@@ -6053,6 +6053,16 @@ function isRetryableGitHubWaitReadError(e) {
6053
6053
  }
6054
6054
  return /Something went wrong while executing your query/.test(text) || /^\s*unexpected end of JSON input\s*$/m.test(text);
6055
6055
  }
6056
+ function refusalWaitMs(e, nowMs) {
6057
+ const err = e;
6058
+ const text = [err?.stdout, err?.stderr, e instanceof Error ? e.message : ""].filter((t) => typeof t === "string").join("\n");
6059
+ const header = (name) => new RegExp(`^${name}:\\s*(\\d+)\\s*$`, "im").exec(text)?.[1];
6060
+ const retryAfter = header("retry-after");
6061
+ if (retryAfter !== void 0) return Number(retryAfter) * 1e3;
6062
+ const reset = header("x-ratelimit-remaining") === "0" ? Number(header("x-ratelimit-reset")) : typeof err?.rateLimitReset === "number" ? err.rateLimitReset : NaN;
6063
+ if (reset * 1e3 > nowMs) return reset * 1e3 - nowMs;
6064
+ return /secondary rate|abuse detection/i.test(text) ? 6e4 : void 0;
6065
+ }
6056
6066
  function waitReadFailureMessage(e) {
6057
6067
  const err = e;
6058
6068
  const stderr = typeof err?.stderr === "string" ? err.stderr.trim() : "";
@@ -6180,9 +6190,21 @@ async function waitForPrChecks(deps) {
6180
6190
  return { policy, status: "failure", reason: waitReadFailureMessage(e), detail: "github-read-failed", waitedMs: now() - started };
6181
6191
  }
6182
6192
  lastDetail = `github-read-transient (${waitReadFailureMessage(e)})`;
6183
- deps.log?.(`transient GitHub read failure \u2014 retrying within the wait budget: ${lastDetail}`);
6184
- report("pending");
6185
- await deps.sleep(PR_CHECKS_POLL_MS);
6193
+ const askedMs = refusalWaitMs(e, now());
6194
+ const jitter = deps.jitterMs ?? (() => Math.floor(Math.random() * PR_CHECKS_RATELIMIT_JITTER_MAX_MS));
6195
+ const waitMs = askedMs !== void 0 && askedMs > PR_CHECKS_POLL_MS ? askedMs + jitter() : PR_CHECKS_POLL_MS;
6196
+ if (askedMs !== void 0 && now() + waitMs >= deadline) {
6197
+ return {
6198
+ policy,
6199
+ status: "rate-limited",
6200
+ reason: `GitHub refused the read for rate limit and asked for ${Math.round(askedMs / 1e3)}s, past the wait deadline (${lastDetail})`,
6201
+ detail: "rate-limited",
6202
+ waitedMs: now() - started
6203
+ };
6204
+ }
6205
+ deps.log?.(`transient GitHub read failure \u2014 retrying in ${Math.round(waitMs / 1e3)}s within the wait budget: ${lastDetail}`);
6206
+ report(askedMs !== void 0 ? "rate-limited" : "pending");
6207
+ await deps.sleep(waitMs);
6186
6208
  continue;
6187
6209
  }
6188
6210
  report(state);
@@ -6300,6 +6322,7 @@ function setPollTokenProvider(provider) {
6300
6322
  pollTokenOnce = void 0;
6301
6323
  resetAppPollGuard();
6302
6324
  lastPollSource = void 0;
6325
+ observedPool.clear();
6303
6326
  }
6304
6327
  async function pollToken() {
6305
6328
  if (!pollTokenProvider) return void 0;
@@ -6351,6 +6374,19 @@ function appPoolDetail() {
6351
6374
  const counts = guard.remaining !== void 0 && guard.limit !== void 0 ? `App pool at ${guard.remaining}/${guard.limit}` : "App pool headroom unverified";
6352
6375
  return guard.resetAtMs !== void 0 ? `${counts}, resets ${new Date(guard.resetAtMs).toISOString()}` : counts;
6353
6376
  }
6377
+ var observedPool = /* @__PURE__ */ new Map();
6378
+ var OBSERVED_POOL_MAX_AGE_MS = 3 * 6e4;
6379
+ function notePoolHeaders(source, out, nowMs) {
6380
+ const { remaining, resetSec } = parseRateLimitHeaders(out);
6381
+ const resource = /^x-ratelimit-resource:\s*(\S+)\s*$/im.exec(out)?.[1];
6382
+ if (remaining !== void 0 && resetSec !== void 0 && (resource ?? "core") === "core") {
6383
+ observedPool.set(source, { remaining, reset: resetSec, atMs: nowMs });
6384
+ }
6385
+ }
6386
+ function observedCorePool(source, nowMs) {
6387
+ const seen = source ? observedPool.get(source) : void 0;
6388
+ return seen && nowMs - seen.atMs <= OBSERVED_POOL_MAX_AGE_MS && seen.reset * 1e3 > nowMs ? { remaining: seen.remaining, reset: seen.reset } : void 0;
6389
+ }
6354
6390
  async function defaultGhApi(args) {
6355
6391
  const appToken = await pollToken();
6356
6392
  let source = appToken && appPollAllowsApp(Date.now()) ? "app" : "user";
@@ -6372,6 +6408,7 @@ async function defaultGhApi(args) {
6372
6408
  const out = e.stdout;
6373
6409
  const errOut = typeof out === "string" ? out : void 0;
6374
6410
  const status = errOut !== void 0 ? parseIncludedOutput(errOut).status : void 0;
6411
+ if (errOut !== void 0) notePoolHeaders(source, errOut, Date.now());
6375
6412
  if (cached && status === 304) {
6376
6413
  lastPollSource = source;
6377
6414
  return cached.body;
@@ -6386,6 +6423,7 @@ async function defaultGhApi(args) {
6386
6423
  throw e;
6387
6424
  }
6388
6425
  const { body, etag, pages } = parseIncludedOutput(stdout);
6426
+ notePoolHeaders(source, stdout, Date.now());
6389
6427
  if (source === "app") noteAppPollResponse(parseRateLimitHeaders(stdout), false, Date.now());
6390
6428
  lastPollSource = source;
6391
6429
  if (etag && pages === 1) etagCache.set(key, { etag, body });
@@ -6858,7 +6896,9 @@ async function fetchRequiredCheckContexts(repo, branch, gh2 = defaultGhApi) {
6858
6896
  }
6859
6897
  return contexts;
6860
6898
  }
6861
- async function fetchRestCorePool(gh2 = defaultGhApi) {
6899
+ async function fetchRestCorePool(gh2 = defaultGhApi, nowMs = Date.now()) {
6900
+ const seen = gh2 === defaultGhApi ? observedCorePool(await pollToken() && appPollAllowsApp(nowMs) ? "app" : "user", nowMs) : void 0;
6901
+ if (seen) return { state: "ok", ...seen };
6862
6902
  let core;
6863
6903
  try {
6864
6904
  core = JSON.parse(await gh2(["rate_limit"])).resources?.core;
@@ -16126,6 +16166,39 @@ function gateConfigToVars(gate) {
16126
16166
  if (extras.steps?.length) out.GATE_EXTRA_STEPS_YAML = gateExtraStepsYaml(extras.steps);
16127
16167
  return out;
16128
16168
  }
16169
+ function suiteModuleSeeds(seeds, suite, deployModel) {
16170
+ if (suite !== "mm-work" || deployModel !== "tenant-container") return seeds;
16171
+ const replacements = {
16172
+ Dockerfile: "suite-Dockerfile.template",
16173
+ "docker-compose.yml": "suite-compose.template.yml",
16174
+ ".github/workflows/gate.yml": "suite-gate.template.yml"
16175
+ };
16176
+ const additions = {
16177
+ "src/index.ts": "suite-server.template.ts",
16178
+ "package.json": "suite-package.template.json",
16179
+ "package-lock.json": "suite-package-lock.template.json",
16180
+ "tsconfig.json": "suite-tsconfig.template.json",
16181
+ "module/package.json": "suite-module-package.template.json",
16182
+ "module/package-lock.json": "suite-module-package-lock.template.json",
16183
+ "module/.npmrc": "suite-npmrc.template",
16184
+ "module/src/module.jsx": "suite-module.template.jsx",
16185
+ "module/vite.config.mjs": "suite-vite.template.mjs",
16186
+ "module/screens/screens.json": "suite-screens.template.json",
16187
+ "module/screens/files.json": "suite-files.template.json",
16188
+ ".github/workflows/screens.yml": "suite-screens.template.yml",
16189
+ "docs/Guides/mm-work-module.md": "suite-guide.template.md"
16190
+ };
16191
+ return [
16192
+ ...seeds.filter((s) => s.target !== "docs/Guides/google-login.md").map((s) => replacements[s.target] ? { ...s, source: `seed:${replacements[s.target]}` } : s),
16193
+ ...Object.entries(additions).map(([target, source]) => ({
16194
+ target,
16195
+ source: `seed:${source}`,
16196
+ ownership: "repo",
16197
+ classes: ["deployable"],
16198
+ deployModels: ["tenant-container"]
16199
+ }))
16200
+ ];
16201
+ }
16129
16202
  function seedMatchesDeployModel(seed, deployModel) {
16130
16203
  if (!seed.deployModels?.length) return true;
16131
16204
  return deployModel != null && seed.deployModels.includes(deployModel);
@@ -16307,7 +16380,11 @@ function buildRegisterPayload(repo, cls, vars, options = {}) {
16307
16380
  // Identity. name/division default off the repo name when the skill didn't pass them.
16308
16381
  name: vars.NAME || parsedRepo.name,
16309
16382
  division: vars.DIVISION || parsedRepo.name.split("-")[0] || void 0,
16310
- suite: vars.SUITE || void 0,
16383
+ suite,
16384
+ ...suite === "mm-work" && deployModel === "tenant-container" ? {
16385
+ requiredBuildSecrets: ["NODE_AUTH_TOKEN=@github-packages-token"],
16386
+ requiredChecks: ["gate", "screens / screens"]
16387
+ } : {},
16311
16388
  repos: [`${parsedRepo.owner}/${slug}`],
16312
16389
  branch: vars.BRANCH || (cls === "content" ? "main" : "development"),
16313
16390
  class: cls,
@@ -17622,10 +17699,10 @@ var rollout_plan_default = {
17622
17699
  note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
17623
17700
  },
17624
17701
  baseline: {
17625
- version: "4.5.102",
17626
- tag: "v4.5.102",
17627
- commit: "69ad9c05771f",
17628
- npm: "@mutmutco/cli@4.5.102"
17702
+ version: "4.5.103",
17703
+ tag: "v4.5.103",
17704
+ commit: "c2dd99cf0d72",
17705
+ npm: "@mutmutco/cli@4.5.103"
17629
17706
  },
17630
17707
  exitCriterion: "fleet-n-of-n",
17631
17708
  hubOnlyShortcut: "forbidden",
@@ -17642,14 +17719,14 @@ var rollout_plan_default = {
17642
17719
  repo: "mutmutco/mmi-hub",
17643
17720
  role: "canary",
17644
17721
  schedule: "train",
17645
- v3Target: "v4.5.102"
17722
+ v3Target: "v4.5.103"
17646
17723
  }
17647
17724
  ],
17648
17725
  rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
17649
17726
  rollback: {
17650
17727
  independent: true,
17651
- mechanism: "npm dist-tag latest -> 4.5.102 and redeploy the Hub Lambda from tag v4.5.102 (69ad9c05771f); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17652
- v3Target: "v4.5.102 (@mutmutco/cli@4.5.102, tag commit 69ad9c05771f \u2014 last known-good release carrying the repo-index v4-only contract)"
17728
+ mechanism: "npm dist-tag latest -> 4.5.103 and redeploy the Hub Lambda from tag v4.5.103 (c2dd99cf0d72); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
17729
+ v3Target: "v4.5.103 (@mutmutco/cli@4.5.103, tag commit c2dd99cf0d72 \u2014 last known-good release carrying the repo-index v4-only contract)"
17653
17730
  }
17654
17731
  },
17655
17732
  {
@@ -33937,14 +34014,15 @@ function deployRowChecks(repo, deployModel, releaseTrack, facts, factsRead, suit
33937
34014
  };
33938
34015
  });
33939
34016
  }
33940
- function edgeCertificateChecks(results) {
34017
+ function edgeCertificateChecks(results, repo) {
33941
34018
  return results.flatMap((result) => {
33942
34019
  if (!result) return [];
33943
34020
  const { stage, status, error } = result;
34021
+ const fix = status?.missing && !status.pending ? ` \u2014 run: mmi-cli devops runtime edge cert ensure ${repo} ${stage}` : "";
33944
34022
  return [{
33945
34023
  ok: !error && (status?.required === false || status?.required === true && status.missing === false),
33946
34024
  label: `Cloudflare edge certificate: ${stage}`,
33947
- detail: error ? `certificate coverage unreadable (${error}); cannot verify TLS` : status?.hint
34025
+ detail: error ? `certificate coverage unreadable (${error}); cannot verify TLS` : status?.hint && `${status.hint}${fix}`
33948
34026
  }];
33949
34027
  });
33950
34028
  }
@@ -34008,6 +34086,8 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
34008
34086
  detail: exactAllowlist ? void 0 : ownersUnverified ? "org owner baseline is unreadable or empty; the exact restriction is unverified" : deps.pushAllowlistOwnersRead === true ? "restriction must contain exactly the org owners + mmi-github-app and no teams" : "restrictions.users is empty or unset"
34009
34087
  });
34010
34088
  }
34089
+ const packageAccess = suiteModulePackageAccessCheck(deps.projectMeta?.suite, deps.deployModel);
34090
+ if (packageAccess) checks.push(packageAccess);
34011
34091
  let environmentDrift2 = [];
34012
34092
  try {
34013
34093
  environmentDrift2 = await verifyRepoEnvironments(repo, effectiveTrack, deps.client, deps.projectMeta?.suite);
@@ -34091,7 +34171,7 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
34091
34171
  const check = npmrcGitHubPackagesAuthCheck(path2, await contentText(deps, repo, baseBranch, path2));
34092
34172
  if (check) checks.push(check);
34093
34173
  }
34094
- checks.push(...edgeCertificateChecks(deps.edgeCertificates ?? []));
34174
+ checks.push(...edgeCertificateChecks(deps.edgeCertificates ?? [], repo));
34095
34175
  checks.push(...edgeDnsChecks(deps.edgeDns ?? [], repo));
34096
34176
  const portRangeCheck = centralContainerPortRangeCheck(deps.deployModel, deps.projectMeta?.portRange, repo);
34097
34177
  if (portRangeCheck) checks.push(portRangeCheck);
@@ -34349,6 +34429,14 @@ function laneNotEvaluatedFromThisBranch(doctor) {
34349
34429
  const only = reasons.length === 1 ? reasons[0] : void 0;
34350
34430
  return only?.code === "branch-mismatch" ? only.title : void 0;
34351
34431
  }
34432
+ function suiteModulePackageAccessCheck(suite, deployModel) {
34433
+ if (suite !== "mm-work" || deployModel !== "tenant-container") return void 0;
34434
+ return {
34435
+ ok: true,
34436
+ label: "mmwork-ds package access (manual org-admin step; unverified)",
34437
+ detail: "An org admin must add this repository with Read under @mutmutco/mmwork-ds \u2192 Package settings \u2192 Manage Actions access. No GitHub REST API can verify or grant this; without it CI fails with 403 read_package."
34438
+ };
34439
+ }
34352
34440
  function attachTrainDoctorVerdicts(report, trainDoctors) {
34353
34441
  const marked = trainDoctors.map((doctor) => {
34354
34442
  const notEvaluatedFromThisBranch = laneNotEvaluatedFromThisBranch(doctor);
@@ -34837,13 +34925,6 @@ function registerBootstrapCommands(program3) {
34837
34925
  const baseBranch = o.class === "content" ? "main" : "development";
34838
34926
  const slug = parsedRepo.slug;
34839
34927
  const onlyTarget = o.fromConfig && o.only.trim() === "gate.yml" ? ".github/workflows/gate.yml" : o.only.trim();
34840
- const seedsToApply = onlyTarget ? manifest.seeds.filter((s) => s.target === onlyTarget || s.target.replace("{{REPO_SLUG}}", slug) === onlyTarget) : manifest.seeds;
34841
- if (onlyTarget && !seedsToApply.length) {
34842
- const known = manifest.seeds.map((s) => s.target).join("\n ");
34843
- return fail(`bootstrap apply: --only '${onlyTarget}' names no seed in ${manifestPath}. Declared targets:
34844
- ${known}`);
34845
- }
34846
- const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
34847
34928
  const gh2 = async (args) => execFileP("gh", args, { timeout: 2e4 });
34848
34929
  const readFile10 = (p) => (0, import_node_fs28.existsSync)(p) ? (0, import_node_fs28.readFileSync)(p, "utf8") : null;
34849
34930
  const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
@@ -34913,6 +34994,20 @@ function registerBootstrapCommands(program3) {
34913
34994
  applyProjectType = o.projectType || registryMeta?.projectType || payload.projectType;
34914
34995
  } catch {
34915
34996
  }
34997
+ const availableSeeds = suiteModuleSeeds(manifest.seeds, vars.SUITE || registryMeta?.suite, applyDeployModel);
34998
+ const seedsToApply = onlyTarget ? availableSeeds.filter((s) => s.target === onlyTarget || s.target.replace("{{REPO_SLUG}}", slug) === onlyTarget) : availableSeeds;
34999
+ if (onlyTarget && !seedsToApply.length) {
35000
+ return fail(`bootstrap apply: --only '${onlyTarget}' names no seed for this project. Declared targets:
35001
+ ${availableSeeds.map((s) => s.target).join("\n ")}`);
35002
+ }
35003
+ const onlyManagedBlock = onlyTarget ? seedsToApply[0]?.managedBlock != null : false;
35004
+ if (vars.SUITE === "mm-work" || registryMeta?.suite === "mm-work") {
35005
+ if (applyDeployModel === "tenant-container") {
35006
+ const moduleGate = withDerivedRepoVars({}, parsedRepo, o.class, effectiveTrack, registryRequiredCheckBranches, ["gate", "screens / screens"]);
35007
+ vars.GATE_RULESET_CONTEXTS_JSON = moduleGate.GATE_RULESET_CONTEXTS_JSON;
35008
+ if (registryMeta) registryMeta = { ...registryMeta, requiredChecks: ["gate", "screens / screens"] };
35009
+ }
35010
+ }
34916
35011
  const controlClient = defaultGitHubClient();
34917
35012
  let controlOwners = [];
34918
35013
  let controlDeployFacts = null;
@@ -53292,6 +53387,17 @@ function registerEdgeCommands(program3) {
53292
53387
  fail(`runtime edge dns: ${e.message}`);
53293
53388
  }
53294
53389
  });
53390
+ edge.command("cert <action> <owner/repo> <stage>").description("read (`status`) or order-when-missing (`ensure`, master-only) the Cloudflare advanced edge certificate pack (<x> + *.<x>) a dev/rc host needs").action(async (action, repo, stage) => {
53391
+ if (action !== "status" && action !== "ensure") return fail("runtime edge cert: <action> must be status or ensure");
53392
+ if (stage !== "dev" && stage !== "rc") return fail("runtime edge cert: <stage> must be dev or rc (main is covered by the zone certificate)");
53393
+ try {
53394
+ const result = await edgeCertificateForStage(slugOf(repo), stage, registryClientDeps(await loadConfig()), action === "ensure");
53395
+ console.log(JSON.stringify(result, null, 2));
53396
+ if (result.required && result.missing) process.exitCode = 1;
53397
+ } catch (e) {
53398
+ fail(`runtime edge cert: ${e.message}`);
53399
+ }
53400
+ });
53295
53401
  }
53296
53402
 
53297
53403
  // src/command-register-developer.ts
package/cli/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "4.5.102",
3
+ "version": "4.5.103",
4
4
  "description": "MMI Future CLI — the org dev toolbox and shared cross-IDE engine for every registry-declared MMI coding surface.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/codex-plugin",
3
- "version": "4.5.102",
3
+ "version": "4.5.103",
4
4
  "description": "MMI workflow skills and org gates delivery.",
5
5
  "author": {
6
6
  "name": "MMI Future",
@@ -507,7 +507,11 @@ the bootstrap seed Dockerfile and compose templates). The seeded compose is file
507
507
  `labels: { mmi.runtime-env: "true" }`, no `env_file`, and a loopback-only port; the deploy injects the stage's
508
508
  vault values at runtime (`DEPLOY#<stage>.noEnvFile: true`) and the app reads plain env vars — it must **not** self-load SSM and must **not** ship a committed `.env`.
509
509
 
510
- For a `web-app` that declares `oauth` META, print the canonical OAuth surface and provision the client once:
510
+ For a `suite: mm-work` tenant-container module, bootstrap apply selects the MM-Work module seed instead: a live `/health` server and `/module.js`, the example module scaffold, secret-backed image build, and `gate` plus `screens / screens`. It sets `requiredBuildSecrets: ["NODE_AUTH_TOKEN=@github-packages-token"]` and those required checks. Before the first CI/deploy, an org admin must grant the repo Read on `@mutmutco/mmwork-ds` under Package settings → Manage Actions access (no GitHub REST API; verify names the manual step).
511
+
512
+ **Never provision a Google OAuth client for an MM-Work suite module, even if old META declares `oauth`.** Modules sign in through the MM-Work pass and reach Google via `ctx.act`; read the package's GUIDE. The suite seed omits the Google-login guide.
513
+
514
+ For a non-suite `web-app` that declares `oauth` META, print the canonical OAuth surface and provision the client once:
511
515
 
512
516
  ```bash
513
517
  mmi-cli vault org oauth plan --repo "$OWNER/$REPO" # the exact JS origins + redirect URIs + canonical SSM keys
@@ -0,0 +1,32 @@
1
+ # MM-Work module tenant container: the TypeScript server plus the module bundle it serves.
2
+
3
+ # The module bundle. The GitHub Packages token (for @mutmutco/mmwork-ds) arrives as a BuildKit secret
4
+ # and is never written into a layer; compose passes it from the registry's requiredBuildSecrets.
5
+ FROM node:24-bookworm-slim AS module-build
6
+ WORKDIR /module
7
+ COPY module/package.json module/package-lock.json module/.npmrc ./
8
+ RUN --mount=type=secret,id=NODE_AUTH_TOKEN,env=NODE_AUTH_TOKEN npm ci --no-audit --no-fund
9
+ COPY module/src ./src
10
+ COPY module/vite.config.mjs ./
11
+ RUN npm run build
12
+
13
+ # The server.
14
+ FROM node:24-bookworm-slim AS server-build
15
+ WORKDIR /app
16
+ COPY package.json package-lock.json ./
17
+ RUN npm ci --no-audit --no-fund
18
+ COPY tsconfig.json ./
19
+ COPY src ./src
20
+ RUN npm run build && npm prune --omit=dev
21
+
22
+ FROM node:24-bookworm-slim AS runtime
23
+ WORKDIR /app
24
+ ENV NODE_ENV=production
25
+ COPY --from=server-build /app/package.json ./package.json
26
+ COPY --from=server-build /app/node_modules ./node_modules
27
+ COPY --from=server-build /app/dist ./dist
28
+ COPY --from=module-build /module/dist/module.js ./module/dist/module.js
29
+ USER node
30
+ EXPOSE 3000
31
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
32
+ CMD ["node", "dist/index.js"]
@@ -0,0 +1,26 @@
1
+ services:
2
+ app:
3
+ build:
4
+ context: .
5
+ dockerfile: Dockerfile
6
+ secrets:
7
+ - NODE_AUTH_TOKEN
8
+ restart: unless-stopped
9
+ # Fileless deploy (DEPLOY#<stage>.noEnvFile: true): no .env exists. tenant-deploy.yml streams the
10
+ # stage-scoped vault values over SSH into a tmpfs (/dev/shm) runtime env, and the box injects them
11
+ # into every service labelled mmi.runtime-env. Never add env_file.
12
+ labels:
13
+ mmi.runtime-env: "true"
14
+ environment:
15
+ NODE_ENV: production
16
+ MMI_STAGE: ${MMI_STAGE:-dev}
17
+ MMI_PORT: ${MMI_PORT:-3000}
18
+ PORT: ${PORT:-3000}
19
+ MMI_EDGE_DOMAIN: ${MMI_EDGE_DOMAIN:-}
20
+ ports:
21
+ # Loopback only: the box Caddy is the one public door (TLS), never the bare port.
22
+ - "127.0.0.1:${MMI_PORT:-3000}:${PORT:-3000}"
23
+
24
+ secrets:
25
+ NODE_AUTH_TOKEN:
26
+ environment: NODE_AUTH_TOKEN
@@ -0,0 +1 @@
1
+ { "result": { "items": [{ "id": "sample", "name": "Sample brief" }] } }
@@ -0,0 +1,35 @@
1
+ name: gate
2
+ on:
3
+ pull_request:
4
+ push:
5
+ branches: {{GATE_PUSH_BRANCHES_YAML}}
6
+ tags: ['v*']
7
+ concurrency:
8
+ group: gate-${{ github.event.pull_request.number || github.ref }}
9
+ cancel-in-progress: true
10
+ permissions:
11
+ contents: read
12
+ packages: read
13
+ jobs:
14
+ gate:
15
+ runs-on: [self-hosted, linux, x64, mmi-live]
16
+ timeout-minutes: 10
17
+ steps:
18
+ - uses: mutmutco/MMI-Hub/.github/actions/runner-checkout@{{GATE_RUNNER_SHA}}
19
+ with:
20
+ lockfile-path: package-lock.json
21
+ - name: Isolate npm cache
22
+ run: echo "NPM_CONFIG_CACHE=$RUNNER_TEMP/npm-cache" >> "$GITHUB_ENV"
23
+ - uses: mutmutco/MMI-Hub/.github/actions/runner-node-toolchain@{{GATE_RUNNER_SHA}}
24
+ with:
25
+ node-version: 24
26
+ npm-version: '12'
27
+ - run: npm ci --no-audit --no-fund
28
+ - run: npm ci --prefix module --no-audit --no-fund
29
+ env:
30
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31
+ - uses: mutmutco/MMI-Hub/.github/actions/run-with-budget@{{GATE_BUDGET_SHA}}
32
+ with:
33
+ command: npm run typecheck && npm run build && npm --prefix module run check
34
+ max-seconds: {{GATE_MAX_SECONDS}}
35
+ working-directory: .
@@ -0,0 +1,11 @@
1
+ # MM-Work module
2
+
3
+ Read the matching `module/node_modules/@mutmutco/mmwork-ds/GUIDE.md` before changing this module. The starter entry and Vite setting come from MM-Work's `examples/example-module`; replace the example screens with your product's screens using only the shared design system.
4
+
5
+ Sign-in belongs to MM-Work: verify its pass before adding protected backend routes. Do not provision a Google OAuth client or implement a separate login. Google and other shared services are reached through `ctx.act`, not module-owned credentials.
6
+
7
+ Before the first CI/deploy, an org admin must grant this repo **Read** on `@mutmutco/mmwork-ds` under **Package settings → Manage Actions access**. This has no GitHub REST API. The Actions token installs the module in CI; the tenant image build uses the registry's `NODE_AUTH_TOKEN=@github-packages-token` BuildKit secret. Never commit a token.
8
+
9
+ The server exposes `/health` and `/module.js`. Register the module's bundle URL, id, name and capabilities with MM-Work following the GUIDE. The seed deliberately exposes no authenticated product API yet.
10
+
11
+ `gate` installs and checks the backend and module. `screens / screens` follows MM-PM's non-blocking PR/push/tag shape, using MM-Work's reusable runner with `run: false` (browser audits are not merge gates). Configure scheduled/on-demand screen audits separately when the product has screens to audit.