@aifabrix/builder 2.56.0 → 2.58.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 (74) hide show
  1. package/lib/agent-kit/session-report.js +76 -0
  2. package/lib/agent-kit/start.js +124 -57
  3. package/lib/agent-kit/tmux-sessions.js +163 -0
  4. package/lib/api/environments.api.js +38 -0
  5. package/lib/api/marketplace-bootstrap.api.js +31 -0
  6. package/lib/app/push.js +98 -41
  7. package/lib/build/standard-docker-build.js +13 -11
  8. package/lib/cli/setup-app.js +16 -5
  9. package/lib/cli/setup-dev-path-commands.js +49 -2
  10. package/lib/cli/setup-environment.js +89 -19
  11. package/lib/commands/agent-kit-help.js +32 -15
  12. package/lib/commands/agent-kit.js +90 -8
  13. package/lib/commands/dev-registry.js +155 -0
  14. package/lib/commands/dev-show-display.js +10 -2
  15. package/lib/commands/setup-image-refresh.js +26 -0
  16. package/lib/core/config-attach-extensions.js +4 -1
  17. package/lib/deployment/installation/azure-infra-stage.js +191 -0
  18. package/lib/deployment/installation/azure-preflight.js +147 -0
  19. package/lib/deployment/installation/azure-readiness.js +101 -0
  20. package/lib/deployment/installation/docker-register.js +112 -0
  21. package/lib/deployment/installation/environment-auth.js +76 -0
  22. package/lib/deployment/installation/environment-manifest.js +165 -0
  23. package/lib/deployment/installation/environment-stage.js +191 -0
  24. package/lib/deployment/installation/index.js +39 -0
  25. package/lib/deployment/installation/infra-catalog.js +211 -0
  26. package/lib/deployment/installation/infra-manifest.js +176 -0
  27. package/lib/deployment/installation/infra-projection.js +97 -0
  28. package/lib/deployment/installation/public-ip.js +65 -0
  29. package/lib/deployment/installation/registry-auth-mode.js +98 -0
  30. package/lib/deployment/installation/stage-input.js +184 -0
  31. package/lib/deployment/installation/stage-output.js +53 -0
  32. package/lib/deployment/installation/workspace-env.js +76 -0
  33. package/lib/deployment/installation/workspace-files.js +178 -0
  34. package/lib/deployment/installation/workspace-schema.js +40 -0
  35. package/lib/deployment/installation/workspace-secrets.js +66 -0
  36. package/lib/deployment/installation/workspace.js +128 -0
  37. package/lib/deployment/marketplace-environment.js +305 -0
  38. package/lib/generator/builders.js +12 -9
  39. package/lib/generator/helpers.js +55 -56
  40. package/lib/generator/split-variables.js +11 -5
  41. package/lib/internal/fs-real-sync.js +29 -0
  42. package/lib/role-assistant/package-validate.js +4 -0
  43. package/lib/role-assistant/test-runner-clarification-resubmit.js +149 -0
  44. package/lib/role-assistant/test-runner.js +14 -17
  45. package/lib/schema/application-schema.json +20 -2
  46. package/lib/schema/infra-parameter.schema.json +7 -0
  47. package/lib/schema/infra.parameter.yaml +26 -36
  48. package/lib/schema/infrastructure-schema.json +1531 -397
  49. package/lib/schema/installation-environment.schema.json +601 -0
  50. package/lib/utils/app-register-config.js +2 -4
  51. package/lib/utils/build-resolve-image.js +2 -21
  52. package/lib/utils/cli-utils.js +8 -1
  53. package/lib/utils/config-registry-preference.js +106 -0
  54. package/lib/utils/docker-build.js +53 -19
  55. package/lib/utils/env-map.js +23 -6
  56. package/lib/utils/image-tags.js +88 -0
  57. package/lib/utils/promote-dev-docker-image.js +6 -0
  58. package/lib/utils/registry-credentials.js +236 -0
  59. package/lib/utils/resolve-docker-image-ref.js +78 -9
  60. package/lib/utils/secrets-helpers.js +9 -1
  61. package/lib/utils/variable-transformer.js +5 -4
  62. package/lib/validation/required-variables.js +41 -0
  63. package/lib/validation/validator.js +8 -5
  64. package/package.json +6 -1
  65. package/templates/README.md +4 -0
  66. package/templates/agent-kit/instructions/AGENTKIT.md +6 -6
  67. package/templates/agent-kit/skills/shared/hosts.md +10 -10
  68. package/templates/applications/dataplane/application.yaml +1 -1
  69. package/templates/applications/dataplane/env.template +1 -1
  70. package/templates/applications/miso-controller/application.yaml +3 -2
  71. package/templates/applications/miso-controller/env.template +15 -6
  72. package/templates/marketplace/createUiDefinition.json +77 -0
  73. package/templates/marketplace/install.parameters.example.json +9 -0
  74. package/templates/marketplace/main.json +4087 -0
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Resolve Docker repository path and tag from application config and optional CLI overrides.
3
- * Precedence: --image (full ref), --registry CLI, image.registry in manifest, else unqualified name.
3
+ * Precedence: --image (full ref), --registry CLI, installation `registry` in config.yaml,
4
+ * image.registry in manifest, else unqualified name.
4
5
  *
