@aifabrix/builder 2.58.0 → 2.60.0

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 (120) hide show
  1. package/lib/agent-kit/codex-app-server.js +44 -0
  2. package/lib/agent-kit/git-identity.js +180 -0
  3. package/lib/agent-kit/init.js +4 -17
  4. package/lib/agent-kit/session-report.js +15 -10
  5. package/lib/agent-kit/setup-auth.js +131 -0
  6. package/lib/agent-kit/setup-context.js +118 -0
  7. package/lib/agent-kit/setup-github.js +70 -0
  8. package/lib/agent-kit/setup-managed.js +79 -0
  9. package/lib/agent-kit/setup-process.js +42 -0
  10. package/lib/agent-kit/setup-session.js +53 -0
  11. package/lib/agent-kit/setup-state.js +52 -0
  12. package/lib/agent-kit/setup-workspace.js +113 -0
  13. package/lib/agent-kit/setup.js +87 -0
  14. package/lib/agent-kit/start.js +48 -3
  15. package/lib/agent-kit/tmux-sessions.js +20 -3
  16. package/lib/api/dev.api.js +1 -1
  17. package/lib/api/miso-health.api.js +26 -0
  18. package/lib/api/role-assistant-test-job.api.js +60 -0
  19. package/lib/api/types/dev.types.js +3 -3
  20. package/lib/api/work-search.api.js +13 -6
  21. package/lib/app/deploy.js +8 -103
  22. package/lib/app/show-display.js +1 -0
  23. package/lib/app/show-online.js +15 -0
  24. package/lib/build/standard-docker-build.js +4 -1
  25. package/lib/cli/index.js +4 -0
  26. package/lib/cli/setup-app.js +14 -1
  27. package/lib/cli/setup-dev-path-commands.js +4 -0
  28. package/lib/cli/setup-dev.js +7 -4
  29. package/lib/cli/setup-help.js +86 -0
  30. package/lib/cli/setup-onboarding.js +37 -0
  31. package/lib/cli/setup-utility-repair.js +7 -5
  32. package/lib/cli/setup-utility.js +24 -1
  33. package/lib/commands/agent-kit-help.js +21 -7
  34. package/lib/commands/agent-kit-sessions.js +126 -0
  35. package/lib/commands/agent-kit-setup.js +44 -0
  36. package/lib/commands/agent-kit.js +5 -79
  37. package/lib/commands/dev-init.js +21 -23
  38. package/lib/commands/dev-refresh.js +2 -2
  39. package/lib/commands/governance-verify-external.js +12 -5
  40. package/lib/commands/repair-datasource.js +9 -9
  41. package/lib/commands/repair.js +45 -8
  42. package/lib/commands/role-assistant.js +7 -0
  43. package/lib/commands/upload-qualification-client.js +56 -11
  44. package/lib/commands/upload-qualification-poll.js +51 -22
  45. package/lib/commands/wizard-dataplane.js +16 -52
  46. package/lib/core/env-platform-expand.js +93 -0
  47. package/lib/core/secrets-env-content.js +37 -3
  48. package/lib/datasource/datasource-validate-summary.js +8 -5
  49. package/lib/datasource/sync-defaults.js +53 -0
  50. package/lib/deployment/installation/azure-infra-stage.js +13 -1
  51. package/lib/deployment/installation/azure-preflight.js +13 -2
  52. package/lib/deployment/installation/azure-readiness.js +24 -2
  53. package/lib/deployment/installation/environment-auth.js +60 -20
  54. package/lib/deployment/installation/environment-stage.js +9 -5
  55. package/lib/deployment/installation/infra-catalog.js +10 -7
  56. package/lib/deployment/installation/infra-manifest.js +5 -0
  57. package/lib/deployment/installation/registry-auth-mode.js +33 -10
  58. package/lib/deployment/marketplace-environment.js +26 -1
  59. package/lib/generator/builders.js +17 -0
  60. package/lib/generator/helpers.js +23 -2
  61. package/lib/generator/index.js +33 -18
  62. package/lib/integration-definition/apply.js +2 -2
  63. package/lib/internal/fs-real-sync.js +4 -1
  64. package/lib/programmatic/bearer-auth.js +3 -62
  65. package/lib/programmatic/builder-api-startup.js +51 -0
  66. package/lib/programmatic/builder-miso-runtime.js +167 -0
  67. package/lib/programmatic/openapi-descriptions.js +3 -3
  68. package/lib/programmatic/openapi-schema-descriptions-core.js +3 -3
  69. package/lib/programmatic/openapi-spec.js +2 -1
  70. package/lib/programmatic/route-handler-map.js +3 -2
  71. package/lib/role-assistant/test-cases-search.js +3 -0
  72. package/lib/role-assistant/test-job-runner.js +125 -0
  73. package/lib/role-assistant/test-runner-search.js +44 -2
  74. package/lib/schema/application-schema.json +193 -2
  75. package/lib/schema/environment-deploy-request.schema.json +1 -1
  76. package/lib/schema/external-datasource.schema.json +20 -5
  77. package/lib/schema/infra-parameter.schema.json +140 -34
  78. package/lib/schema/infra.parameter.yaml +30 -29
  79. package/lib/schema/infrastructure-schema.json +133 -14
  80. package/lib/schema/installation-environment.schema.json +1 -1
  81. package/lib/schema/wizard-config.schema.json +1 -1
  82. package/lib/utils/compose-generate-docker-compose.js +12 -0
  83. package/lib/utils/config-paths.js +24 -29
  84. package/lib/utils/config-registry-preference.js +13 -6
  85. package/lib/utils/dev-init-ssh-merge.js +5 -4
  86. package/lib/utils/dev-user-groups.js +2 -1
  87. package/lib/utils/docker-build.js +29 -8
  88. package/lib/utils/docker-daemon-tls-ca.js +2 -2
  89. package/lib/utils/docker-manifest-public-port.js +36 -0
  90. package/lib/utils/error-formatters/validation-errors.js +25 -5
  91. package/lib/utils/help-builder.js +4 -3
  92. package/lib/utils/platform-resolution.js +226 -0
  93. package/lib/utils/resolve-docker-image-ref.js +8 -3
  94. package/lib/utils/token-manager.js +29 -36
  95. package/package.json +4 -3
  96. package/templates/README.md +2 -1
  97. package/templates/agent-kit/agent-kit.yaml +4 -1
  98. package/templates/agent-kit/instructions/AGENTKIT.md +40 -3
  99. package/templates/agent-kit/instructions/root.AGENTS.md +42 -1
  100. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +33 -1
  101. package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
  102. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -0
  103. package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +10 -3
  104. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +22 -0
  105. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +21 -0
  106. package/templates/agent-kit/skills/shared/feedback.md +31 -0
  107. package/templates/agent-kit/skills/shared/hosts.md +35 -7
  108. package/templates/agent-kit/skills/shared/status.md +85 -0
  109. package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
  110. package/templates/agent-kit/workspace/README.md +14 -1
  111. package/templates/applications/builder-api/application.yaml +1 -1
  112. package/templates/applications/dataplane/application.yaml +30 -2
  113. package/templates/applications/dataplane/env.template +37 -4
  114. package/templates/applications/miso-controller/application.yaml +24 -1
  115. package/templates/applications/miso-controller/env.template +22 -35
  116. package/templates/external-system/external-datasource.yaml.hbs +7 -6
  117. package/templates/marketplace/main.json +281 -75
  118. package/templates/python/Dockerfile.hbs +2 -0
  119. package/templates/typescript/Dockerfile.hbs +2 -0
  120. package/templates/typescript/docker-compose.hbs +12 -0
