@empire-builder-kit/nx 1.0.0-rc.45 → 1.0.0-rc.46

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 (27) hide show
  1. package/package.json +3 -3
  2. package/src/developer-agent/generator-planning.js +10 -3
  3. package/src/developer-agent/operation-registry.js +1 -1
  4. package/src/executors/github-bootstrap/github-bootstrap.d.ts +3 -0
  5. package/src/executors/github-bootstrap/github-bootstrap.js +28 -0
  6. package/src/executors/github-bootstrap/schema.json +14 -0
  7. package/src/generators/preset/workspace-files-thin-deploy.js +5 -1
  8. package/src/generators/preset/workspace-files.js +31 -28
  9. package/src/generators/slice/built-in-plan-data-files.js +10 -3
  10. package/src/generators/slice/built-in-plan-metadata-files.js +4 -1
  11. package/src/generators/slice/built-in-plan-release-files.js +0 -1
  12. package/src/generators/slice/built-in-plan-verification-files.js +0 -1
  13. package/src/generators/slice/built-in-plan-web-files.js +4 -5
  14. package/src/generators/slice/built-in-plan.js +1 -2
  15. package/src/generators/upgrade/upgrade.js +89 -3
  16. package/src/generators/workload/reliability-registration.d.ts +1 -1
  17. package/src/generators/workload/reliability-registration.js +9 -9
  18. package/src/generators/workload/workload.d.ts +1 -1
  19. package/src/generators/workload/workload.js +27 -9
  20. package/src/git-policy/github-bootstrap.d.ts +18 -0
  21. package/src/git-policy/github-bootstrap.js +99 -6
  22. package/src/local-dev/fleet-session.js +41 -4
  23. package/src/local-dev/fleet-supervisor-daemon.js +1 -5
  24. package/src/local-dev/fleet-supervisor.d.ts +1 -0
  25. package/src/local-dev/fleet-supervisor.js +34 -11
  26. package/src/supply-chain/blueprint-verification-authority.js +24 -3
  27. package/src/supply-chain/sbom.js +74 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empire-builder-kit/nx",
3
- "version": "1.0.0-rc.45",
3
+ "version": "1.0.0-rc.46",
4
4
  "description": "Nx generators, executors, and Blueprint contracts for Empire Builder Kit",
5
5
  "keywords": [
6
6
  "aws",
@@ -145,7 +145,7 @@
145
145
  "provenance": false
146
146
  },
147
147
  "dependencies": {
148
- "@empire-builder-kit/runtime": "1.0.0-rc.45",
148
+ "@empire-builder-kit/runtime": "1.0.0-rc.46",
149
149
  "@aws-sdk/client-cloudfront": "^3.700.0",
150
150
  "@aws-sdk/client-cloudwatch": "^3.700.0",
151
151
  "@aws-sdk/client-cloudwatch-logs": "^3.700.0",
@@ -178,5 +178,5 @@
178
178
  "engines": {
179
179
  "node": ">=22.13.0"
180
180
  },
181
- "gitHead": "24b8859bb40fe58e57caf41555513f90fa734a05"
181
+ "gitHead": "2165c67769556e404a2d0e9d2f5bcdd23413fce8"
182
182
  }
@@ -11,7 +11,8 @@ exports.rebindDeveloperAgentGeneratorPlan = rebindDeveloperAgentGeneratorPlan;
11
11
  const node_util_1 = require("node:util");
12
12
  const node_fs_1 = require("node:fs");
13
13
  const node_path_1 = require("node:path");
14
- const index_js_1 = require("../blueprints/index.js");
14
+ const index_js_1 = require("../repository-policy/index.js");
15
+ const index_js_2 = require("../blueprints/index.js");
15
16
  const function_js_1 = require("../generators/function/function.js");
16
17
  const logging_registry_js_1 = require("../generators/logging-registry.js");
17
18
  const built_in_plan_js_1 = require("../generators/slice/built-in-plan.js");
@@ -313,7 +314,7 @@ function slicePlan(context, input, mode, rebindCompleted = false) {
313
314
  workspacePathExists(context, node_path_1.posix.join('packages', input.name))) {
314
315
  return unavailableGeneratorDestination(`Product Slice ${input.name} collides with an existing packages/${input.name} path.`);
315
316
  }