5
6
  * For refs like localhost:5000/repo without an explicit tag, prefer --image with :tag (parse ambiguity).
6
7
  *
@@ -12,6 +13,7 @@
12
13
  'use strict';
13
14
 
14
15
  const { parseImageOverride } = require('./parse-image-ref');
16
+ const { readInstallationRegistrySync } = require('./config-registry-preference');
15
17
  const DOCKER_REPOSITORY_RE = /^[a-z0-9][a-z0-9._/-]*$/i;
16
18
  const DOCKER_TAG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
17
19
 
@@ -92,6 +94,73 @@ function normalizeDockerRegistryPrefix(registry) {
92
94
  return t.slice(0, end);
93
95
  }
94
96
 
97
+ /**
98
+ * Remove a registry host prefix from a Docker repository path.
99
+ * The first segment is a host when it contains `.` or `:` or equals `localhost`.
100
+ * @param {string} repository
101
+ * @returns {string}
102
+ */
103
+ function stripRegistryHostFromRepository(repository) {
104
+ if (!repository || typeof repository !== 'string') {
105
+ return repository;
106
+ }
107
+ const parts = repository.split('/');
108
+ if (parts.length <= 1) {
109
+ return repository;
110
+ }
111
+ const host = parts[0];
112
+ if (host.includes('.') || host.includes(':') || host === 'localhost') {
113
+ return parts.slice(1).join('/');
114
+ }
115
+ return repository;
116
+ }
117
+
118
+ /**
119
+ * Effective registry for an application image.
120
+ * Precedence: `options.registry` (CLI) > installation `registry` > manifest `image.registry`.
121
+ * @param {Object} [appConfig] - Application manifest
122
+ * @param {Object} [options] - `registry` (CLI) and optional `installationRegistry` (preloaded; '' = none)
123
+ * @returns {{ registry: string, source: 'cli'|'installation'|'manifest'|'none' }}
124
+ */
125
+ function resolveEffectiveRegistry(appConfig, options = {}) {
126
+ const opts = options || {};
127
+ const cli = normalizeDockerRegistryPrefix(opts.registry);
128
+ if (cli) return { registry: cli, source: 'cli' };
129
+ const installation = opts.installationRegistry !== undefined
130
+ ? normalizeDockerRegistryPrefix(opts.installationRegistry)
131
+ : readInstallationRegistrySync();
132
+ if (installation) return { registry: installation, source: 'installation' };
133
+ const manifest = normalizeDockerRegistryPrefix(appConfig?.image?.registry ?? '');
134
+ if (manifest) return { registry: manifest, source: 'manifest' };
135
+ return { registry: '', source: 'none' };
136
+ }
137
+
138
+ /**
139
+ * Prefix a repository with a registry exactly once (replacing any existing host).
140
+ * @param {string} registry - Registry prefix ('' keeps the repository unchanged)
141
+ * @param {string} repository - Repository path, possibly already qualified
142
+ * @returns {string}
143
+ */
144
+ function qualifyRepository(registry, repository) {
145
+ const prefix = normalizeDockerRegistryPrefix(registry);
146
+ if (!prefix) return repository;
147
+ return `${prefix}/${stripRegistryHostFromRepository(repository)}`;
148
+ }
149
+
150
+ /**
151
+ * Deployment/registration image reference (`registry/name:tag`) using the shared precedence.
152
+ * @param {Object} variables - Application manifest
153
+ * @param {string} fallbackName - Name when the manifest has no image name or app key
154
+ * @param {Object} [options] - Same options as {@link resolveEffectiveRegistry}
155
+ * @returns {string}
156
+ */
157
+ function buildManifestImageReference(variables, fallbackName, options = {}) {
158
+ const imageName = variables?.image?.name || variables?.app?.key || fallbackName;
159
+ const tag = variables?.image?.tag || 'latest';
160
+ const { registry } = resolveEffectiveRegistry(variables, options);
161
+ return `${qualifyRepository(registry, imageName)}:${tag}`;
162
+ }
163
+
95
164
  /**
96
165
  * Effective image repository (may include registry prefix) and tag for Docker.
97
166
  * @param {string} appName - Application name
@@ -118,13 +187,8 @@ function resolveDockerImageRef(appName, appConfig, runOptions = {}) {
118
187
 
119
188
  const baseRepo = assertSafeRepository(getRepositoryPathFromConfig(appConfig, appName));
120
189
  const imageTag = assertSafeTag(tagFromOpts || imageTagFromConfig(appConfig));
121
- const prefix =
122
- normalizeDockerRegistryPrefix(opts.registry) ||
123
- normalizeDockerRegistryPrefix(appConfig?.image?.registry ?? '');
124
- if (prefix) {
125
- return { imageName: assertSafeRepository(`${prefix}/${baseRepo}`), imageTag };
126
- }
127
- return { imageName: baseRepo, imageTag };
190
+ const { registry } = resolveEffectiveRegistry(appConfig, opts);
191
+ return { imageName: assertSafeRepository(qualifyRepository(registry, baseRepo)), imageTag };
128
192
  }
129
193
 
130
194
  /**
@@ -137,7 +201,7 @@ function resolveDockerImageRef(appName, appConfig, runOptions = {}) {
137
201
  function resolveComposeImageOverrideString(appName, appConfig, options = {}) {
138
202
  if (options.image) return options.image;
139
203
  if (options.imageOverride) return options.imageOverride;
140
- const runOpts = { registry: options.registry, image: undefined };
204
+ const runOpts = { registry: options.registry, installationRegistry: options.installationRegistry, image: undefined };
141
205
  if (options.tag) {
142
206
  const { imageName } = resolveDockerImageRef(appName, appConfig, runOpts);
143
207
  return `${imageName}:${options.tag}`;
@@ -154,6 +218,11 @@ function resolveComposeImageOverrideString(appName, appConfig, options = {}) {
154
218
  module.exports = {
155
219
  resolveDockerImageRef,
156
220
  resolveComposeImageOverrideString,
221
+ resolveEffectiveRegistry,
222
+ qualifyRepository,
223
+ buildManifestImageReference,
224
+ stripRegistryHostFromRepository,
225
+ assertSafeTag,
157
226
  normalizeDockerRegistryPrefix,
158
227
  getRepositoryPathFromConfig,
159
228
  imageTagFromConfig
@@ -29,7 +29,15 @@ const { collectUniqueKvPathStrings, isCommentOrEmptyLine, activeEnvLineForKvScan
29
29
  * @returns {string} Interpolated content
30
30
  */