@@ -29,7 +29,6 @@ standardUpInfraEnsureKeys:
29
29
  - databases-dataplane-3-urlKeyVault
30
30
  - databases-dataplane-3-passwordKeyVault
31
31
  # Miso-controller Entra onboarding (vendor app); required for aifabrix resolve miso-controller
32
- - entra-callback-state-secret
33
32
  - entra-callback-uri
34
33
  - entra-client-idKeyVault
35
34
  - entra-client-secretKeyVault
@@ -98,7 +97,8 @@ parameters:
98
97
  category: Redis
99
98
  sensitive: true
100
99
  valueSource: providerOutput
101
- requiredOnSystemOnboarding: true
100
+ # Not needed at first-time install: Redis is provisioned later (env deploy / TA), not at marketplace bootstrap.
101
+ requiredOnSystemOnboarding: false
102
102
  scope: system
103
103
  generator:
104
104
  type: literal
@@ -348,20 +348,6 @@ parameters:
348
348
 
349
349
  # Miso-controller Entra ID onboarding (vendor multi-tenant app). Empty client/callback values
350
350
  # allow local resolve and start-dev; set real values before Entra admin-consent flows.
351
- - key: entra-callback-state-secret
352
- category: Microsoft Entra ID
353
- sensitive: true
354
- valueSource: generated
355
- requiredOnSystemOnboarding: true
356
- scope: system
357
- generator:
358
- type: randomBytes32
359
- ensureOn: [upInfra, resolveApp]
360
- azure:
361
- notes: >-
362
- Shared HMAC secret (min 16 chars) for OAuth state between controller and Mori callback;
363
- maps to ENTRA_CALLBACK_STATE_SECRET in miso-controller env.template.
364
-
365
351
  - key: entra-callback-uri