316
- const blueprint = (0, index_js_1.resolveBlueprint)({
317
+ const blueprint = (0, index_js_2.resolveBlueprint)({
317
318
  engineVersion: context.workspace.frameworkVersion,
318
319
  reference: 'standard',
319
320
  workspaceRoot: context.workspace.repositoryRoot,
@@ -420,7 +421,13 @@ function workloadPlan(context, input, mode, rebindCompleted = false) {
420
421
  const ownsProtectedLoadAdapter = kind === 'service'
421
422
  ? serviceShouldOwnProtectedLoadAdapter(context, input.slice)
422
423
  : false;
423
- const plan = (0, workload_js_1.createWorkloadFilePlan)(context.workspace.identity, input.slice, input.name, kind, context.workspace.frameworkVersion, input.feature, ownsProtectedLoadAdapter);
424
+ const plan = (0, workload_js_1.createWorkloadFilePlan)(context.workspace.identity, input.slice, input.name, kind, context.workspace.frameworkVersion, input.feature, ownsProtectedLoadAdapter, ownsProtectedLoadAdapter
425
+ ? [
426
+ (0, index_js_1.productionRehearsalStage)((0, index_js_1.readRepositoryOperatingPolicy)(context.workspace.repositoryRoot)
427
+ .deploymentProfile),
428
+ 'production',
429
+ ]
430
+ : []);
424
431
  if (!rebindCompleted) {
425
432
  requireAvailableProspectiveProjectIds(context, [plan.project], `${kind} ${input.name}`);
426
433
  }
@@ -409,7 +409,7 @@ const TARGET_SIGNATURES = [
409
409
  : undefined;
410
410
  }
411
411
  const kind = plural === 'services' ? 'service' : 'job';
412
- return (`docker build -f packages/${slice}/${plural}/${workload}/Dockerfile ` +
412
+ return ('docker build -f Dockerfile ' +
413
413
  `-t ${slice}-${kind}-${workload}:local ../../../..`);
414
414
  }, { nonPlannableRationale: exports.DOCKER_BUILD_OWNER_RATIONALE }),
415
415
  runScriptSignature('dev', (input) => input.project.root.includes('/services/')
@@ -3,6 +3,9 @@ interface GitHubBootstrapExecutorOptions {
3
3
  apply?: boolean;
4
4
  immutableOidcTrustAcknowledgement?: string;
5
5
  npmPrereleaseReviewerIds?: string;
6
+ npmStable?: boolean;
7
+ npmStableReviewerIds?: string;
8
+ npmStablePreventSelfReview?: boolean;
6
9
  productionReviewerIds?: string;
7
10
  repository?: string;
8
11
  }
@@ -10,6 +10,34 @@ function parseReviewerIds(value) {
10
10
  }
11
11
  const executor = async (options, context) => {
12
12
  try {
13
+ if (options.npmStable === true) {
14
+ if (options.npmStableReviewerIds === undefined ||
15
+ !/^[1-9][0-9]*(,[1-9][0-9]*)*$/.test(options.npmStableReviewerIds) ||
16
+ typeof options.npmStablePreventSelfReview !== 'boolean') {
17
+ throw new Error('npm-stable requires explicit reviewer IDs and prevent-self-review choice.');
18
+ }
19
+ if (options.npmPrereleaseReviewerIds !== undefined ||
20
+ options.productionReviewerIds !== undefined ||
21
+ options.immutableOidcTrustAcknowledgement !== undefined) {
22
+ throw new Error('npm-stable does not accept adopter bootstrap options.');
23
+ }
24
+ const report = await (0, index_js_1.runGitHubBootstrap)({
25
+ npmStable: true,
26
+ apply: options.apply === true,
27
+ repository: options.repository?.trim() ?? '',
28
+ npmStableReviewerIds: parseReviewerIds(options.npmStableReviewerIds),
29
+ npmStablePreventSelfReview: options.npmStablePreventSelfReview,
30
+ token: process.env.GH_TOKEN?.trim() ||
31
+ process.env.GITHUB_TOKEN?.trim() ||
32
+ '',
33
+ });
34
+ devkit_1.logger.info(JSON.stringify(report, null, 2));
35
+ return { success: options.apply ? report.ready : true };
36
+ }
37
+ if (options.npmStableReviewerIds !== undefined ||
38
+ options.npmStablePreventSelfReview !== undefined) {
39
+ throw new Error('npm-stable settings require the explicit npmStable selector.');
40
+ }
13
41
  const policy = (0, index_js_2.readRepositoryOperatingPolicy)(context.root);
14
42
  const report = await (0, index_js_1.runGitHubBootstrap)({
15
43
  apply: options.apply === true,
@@ -31,6 +31,20 @@
31
31
  "type": "string",
32
32
  "pattern": "^[1-9][0-9]*(,[1-9][0-9]*)*$",
33
33
  "description": "Optional comma-separated GitHub user IDs when committed Repository Operating Policy selects environment-reviewer mode. Existing reviewers are preserved when omitted."
34
+ },
35
+ "npmStable": {
36
+ "type": "boolean",
37
+ "default": false,
38
+ "description": "Configure only the framework npm-stable environment; exact framework repository required."
39
+ },
40
+ "npmStableReviewerIds": {
41
+ "type": "string",
42
+ "pattern": "^[1-9][0-9]*(,[1-9][0-9]*)*$",
43
+ "description": "Explicit GitHub user IDs for the framework npm-stable required reviewers."
44
+ },
45
+ "npmStablePreventSelfReview": {
46
+ "type": "boolean",
47
+ "description": "Explicit prevent-self-review choice for the framework npm-stable environment."
34
48
  }
35
49
  }
36
50
  }
@@ -759,7 +759,11 @@ ${indentedScript(`set -euo pipefail\n${canonicalStageAuthorityDerivationScript()
759
759
  # production authority is derived here beside the staging authority.
760
760
  - id: production-account
761
761
  name: Derive canonical committed production authority for trunk promotion
762
- if: steps.route.outputs.stage == 'staging'
762
+ if: >-
763
+ steps.route.outputs.action == 'deploy' &&
764
+ steps.route.outputs.stage == 'staging' &&
765
+ steps.policy.outputs.deployment_profile != 'starter' &&
766
+ steps.policy.outputs.has_slices == 'true'
763
767
  env:
764
768
  EBK_DEVELOPMENT_ACCOUNT: \${{ steps.policy.outputs.development_account_id }}
765
769
  EBK_PRODUCTION_ACCOUNT: \${{ steps.policy.outputs.production_account_id }}
@@ -7659,6 +7659,7 @@ console.log('Installed the repository-local EBK pre-push hook at ' + hookPath +
7659
7659
  .open-next
7660
7660
  dist
7661
7661
  *.tsbuildinfo
7662
+ **/next-env.d.ts
7662
7663
  node_modules
7663
7664
  .sst
7664
7665
  **/playwright/.auth
@@ -8365,36 +8366,38 @@ ${foundationPlan.resources.network ? " const natMode = productionConfiguration?
8365
8366
  if (repositoryPolicy.deploymentProfile !== normalizedRouterWafPolicy.deploymentProfile) {
8366
8367
  throw new Error('Router WAF policy profile differs from committed repository policy.');
8367
8368
  }
8368
- const rehearsalStage = productionRehearsalStage(normalizedRouterWafPolicy.deploymentProfile);
8369
- const rehearsalAccount = configuredDeploymentAccountForStage(
8370
- normalizedRouterWafPolicy.deploymentProfile,
8371
- repositoryPolicy.deploymentAccounts,
8372
- rehearsalStage,
8373
- );
8374
- const rehearsalRegion = configuredDeploymentRegion(repositoryPolicy.deploymentRegion);
8375
- const rehearsalRoleArn = createStageAuthorityIdentity({
8376
- accountId: rehearsalAccount,
8377
- partition: rehearsalRegion.startsWith('us-gov-') ? 'aws-us-gov' : 'aws',
8378
- region: rehearsalRegion,
8379
- stage: rehearsalStage,
8380
- }).deploymentRoleArn;
8369
+ let rehearsal: ReturnType<typeof reconcileRouterWafBlockingRehearsal> = null;
8370
+ if (isProduction) {
8371
+ const rehearsalStage = productionRehearsalStage(normalizedRouterWafPolicy.deploymentProfile);
8372
+ const rehearsalAccount = configuredDeploymentAccountForStage(
8373
+ normalizedRouterWafPolicy.deploymentProfile,
8374
+ repositoryPolicy.deploymentAccounts,
8375
+ rehearsalStage,
8376
+ );
8377
+ const rehearsalRegion = configuredDeploymentRegion(repositoryPolicy.deploymentRegion);
8378
+ const rehearsalRoleArn = createStageAuthorityIdentity({
8379
+ accountId: rehearsalAccount,
8380
+ partition: rehearsalRegion.startsWith('us-gov-') ? 'aws-us-gov' : 'aws',
8381
+ region: rehearsalRegion,
8382
+ stage: rehearsalStage,
8383
+ }).deploymentRoleArn;
8384
+ rehearsal = reconcileRouterWafBlockingRehearsal({
8385
+ evidence: routerWafLiveEvidence,
8386
+ expected: {
8387
+ accountId: rehearsalAccount,
8388
+ deploymentRoleArn: rehearsalRoleArn,
8389
+ region: rehearsalRegion,
8390
+ stage: rehearsalStage,
8391
+ wafContractDigest: routerWafContractDigest(normalizedRouterWafPolicy),
8392
+ workspace: '${name}',
8393
+ },
8394
+ policy: normalizedRouterWafPolicy,
8395
+ receipt: routerWafBlockingRehearsal,
8396
+ });
8397
+ }
8381
8398
  const wafPlan = createRouterWafStagePlan({
8382
8399
  policy: normalizedRouterWafPolicy,
8383
- rehearsal: isProduction
8384
- ? reconcileRouterWafBlockingRehearsal({
8385
- evidence: routerWafLiveEvidence,
8386
- expected: {
8387
- accountId: rehearsalAccount,
8388
- deploymentRoleArn: rehearsalRoleArn,
8389
- region: rehearsalRegion,
8390
- stage: rehearsalStage,
8391
- wafContractDigest: routerWafContractDigest(normalizedRouterWafPolicy),
8392
- workspace: '${name}',
8393
- },
8394
- policy: normalizedRouterWafPolicy,
8395
- receipt: routerWafBlockingRehearsal,
8396
- })
8397
- : null,
8400
+ rehearsal,
8398
8401
  stage: $app.stage,
8399
8402
  });
8400
8403
  const deploymentAccount = process.env.AWS_EXPECTED_ACCOUNT_ID?.trim() ?? '';
@@ -350,9 +350,16 @@ value to GitHub secrets, workflow input, argv, or agent-visible material.
350
350
 
351
351
  The protected deploy role needs metadata authority for the Slice prefix:
352
352
  \`ListSecrets\`, \`DescribeSecret\`, \`CreateSecret\`, and \`TagResource\`.
353
- It must be denied \`BatchGetSecretValue\`, \`GetSecretValue\`,
354
- \`PutSecretValue\`, \`UpdateSecret\`, \`UpdateSecretVersionStage\`,
355
- and application deployment outside its separately reviewed deploy policy.
353
+ It is denied \`BatchGetSecretValue\`, \`UpdateSecretVersionStage\`, and
354
+ application deployment outside its separately reviewed deploy policy, and it is
355
+ denied \`GetSecretValue\` and \`PutSecretValue\` for every operator and
356
+ generated secret; the exact tagged Foundation database credential is the only
357
+ value-action exception. \`UpdateSecret\` is permitted only inside the stage's
358
+ \`ebk/<stage>/*\` namespace and the Foundation provider secret, so native
359
+ recovery can reconcile EBK-owned containers. \`CreateSecret\` and
360
+ \`UpdateSecret\` can both carry a value and IAM cannot prove a call is
361
+ metadata-only, so reviewed generated source, temporary deployment credentials,
362
+ and CloudTrail are the controls.
356
363
  \`DeleteSecret\` is used only by the governed generated-secret retirement path
357
364
  after native Slice removal succeeds; the executor binds that call to the exact
358
365
  generated HMAC name and ownership tags described above.
@@ -337,7 +337,10 @@ keep the source checkout, continue an interrupted report with its run
337
337
  reference, and retry the same approved run. Never delete its checkpoint,
338
338
  pending deletion work or retained evidence to silence the alarm.
339
339
  `,
340
- 'ebk-globals.d.ts': `import 'sst/resource';
340
+ 'ebk-globals.d.ts': `// Next.js ambient types, independent of the gitignored web/next-env.d.ts.
341
+ /// <reference types="next" />
342
+ /// <reference types="next/image-types/global" />
343
+ import 'sst/resource';
341
344
 
342
345
  declare global {
343
346
  type EbkSstOutput<T> = {
@@ -6360,7 +6360,6 @@ if (['stopped', 'recovering'].includes(result.rollout.phase)) {
6360
6360
  sourceInputPaths: [
6361
6361
  `packages/${name}/configuration`,
6362
6362
  `packages/${name}/infra`,
6363
- `packages/${name}/web/next-env.d.ts`,
6364
6363
  `packages/${name}/web/next.config.ts`,
6365
6364
  `packages/${name}/web/open-next.config.ts`,
6366
6365
  `packages/${name}/web/src`,
@@ -416,7 +416,6 @@ test('renders the Slice through its exact task HTTPS origin', async ({ page }) =
416
416
  expect(response?.status()).toBe(200);
417
417
  expect(response?.headers()['x-ebk-upstream']).toBe('local');
418
418
  expect(new URL(page.url()).origin).toBe(target.origin);
419
- await expect(page.getByRole('heading', { name: '${pascalName}', exact: true })).toBeVisible();
420
419
  });
421
420
  `,
422
421
  'playwright.config.ts': `import { defineConfig } from '@playwright/test';
@@ -2,11 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderProductWebFiles = renderProductWebFiles;
4
4
  const built_in_plan_shared_js_1 = require("./built-in-plan-shared.js");
5
- const sst_environment_source_format_js_1 = require("../../sst-environment-source-format.js");
6
5
  /** @internal */
7
6
  function renderProductWebFiles(name, pascalName, workspace, sql = false) {
8
7
  return {
9
- 'web/next-env.d.ts': (0, sst_environment_source_format_js_1.generatedNextEnvironmentSource)(),
10
8
  'web/tsconfig.json': (0, built_in_plan_shared_js_1.json)({
11
9
  extends: '../tsconfig.json',
12
10
  compilerOptions: {
@@ -18,9 +16,10 @@ function renderProductWebFiles(name, pascalName, workspace, sql = false) {
18
16
  incremental: true,
19
17
  plugins: [{ name: 'next' }],
20
18
  },
21
- // Next 16.3 regenerates next-env.d.ts and rewrites tsconfig.json when
22
- // these entries are missing; the generated files match its output so
23
- // a build leaves the tracked workspace unchanged.
19
+ // Next rewrites tsconfig.json when these entries are missing, so the
20
+ // generated include matches its output. Next writes the gitignored
21
+ // next-env.d.ts itself on dev, build, and typegen; EBK does not
22
+ // generate or track it.
24
23
  include: [
25
24
  'next-env.d.ts',
26
25
  '.next/types/**/*.ts',
@@ -48,7 +48,6 @@ const FORMAT_SENSITIVE_BUILT_IN_PATHS = new Set([
48
48
  'api/generated/baseline.json',
49
49
  'api/generated/contract.json',
50
50
  'api/generated/openapi.json',
51
- 'web/next-env.d.ts',
52
51
  ]);
53
52
  function isBuiltInSliceFormatSensitivePath(path) {
54
53
  return (path.startsWith('database/migrations/') ||
@@ -257,7 +256,7 @@ function classifyPaths(files, sql) {
257
256
  ]);
258
257
  for (const path of built_in_plan_performance_files_js_1.PRODUCT_PERFORMANCE_SEEDED_PATHS)
259
258
  seededPaths.add(path);
260
- const toolOwnedPaths = new Set(['sst-env.d.ts', 'web/next-env.d.ts']);
259
+ const toolOwnedPaths = new Set(['sst-env.d.ts']);
261
260
  return Object.entries(files)
262
261
  .filter(([path]) => path !== '.ebk/ownership.json' && !toolOwnedPaths.has(path))
263
262
  .map(([path, contents]) => {
@@ -470,6 +470,89 @@ function appendSeededReconciliationMarker(tree, root, report) {
470
470
  return;
471
471
  tree.write(anchor, `${current.replace(/\s*$/, '')}\n\n${SEEDED_RECONCILIATION_MARKER}\n`);
472
472
  }
473
+ function isJsonObject(value) {
474
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
475
+ }
476
+ function appendServiceTaskPresenceReconciliation(currentFiles, currentOwnership, targetOwnership, report) {
477
+ const services = currentOwnership.compositions
478
+ .filter(({ kind }) => kind === 'service')
479
+ .map(({ name }) => name)
480
+ .sort(deterministic_order_js_1.compareUtf16);
481
+ if (services.length === 0)
482
+ return;
483
+ const registryPath = 'operations/reliability.json';
484
+ const source = currentFiles.get(registryPath);
485
+ if (source === null || source === undefined)
486
+ return;
487
+ let operations;
488
+ try {
489
+ operations = JSON.parse(source);
490
+ }
491
+ catch {
492
+ throw new Error('Retained Service reliability registry must be valid JSON before upgrade.');
493
+ }
494
+ if (!isJsonObject(operations) ||
495
+ !Array.isArray(operations.deployedAlarmResources)) {
496
+ throw new Error('Retained Service reliability registry must expose deployedAlarmResources before upgrade.');
497
+ }
498
+ const resources = operations.deployedAlarmResources;
499
+ const append = (path, guidance) => {
500
+ const current = currentFiles.get(path);
501
+ const targetEntry = targetOwnership.paths.find((entry) => entry.path === path);
502
+ const currentEntry = currentOwnership.paths.find((entry) => entry.path === path);
503
+ if (current === null ||
504
+ current === undefined ||
505
+ targetEntry?.mode !== 'seeded')
506
+ return;
507
+ const existing = report.seededReconciliation.find((entry) => entry.path === path);
508
+ if (existing) {
509
+ existing.guidance = `${existing.guidance ? `${existing.guidance}\n\n` : ''}${guidance}`;
510
+ existing.implications = [
511
+ ...new Set([...existing.implications, 'service-live-task-presence']),
512
+ ].sort(deterministic_order_js_1.compareUtf16);
513
+ return;
514
+ }
515
+ report.seededReconciliation.push({
516
+ adopterContentSha256: (0, index_js_5.hashOwnedContent)(current, currentEntry ?? targetEntry),
517
+ category: 'reliability-operations',
518
+ currentFrameworkSha256: currentEntry?.sha256 ?? null,
519
+ guidance,
520
+ implications: ['alarm-registration', 'service-live-task-presence'],
521
+ markerReference: SEEDED_RECONCILIATION_MARKER_REFERENCE,
522
+ path,
523
+ requiredAction: 'review-and-reconcile-preserving-adopter-content',
524
+ targetFrameworkSha256: targetEntry.sha256,
525
+ });
526
+ };
527
+ for (const name of services) {
528
+ const id = `service-${name}-health`;
529
+ const legacy = resources.some((entry) => {
530
+ if (!isJsonObject(entry))
531
+ return false;
532
+ const expected = {
533
+ comparisonOperator: 'LessThanThreshold',
534
+ evaluationPeriods: 1,
535
+ id,
536
+ metricName: 'RunningTaskCount',
537
+ name: `${currentOwnership.slice}-{stage}-service-${name}-running`,
538
+ namespace: 'ECS/ContainerInsights',
539
+ period: 60,
540
+ signal: 'metric',
541
+ statistic: 'Sum',
542
+ threshold: 1,
543
+ treatMissingData: 'breaching',
544
+ };
545
+ return (Object.entries(expected).every(([key, value]) => entry[key] === value) &&
546
+ JSON.stringify(entry.dimensionNames) ===
547
+ JSON.stringify(['ClusterName', 'ServiceName']));
548
+ });
549
+ if (!legacy)
550
+ continue;
551
+ append(registryPath, `For recorded Service ${name}, reconcile only deployedAlarmResources[id=${id}]: namespace ECS/ContainerInsights -> AWS/ECS, metricName RunningTaskCount -> LiveTaskCount, statistic Sum -> Minimum. Keep its ID, physical name, actions, dimensions, threshold 1, LessThanThreshold, 60-second period, one evaluation period and breaching missing-data policy. In alarmRegistrations[id=${id}], set missingData to breaching and review businessSymptom, condition and both expected risk fields for task presence or missing telemetry, startup/emission-gap false positives, and activating/draining/replacement crash-loop false negatives. Preserve unrelated registrations and every adopter-owned field. Review services/${name}/RUNBOOK.md, run the declared reliability verification after authorized deployment, retain evidence, then resolve the existing seeded reconciliation marker.`);
552
+ append(`services/${name}/RUNBOOK.md`, `Review the retained Service ${name} runbook for AWS/ECS LiveTaskCount Minimum: ACTIVATING, RUNNING and DEACTIVATING task presence, not readiness. Missing telemetry breaches but does not prove zero tasks. Document startup/emission-gap false positives and activating/draining/replacement crash-loop false negatives, preserve custom recovery instructions, verify the critical journey and alarm recovery after an authorized roll-forward, retain evidence, then resolve the existing seeded reconciliation marker.`);
553
+ }
554
+ report.seededReconciliation.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.path, right.path));
555
+ }
473
556
  function currentContractConsumers(tree, root) {
474
557
  const source = tree.read(node_path_1.posix.join(root, 'contracts/dependencies.json'), 'utf8');
475
558
  if (source === null)
@@ -1110,7 +1193,7 @@ async function formatMergedManagedJsonFiles(tree, root, merge, ownership) {
1110
1193
  }));
1111
1194
  return { ...merge, files };
1112
1195
  }
1113
- function renderCompositionUpgradePlan(ownership, workspace, runtimeVersion, serviceAdapterOwner) {
1196
+ function renderCompositionUpgradePlan(ownership, workspace, runtimeVersion, serviceAdapterOwner, deploymentProfile) {
1114
1197
  const compositionPaths = ownership.paths.filter(({ origin }) => origin === 'composition');
1115
1198
  const files = {};
1116
1199
  const paths = new Map();
@@ -1139,7 +1222,7 @@ function renderCompositionUpgradePlan(ownership, workspace, runtimeVersion, serv
1139
1222
  }
1140
1223
  else {
1141
1224
  const plan = (0, workload_js_1.createWorkloadFilePlan)(workspace, ownership.slice, composition.name, composition.kind, runtimeVersion, composition.feature, composition.kind === 'service' &&
1142
- composition.name === serviceAdapterOwner);
1225
+ composition.name === serviceAdapterOwner, [(0, index_js_4.productionRehearsalStage)(deploymentProfile), 'production']);
1143
1226
  addPlan(plan.files, plan.paths);
1144
1227
  }
1145
1228
  }
@@ -1672,7 +1755,7 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
1672
1755
  targetFiles = preserveBuiltInDerivedApiArtifacts(tree, root, currentOwnership, targetFiles);
1673
1756
  }
1674
1757
  const targetPathSet = new Set(targetOwnership.paths.map(({ path }) => path));
1675
- const compositionPlan = renderCompositionUpgradePlan(currentOwnership, packageJson.name ?? '', packageJson.dependencies?.['@empire-builder-kit/runtime'] ?? '', serviceAdapterOwner);
1758
+ const compositionPlan = renderCompositionUpgradePlan(currentOwnership, packageJson.name ?? '', packageJson.dependencies?.['@empire-builder-kit/runtime'] ?? '', serviceAdapterOwner, (0, tree_policy_js_1.readRepositoryOperatingPolicyFromTree)(tree).deploymentProfile);
1676
1759
  const compositionPaths = compositionPlan.paths.filter(({ path }) => !targetPathSet.has(path));
1677
1760
  if (externalBlueprintDestinationPaths) {
1678
1761
  (0, index_js_1.assertBlueprintDestinationPaths)(tree.root, root, [
@@ -1745,6 +1828,9 @@ async function executeUpgradeGenerator(tree, options, output = devkit_1.logger,
1745
1828
  mergedTarget.files.set(logging_registry_js_1.SLICE_LOGGING_REGISTRY_PATH, await (0, formatting_js_1.formatTreeContentsToFixedPoint)(loggingAppend, formatting, workspacePath));
1746
1829
  }
1747
1830
  const report = createUpgradeReport(currentFiles, currentOwnership, mergedTarget.files, targetOwnership, mergedTarget);
1831
+ if (blueprint.source === 'built-in') {
1832
+ appendServiceTaskPresenceReconciliation(currentFiles, currentOwnership, targetOwnership, report);
1833
+ }
1748
1834
  if (loggingAppend !== null) {
1749
1835
  report.preservedSeeded = report.preservedSeeded.filter((path) => path !== logging_registry_js_1.SLICE_LOGGING_REGISTRY_PATH);
1750
1836
  report.appendedSeeded.push(logging_registry_js_1.SLICE_LOGGING_REGISTRY_PATH);
@@ -3,7 +3,7 @@ export declare const WORKLOAD_RELIABILITY_REGISTRY_PATH: "operations/reliability
3
3
  export interface WorkloadReliabilityPlan {
4
4
  alarmId: string;
5
5
  contents: string;
6
- metricName: 'MatchedEvents' | 'RunningTaskCount';
6
+ metricName: 'LiveTaskCount' | 'MatchedEvents';
7
7
  path: string;
8
8
  runbookRef: string;
9
9
  }
@@ -61,21 +61,21 @@ function planWorkloadReliabilityRegistration(tree, input) {
61
61
  ...operations.alarmRegistrations,
62
62
  (0, reliability_1.validateAlarmRegistrationMetadata)({
63
63
  businessSymptom: input.kind === 'service'
64
- ? `${input.name} Service health checks are failing.`
64
+ ? `${input.name} Service task presence is below one or telemetry is missing.`
65
65
  : `${input.name} Job executions are failing.`,
66
66
  condition: input.kind === 'service'
67
- ? 'The AWS-native ECS RunningTaskCount signal is below one or missing for the exact Service.'
67
+ ? 'The AWS/ECS LiveTaskCount minimum is below one or missing for the exact Service; it counts ACTIVATING, RUNNING, and DEACTIVATING tasks.'
68
68
  : 'An AWS-native ECS task-state event reports an essential container exit for the exact Job task family.',
69
69
  dependencyAlarmIds: [],
70
70
  escalationRouteKey: `${input.slice}-on-call`,
71
71
  expectedFalseNegativeRisk: input.kind === 'service'
72
- ? 'Container Insights delivery can be delayed; missing data is therefore treated as breaching.'
72
+ ? 'Activating, draining, or repeatedly replaced tasks can keep LiveTaskCount positive while readiness or useful work fails.'
73
73
  : 'ECS task-state events are durable but can be delayed before EventBridge reports a match.',
74
74
  expectedFalsePositiveRisk: input.kind === 'service'
75
- ? 'A synthetic health client can fail while the Service remains available.'
75
+ ? 'Metric emission gaps, startup, or lifecycle transitions can breach the missing-data policy while tasks remain present.'
76
76
  : 'A retried Job attempt can fail before a later attempt succeeds.',
77
77
  id: alarmId,
78
- missingData: 'missing',
78
+ missingData: input.kind === 'service' ? 'breaching' : 'missing',
79
79
  owner,
80
80
  runbookRef: `runbook:${runbookRef}`,
81
81
  severity: 'high',
@@ -90,15 +90,15 @@ function planWorkloadReliabilityRegistration(tree, input) {
90
90
  dimensionNames: input.kind === 'service' ? ['ClusterName', 'ServiceName'] : ['RuleName'],
91
91
  evaluationPeriods: 1,
92
92
  id: alarmId,
93
- metricName: input.kind === 'service' ? 'RunningTaskCount' : 'MatchedEvents',
93
+ metricName: input.kind === 'service' ? 'LiveTaskCount' : 'MatchedEvents',
94
94
  name: `${input.slice}-{stage}-${input.kind}-${input.name}-${input.kind === 'service' ? 'running' : 'hard-crash'}`,
95
- namespace: input.kind === 'service' ? 'ECS/ContainerInsights' : 'AWS/Events',
95
+ namespace: input.kind === 'service' ? 'AWS/ECS' : 'AWS/Events',
96
96
  owner,
97
97
  period: 60,
98
98
  route: `${input.slice}-on-call`,
99
99
  runbook: runbookRef,
100
100
  signal: 'metric',
101
- statistic: 'Sum',
101
+ statistic: input.kind === 'service' ? 'Minimum' : 'Sum',
102
102
  threshold: 1,
103
103
  treatMissingData: input.kind === 'service' ? 'breaching' : 'notBreaching',
104
104
  };
@@ -142,7 +142,7 @@ function planWorkloadReliabilityRegistration(tree, input) {
142
142
  return {
143
143
  alarmId,
144
144
  contents: `${JSON.stringify(operations, null, 2)}\n`,
145
- metricName: input.kind === 'service' ? 'RunningTaskCount' : 'MatchedEvents',
145
+ metricName: input.kind === 'service' ? 'LiveTaskCount' : 'MatchedEvents',
146
146
  path,
147
147
  runbookRef,
148
148
  };
@@ -13,5 +13,5 @@ export interface WorkloadFilePlan {
13
13
  }>;
14
14
  project: string;
15
15
  }
16
- export declare function createWorkloadFilePlan(workspace: string, slice: string, workload: string, kind: WorkloadKind, runtimeVersion: string, feature: string, ownsProtectedLoadAdapter: boolean, protectedRuntimeStages?: readonly ('development' | 'production' | 'staging')[]): WorkloadFilePlan;
16
+ export declare function createWorkloadFilePlan(workspace: string, slice: string, workload: string, kind: WorkloadKind, runtimeVersion: string, feature: string, ownsProtectedLoadAdapter: boolean, protectedRuntimeStages: readonly ('development' | 'production' | 'staging')[]): WorkloadFilePlan;
17
17
  export declare function workloadGenerator(tree: Tree, options: WorkloadGeneratorSchema, kind: WorkloadKind): Promise<GeneratorCallback | void>;
@@ -139,7 +139,7 @@ exports.SERVICE_WORKLOAD_PACKAGE_MANAGED_JSON_POINTERS = [
139
139
  '/dependencies/@aws-sdk~1client-dynamodb',
140
140
  '/dependencies/sst',
141
141
  ];
142
- function createWorkloadFilePlan(workspace, slice, workload, kind, runtimeVersion, feature, ownsProtectedLoadAdapter, protectedRuntimeStages = ['staging', 'production']) {
142
+ function createWorkloadFilePlan(workspace, slice, workload, kind, runtimeVersion, feature, ownsProtectedLoadAdapter, protectedRuntimeStages) {
143
143
  const plural = kind === 'service' ? 'services' : 'jobs';
144
144
  const relativeRoot = node_path_1.posix.join(plural, workload);
145
145
  const className = `${pascal(slice)}${pascal(workload)}${pascal(kind)}`;
@@ -161,7 +161,7 @@ function createWorkloadFilePlan(workspace, slice, workload, kind, runtimeVersion
161
161
  version: '0.0.0',
162
162
  scripts: {
163
163
  build: 'tsc -p tsconfig.json',
164
- 'docker-build': `docker build -f packages/${slice}/${relativeRoot}/Dockerfile ` +
164
+ 'docker-build': 'docker build -f Dockerfile ' +
165
165
  `-t ${slice}-${kind}-${workload}:local ../../../..`,
166
166
  dev: `node --experimental-strip-types ${kind === 'service' ? 'src/main.ts' : 'src/run.ts'}`,
167
167
  lint: 'eslint .',
@@ -212,7 +212,7 @@ function createWorkloadFilePlan(workspace, slice, workload, kind, runtimeVersion
212
212
  [`${relativeRoot}/src/workload.spec.ts`]: kind === 'service'
213
213
  ? `import { describe, expect, it } from 'vitest';\nimport { healthResponse } from './main.js';\n\ndescribe('${workload} service', () => {\n it('reports healthy only on the health endpoint', () => {\n expect(healthResponse('/health')).toEqual({ body: { service: '${workload}', status: 'ok' }, status: 200 });\n expect(healthResponse('/missing').status).toBe(404);\n });\n});\n`
214
214
  : `import { createLogger, type LogEntry } from '@empire-builder-kit/runtime/logging';\nimport { describe, expect, it } from 'vitest';\nimport { run } from './run.js';\n\ndescribe('${workload} job', () => {\n it('emits canonical start and completion records', async () => {\n const entries: LogEntry[] = [];\n const logger = createLogger({\n component: 'job-${workload}', feature: '${feature}', operation: 'run',\n slice: '${slice}', stage: 'test', workspace: '${workspace}',\n sink: (entry) => entries.push(entry as LogEntry),\n });\n await expect(run(logger)).resolves.toEqual({\n ok: true,\n providerDeliveryAssigned: false,\n });\n expect(entries.map(({ message, outcome }) => ({ message, outcome }))).toEqual([\n { message: '${workload} job started', outcome: undefined },\n { message: '${workload} job completed', outcome: 'success' },\n ]);\n });\n});\n`,
215
- [`${relativeRoot}/infra.ts`]: `import { createManagedLogGroupContract } from '@empire-builder-kit/nx/foundation';\nimport { standardSliceExtensionTaskFamily } from '../../infra/workload-roles/effective-intent.js';\n\nexport function ${factoryName}(\n cluster: any,\n alertTopicArn: any,\n roles?: { executionRole: string; taskRole: string },\n links: any[] = [],\n configuration?: {\n applicationId: any;\n deployment: any;\n environmentId: any;\n profileId: any;\n },\n) {\n const contract = createManagedLogGroupContract({\n component: '${kind}-${workload}',\n slice: '${slice}',\n stage: $app.stage,\n workspace: '${workspace}',\n });\n const workload = new sst.aws.${resource}('${className}', {\n ...(roles ?? {}),\n cluster,\n environment: {\n EBK_STAGE: $app.stage,\n ...(configuration\n ? {\n EBK_APPCONFIG_APPLICATION_ID: configuration.applicationId,\n EBK_APPCONFIG_ENVIRONMENT_ID: configuration.environmentId,\n EBK_APPCONFIG_PROFILE_ID: configuration.profileId,\n }\n : {}),\n },\n link: links,\n ${kind === 'service' ? "image: { context: './services/" + workload + "', dockerfile: 'Dockerfile' },\n health: { command: ['CMD-SHELL', 'wget -qO- http://localhost:3000/health || exit 1'] },\n logging: { name: contract.name, retention: '1 month' },\n serviceRegistry: { port: 3000 }," : "image: { context: './jobs/" + workload + "', dockerfile: 'Dockerfile' },\n logging: { name: contract.name, retention: '1 month' },"}\n transform: {\n logGroup: {\n logGroupClass: contract.logGroupClass,\n tags: contract.tags,\n },\n taskDefinition: {\n family: standardSliceExtensionTaskFamily(\n $app.stage,\n '${workload}',\n '${kind}',\n ),\n },\n },\n }, configuration?.deployment\n ? { dependsOn: [configuration.deployment] }\n : {});\n const dataProtection = new aws.cloudwatch.LogDataProtectionPolicy(\n '${className}LogDataProtection',\n {\n logGroupName: contract.name,\n policyDocument: JSON.stringify(contract.dataProtectionPolicy),\n },\n { dependsOn: [workload.nodes.taskDefinition] },\n );\n const dataProtectionAlarm = new aws.cloudwatch.MetricAlarm(\n '${className}LogDataProtectionFindings',\n {\n alarmActions: [alertTopicArn],\n comparisonOperator: 'GreaterThanOrEqualToThreshold',\n dimensions: { LogGroupName: contract.name },\n evaluationPeriods: 1,\n metricName: 'LogEventsWithFindings',\n namespace: 'AWS/Logs',\n period: 300,\n statistic: 'Sum',\n threshold: 1,\n treatMissingData: 'notBreaching',\n },\n { dependsOn: [dataProtection] },\n );\n void dataProtectionAlarm;\n return workload;\n}\n`,
215
+ [`${relativeRoot}/infra.ts`]: `import { createManagedLogGroupContract } from '@empire-builder-kit/nx/foundation';\nimport { standardSliceExtensionTaskFamily } from '../../infra/workload-roles/effective-intent.js';\n\nexport function ${factoryName}(\n cluster: any,\n alertTopicArn: any,\n roles?: { executionRole: string; taskRole: string },\n links: any[] = [],\n configuration?: {\n applicationId: any;\n deployment: any;\n environmentId: any;\n profileId: any;\n },\n) {\n const contract = createManagedLogGroupContract({\n component: '${kind}-${workload}',\n slice: '${slice}',\n stage: $app.stage,\n workspace: '${workspace}',\n });\n const workload = new sst.aws.${resource}('${className}', {\n ...(roles ?? {}),\n cluster,\n environment: {\n EBK_STAGE: $app.stage,\n ...(configuration\n ? {\n EBK_APPCONFIG_APPLICATION_ID: configuration.applicationId,\n EBK_APPCONFIG_ENVIRONMENT_ID: configuration.environmentId,\n EBK_APPCONFIG_PROFILE_ID: configuration.profileId,\n }\n : {}),\n },\n link: links,\n ${kind === 'service' ? "image: { context: './services/" + workload + "', dockerfile: 'Dockerfile' },\n health: { command: ['CMD-SHELL', 'wget -qO- http://localhost:3000/health || exit 1'] },\n logging: { name: contract.name, retention: '1 month' },\n serviceRegistry: { port: 3000 }," : "image: { context: './jobs/" + workload + "', dockerfile: 'Dockerfile' },\n logging: { name: contract.name, retention: '1 month' },"}\n transform: {\n logGroup: {\n logGroupClass: contract.logGroupClass,\n tags: contract.tags,\n },\n taskDefinition: {\n family: standardSliceExtensionTaskFamily(\n $app.stage,\n '${workload}',\n '${kind}',\n ),\n },\n },\n }, configuration?.deployment\n ? { dependsOn: [configuration.deployment] }\n : {});\n${kind === 'service' ? ' if ($dev) return workload;\n' : ''} const dataProtection = new aws.cloudwatch.LogDataProtectionPolicy(\n '${className}LogDataProtection',\n {\n logGroupName: contract.name,\n policyDocument: JSON.stringify(contract.dataProtectionPolicy),\n },\n { dependsOn: [workload.nodes.taskDefinition] },\n );\n const dataProtectionAlarm = new aws.cloudwatch.MetricAlarm(\n '${className}LogDataProtectionFindings',\n {\n alarmActions: [alertTopicArn],\n comparisonOperator: 'GreaterThanOrEqualToThreshold',\n dimensions: { LogGroupName: contract.name },\n evaluationPeriods: 1,\n metricName: 'LogEventsWithFindings',\n namespace: 'AWS/Logs',\n period: 300,\n statistic: 'Sum',\n threshold: 1,\n treatMissingData: 'notBreaching',\n },\n { dependsOn: [dataProtection] },\n );\n void dataProtectionAlarm;\n return workload;\n}\n`,
216
216
  };
217
217
  if (kind === 'service') {
218
218
  const serviceMainPath = `${relativeRoot}/src/main.ts`;
@@ -413,7 +413,7 @@ ${kind !== 'service' || ownsProtectedLoadAdapter
413
413
  alarmDescription: JSON.stringify({
414
414
  businessSymptom:
415
415
  '${kind === 'service'
416
- ? `${workload} Service has no running task.`
416
+ ? `${workload} Service task presence is below one or telemetry is missing.`
417
417
  : `${workload} Job essential container exited.`}',
418
418
  id: '${kind === 'service'
419
419
  ? `service-${workload}-health`
@@ -428,14 +428,14 @@ ${kind !== 'service' || ownsProtectedLoadAdapter
428
428
  ? 'LessThanThreshold'
429
429
  : 'GreaterThanOrEqualToThreshold'}',
430
430
  dimensions: ${kind === 'service'
431
- ? `{ ClusterName: cluster.name, ServiceName: workload.name }`
431
+ ? `{ ClusterName: cluster.nodes.cluster.name, ServiceName: workload.nodes.service.name }`
432
432
  : `{ RuleName: reliabilityTaskStopped.name }`},
433
433
  evaluationPeriods: 1,
434
- metricName: '${kind === 'service' ? 'RunningTaskCount' : 'MatchedEvents'}',
434
+ metricName: '${kind === 'service' ? 'LiveTaskCount' : 'MatchedEvents'}',
435
435
  name: '${slice}-' + $app.stage + '-${kind}-${workload}-${kind === 'service' ? 'running' : 'hard-crash'}',
436
- namespace: '${kind === 'service' ? 'ECS/ContainerInsights' : 'AWS/Events'}',
436
+ namespace: '${kind === 'service' ? 'AWS/ECS' : 'AWS/Events'}',
437
437
  period: 60,
438
- statistic: 'Sum',
438
+ statistic: '${kind === 'service' ? 'Minimum' : 'Sum'}',
439
439
  tags: standardTags,
440
440
  threshold: 1,
441
441
  treatMissingData: '${kind === 'service' ? 'breaching' : 'notBreaching'}',
@@ -551,7 +551,25 @@ through the read-only diagnostic path. Do not restart, scale, retry, or replace
551
551
  protected resources without the named approval.
552
552
 
553
553
  ${kind === 'service'
554
- ? 'Prefer a verified roll-forward; verify readiness, dependency protection, and the critical Service journey after recovery.'
554
+ ? `The native AWS/ECS LiveTaskCount alarm uses Minimum over 60 seconds and
555
+ counts tasks in ACTIVATING, RUNNING, and DEACTIVATING states. It breaches below
556
+ one or when telemetry is missing. ECS can emit no metrics when there are no
557
+ RUNNING tasks; missing telemetry is actionable but does not prove zero tasks.
558
+
559
+ Startup, emission gaps, and lifecycle transitions can cause false positives.
560
+ Activating, draining, or repeatedly replaced tasks can keep the count positive
561
+ while readiness or useful work fails. ALARM is not a diagnosis. OK does not
562
+ prove readiness, freedom from crash loops, or successful business traffic.
563
+
564
+ Inspect ECS events, task states and exit reasons, the existing container health
565
+ check, and the Slice API health and burn alarms. Do not infer that every Service
566
+ has a load-balancer readiness alarm. Prefer a verified roll-forward; verify
567
+ stable tasks, readiness, dependency protection, and the critical Service journey
568
+ after recovery, then confirm telemetry and alarm recovery.
569
+
570
+ LiveTaskCount adds no metric collection charge and needs no Container Insights
571
+ opt-in. Existing alarm, compute, log, and request charges still apply; this is
572
+ not a measured account-cost claim.`
555
573
  : 'Preserve idempotency and checkpoint state; verify the business deadline and postconditions before retrying or declaring recovery.'}
556
574
  `;
557
575
  files[`${relativeRoot}/src/reliability.ts`] = `import {
@@ -72,5 +72,23 @@ export interface GitHubRepositoryIdentity {
72
72
  repositoryId: string;
73
73
  subjectPrefix: string;
74
74
  }
75
+ export interface FrameworkNpmStableBootstrapOptions {
76
+ npmStable: true;
77
+ apiBaseUrl?: string;
78
+ apply?: boolean;
79
+ repository: string;
80
+ token: string;
81
+ npmStableReviewerIds: number[];
82
+ npmStablePreventSelfReview: boolean;
83
+ }
84
+ export interface FrameworkNpmStableBootstrapReport {
85
+ kind: 'framework-npm-stable';
86
+ repository: string;
87
+ ready: boolean;
88
+ blockers: string[];
89
+ changes: string[];
90
+ warnings: string[];
91
+ }
92
+ export declare function runGitHubBootstrap(options: FrameworkNpmStableBootstrapOptions): Promise<FrameworkNpmStableBootstrapReport>;
75
93
  export declare function runGitHubBootstrap(options: GitHubBootstrapOptions): Promise<GitHubBootstrapReport>;
76
94
  export {};
@@ -203,11 +203,7 @@ function requiresImmutableOidcSubjectCutover(state) {
203
203
  function immutableOidcTrustAcknowledgement(identity) {
204
204
  return `${GITHUB_IMMUTABLE_OIDC_TRUST_ACKNOWLEDGEMENT_PREFIX}${identity.subjectPrefix}`;
205
205
  }
206
- const PRIVATE_ENVIRONMENT_REVIEWER_PLANS = new Set([
207
- 'business_plus',
208
- 'enterprise',
209
- 'enterprise_cloud',
210
- ]);
206
+ const PRIVATE_ENVIRONMENT_REVIEWER_PLANS = new Set(['enterprise']);
211
207
  class GitHubApiError extends Error {
212
208
  status;
213
209
  constructor(status, message) {
@@ -575,6 +571,14 @@ async function inspectEnvironment(api, repositoryPath, name) {
575
571
  branchPolicies: branchPolicyValues,
576
572
  customBranchPolicies,
577
573
  exists: true,
574
+ canAdminsBypass: environment.can_admins_bypass === false ? false : true,
575
+ protectedBranches: typeof environment.deployment_branch_policy === 'object' &&
576
+ environment.deployment_branch_policy !== null &&
577
+ 'protected_branches' in environment.deployment_branch_policy &&
578
+ typeof environment.deployment_branch_policy.protected_branches ===
579
+ 'boolean'
580
+ ? environment.deployment_branch_policy.protected_branches
581
+ : undefined,
578
582
  preventSelfReview: reviewers.preventSelfReview,
579
583
  reviewers: reviewers.reviewers,
580
584
  secretNames: namedValues(secrets),
@@ -786,7 +790,7 @@ function evaluate(repository, state, inspectionBlockers, productionApprovalMode,
786
790
  privateRepository &&
787
791
  state.ownerPlanName !== undefined &&
788
792
  !PRIVATE_ENVIRONMENT_REVIEWER_PLANS.has(state.ownerPlanName)) {
789
- blockers.push(`Committed environment-reviewer mode requires GitHub Enterprise support for a private repository; the inspected owner plan is ${state.ownerPlanName}.`);
793
+ blockers.push(`Committed environment-reviewer mode does not support inspected owner plan ${state.ownerPlanName} under EBK's verified private environment-reviewer contract; supported REST plan.name: enterprise.`);
790
794
  }
791
795
  if (state.repo.allow_merge_commit !== true) {
792
796
  changes.push('Enable repository merge commits.');
@@ -1099,7 +1103,96 @@ async function applyChanges(api, repositoryPath, state, productionApprovalMode,
1099
1103
  }
1100
1104
  }
1101
1105
  }
1106
+ async function runFrameworkNpmStableBootstrap(options) {
1107
+ if (options.repository !== 'empire-builder-kit/empire-builder-kit')
1108
+ throw new Error('npm-stable is restricted to the exact framework repository.');
1109
+ if (!options.token.trim())
1110
+ throw new Error('GH_TOKEN or GITHUB_TOKEN is required for GitHub bootstrap.');
1111
+ if (!Array.isArray(options.npmStableReviewerIds) ||
1112
+ options.npmStableReviewerIds.length < 1 ||
1113
+ options.npmStableReviewerIds.length > 6 ||
1114
+ options.npmStableReviewerIds.some((id) => !Number.isSafeInteger(id) || id <= 0) ||
1115
+ new Set(options.npmStableReviewerIds).size !==
1116
+ options.npmStableReviewerIds.length ||
1117
+ typeof options.npmStablePreventSelfReview !== 'boolean') {
1118
+ throw new Error('npm-stable requires exact reviewer IDs and explicit prevent-self-review choice.');
1119
+ }
1120
+ const reviewers = existingReviewerPayload([...options.npmStableReviewerIds].sort((left, right) => left - right));
1121
+ const api = new GitHubApi(options.apiBaseUrl?.trim() || 'https://api.github.com', options.token);
1122
+ const repositoryPath = '/repos/empire-builder-kit/empire-builder-kit';
1123
+ const repo = await api.request('GET', repositoryPath);
1124
+ if (repo.full_name !== options.repository)
1125
+ throw new Error('npm-stable repository readback differs from its exact target.');
1126
+ const environmentPath = `${repositoryPath}/environments/npm-stable`;
1127
+ const inspectStable = () => inspectEnvironment(api, repositoryPath, 'npm-stable');
1128
+ const evaluateStable = (state) => {
1129
+ const changes = [];
1130
+ const blockers = [];
1131
+ if (!state.exists ||
1132
+ !state.customBranchPolicies ||
1133
+ state.protectedBranches !== false ||
1134
+ state.canAdminsBypass !== false ||
1135
+ state.preventSelfReview !== options.npmStablePreventSelfReview ||
1136
+ state.waitTimer !== 0 ||
1137
+ JSON.stringify(state.reviewers) !== JSON.stringify(reviewers)) {
1138
+ changes.push('Set exact npm-stable reviewers, explicit self-review choice, administrator bypass false, and custom main-only policy.');
1139
+ }
1140
+ if (state.branchPolicies.length !== 1 ||
1141
+ state.branchPolicies[0]?.name !== 'main' ||
1142
+ state.branchPolicies[0]?.type !== 'branch')
1143
+ changes.push('Restrict npm-stable to the main branch only.');
1144
+ if ([...state.secretNames, ...state.variableNames].some((name) => FORBIDDEN_NPM_TOKEN_NAMES.some((tokenName) => name.toUpperCase() === tokenName)))
1145
+ blockers.push('npm-stable may not contain npm token credentials.');
1146
+ return {
1147
+ kind: 'framework-npm-stable',
1148
+ repository: options.repository,
1149
+ ready: changes.length === 0 && blockers.length === 0,
1150
+ blockers,
1151
+ changes,
1152
+ warnings: [],
1153
+ };
1154
+ };
1155
+ const initial = await inspectStable();
1156
+ const report = evaluateStable(initial);
1157
+ if (!options.apply)
1158
+ return report;
1159
+ if (report.blockers.length)
1160
+ throw new Error(`npm-stable apply is blocked: ${report.blockers.join(' ')}`);
1161
+ if (report.ready)
1162
+ return report;
1163
+ await api.request('PUT', environmentPath, {
1164
+ can_admins_bypass: false,
1165
+ deployment_branch_policy: {
1166
+ protected_branches: false,
1167
+ custom_branch_policies: true,
1168
+ },
1169
+ prevent_self_review: options.npmStablePreventSelfReview,
1170
+ reviewers,
1171
+ wait_timer: 0,
1172
+ });
1173
+ const policies = await readPagedObjects(api, `${environmentPath}/deployment-branch-policies`, 'branch_policies');
1174
+ let mainRetained = false;
1175
+ for (const policy of policies) {
1176
+ if (policy.name === 'main' && policy.type === 'branch' && !mainRetained) {
1177
+ mainRetained = true;
1178
+ continue;
1179
+ }
1180
+ if (!Number.isSafeInteger(policy.id) ||
1181
+ typeof policy.id !== 'number' ||
1182
+ policy.id <= 0)
1183
+ throw new Error('npm-stable branch policy has no exact deletion identity.');
1184
+ await api.request('DELETE', `${environmentPath}/deployment-branch-policies/${policy.id}`);
1185
+ }
1186
+ if (!mainRetained)
1187
+ await api.request('POST', `${environmentPath}/deployment-branch-policies`, {
1188
+ name: 'main',
1189
+ type: 'branch',
1190
+ });
1191
+ return evaluateStable(await inspectStable());
1192
+ }
1102
1193
  async function runGitHubBootstrap(options) {
1194
+ if ('npmStable' in options)
1195
+ return runFrameworkNpmStableBootstrap(options);
1103
1196
  const repository = options.repository.trim();
1104
1197
  const deploymentProfile = options.deploymentProfile ?? 'fully-isolated';
1105
1198
  const { owner, repo } = parseRepository(repository);
@@ -22,6 +22,7 @@ class FleetSession {
22
22
  #cleanupStates = new Map();
23
23
  #fleetId;
24
24
  #operation = Promise.resolve();
25
+ #pendingStartup;
25
26
  #processFactory;
26
27
  #processes = new Map();
27
28
  #proxy;
@@ -144,7 +145,19 @@ class FleetSession {
144
145
  upstream: route.upstream,
145
146
  });
146
147
  this.#processes.set(name, process);
147
- await process.start();
148
+ const pending = {
149
+ name,
150
+ process,
151
+ interruptionRequested: false,
152
+ };
153
+ this.#pendingStartup = pending;
154
+ try {
155
+ await process.start();
156
+ }
157
+ finally {
158
+ if (this.#pendingStartup === pending)
159
+ this.#pendingStartup = undefined;
160
+ }
148
161
  }
149
162
  }
150
163
  catch (error) {
@@ -228,7 +241,28 @@ class FleetSession {
228
241
  return sessionError('not-running', `Product Slice ${name} is not selected.`);
229
242
  }
230
243
  const previousState = route.state;
231
- await process.restart();
244
+ const pending = {
245
+ name,
246
+ process,
247
+ interruptionRequested: false,
248
+ };
249
+ this.#pendingStartup = pending;
250
+ try {
251
+ await process.restart();
252
+ }
253
+ catch (error) {
254
+ if (pending.interruptionRequested) {
255
+ const failures = await this.#quiesce([name]);
256
+ if (failures.length > 0) {
257
+ return sessionError('cleanup-incomplete', `Fleet restart failed (${startupFailure(error)}) and owned writer quiescence was incomplete: ${failures.join('; ')}`);
258
+ }
259
+ }
260
+ throw error;
261
+ }
262
+ finally {
263
+ if (this.#pendingStartup === pending)
264
+ this.#pendingStartup = undefined;
265
+ }
232
266
  results.push({
233
267
  changed: true,
234
268
  name,
@@ -314,8 +348,11 @@ class FleetSession {
314
348
  // running up operation owns rollback; down waits for that rollback rather
315
349
  // than racing a second native cleanup against a live writer.
316
350
  const ordered = (0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'down', names, this.selectedSlices());
317
- for (const name of ordered)
318
- this.#processes.get(name)?.interruptStartup?.();
351
+ const pending = this.#pendingStartup;
352
+ if (pending && ordered.includes(pending.name)) {
353
+ pending.interruptionRequested = true;
354
+ pending.process.interruptStartup?.();
355
+ }
319
356
  }
320
357
  allSlices() {
321
358
  return this.routes.map(({ name }) => name).sort();
@@ -10,11 +10,7 @@ async function runFleetSupervisorDaemon(startupPath) {
10
10
  return;
11
11
  handlingSignal = true;
12
12
  try {
13
- const selected = runtime.session.selectedSlices();
14
- if (selected.length > 0)
15
- await runtime.session.down(selected);
16
- await runtime.session.closeProxyWhenEmpty();
17
- await runtime.supervisor.stopGracefully();
13
+ await runtime.supervisor.stopForSignal();
18
14
  }
19
15
  catch (error) {
20
16
  handlingSignal = false;
@@ -57,5 +57,6 @@ export declare class FleetSupervisor {
57
57
  waitForIdle(): Promise<void>;
58
58
  waitUntilStopped(): Promise<void>;
59
59
  stopGracefully(): Promise<void>;
60
+ stopForSignal(): Promise<void>;
60
61
  }
61
62
  export {};
@@ -96,6 +96,7 @@ class FleetSupervisor {
96
96
  #shutdownGraceMs;
97
97
  #state;
98
98
  #stopping = false;
99
+ #stopOperation;
99
100
  constructor(options, effects = {}) {
100
101
  this.#adapters = options.adapters;
101
102
  this.#host = options.host;
@@ -422,22 +423,44 @@ class FleetSupervisor {
422
423
  return;
423
424
  await new Promise((resolvePromise) => this.#closedWaiters.push(resolvePromise));
424
425
  }
425
- async stopGracefully() {
426
+ stopGracefully() {
427
+ return this.#stop('empty');
428
+ }
429
+ stopForSignal() {
430
+ return this.#stop('down-selected');
431
+ }
432
+ #stop(mode) {
426
433
  if (this.#closed)
427
- return;
434
+ return Promise.resolve();
435
+ if (this.#stopOperation)
436
+ return this.#stopOperation;
428
437
  this.#stopping = true;
429
- await this.waitForIdle();
430
- try {
431
- this.#assertShutdownOwnership();
432
- await this.#host.closeProxyWhenEmpty();
433
- (0, fleet_runner_evidence_js_1.retireFleetRunnerEvidence)(this.#state, this.#paths, {
434
- security: this.#security,
435
- });
436
- }
437
- catch (error) {
438
+ this.#stopOperation = this.#finishStop(mode).catch((error) => {
438
439
  this.#stopping = false;
440
+ this.#stopOperation = undefined;
439
441
  throw error;
442
+ });
443
+ return this.#stopOperation;
444
+ }
445
+ async #finishStop(mode) {
446
+ await this.waitForIdle();
447
+ if (mode === 'down-selected') {
448
+ const selected = this.#host.selectedSlices();
449
+ if (selected.length > 0) {
450
+ try {
451
+ this.#syncSliceStates(await this.#host.down(selected));
452
+ }
453
+ finally {
454
+ this.#syncLiveSliceStates();
455
+ this.#persistState();
456
+ }
457
+ }
440
458
  }
459
+ this.#assertShutdownOwnership();
460
+ await this.#host.closeProxyWhenEmpty();
461
+ (0, fleet_runner_evidence_js_1.retireFleetRunnerEvidence)(this.#state, this.#paths, {
462
+ security: this.#security,
463
+ });
441
464
  if (this.#heartbeat)
442
465
  clearInterval(this.#heartbeat);
443
466
  this.#heartbeat = undefined;
@@ -439,6 +439,25 @@ function isBlueprintVerificationExecutionAuthorityPath(path) {
439
439
  /^(?:vite|vitest)\.config\.[a-z0-9]+$/u.test(name) ||
440
440
  /^packages\/[^/]+\/\.ebk\/ownership\.json$/u.test(normalized));
441
441
  }
442
+ function isNativeArtifactDependencyBoundary(workspaceRoot, path, candidate) {
443
+ const match = /^packages\/([a-z][a-z0-9]*(?:-[a-z0-9]+)*)\/\.sst\/artifacts\/[^/]+\/node_modules$/u.exec(path);
444
+ const slice = match?.[1];
445
+ if (!slice)
446
+ return false;
447
+ const dependencyRoot = (0, node_path_1.join)(workspaceRoot, 'packages', slice, 'node_modules');
448
+ try {
449
+ const metadata = (0, node_fs_1.lstatSync)(dependencyRoot);
450
+ return (metadata.isDirectory() &&
451
+ !metadata.isSymbolicLink() &&
452
+ (0, node_fs_1.realpathSync)(dependencyRoot) === dependencyRoot &&
453
+ (0, node_path_1.toNamespacedPath)((0, node_path_1.resolve)((0, node_path_1.dirname)(candidate), (0, node_fs_1.readlinkSync)(candidate))) ===
454
+ (0, node_path_1.toNamespacedPath)(dependencyRoot) &&
455
+ (0, node_fs_1.realpathSync)(candidate) === dependencyRoot);
456
+ }
457
+ catch {
458
+ return false;
459
+ }
460
+ }
442
461
  /**
443
462
  * Inventories every execution-loader configuration outside bounded generated,
444
463
  * dependency, cache, and evidence directories. Forbidden ambient config is
@@ -467,15 +486,17 @@ function assertNoMutableBlueprintVerificationConfiguration(workspace) {
467
486
  const path = directoryPath
468
487
  ? `${directoryPath}/${entry.name}`
469
488
  : entry.name;
470
- const skippedDirectory = entry.isDirectory() &&
471
- SKIPPED_CONFIGURATION_DIRECTORIES.has(entry.name.toLowerCase());
489
+ const candidate = (0, node_path_1.join)(directory, entry.name);
490
+ const skippedDirectory = (entry.isDirectory() &&
491
+ SKIPPED_CONFIGURATION_DIRECTORIES.has(entry.name.toLowerCase())) ||
492
+ (entry.isSymbolicLink() &&
493
+ isNativeArtifactDependencyBoundary(workspaceRoot, path, candidate));
472
494
  if (entry.isSymbolicLink() && !skippedDirectory) {
473
495
  throw new Error(`Blueprint verification configuration inventory refuses symbolic-link authority at ${path}.`);
474
496
  }
475
497
  if (forbiddenConfigurationName(entry.name)) {
476
498
  throw new Error(`Blueprint verification refuses mutable environment or package-manager configuration at ${path}.`);
477
499
  }
478
- const candidate = (0, node_path_1.join)(directory, entry.name);
479
500
  if (protectedConfigurationName(entry.name)) {
480
501
  const metadata = (0, node_fs_1.lstatSync)(candidate);
481
502
  const child = (0, node_path_1.relative)(workspaceRoot, candidate);
@@ -160,7 +160,8 @@ function createCycloneDxComponentsFromInventory(input) {
160
160
  }));
161
161
  }
162
162
  function canonicalTimestamp(value, path) {
163
- if (!Number.isFinite(Date.parse(value)) ||
163
+ if (typeof value !== 'string' ||
164
+ !Number.isFinite(Date.parse(value)) ||
164
165
  new Date(value).toISOString() !== value) {
165
166
  throw new errors_js_1.SupplyChainError('supply-chain-input-invalid', path, 'must be a canonical ISO-8601 timestamp');
166
167
  }
@@ -775,8 +776,11 @@ function bindingInput(value) {
775
776
  }
776
777
  function createSupplyChainReleaseBinding(input) {
777
778
  canonicalTimestamp(input.createdAt, 'createdAt');
778
- if (!common_js_1.SUPPLY_CHAIN_REFERENCE.test(input.releaseId) ||
779
- !common_js_1.SUPPLY_CHAIN_SHA256.test(input.releaseManifestDigest) ||
779
+ if (typeof input.releaseId !== 'string' ||
780
+ common_js_1.SUPPLY_CHAIN_REFERENCE.exec(input.releaseId)?.[0] !== input.releaseId ||
781
+ typeof input.releaseManifestDigest !== 'string' ||
782
+ common_js_1.SUPPLY_CHAIN_SHA256.exec(input.releaseManifestDigest)?.[0] !==
783
+ input.releaseManifestDigest ||
780
784
  input.artifacts.length === 0 ||
781
785
  (0, common_js_1.hasSensitiveSupplyChainShape)(input)) {
782
786
  throw new errors_js_1.SupplyChainError('supply-chain-input-invalid', 'releaseBinding', 'must bind at least one artifact to an exact release manifest');
@@ -785,11 +789,21 @@ function createSupplyChainReleaseBinding(input) {
785
789
  const artifacts = [...input.artifacts]
786
790
  .sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.bindingId, right.bindingId))
787
791
  .map((artifact, index) => {
788
- if (!common_js_1.SUPPLY_CHAIN_REFERENCE.test(artifact.bindingId) ||
789
- !common_js_1.SUPPLY_CHAIN_SHA256.test(artifact.candidateDigest) ||
790
- !common_js_1.SUPPLY_CHAIN_SHA256.test(artifact.contentDigest) ||
791
- !common_js_1.SUPPLY_CHAIN_SHA256.test(artifact.sbomDigest) ||
792
- !/^urn:uuid:[a-f0-9-]{36}$/.test(artifact.sbomSerialNumber) ||
792
+ if (typeof artifact.bindingId !== 'string' ||
793
+ common_js_1.SUPPLY_CHAIN_REFERENCE.exec(artifact.bindingId)?.[0] !==
794
+ artifact.bindingId ||
795
+ typeof artifact.candidateDigest !== 'string' ||
796
+ common_js_1.SUPPLY_CHAIN_SHA256.exec(artifact.candidateDigest)?.[0] !==
797
+ artifact.candidateDigest ||
798
+ typeof artifact.contentDigest !== 'string' ||
799
+ common_js_1.SUPPLY_CHAIN_SHA256.exec(artifact.contentDigest)?.[0] !==
800
+ artifact.contentDigest ||
801
+ typeof artifact.sbomDigest !== 'string' ||
802
+ common_js_1.SUPPLY_CHAIN_SHA256.exec(artifact.sbomDigest)?.[0] !==
803
+ artifact.sbomDigest ||
804
+ typeof artifact.sbomSerialNumber !== 'string' ||
805
+ /^urn:uuid:[a-f0-9-]{36}$/.exec(artifact.sbomSerialNumber)?.[0] !==
806
+ artifact.sbomSerialNumber ||
793
807
  seen.has(artifact.bindingId)) {
794
808
  throw new errors_js_1.SupplyChainError('supply-chain-input-invalid', `releaseBinding.artifacts[${index}]`, 'must uniquely bind artifact and SBOM digests');
795
809
  }
@@ -810,14 +824,62 @@ function createSupplyChainReleaseBinding(input) {
810
824
  };
811
825
  }
812
826
  function validateSupplyChainReleaseBinding(value, expected) {
827
+ const exactKeys = (record, keys) => [Object.prototype, null].includes(Object.getPrototypeOf(record)) &&
828
+ Object.keys(record).sort(deterministic_order_js_1.compareUtf16).join('\0') ===
829
+ [...keys].sort(deterministic_order_js_1.compareUtf16).join('\0');
813
830
  if (!(0, validation_js_1.isRecord)(value) ||
831
+ !exactKeys(value, [
832
+ 'artifacts',
833
+ 'createdAt',
834
+ 'kind',
835
+ 'materialDigest',
836
+ 'releaseId',
837
+ 'releaseManifestDigest',
838
+ 'schemaVersion',
839
+ ]) ||
814
840
  value.kind !== exports.SUPPLY_CHAIN_RELEASE_BINDING_KIND ||
815
841
  value.schemaVersion !== exports.SUPPLY_CHAIN_SBOM_SCHEMA_VERSION ||
816
- !common_js_1.SUPPLY_CHAIN_SHA256.test(String(value.materialDigest)) ||
817
- !Array.isArray(value.artifacts)) {
818
- throw new errors_js_1.SupplyChainError('supply-chain-input-invalid', 'releaseBinding', 'must be an EBK supply-chain release binding v1');
842
+ typeof value.materialDigest !== 'string' ||
843
+ common_js_1.SUPPLY_CHAIN_SHA256.exec(value.materialDigest)?.[0] !==
844
+ value.materialDigest ||
845
+ typeof value.createdAt !== 'string' ||
846
+ typeof value.releaseId !== 'string' ||
847
+ typeof value.releaseManifestDigest !== 'string' ||
848
+ !Array.isArray(value.artifacts) ||
849
+ !value.artifacts.length) {
850
+ throw new errors_js_1.SupplyChainError('supply-chain-input-invalid', 'releaseBinding', 'must be an exact EBK supply-chain release binding v1');
851
+ }
852
+ const artifacts = [];
853
+ for (const artifact of value.artifacts) {
854
+ if (!(0, validation_js_1.isRecord)(artifact) ||
855
+ !exactKeys(artifact, [
856
+ 'bindingId',
857
+ 'candidateDigest',
858
+ 'contentDigest',
859
+ 'sbomDigest',
860
+ 'sbomSerialNumber',
861
+ ]) ||
862
+ typeof artifact.bindingId !== 'string' ||
863
+ typeof artifact.candidateDigest !== 'string' ||
864
+ typeof artifact.contentDigest !== 'string' ||
865
+ typeof artifact.sbomDigest !== 'string' ||
866
+ typeof artifact.sbomSerialNumber !== 'string') {
867
+ throw new errors_js_1.SupplyChainError('supply-chain-input-invalid', 'releaseBinding.artifacts', 'must contain exact string artifact fields');
868
+ }
869
+ artifacts.push({
870
+ bindingId: artifact.bindingId,
871
+ candidateDigest: artifact.candidateDigest,
872
+ contentDigest: artifact.contentDigest,
873
+ sbomDigest: artifact.sbomDigest,
874
+ sbomSerialNumber: artifact.sbomSerialNumber,
875
+ });
819
876
  }
820
- const recreated = createSupplyChainReleaseBinding(value);
877
+ const recreated = createSupplyChainReleaseBinding({
878
+ artifacts,
879
+ createdAt: value.createdAt,
880
+ releaseId: value.releaseId,
881
+ releaseManifestDigest: value.releaseManifestDigest,
882
+ });
821
883
  if (recreated.materialDigest !== value.materialDigest) {
822
884
  throw new errors_js_1.SupplyChainError('supply-chain-artifact-mismatch', 'releaseBinding.materialDigest', 'does not match the canonical binding bytes');
823
885
  }