@aifabrix/builder 2.61.2 → 2.62.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 (81) hide show
  1. package/lib/api/applications.api.js +22 -2
  2. package/lib/api/bootstrap-snapshot.api.js +115 -0
  3. package/lib/app/deploy.js +14 -5
  4. package/lib/app/run-env-compose.js +5 -6
  5. package/lib/app/run.js +6 -4
  6. package/lib/build/standard-docker-build.js +4 -0
  7. package/lib/channels/channel-artifact-generator.js +13 -3
  8. package/lib/cli/infra-guided-footers.js +10 -8
  9. package/lib/cli/infra-guided.js +30 -10
  10. package/lib/cli/setup-infra-up-platform-action.js +14 -1
  11. package/lib/cli/setup-platform.js +2 -0
  12. package/lib/commands/auth-config.js +2 -0
  13. package/lib/commands/login.js +4 -0
  14. package/lib/commands/repair-datasource-apply.js +3 -1
  15. package/lib/commands/repair-datasource-legacy.js +94 -0
  16. package/lib/commands/setup-image-refresh.js +26 -0
  17. package/lib/commands/setup-modes.js +7 -1
  18. package/lib/commands/setup-prompts-platform-mode.js +34 -0
  19. package/lib/commands/setup.js +3 -0
  20. package/lib/commands/up-builder-api.js +24 -8
  21. package/lib/commands/up-common.js +16 -1
  22. package/lib/commands/up-dataplane-bootstrap.js +70 -14
  23. package/lib/commands/up-dataplane-credentials.js +21 -0
  24. package/lib/commands/up-dataplane.js +37 -18
  25. package/lib/commands/up-integration-server.js +112 -0
  26. package/lib/commands/wizard-config-normalizer.js +5 -1
  27. package/lib/core/admin-secrets-env-overlay.js +8 -3
  28. package/lib/core/environment-access-policy.js +121 -0
  29. package/lib/core/environment-mode-policy.js +177 -0
  30. package/lib/core/local-env-overrides.js +149 -0
  31. package/lib/core/secrets-ensure.js +4 -2
  32. package/lib/core/secrets-env-content.js +12 -9
  33. package/lib/core/secrets-env-write.js +1 -1
  34. package/lib/datasource/capability/capability-manifest.js +1 -0
  35. package/lib/deployment/installation/index.js +14 -1
  36. package/lib/deployment/installation/infra-catalog.js +3 -0
  37. package/lib/deployment/installation/local-environment-stage.js +285 -0
  38. package/lib/deployment/installation/local-installation.js +76 -0
  39. package/lib/deployment/installation/stage-input.js +5 -1
  40. package/lib/generator/builders.js +3 -1
  41. package/lib/generator/split-variables.js +2 -2
  42. package/lib/parameters/database-adoption.js +125 -0
  43. package/lib/parameters/physical-database-naming.js +136 -0
  44. package/lib/parameters/physical-secret-name.js +194 -0
  45. package/lib/parameters/rsa-secret-values.js +165 -0
  46. package/lib/programmatic/run-channel-install-artifacts.js +28 -2
  47. package/lib/programmatic/run-channel-package.js +3 -1
  48. package/lib/programmatic/workspace-context.js +37 -2
  49. package/lib/schema/infra.parameter.yaml +61 -27
  50. package/lib/schema/infrastructure-schema.json +213 -120
  51. package/lib/utils/compose-generate-docker-compose.js +2 -2
  52. package/lib/utils/compose-generator.js +4 -7
  53. package/lib/utils/compose-miso-env.js +2 -2
  54. package/lib/utils/compose-traefik-ingress-base.js +3 -4
  55. package/lib/utils/declarative-url-ports.js +3 -0
  56. package/lib/utils/env-environment-file-paths.js +105 -0
  57. package/lib/utils/environment-scoped-resources.js +48 -14
  58. package/lib/utils/paths-system-builder-keys.js +7 -1
  59. package/lib/utils/paths.js +7 -2
  60. package/lib/utils/postgres-platform-bootstrap.js +35 -1
  61. package/lib/utils/redis-env-scope.js +21 -1
  62. package/lib/utils/registry-auth-sources.js +154 -0
  63. package/lib/utils/registry-credentials.js +131 -14
  64. package/lib/utils/secrets-generator.js +30 -21
  65. package/lib/utils/secrets-helpers.js +5 -7
  66. package/lib/utils/secrets-kv-scope.js +127 -28
  67. package/lib/utils/secrets-materialize-local.js +9 -0
  68. package/lib/utils/secrets-missing-error.js +35 -1
  69. package/lib/utils/token-manager.js +14 -8
  70. package/lib/utils/url-public-path-prefix.js +6 -10
  71. package/package.json +3 -2
  72. package/templates/applications/builder-api/application.yaml +1 -1
  73. package/templates/applications/dataplane/application.yaml +2 -2
  74. package/templates/applications/dataplane/env.template +1 -1
  75. package/templates/applications/integration-server/application.yaml +91 -0
  76. package/templates/applications/integration-server/env.template +215 -0
  77. package/templates/applications/mirrored-templates.json +52 -0
  78. package/templates/applications/miso-controller/application.yaml +43 -1
  79. package/templates/applications/miso-controller/env.template +45 -39
  80. package/templates/marketplace/createUiDefinition.json +52 -4
  81. package/templates/marketplace/main.json +85 -7