366
352
  category: Microsoft Entra ID
367
353
  sensitive: false
@@ -440,8 +426,9 @@ parameters:
440
426
  # side is decrypted by the other. The read was already happening in the shipped template
441
427
  # and the policy denied it. Declared rather than silently permitted, and kept to the one
442
428
  # application that needs it instead of moving the key to a scope that grants everyone.
443
- sharedWithAppKeys:
429
+ visibility:
444
430
  - dataplane
431
+ - mori-controller
445
432
  scope: app
446
433
  generator:
447
434
  type: randomBytes32
@@ -453,6 +440,32 @@ parameters:
453
440
  notes: >-
454
441
  32-byte secret as base64; generated on first ensure. Rotate in production.
455
442
 
443
+ - key: integration-server-secrets-encryptionKeyVault
444
+ ownerAppKey: integration-server
445
+ # Deliberately NOT shared with any other app, which is the point of the row.
446
+ # An integrator installation hands this key to every developer it provisions: it is
447
+ # written into each managed ~/.aifabrix/config.yaml as `secrets-encryption`, so the
448
+ # CLI can decrypt what that installation's own secrets API encrypted. A key shared
449
+ # with another application would therefore be readable by everyone holding a shell
450
+ # in that container, and one leak would reach every installation using it.
451
+ # One installation, one key, so the blast radius stays inside it.
452
+ #
453
+ # Before this row existed, integration-server's env.template read
454
+ # `kv://builder-server-secrets-encryptionKeyVault` — another application's key.
455
+ # That resolves only by being granted, and granting it grants exactly the access
456
+ # above; without a grant it silently generated an unrelated value instead, so the
457
+ # two server types drifted apart with nothing reporting it.
458
+ scope: app
459
+ generator:
460
+ type: randomBytes32
461
+ ensureOn: [upInfra, resolveApp]
462
+ azure:
463
+ vaultSecretName: integration-server-secrets-encryptionKeyVault
464
+ notes: >-
465
+ 32-byte secret as base64; generated on first ensure, per installation. Rotating it
466
+ requires re-running configuration sync so every managed config.yaml is rewritten;
467
+ secrets already stored by that installation do not survive the rotation.
468
+
456
469
  # Local kv:// name matches miso-controller env.template (JWT_SECRET); not the generic {appKey}-secrets-jwtKeyVault Azure name.
457
470
  - key: miso-controller-jwt-secretKeyVault
458
471
  ownerAppKey: miso-controller
@@ -793,18 +806,6 @@ parameters:
793
806
  Optional local testing/development bearer bypass. A supplied local value is resolved; when absent,
794
807
  local and Azure application deployment resolve it as empty and do not fail.
795
808
 
796
- - key: mori-callback-api-keyKeyVault
797
- ownerAppKey: miso-controller
798
- scope: app
799
- generator:
800
- type: randomBytes32
801
- ensureOn: [resolveApp]
802
- azure:
803
- vaultSecretName: mori-callback-api-keyKeyVault
804
- legacyVaultSecretNames:
805
- - marketplace-mori-callback-api-key
806
- notes: Miso Controller callback credential; generated only when its resolved manifest requires it.
807
-
808
809
  # App semver when env.template / conversion yields kv://version (local secrets backfill).
809
810
  - key: version
810
811
  scope: app
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/infrastructure-schema.json",
3
+ "$id": "aifabrix://schema/infrastructure-schema.json",
4
4
  "title": "AI Fabrix Installation Infrastructure Manifest",
5
5
  "description": "Committed, non-secret environment-infra.json for an installation workspace under infrastructure/<resource-group>/. Secrets appear only as valueFromEnv references.",