31
31
  function interpolateEnvVars(content, envVars) {
32
- return content.replace(/\$\{([A-Z_]+)\}/g, (match, envVar) => {
32
+ const hasOwn = (key) => Object.prototype.hasOwnProperty.call(envVars, key);
33
+ const interpolated = content.replace(
34
+ /\$\{DEV_USERNAME\}\.\$\{REMOTE_HOST\}/g,
35
+ (match) => {
36
+ if (!hasOwn('DEV_USERNAME') || !hasOwn('REMOTE_HOST')) return match;
37
+ return [envVars.DEV_USERNAME, envVars.REMOTE_HOST].filter(Boolean).join('.');
38
+ }
39
+ );
40
+ return interpolated.replace(/\$\{([A-Z_]+)\}/g, (match, envVar) => {
33
41
  return envVars[envVar] || match;
34
42
  });
35
43
  }
@@ -126,10 +126,8 @@ function transformFlatStructure(variables, appName) {
126
126
  * @returns {string} Image reference string
127
127
  */
128
128
  function buildImageReference(variables, appName) {
129
- const imageName = variables.image?.name || variables.app?.key || appName;
130
- const registry = variables.image?.registry;
131
- const tag = variables.image?.tag || 'latest';
132
- return registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}`;
129
+ const { buildManifestImageReference } = require('./resolve-docker-image-ref');
130
+ return buildManifestImageReference(variables, appName);
133
131
  }
134
132
 
135
133
  /**
@@ -301,6 +299,9 @@ function transformSimpleOptionalFields(variables, transformed) {
301
299
  if (variables.externalIntegration) {
302
300
  transformed.externalIntegration = variables.externalIntegration;
303
301
  }
302
+ if (variables.required !== undefined) {
303
+ transformed.required = variables.required;
304
+ }
304
305
  }
305
306
 
306
307
  /**
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @fileoverview Semantic checks for the application top-level `required` variable list:
3
+ * every name must be declared by the manifest configuration and have a resolvable source.
4
+ * Shape (array, unique, name pattern) is enforced by application-schema.json.
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ function hasResolvableValue(item) {
12
+ if (item.location === 'keyvault') return typeof item.value === 'string' && item.value !== '';
13
+ return (typeof item.value === 'string' && item.value !== '') || Boolean(item.portalInput);
14
+ }
15
+
16
+ /**
17
+ * Validates top-level `required` names against the deployment manifest configuration.
18
+ * @param {Object} manifest - Deployment manifest (application schema shape)
19
+ * @returns {string[]} Error messages (empty when valid or when `required` is absent)
20
+ */
21
+ function validateRequiredVariables(manifest) {
22
+ const required = manifest && manifest.required;
23
+ if (!Array.isArray(required) || required.length === 0) return [];
24
+ const byName = new Map();
25
+ for (const item of Array.isArray(manifest.configuration) ? manifest.configuration : []) {
26
+ if (item && typeof item.name === 'string') byName.set(item.name, item);
27
+ }
28
+ const errors = [];
29
+ for (const name of required) {
30
+ if (typeof name !== 'string') continue;
31
+ const item = byName.get(name);
32
+ if (!item) {
33
+ errors.push(`required: ${name} is not declared in the application configuration (env.template)`);
34
+ } else if (!hasResolvableValue(item)) {
35
+ errors.push(`required: ${name} has no value or portal input, so it cannot resolve for the target`);
36
+ }
37
+ }
38
+ return errors;
39
+ }
40
+
41
+ module.exports = { validateRequiredVariables };
@@ -24,6 +24,7 @@ const { loadConfigFile } = require('../utils/config-format');
24
24
  const { validateAuthKvCoverage, validateAuthSecurityPathConsistency } = require('./env-template-auth');
25
25
  const { validateKvReferencesInLines } = require('./env-template-kv');
26
26
 
27
+ const { validateRequiredVariables } = require('./required-variables');
27
28
  /**
28
29
  * Validates application config file against application schema
29
30
  * Provides detailed error messages for configuration issues
@@ -366,13 +367,15 @@ function validateDeploymentJson(deployment) {
366
367
  ajv.addSchema(externalDataSourceSchemaCopy, externalDataSourceSchema.$id);
367
368
  const validate = ajv.compile(applicationSchema);
368
369
  const valid = validate(deployment);
370
+ const schemaErrors = valid ? [] : formatValidationErrors(validate.errors, {
371
+ deploymentManifest: deployment,
372
+ rootData: deployment
373
+ });
374
+ const requiredErrors = validateRequiredVariables(deployment);
369
375
 
370
376
  return {
371
- valid,
372
- errors: valid ? [] : formatValidationErrors(validate.errors, {
373
- deploymentManifest: deployment,
374
- rootData: deployment
375
- })
377
+ valid: valid && requiredErrors.length === 0,
378
+ errors: [...schemaErrors, ...requiredErrors]
376
379
  };
377
380
  }
378
381
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.56.0",
3
+ "version": "2.58.0",
4
4
  "description": "AI Fabrix Builder — CLI and developer scripts (pnpm + af)",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -24,6 +24,11 @@
24
24
  "dev-deploy": "node scripts/pnpm/dev-deploy.mjs",
25
25
  "af": "node scripts/pnpm/af.mjs",
26
26
  "check-quiet": "node scripts/pnpm/check-quiet.mjs",
27
+ "agent-skills:sync": "node scripts/pnpm/agent-skills-sync.mjs",
28
+ "agent-skills:check": "node scripts/pnpm/agent-skills-sync.mjs --check",
29
+ "agent-skills:sync:codex": "node scripts/pnpm/agent-skills-sync.mjs --codex-home",
30
+ "agent-skills:test": "node --test scripts/pnpm/agent-skills-sync.test.mjs",
31
+ "dev-deploy:test": "node --test scripts/pnpm/dev-deploy.test.mjs",
27
32
  "check": "npm run lint && npm run test",
28
33
  "lint": "eslint . --ext .js",
29
34
  "lint:fix": "eslint . --ext .js --fix",
@@ -22,12 +22,16 @@ Application templates are folder-based and located under `templates/applications
22
22
  | `aifabrix-builder/builder/builder-api/` | `templates/applications/builder-api/` |
23
23
  | `aifabrix-miso/builder/miso-controller/` (`env.template`, `rbac.yaml`, `application.yaml`) | `templates/applications/miso-controller/` |
24
24
  | `aifabrix-miso/builder/keycloak/` (`env.template`, `application.yaml`, `Dockerfile`, `cimd-lenient-parse/`) | `templates/applications/keycloak/` |
25
+ | `aifabrix-miso/infrastructure/marketplace/` (`main.json`, `createUiDefinition.json`) | `templates/marketplace/` |
25
26
  | `aifabrix-dataplane/builder/dataplane/` (`env.template`, `rbac.yaml`, `application.yaml`) | `templates/applications/dataplane/` |
26
27
 
27
28
  **Schema master (dataplane):** Also copies dataplane `app/schemas/json/` core integration schemas + `type/*` and `configs/deployment-rules.yaml` into `lib/schema/` (same contract as dataplane `make sync-json`). Do not hand-edit those mirrors — edit dataplane SSOT, then sync. Builder-owned schemas in `lib/schema/` (wizard, infra, etc.) are left alone.
28
29
 
29
30
  Overrides: `AIFABRIX_MISO_SRC`, `AIFABRIX_DATAPLANE_SRC` / `AIFABRIX_DATAPLANE_ROOT` (repo roots). Install with `aifabrix up-platform` / `up-builder-api` — do not hand-edit the template copies as primary.
30
31
 
32
+ The Marketplace copies are compiled/source artifacts for a future Azure install command. `main.parameters.json` is intentionally excluded because installation secrets and customer-specific values must come from the installer's secure parameter file.
33
+ The two-stage CLI procedure and a blank parameter-file example are in [docs/marketplace-env-deploy.md](../docs/marketplace-env-deploy.md).
34
+
31
35
  **Template Validation:**
32
36
  - Template folder must exist in `templates/applications/<name>/`
33
37
  - Template folder must contain at least one file
@@ -57,13 +57,13 @@ curl -fsSL https://claude.ai/install.sh | bash
57
57
  claude auth login
58
58
  ```
59
59
 
60
- **Run in tmux** from this workspace so closing the terminal does not kill Claude. `--remote-control` is the **same** session on this PC, [claude.ai/code](https://claude.ai/code), and the Claude mobile app (Code). That is not `claude --cloud` (a separate cloud VM). Claude `--tmux` is for worktrees only.
60
+ **Run in tmux** from this workspace so closing the terminal does not kill Claude. The command enables Remote Control and this workspace's Claude auto-approval by default. Remote Control is the **same** session on this PC, [claude.ai/code](https://claude.ai/code), and the Claude mobile app (Code). That is not `claude --cloud` (a separate cloud VM). Claude `--tmux` is for worktrees only.
61
61
 
62
62
  ```bash
63
- aifabrix agent-kit start claude --remote-control --approve-all --yes
63
+ aifabrix agent-kit start claude
64
64
  ```
65
65
 
66
- tmux session name defaults to this folder. Remote Control name is `hostname-user-folder` (for example `builder01-dev03-aifabrix-docs`). Override the folder with `--name`. Detach: `Ctrl-b` then `d`. Reattach: `tmux attach -t <folder>`. Several workspaces can run at once. Same account on phone and PC (`claude auth login`, Pro/Max/Team — not an API key). On the phone: Claude app → **Code**. Skills: `/aifabrix-plan` (and the other `/aifabrix-*` names). First `claude` must run inside this workspace, not your home directory. Full notes: `.agents/skills/shared/hosts.md`.
66
+ tmux session name defaults to this folder. Remote Control name is `hostname-user-folder` (for example `builder01-dev03-aifabrix-docs`). Override with `--name`; opt out with `--no-remote-control` and/or `--skip-approve-all`. Detach: `Ctrl-b` then `d`. Reattach: `tmux attach -t <folder>`. Use `aifabrix agent-kit list` and `agent-kit kill <folder>` to manage sessions. Same account on phone and PC (`claude auth login`, Pro/Max/Team — not an API key). On the phone: Claude app → **Code**. Skills: `/aifabrix-plan` (and the other `/aifabrix-*` names). First `claude` must run inside this workspace, not your home directory. Full notes: `.agents/skills/shared/hosts.md`.
67
67
  {{/if}}
68
68
  {{#if codex}}
69
69
 
@@ -76,13 +76,13 @@ npm install -g @openai/codex
76
76
  codex login
77
77
  ```
78
78
 
79
- **Run in tmux** from this workspace so the session stays if you close the terminal:
79
+ **Run in tmux** from this workspace so the session stays if you close the terminal. The command starts native Codex Remote Control and writes this workspace's Codex auto-approval by default:
80
80
 
81
81
  ```bash
82
- aifabrix agent-kit start codex --approve-all --yes
82
+ aifabrix agent-kit start codex
83
83
  ```
84
84
 
85
- Detach: `Ctrl-b` then `d`. Reattach: `tmux attach -t <folder>`, or `aifabrix agent-kit start codex` from this workspace. Default session name is this folder. Skills: `$aifabrix-plan` (and the other `$aifabrix-*` names). Codex reads `AGENTS.md`. Full notes: `.agents/skills/shared/hosts.md`.
85
+ Detach: `Ctrl-b` then `d`. Reattach: `tmux attach -t <folder>`. A duplicate `start` fails instead of attaching. Default session name is this folder. Use `--no-remote-control` and/or `--skip-approve-all` to opt out. Skills: `$aifabrix-plan` (and the other `$aifabrix-*` names). Codex reads `AGENTS.md`. Full notes: `.agents/skills/shared/hosts.md`.
86
86
  {{/if}}
87
87
 
88
88
  ## Auth for delivery CLI
@@ -6,14 +6,14 @@ Install and sign-in steps are in `AGENTKIT.md`. This file is how to **run** each
6
6
 
7
7
  Do not start Claude or Codex in your home directory. `cd` into this git workspace first.
8
8
 
9
- ## Auto-approve (optional)
9
+ ## Auto-approve
10
10
 
11
11
  `aifabrix agent-kit install` can write gitignored host configs so Claude Code and Codex do not prompt for every tool:
12
12
 
13
13
  - Claude Code: `.claude/settings.local.json` (`permissions.defaultMode` = `bypassPermissions`, `allow` = `Bash(aifabrix *)` and `Read(~/**)`)
14
14
  - Codex: `.codex/config.toml` (`approval_policy = "never"`, `sandbox_mode = "workspace-write"`)
15
15
 
16
- Those files stay in **this workspace**. Claude may **read** files under the developer home (`Read(~/**)`) and run the delivery CLI (`Bash(aifabrix *)`). They do not add sibling folders as extra workspaces, do not grant Write outside this repo, and do not allow every shell command. TTY asks which host to auto-approve when both are selected. `--approve-all claude` or `--approve-all codex` writes only that host. `--yes` or a non-TTY / `--json` run writes them. `--skip-approve-all` skips. `--force` overwrites existing local files. Builder never writes `~/.claude` or `~/.codex`.
16
+ Those files stay in **this workspace**. Claude may **read** files under the developer home (`Read(~/**)`) and run the delivery CLI (`Bash(aifabrix *)`). They do not add sibling folders as extra workspaces, do not grant Write outside this repo, and do not allow every shell command. `agent-kit start claude|codex` writes only the selected host's file by default; `--skip-approve-all` opts out. Install/update retain their explicit approval prompts and flags. `--force` overwrites existing local files. Builder never writes `~/.claude` or `~/.codex`.
17
17
 
18
18
  ## Cursor
19
19
 
@@ -23,13 +23,13 @@ Those files stay in **this workspace**. Claude may **read** files under the deve
23
23
 
24
24
  ## Claude Code
25
25
 
26
- **Local or SSH — tmux.** Closing the terminal must not kill Claude. Agent Kit starts Claude **inside tmux**. `--remote-control` is the **same** session on this PC, [claude.ai/code](https://claude.ai/code), and the Claude mobile app (**Code**). Do not use `claude --cloud` for this — that starts a different machine that clones GitHub. Claude `--tmux` is only for `--worktree` sessions, not this wrapper.
26
+ **Local or SSH — tmux.** Closing the terminal must not kill Claude. Agent Kit starts Claude **inside tmux**, with Remote Control and workspace auto-approval enabled by default. Remote Control is the **same** session on this PC, [claude.ai/code](https://claude.ai/code), and the Claude mobile app (**Code**). Do not use `claude --cloud` for this — that starts a different machine that clones GitHub. Claude `--tmux` is only for `--worktree` sessions, not this wrapper.
27
27
 
28
28
  ```bash
29
- aifabrix agent-kit start claude --remote-control --approve-all --yes
29
+ aifabrix agent-kit start claude
30
30
  ```
31
31
 
32
- tmux session name is this folder (for example `aifabrix-docs`). Remote Control name is `hostname-user-folder` (for example `builder01-dev03-aifabrix-docs`). `--name` overrides the folder. Several workspaces can run at once. Detach without killing Claude: `Ctrl-b` then `d`. Reattach: `tmux attach -t aifabrix-docs`, or run `start` again from the same folder. Then `/aifabrix-plan` (or another `/aifabrix-*` skill). First run in a project asks you to trust the folder; accept only this workspace.
32
+ tmux session name is this folder (for example `aifabrix-docs`). Remote Control name is `hostname-user-folder` (for example `builder01-dev03-aifabrix-docs`). `--name` overrides the folder; `--no-remote-control` and `--skip-approve-all` opt out. Several workspaces can run at once. Detach without killing Claude: `Ctrl-b` then `d`. Reattach: `tmux attach -t aifabrix-docs`. A duplicate `start` fails instead of attaching. Then `/aifabrix-plan` (or another `/aifabrix-*` skill). First run in a project asks you to trust the folder; accept only this workspace.
33
33
 
34
34
  **Phone and PC.** Sign in with the same Claude Pro/Max/Team account (`claude auth login` — not an API key). After Remote Control starts, open **Code** in the Claude mobile app and/or [claude.ai/code](https://claude.ai/code) on the PC. In an already-running session, `/remote-control` or `/mobile`.
35
35
 
@@ -37,13 +37,13 @@ Closing the terminal (or SSH) does not stop a tmux session. Stopping tmux or the
37
37
 
38
38
  ## Codex
39
39
 
40
- **Local or SSH — tmux (recommended).** Same recipe on your PC and on a remote host. tmux keeps Codex running if you close the terminal or disconnect SSH. Codex has no Claude-style phone/web Remote Control.
40
+ **Local or SSH — tmux and Remote Control.** tmux keeps Codex running if you close the terminal or disconnect SSH. Agent Kit also starts Codex's native remote-control daemon and workspace auto-approval by default.
41
41
 
42
42
  ```bash
43
- aifabrix agent-kit start codex --approve-all --yes
43
+ aifabrix agent-kit start codex
44
44
  ```
45
45
 
46
- Equivalent: `tmux new -s aifabrix-docs -c /path/to/this-workspace` then `codex`. Default `--name` is the last folder. If that session already exists, `start` attaches. Detach: `Ctrl-b` then `d`. Then `$aifabrix-plan` (and the other `$aifabrix-*` skills). Codex reads `AGENTS.md`.
46
+ Default `--name` is the last folder. `--no-remote-control` keeps the launch local; `--skip-approve-all` leaves Codex permissions unchanged. If that session already exists, `start` fails with list/kill guidance. Detach: `Ctrl-b` then `d`. Reattach with `tmux attach -t aifabrix-docs`. Then `$aifabrix-plan` (and the other `$aifabrix-*` skills). Codex reads `AGENTS.md`. Killing tmux does not stop the shared daemon; use `codex remote-control stop` when required.
47
47
 
48
48
  **Foreground only (no tmux).**
49
49
 
@@ -58,8 +58,8 @@ codex
58
58
  | --- | --- |
59
59
  | New named session | `tmux new -s <name> -c /path/to/this-workspace` |
60
60
  | Detach (leave it running) | `Ctrl-b`, then `d` |
61
- | List sessions | `tmux ls` |
61
+ | List sessions | `aifabrix agent-kit list` |
62
62
  | Attach | `tmux attach -t <name>` |
63
- | Kill session | `tmux kill-session -t <name>` |
63
+ | Kill session | `aifabrix agent-kit kill <name>` |
64
64
 
65
65
  Use a different `--name` per workspace so several Claude or Codex sessions can run on the same host. Claude and Codex in the **same** folder also need two names.
@@ -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.68
8
+ version: 2.0.70
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).
@@ -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.68
47
+ VERSION=2.0.70
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
@@ -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.78'
7
+ version: '2.0.81'
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).
@@ -58,7 +58,8 @@ build:
58
58
  dockerfile: builder/miso-controller/Dockerfile # Dockerfile name (relative to project root)
59
59
  envOutputPath: ../../packages/miso-controller/.env # Copy .env to repo root for local dev (relative to builder/) (if null, no .env file is copied) (if empty, .env file is copied to repo root)
60
60
  language: typescript # Runtime language for template selection (typescript or python)
61
- reloadStart: pnpm --filter @aifabrix/miso-controller run dev:watch:public # Docker --reload: nodemon backend + pre-built UI (no Vite in container)
61
+ # Runtime image has npm/npx, not pnpm. Keep the existing dev:watch:public script.
62
+ reloadStart: npm --prefix packages/miso-controller run dev:watch:public
62
63
 
63
64
  # Repository Configuration (pipeline validate: REPOSITORY_URL_MISMATCH)
64
65
  repository:
@@ -213,12 +213,6 @@ MISO_ENVIRONMENT=miso
213
213
  MISO_CLIENTID=kv://miso-controller-client-idKeyVault
214
214
  MISO_CLIENTSECRET=kv://miso-controller-client-secretKeyVault
215
215
 
216
- # Evaluation mode (optional .env override of DB controller.configuration.evaluation):
217
- # When true (default if DB omits flag), infra deploy may coerce :envKey to `miso` — e2e poll on `dev` can 404.
218
- # Set false locally to force path envKey to match deploy + GET .../deployments/:id.
219
- # Unset = use DB only.
220
- CONTROLLER_EVALUATION=
221
-
222
216
  # =============================================================================
223
217
  # ENTRA ID ONBOARDING (vendor multi-tenant app — admin consent) — [always]
224
218
  # =============================================================================
@@ -361,6 +355,21 @@ API_KEY=kv://miso-controller-secrets-apiKeyVault
361
355
  # LICENSE_VALIDATION_DISABLED=true switch; do not use it in deployed installations.
362
356
  LICENSE_JWT=kv://miso-controller-jwt-licenseKeyVault
363
357
 
358
+ # Signed licenses require a Mori issuer key trusted independently of the JWT.
359
+ # Provision the SPKI PEM public key and exact issuer during installation, before
360
+ # pasting LICENSE_JWT or using Update license. An embedded JWT publicKey/jwk or Mori URL
361
+ # is not a trust source. For disconnected installs, deliver the key through the
362
+ # authenticated installation package or another operator-verified channel.
363
+ # Azure resolves this from Key Vault using the app's managed identity.
364
+ # Builder Docker resolves the same kv:// catalog key from the installer's local
365
+ # secrets store before starting the container. Provision a trusted Mori public key
366
+ # there first; the empty catalog default is only for unlicensed Community mode.
367
+ MORI_LICENSE_PUBLIC_KEY=kv://mori-license-public-keyKeyVault
368
+ # Plain docker run can instead leave MORI_LICENSE_PUBLIC_KEY unset and use a
369
+ # trusted, read-only PEM mount at this path.
370
+ # MORI_LICENSE_PUBLIC_KEY_FILE=/run/secrets/mori-license-public-key.pem
371
+ MORI_LICENSE_ISSUER=mori-controller
372
+
364
373
  # =============================================================================
365
374
  # ENTRA ID ONBOARDING (delegated Graph via Mori)
366
375
  # =============================================================================
@@ -0,0 +1,77 @@
1
+ {
2
+ "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#",
3
+ "handler": "Microsoft.Azure.CreateUIDef",
4
+ "version": "0.1.2-preview",
5
+ "parameters": {
6
+ "config": {
7
+ "basics": {
8
+ "description": "Choose subscription, resource group, and region above. Under **Application resources Resource group name**, enter your install id (for example `aifabrixpoc02`). That managed resource group name is the naming prefix: resources become `{rg}-miso-asp`, `{rg}-miso-miso`, and so on. Set the Azure **Application name** to the same id when the portal asks. Enter the initial platform administrator password and choose the license below. Container images are pulled with the web apps' managed identities; no registry credential is needed."
9
+ }
10
+ },
11
+ "basics": [
12
+ {
13
+ "name": "adminPassword",
14
+ "type": "Microsoft.Common.PasswordBox",
15
+ "label": "Administrator password",
16
+ "toolTip": "Password for the initial AI Fabrix platform administrator (user admin). PostgreSQL, Keycloak, and encryption secrets are generated automatically and stored in Key Vault — do not reuse this password for infrastructure.",
17
+ "constraints": {
18
+ "required": true,
19
+ "regex": "^.{8,128}$",
20
+ "validationMessage": "Password must be 8–128 characters."
21
+ },
22
+ "options": {
23
+ "hideConfirmation": false
24
+ }
25
+ },
26
+ {
27
+ "name": "licenseChoice",
28
+ "type": "Microsoft.Common.DropDown",
29
+ "label": "License",
30
+ "toolTip": "License AI Fabrix requests during tenant activation. Community is permanent. Standard or Enterprise requested by name is a 30-day evaluation. For a purchased plan, choose 'Purchased Marketplace subscription' and enter its subscription ID; the edition is then resolved from the purchased plan.",
31
+ "defaultValue": "Community",
32
+ "constraints": {
33
+ "required": true,
34
+ "allowedValues": [
35
+ {
36
+ "label": "Community",
37
+ "value": "COMMUNITY"
38
+ },
39
+ {
40
+ "label": "Standard (30-day evaluation)",
41
+ "value": "STANDARD"
42
+ },
43
+ {
44
+ "label": "Enterprise (30-day evaluation)",
45
+ "value": "ENTERPRISE"
46
+ },
47
+ {
48
+ "label": "Purchased Marketplace subscription",
49
+ "value": "SUBSCRIPTION_ID"
50
+ }
51
+ ]
52
+ }
53
+ },
54
+ {
55
+ "name": "licenseSubscriptionKey",
56
+ "type": "Microsoft.Common.TextBox",
57
+ "label": "Marketplace subscription ID",
58
+ "toolTip": "The ID (GUID) of the purchased AI Fabrix Azure Marketplace subscription.",
59
+ "defaultValue": "",
60
+ "visible": "[equals(basics('licenseChoice'), 'SUBSCRIPTION_ID')]",
61
+ "constraints": {
62
+ "required": "[equals(basics('licenseChoice'), 'SUBSCRIPTION_ID')]",
63
+ "regex": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
64
+ "validationMessage": "Enter the Marketplace subscription ID as a GUID (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)."
65
+ }
66
+ }
67
+ ],
68
+ "steps": [],
69
+ "outputs": {
70
+ "location": "[location()]",
71
+ "deploymentEnvironment": "evaluation",
72
+ "adminPassword": "[basics('adminPassword')]",
73
+ "licenseEdition": "[if(equals(basics('licenseChoice'), 'SUBSCRIPTION_ID'), basics('licenseSubscriptionKey'), basics('licenseChoice'))]",
74
+ "acrResourceId": "__MARKETPLACE_ACR_RESOURCE_ID__"
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
3
+ "contentVersion": "1.0.0.0",
4
+ "parameters": {
5
+ "adminPassword": { "value": "" },
6
+ "marketplaceSubscriptionId": { "value": "" },
7
+ "location": { "value": "swedencentral" }
8
+ }
9
+ }