@@ -10,7 +10,6 @@
10
10
  */
11
11
 
12
12
  const config = require('../core/config');
13
- const logger = require('./logger');
14
13
  const { maskSensitiveData } = require('./log-redaction');
15
14
  const {
16
15
  refreshClientToken,
@@ -205,12 +204,17 @@ async function tryClientTokenAuth(environment, appName, controllerUrl) {
205
204
  return null;
206
205
  }
207
206
 
208
- function throwNoDeploymentAuth(credentialApp) {
209
- const err = new Error(
210
- `No authentication method available. For CI: set AIFABRIX_DEPLOYMENT_AUTH=client-credentials and MISO_CLIENTID/MISO_CLIENTSECRET in .env or '${credentialApp}-client-idKeyVault' in secrets.local.yaml. For interactive use: aifabrix login`
211
- );
207
+ // Two causes reach here: no credentials at all, or credentials the controller rejected.
208
+ // Saying "put it in secrets.local.yaml" for the second sends the reader after a key that is
209
+ // already present and simply not valid, so name the rejection instead.
210
+ function throwNoDeploymentAuth(credentialApp, exchangeError) {
211
+ const err = new Error(exchangeError
212
+ ? `Client credentials for '${credentialApp}' were rejected by the controller: ${exchangeError.message}. ` +
213
+ `Re-issue them with: aifabrix login && aifabrix app rotate-secret ${credentialApp}`
214
+ : `No authentication method available. For CI: set AIFABRIX_DEPLOYMENT_AUTH=client-credentials and MISO_CLIENTID/MISO_CLIENTSECRET in .env or '${credentialApp}-client-idKeyVault' in secrets.local.yaml. For interactive use: aifabrix login`);
212
215
  err.statusCode = 401;
213
216
  err.authFailure = true;
217
+ if (exchangeError) err.cause = exchangeError;
214
218
  throw err;
215
219
  }
216
220
 
@@ -262,8 +266,10 @@ async function getDeploymentAuth(controllerUrl, environment, appName, options =
262
266
  controller: controllerUrl
263
267
  };
264
268
  }
265
- } catch {
266
- // Refresh failed; fall through to throw below
269
+ } catch (err) {
270
+ // Keep it: credentials existed and the controller turned them down, which is a
271
+ // different problem from having none, and the only place that fact is observable.
272
+ throwNoDeploymentAuth(credentialApp, err);
267
273
  }
268
274
  }
269
275
 