6
6
  "metadata": {
7
7
  "key": "infrastructure-schema",
8
8
  "name": "Installation Infrastructure Manifest Schema",
9
9
  "description": "JSON schema for infrastructure/<resource-group>/environment-infra.json",
10
- "version": "2.0.0",
10
+ "version": "2.1.0",
11
11
  "type": "schema",
12
12
  "category": "infrastructure",
13
13
  "author": "AI Fabrix Team",
14
14
  "createdAt": "2024-01-01T00:00:00Z",
15
- "updatedAt": "2026-09-17T00:00:00Z",
15
+ "updatedAt": "2026-09-24T00:00:00Z",
16
16
  "compatibility": {
17
17
  "minVersion": "2.0.0",
18
18
  "maxVersion": "2.0.0",
@@ -26,6 +26,14 @@
26
26
  ],
27
27
  "dependencies": [],
28
28
  "changelog": [
29
+ {
30
+ "version": "2.1.0",
31
+ "date": "2026-09-24T00:00:00Z",
32
+ "breaking": false,
33
+ "changes": [
34
+ "Removed moriCallbackApiKey and entraCallbackStateSecret: the shipped Marketplace template no longer declares them. The installation receives its Mori key at licence registration, and Entra consent state is issued server-side (Mori 227.5)."
35
+ ]
36
+ },
29
37
  {
30
38
  "version": "2.0.0",
31
39
  "date": "2026-09-16T00:00:00Z",
@@ -501,12 +509,11 @@
501
509
  "acrResourceId",
502
510
  "deployRoleAssignments",
503
511
  "moriBaseUrl",
504
- "moriCallbackApiKey",
505
- "entraCallbackStateSecret",
506
512
  "entraIdClientId",
507
513
  "entraIdClientSecret",
508
514
  "entraIdTenantId",
509
515
  "licenseEdition",
516
+ "capacityProfile",
510
517
  "onboardingMode",
511
518
  "misoImageTag",
512
519
  "keycloakImageTag"
@@ -981,7 +988,7 @@
981
988
  }
982
989
  }
983
990
  },
984
- "moriBaseUrl": {
991
+ "deployRegistryRoleAssignment": {
985
992
  "type": "object",
986
993
  "required": [
987
994
  "value",
@@ -1009,13 +1016,24 @@
1009
1016
  },
1010
1017
  "required": {
1011
1018
  "type": "boolean"
1019
+ },
1020
+ "restoreValue": {
1021
+ "type": [
1022
+ "string",
1023
+ "boolean",
1024
+ "number",
1025
+ "object",
1026
+ "array"
1027
+ ],
1028
+ "description": "Value to restore when credential mode no longer applies."
1029
+ },
1030
+ "restoreSource": {
1031
+ "type": "string",
1032
+ "minLength": 1
1012
1033
  }
1013
1034
  }
1014
1035
  },
1015
- "moriCallbackApiKey": {
1016
- "$ref": "#/definitions/envReference"
1017
- },
1018
- "entraCallbackStateSecret": {
1036
+ "moriBaseUrl": {
1019
1037
  "type": "object",
1020
1038
  "required": [
1021
1039
  "value",
@@ -1025,7 +1043,13 @@
1025
1043
  "additionalProperties": false,
1026
1044
  "properties": {
1027
1045
  "value": {
1028
- "type": "null"
1046
+ "type": [
1047
+ "string",
1048
+ "boolean",
1049
+ "number",
1050
+ "object",
1051
+ "array"
1052
+ ]
1029
1053
  },
1030
1054
  "source": {
1031
1055
  "type": "string",
@@ -1037,9 +1061,6 @@
1037
1061
  },
1038
1062
  "required": {
1039
1063
  "type": "boolean"
1040
- },
1041
- "requiredAt": {
1042
- "const": "tenantActivation"
1043
1064
  }
1044
1065
  }
1045
1066
  },
@@ -1242,6 +1263,98 @@
1242
1263
  "type": "boolean"
1243
1264
  }
1244
1265
  }
