@enterpriseai/cli 3.20.3 → 3.20.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enterpriseai/cli",
3
- "version": "3.20.3",
3
+ "version": "3.20.4",
4
4
  "description": "EnterpriseAI CLI for app setup, validation, deployment readiness, and platform operations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,6 +44,12 @@ on:
44
44
  local_e2e_expires_at:
45
45
  description: Exact server-issued local dispatch expiry.
46
46
  required: false
47
+ local_e2e_template_commit_sha:
48
+ description: Exact server-pinned template SHA for a local CLI customer successor.
49
+ required: false
50
+ local_e2e_predecessor_operation_id:
51
+ description: Completed source operation recorded in the reviewed customer commit.
52
+ required: false
47
53
  publicapi_base_url:
48
54
  description: Deprecated alias for public_api_url; retained for compatibility.
49
55
  required: false
@@ -223,6 +229,8 @@ jobs:
223
229
  SOURCE_MODE: ${{ inputs.source_mode || 'source-unknown' }}
224
230
  LOCAL_E2E_TUNNEL: ${{ inputs.local_e2e_tunnel }}
225
231
  LOCAL_E2E_EXPIRES_AT: ${{ inputs.local_e2e_expires_at }}
232
+ LOCAL_E2E_TEMPLATE_COMMIT_SHA: ${{ inputs.local_e2e_template_commit_sha }}
233
+ LOCAL_E2E_PREDECESSOR_OPERATION_ID: ${{ inputs.local_e2e_predecessor_operation_id }}
226
234
  GITHUB_EVENT_NAME: ${{ github.event_name }}
227
235
  REUSABLE_CALL: ${{ inputs.eai_reusable_call }}
228
236
  REPOSITORY_ID: ${{ github.repository_id }}
@@ -244,6 +252,8 @@ jobs:
244
252
  --legacy-public-api-url "$LEGACY_PUBLIC_API_URL" \
245
253
  --local-e2e-tunnel "$LOCAL_E2E_TUNNEL" \
246
254
  --local-e2e-expires-at "$LOCAL_E2E_EXPIRES_AT" \
255
+ --local-e2e-template-commit-sha "$LOCAL_E2E_TEMPLATE_COMMIT_SHA" \
256
+ --local-e2e-predecessor-operation-id "$LOCAL_E2E_PREDECESSOR_OPERATION_ID" \
247
257
  --github-event-name "$GITHUB_EVENT_NAME" \
248
258
  --reusable-call "$REUSABLE_CALL" \
249
259
  --repository-id "$REPOSITORY_ID" \
@@ -351,6 +361,8 @@ jobs:
351
361
  EAI_BOUND_PUBLIC_API_URL: ${{ inputs.public_api_url || inputs.publicapi_base_url }}
352
362
  LOCAL_E2E_TUNNEL: ${{ inputs.local_e2e_tunnel }}
353
363
  LOCAL_E2E_EXPIRES_AT: ${{ inputs.local_e2e_expires_at }}
364
+ LOCAL_E2E_TEMPLATE_COMMIT_SHA: ${{ inputs.local_e2e_template_commit_sha }}
365
+ LOCAL_E2E_PREDECESSOR_OPERATION_ID: ${{ inputs.local_e2e_predecessor_operation_id }}
354
366
  GITHUB_EVENT_NAME: ${{ github.event_name }}
355
367
  REUSABLE_CALL: ${{ inputs.eai_reusable_call }}
356
368
  REPOSITORY_ID: ${{ github.repository_id }}
@@ -364,6 +376,8 @@ jobs:
364
376
  --public-api-url "$EAI_BOUND_PUBLIC_API_URL" \
365
377
  --local-e2e-tunnel "$LOCAL_E2E_TUNNEL" \
366
378
  --local-e2e-expires-at "$LOCAL_E2E_EXPIRES_AT" \
379
+ --local-e2e-template-commit-sha "$LOCAL_E2E_TEMPLATE_COMMIT_SHA" \
380
+ --local-e2e-predecessor-operation-id "$LOCAL_E2E_PREDECESSOR_OPERATION_ID" \
367
381
  --github-event-name "$GITHUB_EVENT_NAME" \
368
382
  --reusable-call "$REUSABLE_CALL" \
369
383
  --repository-id "$REPOSITORY_ID" \
@@ -437,6 +451,8 @@ jobs:
437
451
  SOURCE_MODE: ${{ inputs.source_mode || 'source-unknown' }}
438
452
  LOCAL_E2E_TUNNEL: ${{ inputs.local_e2e_tunnel }}
439
453
  LOCAL_E2E_EXPIRES_AT: ${{ inputs.local_e2e_expires_at }}
454
+ LOCAL_E2E_TEMPLATE_COMMIT_SHA: ${{ inputs.local_e2e_template_commit_sha }}
455
+ LOCAL_E2E_PREDECESSOR_OPERATION_ID: ${{ inputs.local_e2e_predecessor_operation_id }}
440
456
  GITHUB_EVENT_NAME: ${{ github.event_name }}