@@ -341,7 +347,7 @@ async function forceRefreshDeviceToken(controllerUrl) {
341
347
 
342
348
  // Must have refresh token to force refresh
343
349
  if (!tokenInfo.refreshToken) {
344
- logger.warn('Cannot refresh: no refresh token available. Please login again using: aifabrix login');
350
+ warnRefreshFailureOnce(controllerUrl, 'Cannot refresh: no refresh token available. Please login again using: aifabrix login');
345
351
  return null;
346
352
  }
347
353
 
@@ -1,18 +1,20 @@
1
1
  /**
2
- * Plan 117 URL path prefix for url://public (dev/tst only when gated).
2
+ * Plan 117 URL path prefix for url://public (scoped environments only when gated).
3
3
  *
4
- * @fileoverview baseEffective ∧ derived envKey ∈ {dev,tst}. Caller must pass prefix only when `traefik` is on (see url-declarative-resolve).
4
+ * @fileoverview baseEffective ∧ derived envKey ∈ SCOPED_RUN_ENVIRONMENTS. Caller must pass prefix only when `traefik` is on (see url-declarative-resolve).
5
5
  * @author AI Fabrix Team
6
6
  * @version 1.0.0
7
7
  */
8
8
 
9
9
  'use strict';
10
10
 
11
+ const { isScopedRunEnvironmentKey } = require('./environment-scoped-resources');
12
+
11
13
  /**
12
14
  * @param {boolean} useEnvironmentScopedResources - config gate
13
15
  * @param {boolean} appEnvironmentScopedResources - application.yaml
14
16
  * @param {string} derivedEnvKey - from client id (dev|tst|pro|miso)
15
- * @returns {string} '' | '/dev' | '/tst'
17
+ * @returns {string} '' | '/dev' | '/tst' | '/pro'
16
18
  */
17
19
  function computePublicUrlPathPrefix(useEnvironmentScopedResources, appEnvironmentScopedResources, derivedEnvKey) {
18
20
  const baseEffective = Boolean(useEnvironmentScopedResources) && Boolean(appEnvironmentScopedResources);
@@ -20,13 +22,7 @@ function computePublicUrlPathPrefix(useEnvironmentScopedResources, appEnvironmen
20
22
  return '';
21
23
  }
22
24
  const k = String(derivedEnvKey || '').toLowerCase();
23
- if (k === 'dev') {
24
- return '/dev';
25
- }
26
- if (k === 'tst') {
27
- return '/tst';
28
- }
29
- return '';
25
+ return isScopedRunEnvironmentKey(k) ? `/${k}` : '';
30
26
  }
31
27
 
32
28
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.61.2",
3
+ "version": "2.62.0",
4
4
  "description": "AI Fabrix Builder — CLI and developer scripts (pnpm + af)",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -35,11 +35,12 @@
35
35
  "lint:fix": "eslint . --ext .js --fix",
36
36
  "test": "node tests/scripts/test-wrapper.js",
37
37
  "build": "npm run lint && npm run test",
38
- "build:ci": "npm run lint && npm run check:schema-sync && npm run check:flags && npm run check:cli-help && npm run test:ci",
38
+ "build:ci": "npm run lint && npm run check:schema-sync && npm run check:flags && npm run check:cli-help && npm run check:mirrored-templates && npm run test:ci",
39
39
  "test:ci": "bash tests/scripts/ci-simulate.sh",
40
40
  "check:schema-sync": "node scripts/check-datasource-test-run-schema-sync.js",
41
41
  "check:flags": "jest tests/lib/schema/flag-map-validation-run.test.js --runInBand --config jest.config.default.js",
42
42
  "check:cli-help": "node scripts/check-cli-help-examples.js",
43
+ "check:mirrored-templates": "node scripts/check-mirrored-templates.js",
43
44
  "test:manual": "jest --config jest.config.manual.js --runInBand",
44
45
  "test:integration": "jest --config jest.config.integration.js --runInBand",
45
46
  "test:integration:fixtures": "jest --config jest.config.integration.fixtures.js --runInBand",
@@ -5,7 +5,7 @@ app:
5
5
  description: 'Business Transformation services — HTTP orchestration for connected-system workspace lifecycle via Dataplane Enterprise MCP and governance APIs.'
6
6
  type: webapp
7
7
  language: typescript
8
- version: 2.61.2
8
+ version: 2.62.0
9
9
 
10
10
  # Image Configuration
11
11
  image:
@@ -5,7 +5,7 @@ app:
5
5
  description: "AI Fabrix Dataplane is a secure, in-tenant integration and automation layer that supplies governed, normalized, and explainable enterprise data to AI agents. Using CIP as a declarative standard, it enforces RBAC and ABAC, executes integrations, and exposes trusted data via MCP and OpenAPI."
6
6
  type: webapp
7
7
  language: python # Explicitly specify Python language
8
- version: 2.0.79
8
+ version: 2.0.82
9
9
  # Image Configuration
10
10
  # Set tag to match your build (example: aifabrix build dataplane -t 1.0.0).
11
11
  # Registry is required so the controller can pull the image (avoids docker-not-found on the controller host).
@@ -26,7 +26,7 @@ frontDoorRouting:
26
26
  pattern: /data/*
27
27
  enabled: true
28
28
  # Docker url://internal full URLs: http://dataplane:PORT only (ingress /data is not on in-container routes).
29
- internalDockerUseOriginOnly: false
29
+ internalDockerUseOriginOnly: true
30
30
  host: ${DEV_USERNAME}.${REMOTE_HOST}
31
31
  tls: ${TLS_ENABLED}
32
32
  # Origin RFC 9728 PRM (outside /data PathPrefix) — Builder Traefik pass-through (plan 171.0).
@@ -44,7 +44,7 @@ API_KEY_BYPASS_ROLES=full-access
44
44
 
45
45
  # API Configuration
46
46
  API_V1_STR=/api/v1
47
- VERSION=2.0.79
47
+ VERSION=2.0.82
48
48
  # Minimum supported aifabrix-builder CLI semver (advertised on /health and /api/v1/health; empty = no gate)
49
49
  # MIN_BUILDER_CLI_VERSION=2.45.0
50
50
  MIN_BUILDER_CLI_VERSION=2.45.5
@@ -0,0 +1,91 @@
1
+ # AI Fabrix Builder app config — INTEGRATOR server type.
2
+ #
3
+ # Same code as builder/builder-server/, different installation. One source tree, two
4
+ # manifests: the Dockerfile's `integrator` target and this folder's env.template are the
5
+ # only things that differ, so there is no forked codebase to keep in step.
6
+ #
7
+ # Enterprise is the existing full development box with Docker. Integrator is a single
8
+ # container giving developers SSH-only access, with no Docker execution inside it.
9
+
10
+ app:
11
+ key: integration-server
12
+ displayName: AI Fabrix Integration Server
13
+ description: AI Fabrix Integration Server onboards developers over SSH into one container. It creates their Linux accounts and authorized keys, and gives them SSH-only access without Docker inside that container.
14
+ type: webapp
15
+ version: 2.0.10
16
+ image:
17
+ name: aifabrix/integration-server
18
+ tag: latest
19
+ registry: aifabrixacr.azurecr.io
20
+ registryMode: external
21
+ # Base 3006 keeps it clear of builder-server (3002), dataplane (3001), mori-controller
22
+ # (3004) and builder-api (3005), so both server types can run on one dev host.
23
+ # Builder derives the host port as `port` + developerId * 100 (dev01 -> 3106).
24
+ port: 3006
25
+ webSocketsEnabled: true
26
+ # This container administers its own operating system: it creates the developers'
27
+ # Linux accounts, writes /etc/ssh/authorized_keys.d and runs sshd. Builder otherwise
28
+ # runs containers as the invoking host user, which cannot do any of that.
29
+ runAsRoot: true
30
+ # Developers reach this installation over SSH, so that port has to be published as well
31
+ # as the HTTP one. The base keeps SSH's well-known number and the published port follows
32
+ # the same formula as the main port — 22 + developerId * 100 — so dev01 gets 122 and two
33
+ # developers on one host cannot collide. Bootstrap advertises it via SSH_PUBLIC_PORT.
34
+ additionalPorts:
35
+ - name: ssh
36
+ containerPort: 22
37
+ hostPort: 22
38
+ requires:
39
+ database: false
40
+ redis: false
41
+ storage: true
42
+ build:
43
+ language: typescript
44
+ context: ../..
45
+ dockerfile: builder/builder-server/Dockerfile
46
+ # Selects the Dockerfile's integrator stage. Without it Docker builds the last stage,
47
+ # which is enterprise — the same image under a different name. This is the one build
48
+ # difference between the two installations.
49
+ target: integrator
50
+ # No envOutputPath: this installation only runs in Docker, and Builder treats the
51
+ # value as a directory — pointing it at a file name creates a folder of that name.
52
+ #
53
+ # Reload runs the real entrypoint, not a bare watcher. The generated compose bind-mounts
54
+ # the repository over /app and clears the image ENTRYPOINT, which for this installation
55
+ # meant no sshd, no account-ops worker, and the API running as root — so nothing about
56
+ # SSH, provisioning or privilege separation worked in the mode developers actually use,
57
+ # and a queued account was never created. The entrypoint finds the build itself —
58
+ # /app/dist in the image, /app/builder-server/dist under the bind mount — because the
59
+ # compose template HTML-escapes `=`, so a VAR=value prefix never survives as an
60
+ # assignment.
61
+ #
62
+ # The API therefore starts from the host's build, so a code change is
63
+ # `npm run build --prefix builder-server` and a restart, not an in-container watch. That
64
+ # is deliberate: the watcher ran as root and left root-owned dist/ behind, which then
65
+ # broke the next `pnpm check` on the host.
66
+ reloadStart: exec /docker-entrypoint-integrator.sh
67
+ # Ingress. A distinct prefix from builder-server's /server/*, which is not cosmetic: the
68
+ # two installation types are alternatives in production, but a development host runs both,
69
+ # and two applications declaring the same host and prefix collide. Traefik then routes the
70
+ # prefix to whichever container was created last, so integrator-only routes answer 404
71
+ # from the enterprise container — a failure that looks like a bug in the route rather than
72
+ # a collision in the manifests.
73
+ #
74
+ # The pattern is a PathPrefix, not an allowlist: it forwards every path beneath it, so the
75
+ # application refuses anything else that arrives through the front door. Filtering at the
76
+ # ingress is still required and is tracked as open in 029 V4.
77
+ frontDoorRouting:
78
+ pattern: /integrator/*
79
+ enabled: true
80
+ host: ${DEV_USERNAME}.${REMOTE_HOST}
81
+ tls: ${TLS_ENABLED}
82
+ internalDockerUseOriginOnly: true
83
+ repository:
84
+ enabled: false
85
+ repositoryUrl: ''
86
+ deployment:
87
+ controllerUrl: ''
88
+ environment: dev
89
+ healthCheck:
90
+ path: /health
91
+ interval: 30
@@ -0,0 +1,215 @@
1
+ # Integration Server — INTEGRATOR installation
2
+ # =============================================================================
3
+ # A single managed container giving developers SSH-only access, with no Docker
4
+ # execution inside it. Identity and configuration are pushed in by Miso; secrets
5
+ # come from the secret store, not from this container's filesystem.
6
+ #
7
+ # The enterprise installation is a different manifest with its own template —
8
+ # builder/builder-server/env.template. Same code, different deployment; each
9
+ # template lists only the variables its own installation reads.
10
+ #
11
+ # Conventions (as used across AI Fabrix apps):
12
+ # kv://name secret resolved from the secret store — never a literal here
13
+ # url://name URL derived by Builder from the manifest
14
+ # ${VAR} value supplied per environment
15
+
16
+ # =============================================================================
17
+ # CORE
18
+ # =============================================================================
19
+
20
+ # HTTP listen port (main.ts). Default: 3006
21
+ PORT=3006
22
+
23
+ # SQLite DB, audit log and managed state.
24
+ #
25
+ # A subdirectory of the mount, not its root: this is a separate installation with its
26
+ # own database, users and identity model. On a dev host running both side by side the
27
+ # bind mount is shared, and pointing both at the root would have each read the other's
28
+ # database — which fails closed, because the recorded server type would not match.
29
+ DATA_DIR=/mnt/data/integrator
30
+
31
+ # Installation type (config/server-type.ts), resolved once at startup and then
32
+ # persisted in builder.db. This installation IS an integrator, so it is set
33
+ # explicitly: unset would mean enterprise. A populated installation cannot switch
34
+ # type on a restart — that needs a migration.
35
+ BUILDER_SERVER_TYPE=integrator
36
+
37
+ # Set false or 0 to disable DATA_DIR/audit.log (audit.service.ts). Default: true
38
+ # AUDIT_LOG_ENABLED=true
39
+
40
+ # =============================================================================
41
+ # SECRETS
42
+ # =============================================================================
43
+
44
+ # AES-GCM key for the secrets API, as a value from the secret store — this
45
+ # installation has no host filesystem to mount a key file from.
46
+ # 64 hex characters or 32 bytes.
47
+ #
48
+ # Generate a NEW key when installing this server. Never point this at the platform's
49
+ # own key — Miso's, or the enterprise Builder Server's ENCRYPTION_KEY_PATH file.
50
+ # Installing a customer's integrator with the platform key would let every developer
51
+ # with a shell in that container decrypt every other installation's secrets, because
52
+ # the same key is handed to each of them in their config.yaml. One installation, one
53
+ # key: the blast radius of a leaked key is then that installation and nothing else.
54
+ #
55
+ # openssl rand -hex 32
56
+ #
57
+ # The name follows the catalog's own `{appKey}-secrets-encryptionKeyVault` convention,
58
+ # so it is this application's app-scoped secret and nothing else's. The earlier name said
59
+ # `builder-server`: that is another application's key, and reading it from here would be
60
+ # a cross-application read that only works by being deliberately shared — which is the
61
+ # hole this avoids. Register it at install:
62
+ #
63
+ # aifabrix secret set integration-server-secrets-encryptionKeyVault "$(openssl rand -hex 32)"
64
+ #
65
+ # The same key is written into every managed developer's config.yaml as
66
+ # `secrets-encryption`, so the CLI can decrypt what this server encrypted. Those two
67
+ # are one value from one source by construction — see account-ops-worker.ts, which
68
+ # resolves it from this variable at the moment it writes the file. Rotating it means
69
+ # re-running configuration sync so the homes are rewritten; values already stored in
70
+ # the secrets table are encrypted with the old key and do not survive a rotation.
71
+ ENCRYPTION_KEY=kv://integration-server-secrets-encryptionKeyVault
72
+
73
+ # =============================================================================
74
+ # MISO
75
+ # =============================================================================
76
+ # Where identity and configuration come from. Builder Server knows Miso only: it
77
+ # never configures or verifies the identity provider — miso-client owns that.
78
+ # /health carries a `miso` check on this installation, and fails without these.
79
+
80
+ # Server-to-controller API calls on the environment network.
81
+ MISO_CONTROLLER_URL=url://miso-controller-internal
82
+
83
+ # Application client credentials, used to obtain an application token and to verify
84
+ # the client tokens on POST /api/integrations/miso/*.
85
+ MISO_CLIENTID=kv://integration-server-client-idKeyVault
86
+ MISO_CLIENTSECRET=kv://integration-server-client-secretKeyVault
87
+
88
+ # Reachable addresses reported to Miso after the HTTP listener starts. Miso uses the
89
+ # internal address for user/configuration delivery and exposes the public address in
90
+ # application status. Keep these derived from the manifest, like Dataplane's status
91
+ # registration; a typed hostname or port drifts in local developer environments.
92
+ INTEGRATION_SERVER_WEB_SERVER_URL=url://public
93
+ INTEGRATION_SERVER_INTERNAL_URL=url://internal
94
+
95
+ # The `environment` value written into each developer's ~/.aifabrix/config.yaml.
96
+ # `controller` and `codingPlatform` are pushed by Miso; this one is the
97
+ # installation's own deployment configuration and is never taken from the request.
98
+ MANAGED_CONFIG_ENVIRONMENT=dev
99
+ MISO_ENVIRONMENT=dev
100
+
101
+ # =============================================================================
102
+ # DEVELOPER ACCESS — SSH
103
+ # =============================================================================
104
+
105
+ # SSH endpoint returned to enrolling developers by POST /api/bootstrap/ssh.
106
+ #
107
+ # This is what the client is told to connect to, so it must be the address that is
108
+ # actually reachable, not the container's own view. Both are derived rather than typed:
109
+ # the host from the front door, and the port from the `ssh` entry in the manifest's
110
+ # `additionalPorts` (Builder publishes it as SSH_PUBLIC_PORT = hostPort + developerId *
111
+ # 100). Typing either by hand drifts from the manifest and advertises an endpoint
112
+ # nothing is listening on.
113
+ INTEGRATOR_SSH_HOST=url://public
114
+ INTEGRATOR_SSH_PORT=${SSH_PUBLIC_PORT}
115
+
116
+ # Where sshd reads each developer's authorized keys from.
117
+ #
118
+ # State, not configuration: it records which key each developer enrolled. Kept in the
119
+ # image it was destroyed by every redeploy, and although reconciliation rebuilds it from
120
+ # the database at startup, a developer reconnecting before that finishes is refused.
121
+ # On the volume the file is already correct when sshd starts.
122
+ #
123
+ # Root-owned, inside a directory developers cannot write to.
124
+ # Default: ${DATA_DIR}/authorized_keys.d
125
+ # AUTHORIZED_KEYS_DIR=/mnt/data/integrator/authorized_keys.d
126
+
127
+ # Root of the managed developer home directories.
128
+ #
129
+ # Must be on the persistent volume. A managed home is not scratch space: it holds the
130
+ # developer's cloned repositories and build trees, the credentials their agent uses, the
131
+ # managed config.yaml and the client certificate this server issued. An agent run takes
132
+ # hours and outlives any single connection, so the work has to survive a redeploy.
133
+ #
134
+ # Inside the image it did not. Every redeploy destroyed the homes, which silently revoked
135
+ # access as well as losing the work: the Linux accounts and their authorized_keys went
136
+ # with them, while the projection in builder.db still said the developer was entitled.
137
+ #
138
+ # The entrypoint creates this root 755 and grants traverse on the path leading to it;
139
+ # each developer's own home inside is 700 and owned by them.
140
+ # Default: ${DATA_DIR}/home
141
+ # DEVELOPER_HOME_ROOT=/mnt/data/integrator/home
142
+
143
+ # Restores keys even though the projection has gone backwards.
144
+ #
145
+ # This installation records the highest Miso revision it has ever seen, outside the
146
+ # database, so restoring builder.db from before a revocation is detectable: the mark is
147
+ # higher than anything the projection now holds. When that happens the installation fails
148
+ # closed — every developer is reconciled to no keys until Miso re-synchronises a revision
149
+ # at least as high as the one already seen. Homes are retained and the server still boots,
150
+ # so it can be repaired.
151
+ #
152
+ # A database rebuilt deliberately looks identical to one restored from before a
153
+ # revocation, so this is the way past it. Set it only when you know the rebuild was
154
+ # intended; it is logged when used. Only the exact value `true` is accepted.
155
+ # ONBOARDING_ALLOW_BACKWARDS_PROJECTION=true
156
+
157
+ # =============================================================================
158
+ # CLIENT DOWNLOAD
159
+ # =============================================================================
160
+ # What GET /client/<target>.zip writes into the aifabrix.json beside the executable.
161
+ #
162
+ # The executable itself is identical for every installation — SmartScreen builds
163
+ # reputation against a file hash, so a per-customer binary would never accumulate any.
164
+ # The customer scope travels in this small file instead.
165
+
166
+ # Platform origin the client posts its SSH key to. Only the origin is used; the client
167
+ # appends its own path, so a value carrying /server would double it.
168
+ CLIENT_PLATFORM_URL=url://public
169
+
170
+ # Controller the client signs in against, as a browser reaches it.
171
+ CLIENT_CONTROLLER_URL=url://miso-controller-public
172
+
173
+ # Shown while onboarding, e.g. "Acme Corp". Optional.
174
+ # CLIENT_DISPLAY_NAME=
175
+
176
+ # How this installation is reached: `ssh` or `wss`. Exactly one is served.
177
+ #
178
+ # ssh Developers reach sshd over a private network overlay, which is the only route to
179
+ # the port — this installation must not publish 22 publicly. Deactivating a person
180
+ # in the overlay removes their route, and that is what ends access, including for
181
+ # sessions already open. The tunnel endpoint does not exist.
182
+ #
183
+ # wss There is no overlay: the session travels inside a WebSocket on the published
184
+ # HTTPS port, and sshd listens on loopback only, so the tunnel is the only way in.
185
+ # This server does the authorizing — a short-lived token at the open, refreshed
186
+ # over the tunnel while the developer works, and dropped on idle or revocation.
187
+ #
188
+ # Not both. Publishing 22 alongside the tunnel would make the overlay stop being a
189
+ # control, and running both against one sshd means "does this person still have access?"
190
+ # depends on which door they used — with the door that fails open deciding.
191
+ #
192
+ # Stated by the server because the server knows how it is published; the client reads it
193
+ # from aifabrix.json and never has to guess. Guessing wrong fails as a timeout, which is
194
+ # the least diagnosable failure there is.
195
+ CLIENT_TRANSPORT=wss
196
+
197
+ # =============================================================================
198
+ # FRONT DOOR
199
+ # =============================================================================
200
+ # The application is published whole at its virtual directory, the same way
201
+ # miso-controller serves /miso and dataplane serves /data. What is reachable from
202
+ # outside is decided by `frontDoorRouting` in application.yaml and by the ingress.
203
+
204
+ # Injected by Builder from `frontDoorRouting.pattern` in application.yaml; listed
205
+ # because the app reads it. Routes are served unprefixed — the ingress strips the
206
+ # prefix before the request arrives — so this only builds URLs handed to clients.
207
+ # BASE_PATH=/integrator
208
+ # X_FORWARDED_PREFIX=/integrator
209
+
210
+ # =============================================================================
211
+ # TESTING
212
+ # =============================================================================
213
+
214
+ # Enables POST /api/dev/e2e-reset. Never set in production.
215
+ # E2E_RESET_SECRET=
@@ -0,0 +1,52 @@
1
+ {
2
+ "mirrors": [
3
+ {
4
+ "path": "templates/applications/miso-controller/env.template",
5
+ "master": {
6
+ "repo": "aifabrix-miso",
7
+ "path": "builder/miso-controller/env.template"
8
+ },
9
+ "sha256": "9b74b94d5d6e7457d22cfe213f2ec9c3dceb75c540df3953aba3b51c35df05c7"
10
+ },
11
+ {
12
+ "path": "templates/applications/builder-api/env.template",
13
+ "master": {
14
+ "repo": "aifabrix-miso",
15
+ "path": "builder/builder-api/env.template"
16
+ },
17
+ "sha256": "1a6b013aa0a34b72c5a0be3dd714bd718a8adb981bcbb9de3d5370e2dd33786c"
18
+ },
19
+ {
20
+ "path": "templates/applications/keycloak/env.template",
21
+ "master": {
22
+ "repo": "aifabrix-miso",
23
+ "path": "builder/keycloak/env.template"
24
+ },
25
+ "sha256": "72de7dc5345b842bc20566f8d5105b6541e8185dff157e9af2d37db9590b01fb"
26
+ },
27
+ {
28
+ "path": "templates/applications/dataplane/env.template",
29
+ "master": {
30
+ "repo": "aifabrix-dataplane",
31
+ "path": "builder/dataplane/env.template"
32
+ },
33
+ "sha256": "d35eda839464a65ebd018064ea7c2d292654891b41d1f56862b052720a78a3cc"
34
+ },
35
+ {
36
+ "path": "templates/applications/integration-server/env.template",
37
+ "master": {
38
+ "repo": "aifabrix-setup",
39
+ "path": "builder/integration-server/env.template"
40
+ },
41
+ "sha256": "a80584bcf6880937c43557a71ca1c423104ab4e15badf4bb14a0a9a9dcaa01d6"
42
+ },
43
+ {
44
+ "path": "templates/applications/integration-server/application.yaml",
45
+ "master": {
46
+ "repo": "aifabrix-setup",
47
+ "path": "builder/integration-server/application.yaml"
48
+ },
49
+ "sha256": "04cbfdb9a012393a5e97a1dda1b8f304e97c9532339f045e5912cd250a26a179"
50
+ }
51
+ ]
52
+ }
@@ -4,7 +4,7 @@ app:
4
4
  displayName: 'Miso Controller'
5
5
  description: 'Miso is the AI Fabrix in-tenant controller and portal layer for securely operating enterprise AI apps inside a customer’s Azure tenant. It provides Entra ID SSO, RBAC, audit logs, environment/app configuration via schemas, and safe secret handling via Key Vault references—ensuring governance, traceability, and predictable UX across portal, SDK, and CLI.'
6
6
  type: webapp
7
- version: '2.0.89'
7
+ version: '2.0.91'
8
8
  # Image Configuration
9
9
  # Set tag to match your build (e.g. aifabrix build miso-controller -t 1.0.0 then tag: 1.0.0)
10
10
  # Registry is required so the controller can pull the image (avoids "docker: not found" on the controller host).
@@ -92,6 +92,8 @@ platforms:
92
92
  configuration:
93
93
  - name: LOCATION
94
94
  value: 'swedencentral'
95
+ - name: CLOUD_VENDOR
96
+ value: 'azure'
95
97
  - name: MORI_BASE_URL
96
98
  value: 'https://platform.aifabrix.dev/mori'
97
99
 
@@ -133,6 +135,42 @@ platforms:
133
135
  value: 'swedencentral'
134
136
  - name: MORI_BASE_URL
135
137
  value: 'url://mori-controller-internal'
138
+ # Declared, because the fallback infers it and infers it wrongly here:
139
+ # `resolveCloudVendor` returns 'azure' whenever AZURE_SUBSCRIPTION_ID is set, and every
140
+ # developer box has it from the shared development service principal. A local stack was
141
+ # therefore treated as an Azure installation, `verifyInstallationBinding` took
142
+ # `verifyAzureBinding`, and that demands a directory tenant AND a resource group. A
143
+ # developer box has no resource group, so the binding refused with
144
+ # "Azure licence binding requires the tenant ID and resource group." The licence guard
145
+ # turns that into HTTP 403 on every guarded operation — including
146
+ # `app rotate-secret`, which is the one command that recovers a re-minted credential.
147
+ #
148
+ # Measured on dev01 2026-10-04: the dataplane could not start (its client secret had been
149
+ # re-minted, plan 264.0) and the recovery command was itself refused. The two defects
150
+ # compose into a dead end that no developer can get out of.
151
+ - name: CLOUD_VENDOR
152
+ value: 'docker'
153
+ # The one installation name the licence binds to (plan 266.0). Sent to Mori as
154
+ # `resourceGroupName`; docker requires exactly `local-dev{NN}` or registration refuses.
155
+ # Only on this entry — the Azure platforms below must not be `local-*` and keep SERVICE_NAME.
156
+ - name: MISO_INSTALLATION_NAME
157
+ value: 'local-${DEV_USERNAME}'
158
+ # Plan 262.0: a local stack onboards as SaaS, so the full activation cycle is reachable here.
159
+ #
160
+ # `requiresTenantActivationWizard()` is `isAzureDeployment() || isAzureMockDeployment() ||
161
+ # isSaasDirectOnboarding()`. With `customerOwned` all three are false on a dev box, so the
162
+ # wizard never ran — and the wizard is what collects the customer directory tenant and
163
+ # records consent. Without it the tenant is only ever read from the environment, which is the
164
+ # in-tenant assumption and wrong for SaaS.
165
+ #
166
+ # This is the ONBOARDING axis, deliberately not the DEPLOYMENT axis. `DEPLOYMENT` stays
167
+ # `database` (DB-only; no Docker, no Azure), which `isLocalOnboardingDeployment()` already
168
+ # treats as local. `DEPLOYMENT=local` means Miso orchestrating Docker itself, which is not
169
+ # ready and is not what a dev box runs — keying this on it would never fire.
170
+ #
171
+ # Only on this entry: the Azure platforms below stay `customerOwned`.
172
+ - name: ONBOARDING_MODE
173
+ value: 'saasDirect'
136
174
 
137
175
  - resource: aifabrix-int-{platform}
138
176
  name: aifabrix
@@ -140,6 +178,8 @@ platforms:
140
178
  configuration:
141
179
  - name: LOCATION
142
180
  value: 'swedencentral'
181
+ - name: CLOUD_VENDOR
182
+ value: 'azure'
143
183
  # Mori the installation talks to. Previously `kv://mori-controller-web-server-url`, which
144
184
  # resolved to whatever the *application record* held — on aifabrix-ce-16 that was
145
185
  # `http://localhost:3004`, because an Azure deployment never overwrites a stale URL, so every
@@ -154,6 +194,8 @@ platforms:
154
194
  configuration:
155
195
  - name: LOCATION
156
196
  value: 'swedencentral'
197
+ - name: CLOUD_VENDOR
198
+ value: 'azure'
157
199
  # The App Service hostname, not the Front Door one, because only this is derivable.
158
200
  #
159
201
  # The Front Door endpoint carries an Azure-generated suffix (`fzahchbachcfbgaj`) that no