1266
+ },
1267
+ "moriLicensePublicKey": {
1268
+ "type": "object",
1269
+ "required": [
1270
+ "value",
1271
+ "source",
1272
+ "description"
1273
+ ],
1274
+ "additionalProperties": false,
1275
+ "properties": {
1276
+ "value": {
1277
+ "type": [
1278
+ "string",
1279
+ "boolean",
1280
+ "number",
1281
+ "object",
1282
+ "array"
1283
+ ]
1284
+ },
1285
+ "source": {
1286
+ "type": "string",
1287
+ "minLength": 1
1288
+ },
1289
+ "description": {
1290
+ "type": "string",
1291
+ "minLength": 1
1292
+ },
1293
+ "required": {
1294
+ "type": "boolean"
1295
+ }
1296
+ }
1297
+ },
1298
+ "moriLicenseIssuer": {
1299
+ "type": "object",
1300
+ "required": [
1301
+ "value",
1302
+ "source",
1303
+ "description"
1304
+ ],
1305
+ "additionalProperties": false,
1306
+ "properties": {
1307
+ "value": {
1308
+ "type": [
1309
+ "string",
1310
+ "boolean",
1311
+ "number",
1312
+ "object",
1313
+ "array"
1314
+ ]
1315
+ },
1316
+ "source": {
1317
+ "type": "string",
1318
+ "minLength": 1
1319
+ },
1320
+ "description": {
1321
+ "type": "string",
1322
+ "minLength": 1
1323
+ },
1324
+ "required": {
1325
+ "type": "boolean"
1326
+ }
1327
+ }
1328
+ },
1329
+ "capacityProfile": {
1330
+ "type": "object",
1331
+ "required": [
1332
+ "value",
1333
+ "source",
1334
+ "description"
1335
+ ],
1336
+ "additionalProperties": false,
1337
+ "properties": {
1338
+ "value": {
1339
+ "enum": [
1340
+ "s",
1341
+ "m",
1342
+ "l",
1343
+ "xl"
1344
+ ]
1345
+ },
1346
+ "source": {
1347
+ "type": "string",
1348
+ "minLength": 1
1349
+ },
1350
+ "description": {
1351
+ "type": "string",
1352
+ "minLength": 1
1353
+ },
1354
+ "required": {
1355
+ "type": "boolean"
1356
+ }
1357
+ }
1245
1358
  }
1246
1359
  }
1247
1360
  },
@@ -1326,6 +1439,12 @@
1326
1439
  },
1327
1440
  "portalUrl": {
1328
1441
  "$ref": "#/definitions/output"
1442
+ },
1443
+ "azureOpenAiCreated": {
1444
+ "$ref": "#/definitions/output"
1445
+ },
1446
+ "azureOpenAiSkipReason": {
1447
+ "$ref": "#/definitions/output"
1329
1448
  }
1330
1449
  }
1331
1450
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/installation-environment.schema.json",
3
+ "$id": "aifabrix://schema/installation-environment.schema.json",
4
4
  "title": "AI Fabrix Installation Environment File",
5
5
  "description": "Committed, non-secret infrastructure/<resource-group>/environment-dev|tst|pro.json. Inherited shared values name environment-infra.json as their source; request is the exact non-secret Miso deploy projection.",
6
6
  "metadata": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://raw.githubusercontent.com/esystemsdev/aifabrix-builder/refs/heads/main/lib/schema/wizard-config.schema.json",
3
+ "$id": "aifabrix://schema/wizard-config.schema.json",
4
4
  "title": "AI Fabrix Wizard Configuration Schema",
5
5
  "description": "Schema for validating wizard.yaml configuration files for headless external system creation",
6
6
  "type": "object",
@@ -249,6 +249,18 @@ function buildComposeTemplateContext(ctx) {
249
249
  networkName: ctx.networkName,
250
250
  containerName: ctx.containerName,
251
251
  misoEnvironment: resolveMisoEnvironment(ctx.options),
252
+ // Containers are run as the invoking host user so bind-mounted files stay writable.
253
+ // An app that administers its own operating system — creating accounts, managing
254
+ // sshd — cannot work that way and declares `runAsRoot: true` to opt out.
255
+ runAsRoot: ctx.appConfig?.runAsRoot === true,
256
+ // Extra ports for an application serving a second protocol from the same container,
257
+ // such as SSH beside HTTP. Each host port uses the main port's formula so developers
258
+ // on one host do not collide.
259
+ additionalPorts: (ctx.appConfig?.additionalPorts ?? []).map((entry) => ({
260
+ name: entry.name,
261
+ containerPort: entry.containerPort,
262
+ hostPort: ctx.idNum === 0 ? entry.hostPort : entry.hostPort + ctx.idNum * 100
263
+ })),
252
264
  devMountPath: ctx.devMountPath,
253
265
  localDataMountPath,
254
266
  reloadStartCommand: ctx.reloadStartCommand,
@@ -101,13 +101,8 @@ function createHomeAndSecretsPathFunctions(getConfigFn, saveConfigFn) {
101
101
  return;
102
102
  }
103
103
  const resolved = path.resolve(trimmed);
104
- await setPathConfig(
105
- getConfigFn,
106
- saveConfigFn,
107
- 'aifabrix-work',
108
- resolved,
109
- 'Work path must be a non-empty string'
110
- );
104
+ await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-work', resolved,
105
+ 'Work path must be a non-empty string');
111
106
  },