441
457
  REUSABLE_CALL: ${{ inputs.eai_reusable_call }}
442
458
  GITHUB_TOKEN: ${{ github.token }}
@@ -454,9 +470,13 @@ jobs:
454
470
  [[ ( "$SOURCE_MODE" == eai-cli-generated || "$SOURCE_MODE" == source-unknown ) && "$GITHUB_EVENT_NAME" == workflow_dispatch && "$REUSABLE_CALL" != true ]]
455
471
  [[ "$DEPLOY_ENVIRONMENT" == dev || "$DEPLOY_ENVIRONMENT" == preview ]]
456
472
  [[ "$EAI_BOUND_PUBLIC_API_URL" =~ ^https://[a-z0-9-]+-8000\.[a-z0-9-]+\.devtunnels\.ms$ ]]
473
+ if [[ -n "$LOCAL_E2E_PREDECESSOR_OPERATION_ID" ]]; then
474
+ [[ "$SOURCE_MODE" == eai-cli-generated && -n "$LOCAL_E2E_TEMPLATE_COMMIT_SHA" ]]
475
+ fi
457
476
  else
458
477
  [[ "$LOCAL_E2E_TUNNEL" == false || -z "$LOCAL_E2E_TUNNEL" ]]
459
478
  [[ -z "$LOCAL_E2E_EXPIRES_AT" ]]
479
+ [[ -z "$LOCAL_E2E_TEMPLATE_COMMIT_SHA" && -z "$LOCAL_E2E_PREDECESSOR_OPERATION_ID" ]]
460
480
  [[ "$EAI_BOUND_PUBLIC_API_URL" =~ ^https://(dev-api\.au|(test-api|api)\.(au|ca|eu))\.myenterprise\.ai/public/?$ ]]
461
481
  fi
462
482
  [[ "$APP_KEY" =~ ^[a-z][a-z0-9-]{1,62}$ ]]
@@ -813,6 +833,12 @@ jobs:
813
833
  if (!sourceUnknownLocal && !/^[a-f0-9]{40}$/.test(local.templateCommitSha)) {
814
834
  throw new Error('Build evidence local template binding changed.');
815
835
  }
836
+ if (sourceUnknownLocal
837
+ ? (process.env.LOCAL_E2E_TEMPLATE_COMMIT_SHA || process.env.LOCAL_E2E_PREDECESSOR_OPERATION_ID)
838
+ : (process.env.LOCAL_E2E_TEMPLATE_COMMIT_SHA &&
839
+ local.templateCommitSha !== process.env.LOCAL_E2E_TEMPLATE_COMMIT_SHA)) {
840
+ throw new Error('Build evidence local successor binding changed.');
841
+ }
816
842
  const tuple = sourceUnknownLocal
817
843
  ? ['eai.source-unknown-local-e2e-aud.v1', local.origin, process.env.TENANT_ID,
818
844
  process.env.TARGET_TENANT_ID || process.env.TENANT_ID, process.env.APP_KEY, evidence.operationId,
@@ -1014,7 +1040,37 @@ jobs:
1014
1040
  });
1015
1041
  NODE
1016
1042
 
1043
+ - name: Check GitHub attestation availability
1044
+ id: attestation-availability
1045
+ env:
1046
+ EXPECTED_REPOSITORY: ${{ github.repository }}
1047
+ EXPECTED_REPOSITORY_ID: ${{ github.repository_id }}
1048
+ EXPECTED_OWNER_ID: ${{ github.repository_owner_id }}
1049
+ shell: bash
1050
+ run: |
1051
+ node - <<'NODE'
1052
+ const { appendFileSync, readFileSync } = require('node:fs');
1053
+ const event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'));
1054
+ const repository = event.repository;
1055
+ const owner = repository?.owner;
1056
+ if (!repository || !owner ||
1057
+ !Number.isSafeInteger(repository.id) || repository.id < 1 ||
1058
+ String(repository.id) !== process.env.EXPECTED_REPOSITORY_ID ||
1059
+ repository.full_name !== process.env.EXPECTED_REPOSITORY ||
1060
+ typeof repository.private !== 'boolean' ||
1061
+ !Number.isSafeInteger(owner.id) || owner.id < 1 ||
1062
+ String(owner.id) !== process.env.EXPECTED_OWNER_ID ||
1063
+ !['User', 'Organization'].includes(owner.type) ||
1064
+ owner.login !== process.env.EXPECTED_REPOSITORY.split('/')[0]) {
1065
+ throw new Error('GitHub attestation availability requires exact provider repository metadata.');
1066
+ }
1067
+ // SECURITY: GitHub rejects attestations for personal-account private repositories; OIDC-bound artifact verification remains mandatory.
1068
+ const available = !(repository.private && owner.type === 'User');
1069
+ appendFileSync(process.env.GITHUB_OUTPUT, `available=${available}\n`);
1070
+ NODE
1071
+
1017
1072
  - name: Attest immutable image archive provenance
1073
+ if: ${{ steps.attestation-availability.outputs.available == 'true' }}
1018
1074
  uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
1019
1075
  with:
1020
1076
  subject-path: .eai-build/eai-generated-app-image.tar
@@ -1028,6 +1084,8 @@ jobs:
1028
1084
  SOURCE_MODE: ${{ inputs.source_mode || 'source-unknown' }}
1029
1085
  LOCAL_E2E_TUNNEL: ${{ inputs.local_e2e_tunnel }}
1030
1086
  LOCAL_E2E_EXPIRES_AT: ${{ inputs.local_e2e_expires_at }}
1087
+ LOCAL_E2E_TEMPLATE_COMMIT_SHA: ${{ inputs.local_e2e_template_commit_sha }}
1088
+ LOCAL_E2E_PREDECESSOR_OPERATION_ID: ${{ inputs.local_e2e_predecessor_operation_id }}
1031
1089
  DEPLOY_ENVIRONMENT: ${{ needs.build.outputs.deployment_environment }}
1032
1090
  GITHUB_EVENT_NAME: ${{ github.event_name }}
1033
1091
  REUSABLE_CALL: ${{ inputs.eai_reusable_call }}
@@ -1053,6 +1111,10 @@ jobs:
1053
1111
  const repoId = process.env.GITHUB_REPOSITORY_ID;
1054
1112
  const expiry = process.env.LOCAL_E2E_EXPIRES_AT;
1055
1113
  const sourceUnknownLocal = process.env.SOURCE_MODE === 'source-unknown';
1114
+ if (sourceUnknownLocal
1115
+ ? (process.env.LOCAL_E2E_TEMPLATE_COMMIT_SHA || process.env.LOCAL_E2E_PREDECESSOR_OPERATION_ID)
1116
+ : (process.env.LOCAL_E2E_TEMPLATE_COMMIT_SHA &&
1117
+ local?.templateCommitSha !== process.env.LOCAL_E2E_TEMPLATE_COMMIT_SHA)) process.exit(1);
1056
1118
  const expectedKeys = sourceUnknownLocal
1057
1119
  ? 'audience,expiresAt,mode,nonceDigest,origin'
1058
1120
  : 'audience,expiresAt,mode,origin,templateCommitSha';
@@ -2,28 +2,28 @@
2
2
  "schemaVersion": "eai.managed-deploy-producer-pin.v1",
3
3
  "repository": "eai-support/eai-app-template",
4
4
  "candidate": {
5
- "commit": "c8a8aed26c4d159a4fa2598d9b5e8a495203dd8b",
5
+ "commit": "3162df8dffd36465ef2cec474eee4f8e6285ec3c",
6
6
  "workflow": {
7
7
  "path": ".github/workflows/eai-app.yml",
8
- "sha256": "sha256:bce4e34fe5c47377e6a679966a37257d944cd00ccf3c15b4ba437f144ed559ed"
8
+ "sha256": "sha256:74ede3c4e1de12fa5f63135349e61365bd845443dbb4dbe51573d8c490baa1d5"
9
9
  },
10
10
  "collector": {
11
11
  "path": "scripts/source-unknown-deployment-evidence.mjs",
12
- "sha256": "sha256:06e7f33bf1db1d7adbebb75a76d2756e46371318f36d2518c5cfbb58ad4f5e1c"
12
+ "sha256": "sha256:f20a4291b03b6f877ebaa9bdba9c2c2a839225d181c23b8be19a617a25536dc6"
13
13
  },
14
14
  "evidenceTest": {
15
15
  "path": "tests/source-unknown-deployment-evidence.test.mjs",
16
- "sha256": "sha256:fe5f8381d882ac39656b67b30e1e18193cb676a8aba26a6776a88880a3bb5827"
16
+ "sha256": "sha256:75140ba332d49d364f1f2792267aabcc9c50ed7a38a76e6e1702955ebd4e99f7"
17
17
  }
18
18
  },
19
19
  "releaseGate": {
20
20
  "status": "released",
21
- "tag": "v1.0.14",
22
- "commit": "c8a8aed26c4d159a4fa2598d9b5e8a495203dd8b"
21
+ "tag": "v1.0.17",
22
+ "commit": "3162df8dffd36465ef2cec474eee4f8e6285ec3c"
23
23
  },
24
24
  "githubProducerIdentity": {
25
25
  "profile": "github-environment-v1",
26
- "templateCommitSha": "c8a8aed26c4d159a4fa2598d9b5e8a495203dd8b",
27
- "workflowSha256": "sha256:bce4e34fe5c47377e6a679966a37257d944cd00ccf3c15b4ba437f144ed559ed"
26
+ "templateCommitSha": "3162df8dffd36465ef2cec474eee4f8e6285ec3c",
27
+ "workflowSha256": "sha256:74ede3c4e1de12fa5f63135349e61365bd845443dbb4dbe51573d8c490baa1d5"
28
28
  }
29
29
  }
@@ -173,7 +173,10 @@ function validateDeploymentBinding(options, mode) {
173
173
  function localE2eBinding(options, mode, environment, root) {
174
174
  const flag = option(options, 'localE2eTunnel');
175
175
  const expiresAt = option(options, 'localE2eExpiresAt');
176
- if (!['', 'false', 'true'].includes(flag) || (flag !== 'true' && expiresAt)) {
176
+ const serverTemplateCommitSha = option(options, 'localE2eTemplateCommitSha');
177
+ const predecessorOperationId = option(options, 'localE2ePredecessorOperationId');
178
+ if (!['', 'false', 'true'].includes(flag) ||
179
+ (flag !== 'true' && (expiresAt || serverTemplateCommitSha || predecessorOperationId))) {
177
180
  throw new Error('Local deployment inputs conflict.');
178
181
  }
179
182
  if (flag !== 'true') return null;
@@ -181,6 +184,7 @@ function localE2eBinding(options, mode, environment, root) {
181
184
  const operationId = option(options, 'operationId');
182
185
  const repositoryId = option(options, 'repositoryId');
183
186
  if (
187
+ (mode !== 'eai-cli-generated' && (serverTemplateCommitSha || predecessorOperationId)) ||
184
188
  !['eai-cli-generated', 'source-unknown'].includes(mode) ||
185
189
  !['preview', 'dev'].includes(environment) ||
186
190
  option(options, 'githubEventName') !== 'workflow_dispatch' ||
@@ -224,21 +228,39 @@ function localE2eBinding(options, mode, environment, root) {
224
228
  if (!/^cli-managed-[a-f0-9]{32}$/.test(operationId)) {
225
229
  throw new Error('Local CLI operation binding is invalid.');
226
230
  }
231
+ if ((serverTemplateCommitSha && !/^[a-f0-9]{40}$/.test(serverTemplateCommitSha)) ||
232
+ (predecessorOperationId && (!/^cli-managed-[a-f0-9]{32}$/.test(predecessorOperationId) ||
233
+ predecessorOperationId === operationId || !serverTemplateCommitSha))) {
234
+ throw new Error('Local CLI successor binding is invalid.');
235
+ }
227
236
  const binding = JSON.parse(readRegularFileNoFollow(root, '.eai/cli-managed-source-operation.json').toString('utf8'));
228
237
  if (
229
238
  binding.schemaVersion !== 'eai.cli_managed_source_operation.v1' ||
230
239
  binding.sourceMode !== mode ||
231
- binding.operationId !== operationId ||
240
+ (predecessorOperationId
241
+ ? (binding.predecessor?.operationId !== predecessorOperationId ||
242
+ !/^cli-managed-[a-f0-9]{32}$/.test(binding.operationId || '') ||
243
+ binding.operationId === operationId)
244
+ : binding.operationId !== operationId) ||
245
+ binding.tenantId !== option(options, 'tenantId') ||
246
+ binding.appKey !== option(options, 'appKey') ||
247
+ binding.targetTenantId !== option(options, 'targetTenantId') ||
232
248
  binding.environment !== environment ||
233
249
  binding.templateCommitSha?.match(/^[a-f0-9]{40}$/) === null ||
234
- !/^[a-f0-9]{40}$/.test(binding.templateCommitSha || '')
250
+ !/^[a-f0-9]{40}$/.test(binding.templateCommitSha || '') ||
251
+ (predecessorOperationId && binding.repositoryAuthority !== 'customer-owned') ||
252
+ (!predecessorOperationId && binding.repositoryAuthority === 'customer-owned') ||
253
+ (!predecessorOperationId && serverTemplateCommitSha &&
254
+ binding.templateCommitSha !== serverTemplateCommitSha)
235
255
  ) {
236
256
  throw new Error('Local deployment original source binding is invalid.');
237
257
  }
238
- const tuple = ['eai.cli-managed-local-e2e-aud.v1', origin, operationId, repositoryId, binding.templateCommitSha, expiresAt];
258
+ // SECURITY: the committed marker is historical for customer successors; the server pins the current template in its OIDC audience.
259
+ const templateCommitSha = serverTemplateCommitSha || binding.templateCommitSha;
260
+ const tuple = ['eai.cli-managed-local-e2e-aud.v1', origin, operationId, repositoryId, templateCommitSha, expiresAt];
239
261
  return {
240
262
  mode: 'local-tunnel-v1', origin, expiresAt,
241
- templateCommitSha: binding.templateCommitSha,
263
+ templateCommitSha,
242
264
  audience: LOCAL_E2E_AUDIENCE_PREFIX + createHash('sha256').update(JSON.stringify(tuple)).digest('hex'),
243
265
  };
244
266
  }
@@ -32,6 +32,8 @@ const canonicalWorkflowRelativePath = '.github/workflows/eai-app.yml';
32
32
  const fixtureDirectory = 'tests/fixtures/source-unknown';
33
33
  const canonicalDigest =
34
34
  'bce4e34fe5c47377e6a679966a37257d944cd00ccf3c15b4ba437f144ed559ed';
35
+ const currentWorkflowDigest =
36
+ '74ede3c4e1de12fa5f63135349e61365bd845443dbb4dbe51573d8c490baa1d5';
35
37
  const generatedDigest =
36
38
  '77d4951b3e6852ead73cef8b9e8901e3de774f9aca70f71cc0c932c13903ff32';
37
39
  const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
@@ -749,7 +751,7 @@ function resolveDeploymentWorkflowContext(
749
751
  if (!existsSync(manifestPath)) {
750
752
  assert.equal(
751
753
  digest(actual),
752
- canonicalDigest,
754
+ currentWorkflowDigest,
753
755
  'Template workflow must retain the pinned canonical digest.',
754
756
  );
755
757
  return {
@@ -762,7 +764,7 @@ function resolveDeploymentWorkflowContext(
762
764
  validateCliManifest(manifest);
763
765
  assert.equal(
764
766
  digest(actual),
765
- canonicalDigest,
767
+ currentWorkflowDigest,
766
768
  'CLI workflow must retain the pinned canonical digest.',
767
769
  );
768
770
  return {
@@ -1616,6 +1618,77 @@ test('workflow sends OIDC evidence directly to the canonical PublicAPI route', (
1616
1618
  }
1617
1619
  });
1618
1620
 
1621
+ test('GitHub attestation availability is exact and leaves signed handoff required', () => {
1622
+ const workflow = readFileSync(workflowPath, 'utf8');
1623
+ const decision = workflow.match(
1624
+ /- name: Check GitHub attestation availability[\s\S]*?run: \|\n node - <<'NODE'\n([\s\S]*?)\n NODE/,
1625
+ );
1626
+ assert.ok(decision);
1627
+ const script = decision[1].replace(/^ {10}/gm, '');
1628
+ assert.match(
1629
+ workflow,
1630
+ /- name: Attest immutable image archive provenance\n if: \$\{\{ steps\.attestation-availability\.outputs\.available == 'true' \}\}/,
1631
+ );
1632
+ assert.match(
1633
+ workflow,
1634
+ /- name: Request GitHub OIDC token and submit workflow evidence\n env:/,
1635
+ );
1636
+ const directory = mkdtempSync(join(tmpdir(), 'eai-attestation-availability-'));
1637
+ const eventPath = join(directory, 'event.json');
1638
+ const outputPath = join(directory, 'output');
1639
+ const repository = {
1640
+ id: 1414837259,
1641
+ full_name: 'customer/app',
1642
+ private: true,
1643
+ owner: { id: 332652381, login: 'customer', type: 'User' },
1644
+ };
1645
+ const check = (candidate) => {
1646
+ writeFileSync(eventPath, JSON.stringify({ repository: candidate }));
1647
+ writeFileSync(outputPath, '');
1648
+ const result = spawnSync(process.execPath, ['-'], {
1649
+ input: script,
1650
+ encoding: 'utf8',
1651
+ env: {
1652
+ ...process.env,
1653
+ GITHUB_EVENT_PATH: eventPath,
1654
+ GITHUB_OUTPUT: outputPath,
1655
+ EXPECTED_REPOSITORY: 'customer/app',
1656
+ EXPECTED_REPOSITORY_ID: '1414837259',
1657
+ EXPECTED_OWNER_ID: '332652381',
1658
+ },
1659
+ });
1660
+ return { result, output: readFileSync(outputPath, 'utf8') };
1661
+ };
1662
+ try {
1663
+ const personalPrivate = check(repository);
1664
+ assert.equal(personalPrivate.result.status, 0, personalPrivate.result.stderr);
1665
+ assert.equal(personalPrivate.output, 'available=false\n');
1666
+ for (const supported of [
1667
+ { ...repository, private: false },
1668
+ { ...repository, owner: { ...repository.owner, type: 'Organization' } },
1669
+ ]) {
1670
+ const outcome = check(supported);
1671
+ assert.equal(outcome.result.status, 0, outcome.result.stderr);
1672
+ assert.equal(outcome.output, 'available=true\n');
1673
+ }
1674
+ for (const invalid of [
1675
+ { ...repository, private: undefined },
1676
+ { ...repository, private: 'true' },
1677
+ { ...repository, id: 3 },
1678
+ { ...repository, full_name: 'other/app' },
1679
+ { ...repository, owner: { ...repository.owner, type: undefined } },
1680
+ { ...repository, owner: { ...repository.owner, id: 3 } },
1681
+ { ...repository, owner: { ...repository.owner, login: 'other' } },
1682
+ ]) {
1683
+ const outcome = check(invalid);
1684
+ assert.notEqual(outcome.result.status, 0);
1685
+ assert.equal(outcome.output, '');
1686
+ }
1687
+ } finally {
1688
+ rmSync(directory, { recursive: true, force: true });
1689
+ }
1690
+ });
1691
+
1619
1692
  test('reusable workflow compatibility keeps manual same-repository OIDC authority', () => {
1620
1693
  const workflow = readFileSync(workflowPath, 'utf8');
1621
1694
  const readme = readFileSync(readmePath, 'utf8');
@@ -1703,6 +1776,8 @@ test('local CLI dispatch binds direct DEV tunnel, original template and numeric
1703
1776
  writeFileSync(join(root, '.eai/cli-managed-source-operation.json'), JSON.stringify({
1704
1777
  schemaVersion: 'eai.cli_managed_source_operation.v1',
1705
1778
  sourceMode: 'eai-cli-generated', operationId, environment: 'dev',
1779
+ tenantId: 'tenant-parent', targetTenantId: 'hosting-tenant', appKey: 'rates-review',
1780
+ repositoryAuthority: 'eai-managed',
1706
1781
  templateCommitSha: 'b'.repeat(40),
1707
1782
  }));
1708
1783
  execFileSync('git', ['init', '-q'], { cwd: root });
@@ -1720,6 +1795,68 @@ test('local CLI dispatch binds direct DEV tunnel, original template and numeric
1720
1795
  '--local-e2e-expires-at', expiry, '--github-event-name', 'workflow_dispatch',
1721
1796
  '--reusable-call', 'false', '--repository-id', '12345'];
1722
1797
  runEvidenceScript(args);
1798
+ writeFileSync(join(root, '.eai/cli-managed-source-operation.json'), JSON.stringify({
1799
+ schemaVersion: 'eai.cli_managed_source_operation.v1', sourceMode: 'eai-cli-generated',
1800
+ operationId, environment: 'dev', tenantId: 'tenant-parent', targetTenantId: 'hosting-tenant',
1801
+ appKey: 'rates-review', repositoryAuthority: 'customer-owned', templateCommitSha: 'b'.repeat(40),
1802
+ }));
1803
+ assert.equal(spawnSync(process.execPath, [evidenceScript, ...args], { encoding: 'utf8' }).status, 1);
1804
+ writeFileSync(join(root, '.eai/cli-managed-source-operation.json'), JSON.stringify({
1805
+ schemaVersion: 'eai.cli_managed_source_operation.v1', sourceMode: 'eai-cli-generated',
1806
+ operationId, environment: 'dev', tenantId: 'tenant-parent', targetTenantId: 'hosting-tenant',
1807
+ appKey: 'rates-review', repositoryAuthority: 'eai-managed', templateCommitSha: 'b'.repeat(40),
1808
+ }));
1809
+ const sameTemplate = [...args, '--local-e2e-template-commit-sha', 'b'.repeat(40)];
1810
+ runEvidenceScript(sameTemplate);
1811
+ const wrongInitialTemplate = [...args, '--local-e2e-template-commit-sha', 'd'.repeat(40)];
1812
+ assert.equal(spawnSync(process.execPath, [evidenceScript, ...wrongInitialTemplate], { encoding: 'utf8' }).status, 1);
1813
+ const predecessorOperationId = `cli-managed-${'e'.repeat(32)}`;
1814
+ const historicalOperationId = `cli-managed-${'9'.repeat(32)}`;
1815
+ const successorMarker = {
1816
+ schemaVersion: 'eai.cli_managed_source_operation.v1',
1817
+ sourceMode: 'eai-cli-generated', operationId: historicalOperationId, environment: 'dev',
1818
+ tenantId: 'tenant-parent', targetTenantId: 'hosting-tenant', appKey: 'rates-review',
1819
+ repositoryAuthority: 'customer-owned', templateCommitSha: 'b'.repeat(40),
1820
+ predecessor: { operationId: predecessorOperationId },
1821
+ };
1822
+ const markerPath = join(root, '.eai/cli-managed-source-operation.json');
1823
+ writeFileSync(markerPath, JSON.stringify(successorMarker));
1824
+ const successor = [...args,
1825
+ '--local-e2e-template-commit-sha', 'd'.repeat(40),
1826
+ '--local-e2e-predecessor-operation-id', predecessorOperationId];
1827
+ runEvidenceScript(successor);
1828
+ runEvidenceScript(['collect', ...successor.slice(1),
1829
+ '--repo', 'customer/rates-review', '--workflow', '.github/workflows/eai-app.yml',
1830
+ '--ref', 'refs/heads/main', '--branch', 'main', '--workflow-run-id', '123456789',
1831
+ '--workflow-run-attempt', '1', '--artifact-id', '987654321',
1832
+ '--artifact-digest', `sha256:${'d'.repeat(64)}`, '--image-digest', `sha256:${'c'.repeat(64)}`]);
1833
+ const collected = JSON.parse(readFileSync(join(root,
1834
+ '.eai-build/evidence/source-unknown-deployment-evidence.json'), 'utf8'));
1835
+ const successorTuple = ['eai.cli-managed-local-e2e-aud.v1',
1836
+ 'https://careful-8000.eai.devtunnels.ms', operationId, '12345', 'd'.repeat(40), expiry];
1837
+ assert.equal(collected.localE2e.templateCommitSha, 'd'.repeat(40));
1838
+ assert.equal(collected.localE2e.audience,
1839
+ 'api://enterprise-ai-publicapi/eai-cli-generated/local-v1/' + digest(JSON.stringify(successorTuple)));
1840
+ writeFileSync(markerPath, JSON.stringify({ ...successorMarker,
1841
+ predecessor: { operationId: `cli-managed-${'f'.repeat(32)}` } }));
1842
+ assert.equal(spawnSync(process.execPath, [evidenceScript, ...successor], { encoding: 'utf8' }).status, 1);
1843
+ writeFileSync(markerPath, JSON.stringify({ ...successorMarker, repositoryAuthority: 'eai-managed' }));
1844
+ assert.equal(spawnSync(process.execPath, [evidenceScript, ...successor], { encoding: 'utf8' }).status, 1);
1845
+ writeFileSync(markerPath, JSON.stringify(successorMarker));
1846
+ for (const mutated of [
1847
+ [...args],
1848
+ [...successor.slice(0, -1), `cli-managed-${'f'.repeat(32)}`],
1849
+ [...args, '--local-e2e-predecessor-operation-id', predecessorOperationId],
1850
+ [...successor.slice(0, -3), 'not-a-sha', ...successor.slice(-2)],
1851
+ ]) {
1852
+ assert.equal(spawnSync(process.execPath, [evidenceScript, ...mutated], { encoding: 'utf8' }).status, 1);
1853
+ }
1854
+ const wrongScope = [...successor];
1855
+ wrongScope[wrongScope.indexOf('--tenant-id') + 1] = 'another-tenant';
1856
+ assert.equal(spawnSync(process.execPath, [evidenceScript, ...wrongScope], { encoding: 'utf8' }).status, 1);
1857
+ const wrongRepository = [...successor];
1858
+ wrongRepository[wrongRepository.indexOf('--repository-id') + 1] = '0';
1859
+ assert.equal(spawnSync(process.execPath, [evidenceScript, ...wrongRepository], { encoding: 'utf8' }).status, 1);
1723
1860
  for (const [key, value] of [
1724
1861
  ['--public-api-url', 'https://attacker.example'],
1725
1862
  ['--environment', 'test'],
@@ -1742,12 +1879,44 @@ test('local CLI dispatch binds direct DEV tunnel, original template and numeric
1742
1879
  const reusableInputs = workflow.slice(workflow.indexOf(' workflow_call:'), workflow.indexOf('\npermissions:'));
1743
1880
  assert.match(directInputs, /local_e2e_tunnel:/);
1744
1881
  assert.match(directInputs, /local_e2e_expires_at:/);
1745
- assert.doesNotMatch(reusableInputs, /local_e2e_tunnel:|local_e2e_expires_at:/);
1882
+ assert.match(directInputs, /local_e2e_template_commit_sha:/);
1883
+ assert.match(directInputs, /local_e2e_predecessor_operation_id:/);
1884
+ assert.doesNotMatch(reusableInputs, /local_e2e_tunnel:|local_e2e_expires_at:|local_e2e_template_commit_sha:|local_e2e_predecessor_operation_id:/);
1746
1885
  } finally {
1747
1886
  rmSync(root, { recursive: true, force: true });
1748
1887
  }
1749
1888
  });
1750
1889
 
1890
+ test('build and OIDC workflow stages enforce the same server-pinned successor template', () => {
1891
+ const workflow = readFileSync(workflowPath, 'utf8');
1892
+ const buildError = workflow.indexOf("throw new Error('Build evidence local successor binding changed.');");
1893
+ const buildStart = workflow.lastIndexOf('if (sourceUnknownLocal', buildError);
1894
+ const buildEnd = workflow.indexOf('\n const tuple =', buildError);
1895
+ const oidcStep = workflow.indexOf('name: Request GitHub OIDC token and submit workflow evidence');
1896
+ const oidcStart = workflow.indexOf('if (sourceUnknownLocal\n', oidcStep);
1897
+ const oidcEnd = workflow.indexOf('\n const expectedKeys', oidcStart);
1898
+ assert.ok(buildError > 0 && buildStart > 0 && buildEnd > buildError);
1899
+ assert.ok(oidcStep > 0 && oidcStart > oidcStep && oidcEnd > oidcStart);
1900
+ for (const guard of [workflow.slice(buildStart, buildEnd), workflow.slice(oidcStart, oidcEnd)]) {
1901
+ const runGuard = (sourceUnknownLocal, localSha, serverSha, predecessorId = '') => runInNewContext(guard, {
1902
+ sourceUnknownLocal,
1903
+ local: { templateCommitSha: localSha },
1904
+ process: {
1905
+ env: {
1906
+ LOCAL_E2E_TEMPLATE_COMMIT_SHA: serverSha,
1907
+ LOCAL_E2E_PREDECESSOR_OPERATION_ID: predecessorId,
1908
+ },
1909
+ exit(code) { throw new Error(`exit ${code}`); },
1910
+ },
1911
+ });
1912
+ assert.doesNotThrow(() => runGuard(false, 'a'.repeat(40), 'a'.repeat(40), `cli-managed-${'b'.repeat(32)}`));
1913
+ assert.throws(() => runGuard(false, 'a'.repeat(40), 'c'.repeat(40), `cli-managed-${'b'.repeat(32)}`));
1914
+ assert.doesNotThrow(() => runGuard(false, 'a'.repeat(40), ''));
1915
+ assert.throws(() => runGuard(true, 'a'.repeat(40), 'c'.repeat(40)));
1916
+ assert.throws(() => runGuard(true, 'a'.repeat(40), '', `cli-managed-${'b'.repeat(32)}`));
1917
+ }
1918
+ });
1919
+
1751
1920
  test('local customer-owned source uses its own exact direct-operation audience', () => {
1752
1921
  const root = mkdtempSync(join(tmpdir(), 'eai-source-unknown-local-dispatch-'));
1753
1922
  try {
@@ -1768,6 +1937,12 @@ test('local customer-owned source uses its own exact direct-operation audience',
1768
1937
  '--github-event-name', 'workflow_dispatch', '--reusable-call', 'false',
1769
1938
  '--repository-id', '12345'];
1770
1939
  runEvidenceScript(args);
1940
+ for (const extra of [
1941
+ ['--local-e2e-template-commit-sha', 'd'.repeat(40)],
1942
+ ['--local-e2e-predecessor-operation-id', `cli-managed-${'e'.repeat(32)}`],
1943
+ ]) {
1944
+ assert.equal(spawnSync(process.execPath, [evidenceScript, ...args, ...extra], { encoding: 'utf8' }).status, 1);
1945
+ }
1771
1946
  const withoutTarget = [...args];
1772
1947
  withoutTarget.splice(withoutTarget.indexOf('--target-tenant-id'), 2);
1773
1948
  runEvidenceScript(withoutTarget);
@@ -5583,6 +5758,9 @@ syncBuiltinESMExports();
5583
5758
  const root = repoRoot;
5584
5759
  const fixtures = join(root, 'tests/fixtures/source-unknown');
5585
5760
  const canonical = regularBytes(workflowPath);
5761
+ const legacyCanonical = existsSync(join(fixtures, 'eai-app.yml'))
5762
+ ? regularBytes(join(fixtures, 'eai-app.yml'))
5763
+ : canonical;
5586
5764
  // INVARIANT: synthetic controlled input stays inline so legacy four-file repairs execute every control without new fixture dependencies.
5587
5765
  const generated = gunzipSync(
5588
5766
  Buffer.from(
@@ -5654,7 +5832,7 @@ function contextFixture(t, manifest, workflow = canonical, operation) {
5654
5832
  mkdirSync(join(directory, '.github/workflows'), { recursive: true });
5655
5833
  mkdirSync(join(directory, 'tests/fixtures'), { recursive: true });
5656
5834
  mkdirSync(join(directory, fixtureDirectory), { recursive: true });
5657
- writeFileSync(join(directory, fixtureDirectory, 'eai-app.yml'), canonical);
5835
+ writeFileSync(join(directory, fixtureDirectory, 'eai-app.yml'), legacyCanonical);
5658
5836
  writeFileSync(
5659
5837
  join(directory, fixtureDirectory, 'generated-eai-app.yml'),
5660
5838
  generated,
@@ -6007,7 +6185,7 @@ test('generated contract fixture drift fails before comparison', () => {
6007
6185
  test('inline synthetic generated input retains repository and actual normalized export parity', () => {
6008
6186
  assert.equal(digest(generated), generatedDigest);
6009
6187
  if (existsSync(fixtures)) {
6010
- assert.ok(regularBytes(join(fixtures, 'eai-app.yml')).equals(canonical));
6188
+ assert.equal(digest(regularBytes(join(fixtures, 'eai-app.yml'))), canonicalDigest);
6011
6189
  assert.ok(
6012
6190
  regularBytes(join(fixtures, 'generated-eai-app.yml')).equals(generated),
6013
6191
  );
@@ -6023,7 +6201,7 @@ test('inline synthetic generated input retains repository and actual normalized
6023
6201
  );
6024
6202
  assert.equal(
6025
6203
  digest(regularBytes(join(root, canonicalWorkflowRelativePath))),
6026
- canonicalDigest,
6204
+ currentWorkflowDigest,
6027
6205
  );
6028
6206
  }
6029
6207
  });
@@ -7,8 +7,8 @@
7
7
  },
8
8
  "appTemplate": {
9
9
  "repo": "https://github.com/eai-support/eai-app-template.git",
10
- "version": "v1.0.14",
11
- "commit": "c8a8aed26c4d159a4fa2598d9b5e8a495203dd8b",
10
+ "version": "v1.0.17",
11
+ "commit": "3162df8dffd36465ef2cec474eee4f8e6285ec3c",
12
12
  "packageLockSha256": "5b04e0be44ef93a817e4dc5076914d0f0684712b41155ee2934ce42f184df415",
13
13
  "enterpriseRegistry": "https://enterpriseaigroup.github.io/enterpriseai-packages/registry",
14
14
  "enterprisePackageFingerprint": "3286fdf91d6a25a55a4d84de21bf5c592bcf11abffb96dc2be90e93414eeb646",