112
107
  async getAifabrixSecretsPath() {
113
108
  return getPathConfig(getConfigFn, 'aifabrix-secrets');
@@ -168,6 +163,17 @@ async function resolveEnvConfigPathToAbsolute(raw, getConfigFn) {
168
163
  return path.normalize(path.resolve(base, trimmed));
169
164
  }
170
165
 
166
+ function findBuilderDirectory(base) {
167
+ if (!base) return null;
168
+ const candidate = path.join(base, 'builder');
169
+ try {
170
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return candidate;
171
+ } catch {
172
+ /* Missing or inaccessible builder directory. */
173
+ }
174
+ return null;
175
+ }
176
+
171
177
  function createEnvConfigPathFunctions(getConfigFn, saveConfigFn) {
172
178
  return {
173
179
  /**
@@ -199,30 +205,16 @@ function createEnvConfigPathFunctions(getConfigFn, saveConfigFn) {
199
205
  return path.dirname(absolute);
200
206
  }
201
207
  const pathsMod = require('./paths');
202
- const tryDir = (base) => {
203
- if (!base) {
204
- return null;
205
- }
206
- const b = path.join(base, 'builder');
207
- try {
208
- if (fs.existsSync(b) && fs.statSync(b).isDirectory()) {
209
- return b;
210
- }
211
- } catch {
212
- /* ignore */
213
- }
214
- return null;
215
- };
216
- const fromProject = tryDir(pathsMod.getProjectRoot());
208
+ const fromProject = findBuilderDirectory(pathsMod.getProjectRoot());
217
209
  if (fromProject) {
218
210
  return fromProject;
219
211
  }
220
- const fromEffective = tryDir(pathsMod.getBuilderRoot());
212
+ const fromEffective = findBuilderDirectory(pathsMod.getBuilderRoot());
221
213
  if (fromEffective) {
222
214
  return fromEffective;
223
215
  }
224
216
  const work = pathsMod.getAifabrixWork();
225
- return tryDir(work);
217
+ return findBuilderDirectory(work);
226
218
  }
227
219
  };
228
220
  }
@@ -368,11 +360,10 @@ function applySecretsUrlFromRemote(config) {
368
360
  config['aifabrix-secrets'] = `${base}/api/dev/secrets`;
369
361
  }
370
362
 
371
- function applySyncAndDockerFromHost(config) {
363
+ function applySyncHost(config) {
372
364
  const host = hostnameFromUrl(config['remote-server']);
373
365
  if (!host) return;
374
366
  if (!config['sync-ssh-host']) config['sync-ssh-host'] = host;
375
- if (!config['docker-endpoint']) config['docker-endpoint'] = `tcp://${host}:2376`;
376
367
  }
377
368
 
378
369
  async function mergeRemoteSettingsImpl(getConfigFn, saveConfigFn, settings) {
@@ -386,8 +377,13 @@ async function mergeRemoteSettingsImpl(getConfigFn, saveConfigFn, settings) {
386
377
  if (value === '') continue;
387
378
  config[key] = value;
388
379
  }
380
+ // Current settings are authoritative: never inherit Docker access from another identity.
381
+ if (typeof settings['docker-endpoint'] !== 'string' || !settings['docker-endpoint'].trim()) {
382
+ delete config['docker-endpoint'];
383
+ delete config['docker-tls-skip-verify'];
384
+ }
389
385
  applySecretsUrlFromRemote(config);
390
- applySyncAndDockerFromHost(config);
386
+ applySyncHost(config);
391
387
  await saveConfigFn(config);
392
388
  }
393
389
 
@@ -428,7 +424,6 @@ module.exports = {
428
424
  resolveEnvConfigPathToAbsolute,
429
425
  SETTINGS_RESPONSE_KEYS,
430
426
  applySecretsUrlFromRemote,
431
- applySyncAndDockerFromHost,
427
+ applySyncAndDockerFromHost: applySyncHost,
432
428
  hostnameFromUrl
433
429
  };
434
-
@@ -14,13 +14,15 @@ const { getRuntimeConfigFile } = require('../core/config-runtime-paths');
14
14
 
15
15
  const REGISTRY_CONFIG_KEY = 'registry';
16
16
  const HOST_LABEL = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?';
17
- const REGISTRY_HOST_RE = new RegExp(`^${HOST_LABEL}(?:\\.${HOST_LABEL})*(?::\\d{1,5})?$`);
17
+ const REGISTRY_HOST_RE = new RegExp(`^${HOST_LABEL}(?:\\.${HOST_LABEL})*$`);
18
18
 
19
19
  /**
20
- * Normalizes and validates a registry host (optionally with a port).
21
- * Rejects schemes, paths, embedded credentials, query/fragment text and whitespace.
20
+ * Normalizes and validates a registry host.
21
+ * Rejects schemes, paths, embedded credentials, query/fragment text and whitespace. A port is
22
+ * rejected because the deployment `image` contract (application-schema.json, shared with the
23
+ * Controller) accepts only one colon, which separates the tag.
22
24
  * @param {string} value - Raw registry value
23
- * @returns {string} Lower-case host[:port]
25
+ * @returns {string} Lower-case host
24
26
  * @throws {Error} When the value is empty or not a plain host
25
27
  */
26
28
  function normalizeRegistryHost(value) {
@@ -33,8 +35,13 @@ function normalizeRegistryHost(value) {
33
35
  throw new Error(`Invalid registry "${raw}": use a host name such as myregistry.azurecr.io (no scheme, path or credentials)`);
34
36
  }
35
37
  const host = withoutSlash.toLowerCase();
36
- const port = host.includes(':') ? Number(host.split(':')[1]) : null;
37
- if (!REGISTRY_HOST_RE.test(host) || (port !== null && (port < 1 || port > 65535))) {
38
+ if (host.includes(':')) {
39
+ throw new Error(
40
+ `Invalid registry "${raw}": a port is not supported because the deployed image reference keeps one colon for the tag. ` +
41
+ 'Use a host name such as myregistry.azurecr.io.'
42
+ );
43
+ }
44
+ if (!REGISTRY_HOST_RE.test(host)) {
38
45
  throw new Error(`Invalid registry "${raw}": use a host name such as myregistry.azurecr.io`);
39
46
  }
40
47
  return host;
@@ -163,16 +163,17 @@ async function mergeDevSshConfigBlock(p) {
163
163
  * Update ~/.ssh/config with Host <user>.<host> for interactive SSH; log result.
164
164
  * @param {string} baseUrl - Builder Server base URL
165
165
  * @param {string} devId - Developer ID (digits)
166
+ * @param {Object} [freshSettings] - Current authenticated server settings; never mix with saved identity
166
167
  * @returns {Promise<{ hostAlias: string|null, syncUser: string, syncHost: string|null }>}
167
168
  */
168
- async function mergeDevSshConfigAfterInit(baseUrl, devId) {
169
- const syncHost = (await config.getSyncSshHost()) || hostnameFromBaseUrl(baseUrl);
170
- const syncUser = (await config.getSyncSshUser()) || `dev${devId}`;
169
+ async function mergeDevSshConfigAfterInit(baseUrl, devId, freshSettings) {
170
+ const syncHost = (freshSettings ? freshSettings['sync-ssh-host'] : await config.getSyncSshHost()) || hostnameFromBaseUrl(baseUrl);
171
+ const syncUser = (freshSettings ? freshSettings['sync-ssh-user'] : await config.getSyncSshUser()) || `dev${devId}`;
171
172
  if (!syncHost) {
172
173
  return { hostAlias: null, syncUser, syncHost: null };
173
174
  }
174
175
  try {
175
- const rootTrimmed = trimDeveloperRootDomain(await config.getDeveloperRootDomain());
176
+ const rootTrimmed = trimDeveloperRootDomain(freshSettings ? freshSettings['developer-root-domain'] : await config.getDeveloperRootDomain());
176
177
  return await mergeDevSshConfigBlock({
177
178
  baseUrl,
178
179
  devId,
@@ -10,7 +10,8 @@ const ALLOWED_DEV_GROUPS = Object.freeze([
10
10
  'secret-manager',
11
11
  'secret-reader',
12
12
  'developer',
13
- 'docker'
13
+ 'docker',
14
+ 'integrator'
14
15
  ]);
15
16
 
16
17
  /**
@@ -96,8 +96,15 @@ function handleDockerClose(code, ctx) {
96
96
  }
97
97
  }
98
98
 
99
- function buildDockerCliArgs(imageName, tag, dockerfilePath, contextPath, buildArgs, noCache) {
99
+ function buildDockerCliArgs(imageName, tag, dockerfilePath, contextPath, buildArgs, options = {}) {
100
+ const { noCache = false, target } = options;
100
101
  const args = ['build', '-t', `${imageName}:${tag}`, '-f', dockerfilePath];
102
+ // A multi-stage Dockerfile builds its last stage unless told otherwise. `build.target`
103
+ // lets one Dockerfile serve several images from the same source, which is the only way
104
+ // an app can ship two variants without duplicating its build.
105
+ if (target) {
106
+ args.push('--target', String(target));
107
+ }
101
108
  for (const [key, value] of Object.entries(buildArgs)) {
102
109
  if (value !== null && value !== undefined && String(value).length > 0) {
103
110
  args.push('--build-arg', `${key}=${String(value)}`);
@@ -111,9 +118,12 @@ function buildDockerCliArgs(imageName, tag, dockerfilePath, contextPath, buildAr
111
118
  }
112
119
 
113
120
  function createDockerSpawnParams(options) {
114
- const { imageName, tag, dockerfilePath, contextPath, buildArgs, noCache, env } = options;
121
+ const { imageName, tag, dockerfilePath, contextPath, buildArgs, noCache, env, target } = options;
115
122
  const spawnEnv = { ...process.env, ...(env || {}) };
116
- const args = buildDockerCliArgs(imageName, tag, dockerfilePath, contextPath, buildArgs || {}, noCache);
123
+ const args = buildDockerCliArgs(imageName, tag, dockerfilePath, contextPath, buildArgs || {}, {
124
+ noCache,
125
+ target
126
+ });
117
127
  return { spawnEnv, args };
118
128
  }
119
129
 
@@ -163,7 +173,8 @@ function runDockerBuildProcess(buildOpts) {
163
173
  reject,
164
174
  env = {},
165
175
  buildArgs = {},
166
- noCache = false
176
+ noCache = false,
177
+ target
167
178
  } = buildOpts;
168
179
  const { spawnEnv, args } = createDockerSpawnParams({
169
180
  imageName,
@@ -172,7 +183,8 @@ function runDockerBuildProcess(buildOpts) {
172
183
  contextPath,
173
184
  buildArgs,
174
185
  noCache,
175
- env
186
+ env,
187
+ target
176
188
  });
177
189
  const dockerProcess = spawn('docker', args, {
178
190
  shell: process.platform === 'win32',
@@ -192,7 +204,12 @@ function runDockerBuildProcess(buildOpts) {
192
204
  * @returns {Promise<void>} Resolves when build completes
193
205
  * @throws {Error} If build fails
194
206
  */
195
- async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag, buildArgs = {}, noCache = false) {
207
+ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag, buildArgs = {}, options = false) {
208
+ // Historically the sixth argument was the `noCache` boolean. It is now an options
209
+ // object so a build stage can be named too; the boolean form still works, because
210
+ // this function is exported and callers outside this module pass it that way.
211
+ const { noCache = false, target } =
212
+ typeof options === 'boolean' ? { noCache: options } : options || {};
196
213
  const spinner = ora({ text: 'Starting Docker build...', spinner: 'dots' }).start();
197
214
  const fsSync = require('fs');
198
215
  const path = require('path');
@@ -228,7 +245,8 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag, b
228
245
  reject,
229
246
  env: dockerCliEnv,
230
247
  buildArgs: resolvedBuildArgs,
231
- noCache
248
+ noCache,
249
+ target
232
250
  });
233
251
  });
234
252
  }
@@ -251,7 +269,10 @@ async function executeBuild(imageName, dockerfilePath, contextPath, tag, options
251
269
  options &&
252
270
  (options.cache === false || options.noCache === true || options['no-cache'] === true)
253
271
  );
254
- await executeDockerBuild(imageName, dockerfilePath, contextPath, tag, buildArgs, noCache);
272
+ await executeDockerBuild(imageName, dockerfilePath, contextPath, tag, buildArgs, {
273
+ noCache,
274
+ target: options && options.target
275
+ });
255
276
  await applyTagAliases(imageName, tag, resolveTagAliases(tag, options));
256
277
  }
257
278
 
@@ -146,7 +146,7 @@ function dockerEndpointFromRemoteServer(remoteServerUrl) {
146
146
  }
147
147
 
148
148
  /**
149
- * Resolve docker-endpoint for CA augmentation (settings override, else derive from remote-server).
149
+ * Resolve only an explicitly advertised docker-endpoint; a server hostname is not Docker access.
150
150
  * @param {{ settings?: Object|null, remoteServerUrl?: string|null }} ctx
151
151
  * @returns {string|null}
152
152
  */
@@ -156,7 +156,7 @@ function resolveDockerEndpointForCaAugment(ctx) {
156
156
  if (typeof fromSettings === 'string' && fromSettings.trim()) {
157
157
  return fromSettings.trim();
158
158
  }
159
- return dockerEndpointFromRemoteServer(ctx && ctx.remoteServerUrl);
159
+ return null;
160
160
  }
161
161
 
162
162
  /**