@aifabrix/builder 2.58.0 → 2.59.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.
- package/lib/agent-kit/codex-app-server.js +44 -0
- package/lib/agent-kit/init.js +4 -17
- package/lib/agent-kit/session-report.js +15 -10
- package/lib/agent-kit/setup-auth.js +131 -0
- package/lib/agent-kit/setup-context.js +118 -0
- package/lib/agent-kit/setup-github.js +70 -0
- package/lib/agent-kit/setup-managed.js +79 -0
- package/lib/agent-kit/setup-process.js +42 -0
- package/lib/agent-kit/setup-session.js +53 -0
- package/lib/agent-kit/setup-state.js +52 -0
- package/lib/agent-kit/setup-workspace.js +113 -0
- package/lib/agent-kit/setup.js +84 -0
- package/lib/agent-kit/start.js +4 -3
- package/lib/agent-kit/tmux-sessions.js +20 -3
- package/lib/api/dev.api.js +1 -1
- package/lib/api/miso-health.api.js +26 -0
- package/lib/api/types/dev.types.js +3 -3
- package/lib/app/deploy.js +0 -102
- package/lib/cli/index.js +4 -0
- package/lib/cli/setup-dev-path-commands.js +4 -0
- package/lib/cli/setup-dev.js +7 -4
- package/lib/cli/setup-help.js +86 -0
- package/lib/cli/setup-onboarding.js +37 -0
- package/lib/cli/setup-utility-repair.js +7 -5
- package/lib/commands/agent-kit-help.js +21 -7
- package/lib/commands/agent-kit-sessions.js +126 -0
- package/lib/commands/agent-kit-setup.js +44 -0
- package/lib/commands/agent-kit.js +5 -79
- package/lib/commands/dev-init.js +21 -23
- package/lib/commands/dev-refresh.js +2 -2
- package/lib/commands/governance-verify-external.js +12 -5
- package/lib/commands/repair-datasource.js +9 -9
- package/lib/commands/repair.js +45 -8
- package/lib/commands/upload-qualification-client.js +56 -11
- package/lib/commands/upload-qualification-poll.js +51 -22
- package/lib/commands/wizard-dataplane.js +16 -52
- package/lib/datasource/datasource-validate-summary.js +8 -5
- package/lib/datasource/sync-defaults.js +53 -0
- package/lib/deployment/installation/azure-infra-stage.js +10 -0
- package/lib/deployment/installation/azure-preflight.js +13 -2
- package/lib/deployment/installation/azure-readiness.js +24 -2
- package/lib/deployment/installation/environment-auth.js +60 -20
- package/lib/deployment/installation/environment-stage.js +9 -5
- package/lib/deployment/installation/infra-catalog.js +8 -2
- package/lib/deployment/installation/infra-manifest.js +5 -0
- package/lib/deployment/installation/registry-auth-mode.js +33 -10
- package/lib/deployment/marketplace-environment.js +26 -1
- package/lib/generator/index.js +23 -12
- package/lib/integration-definition/apply.js +2 -2
- package/lib/internal/fs-real-sync.js +4 -1
- package/lib/programmatic/bearer-auth.js +3 -62
- package/lib/programmatic/builder-api-startup.js +51 -0
- package/lib/programmatic/builder-miso-runtime.js +167 -0
- package/lib/programmatic/openapi-descriptions.js +3 -3
- package/lib/programmatic/openapi-schema-descriptions-core.js +3 -3
- package/lib/programmatic/openapi-spec.js +2 -1
- package/lib/programmatic/route-handler-map.js +3 -2
- package/lib/schema/environment-deploy-request.schema.json +1 -1
- package/lib/schema/external-datasource.schema.json +20 -5
- package/lib/schema/infra-parameter.schema.json +1 -1
- package/lib/schema/infra.parameter.yaml +2 -1
- package/lib/schema/infrastructure-schema.json +145 -1
- package/lib/schema/installation-environment.schema.json +1 -1
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/config-paths.js +24 -29
- package/lib/utils/config-registry-preference.js +13 -6
- package/lib/utils/dev-init-ssh-merge.js +5 -4
- package/lib/utils/dev-user-groups.js +2 -1
- package/lib/utils/docker-daemon-tls-ca.js +2 -2
- package/lib/utils/error-formatters/validation-errors.js +25 -5
- package/lib/utils/help-builder.js +4 -3
- package/lib/utils/token-manager.js +29 -36
- package/package.json +3 -2
- package/templates/agent-kit/instructions/AGENTKIT.md +39 -2
- package/templates/agent-kit/instructions/root.AGENTS.md +40 -1
- package/templates/agent-kit/skills/shared/hosts.md +23 -5
- package/templates/agent-kit/workspace/README.md +14 -1
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +1 -1
- package/templates/applications/miso-controller/application.yaml +1 -1
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/external-system/external-datasource.yaml.hbs +7 -6
- package/templates/marketplace/main.json +438 -80
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Machine-local onboarding progress, outside the repository and without credentials. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('../internal/fs-real-sync');
|
|
6
|
+
const { randomUUID } = require('crypto');
|
|
7
|
+
const { assertDirectory } = require('./setup-context');
|
|
8
|
+
|
|
9
|
+
function readState(file) {
|
|
10
|
+
if (!fs.existsSync(file)) return null;
|
|
11
|
+
if (!fs.lstatSync(file).isFile()) throw new Error('Onboarding state must be a regular file.');
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
14
|
+
} catch {
|
|
15
|
+
throw new Error('Onboarding state is invalid; preserve the workspace and inspect its state file.');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function acquireWorkspace(context) {
|
|
20
|
+
const dir = path.join(context.root, '.aifabrix-onboarding');
|
|
21
|
+
assertDirectory(dir);
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
23
|
+
const file = path.join(dir, `${context.name}.json`);
|
|
24
|
+
const lock = `${file}.lock`;
|
|
25
|
+
let descriptor;
|
|
26
|
+
try {
|
|
27
|
+
descriptor = fs.openSync(lock, 'wx', 0o600);
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error(`Workspace setup is locked. If no setup is running, remove the stale lock: ${lock}`);
|
|
30
|
+
}
|
|
31
|
+
fs.closeSync(descriptor);
|
|
32
|
+
const release = () => fs.unlinkSync(lock);
|
|
33
|
+
try {
|
|
34
|
+
const state = readState(file);
|
|
35
|
+
if (state && (state.controller !== context.controller || state.fullName !== context.fullName || state.target !== context.target)) {
|
|
36
|
+
throw new Error('This workspace belongs to a different Controller or repository. Use a different AI Fabrix work directory.');
|
|
37
|
+
}
|
|
38
|
+
return { file, release, state: state || { controller: context.controller, fullName: context.fullName, target: context.target } };
|
|
39
|
+
} catch (error) {
|
|
40
|
+
release();
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function saveProgress(progress, patch) {
|
|
46
|
+
Object.assign(progress.state, patch);
|
|
47
|
+
const temporary = `${progress.file}.${randomUUID()}.tmp`;
|
|
48
|
+
fs.writeFileSync(temporary, `${JSON.stringify(progress.state, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
49
|
+
fs.renameSync(temporary, progress.file);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { acquireWorkspace, saveProgress };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** Local workspace preparation and optional GitHub clone/create with safe resumability. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('../internal/fs-real-sync');
|
|
5
|
+
const { runInit } = require('./init');
|
|
6
|
+
const { runInstall, runUpdate } = require('./run');
|
|
7
|
+
const { resolveGitRoot } = require('./repo-guard');
|
|
8
|
+
const { ensureInitialCommit } = require('./scm/git');
|
|
9
|
+
const { capture, checked } = require('./setup-process');
|
|
10
|
+
const github = require('./setup-github');
|
|
11
|
+
const { saveProgress } = require('./setup-state');
|
|
12
|
+
|
|
13
|
+
function validateExisting(context, progress, options) {
|
|
14
|
+
if (progress.state.initializing || progress.state.cloning) {
|
|
15
|
+
throw new Error('Workspace initialization or clone was interrupted. Inspect the preserved folder and onboarding state before retrying.');
|
|
16
|
+
}
|
|
17
|
+
if (!fs.existsSync(context.target) || fs.readdirSync(context.target).length === 0) return false;
|
|
18
|
+
if (resolveGitRoot(context.target) !== context.target) throw new Error('Workspace must be its own Git repository.');
|
|
19
|
+
if (context.fullName) {
|
|
20
|
+
const origin = github.originIdentity(context.target, options);
|
|
21
|
+
if (origin || !progress.state.initialized) github.assertOrigin(context, options);
|
|
22
|
+
} else if (!progress.state.initialized || github.originIdentity(context.target, options)) {
|
|
23
|
+
throw new Error('Local workspace is not an existing local-only onboarding workspace. No files were changed.');
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function checkKit(result) {
|
|
29
|
+
if (result.conflicts && result.conflicts.length) {
|
|
30
|
+
throw new Error('Agent Kit files have local conflicts. Resolve them with agent-kit status/update before retrying setup.');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function initializeLocal(context, progress, options) {
|
|
35
|
+
// Persist the operation first so an interrupted partial init cannot be adopted on retry.
|
|
36
|
+
saveProgress(progress, { initializing: true });
|
|
37
|
+
runInit({ nameOrPath: context.target, host: context.code, noRemote: true, approveAll: false,
|
|
38
|
+
interactive: false, execFileSync: options.execFileSync });
|
|
39
|
+
saveProgress(progress, { initialized: true, initializing: false });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function finishNewRemote(context, progress, options) {
|
|
43
|
+
const opts = { ...options, cwd: context.target };
|
|
44
|
+
if (!progress.state.initialCommit) {
|
|
45
|
+
ensureInitialCommit(context.target, opts);
|
|
46
|
+
const initialCommit = String(capture('git', ['rev-parse', 'HEAD'], opts)).trim();
|
|
47
|
+
saveProgress(progress, { initialCommit });
|
|
48
|
+
}
|
|
49
|
+
if (!progress.state.remoteCreated) {
|
|
50
|
+
github.createRepository(context.fullName, options);
|
|
51
|
+
saveProgress(progress, { remoteCreated: true });
|
|
52
|
+
}
|
|
53
|
+
if (!github.originIdentity(context.target, options)) {
|
|
54
|
+
checked('git', ['remote', 'add', 'origin', `https://github.com/${context.fullName}.git`],
|
|
55
|
+
'Could not configure the GitHub origin. Local files are preserved.', opts);
|
|
56
|
+
}
|
|
57
|
+
github.pushInitialCommit(context, progress, saveProgress, options);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function cloneWorkspace(context, progress, options) {
|
|
61
|
+
if (progress.state.initialized) throw new Error('Previously initialized workspace is missing. Restore it before retrying.');
|
|
62
|
+
saveProgress(progress, { cloning: true });
|
|
63
|
+
github.cloneRepository(context, options);
|
|
64
|
+
saveProgress(progress, { initialized: true, cloning: false });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function prepareGithub(context, progress, options, existing) {
|
|
68
|
+
if (existing && github.originIdentity(context.target, options)) {
|
|
69
|
+
if (progress.state.remoteCreated && !progress.state.pushed) finishNewRemote(context, progress, options);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const remote = github.lookupRepository(context.fullName, options);
|
|
73
|
+
if (!remote) {
|
|
74
|
+
if (progress.state.remoteCreated) {
|
|
75
|
+
throw new Error('The existing remote is no longer accessible. Restore access before retrying.');
|
|
76
|
+
}
|
|
77
|
+
if (existing && !progress.state.initialCommit) {
|
|
78
|
+
throw new Error('Initialization stopped before recording the initial commit. Inspect the preserved workspace before creating its remote.');
|
|
79
|
+
}
|
|
80
|
+
if (!existing) initializeLocal(context, progress, options);
|
|
81
|
+
finishNewRemote(context, progress, options);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (!existing) {
|
|
85
|
+
cloneWorkspace(context, progress, options);
|
|
86
|
+
} else if (!progress.state.remoteCreated) {
|
|
87
|
+
throw new Error('Repository appeared during creation. Inspect the local workspace and remote before connecting them.');
|
|
88
|
+
}
|
|
89
|
+
if (progress.state.remoteCreated && !progress.state.pushed) finishNewRemote(context, progress, options);
|
|
90
|
+
github.assertOrigin(context, options);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function prepareWorkspace(context, progress, options = {}) {
|
|
95
|
+
const existing = validateExisting(context, progress, options);
|
|
96
|
+
let installed = false;
|
|
97
|
+
if (context.fullName) installed = prepareGithub(context, progress, options, existing);
|
|
98
|
+
else if (!existing) {
|
|
99
|
+
if (progress.state.initialized) throw new Error('Previously initialized workspace is missing. Restore it before retrying.');
|
|
100
|
+
initializeLocal(context, progress, options);
|
|
101
|
+
installed = true;
|
|
102
|
+
}
|
|
103
|
+
if (!installed) {
|
|
104
|
+
const install = options.managed ? runUpdate : runInstall;
|
|
105
|
+
const preview = install({ cwd: context.target, host: context.code, approveAll: false, dryRun: true });
|
|
106
|
+
checkKit(preview);
|
|
107
|
+
checkKit(install({ cwd: context.target, host: context.code, approveAll: false }));
|
|
108
|
+
}
|
|
109
|
+
if (!fs.existsSync(context.target)) throw new Error('Workspace preparation did not create its directory.');
|
|
110
|
+
saveProgress(progress, { initialized: true, ready: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { validateExisting, prepareWorkspace, checkKit };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Portable local-first agent onboarding. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { randomUUID } = require('crypto');
|
|
5
|
+
const logger = require('../utils/logger');
|
|
6
|
+
const { formatSuccessLine } = require('../utils/cli-layout-chalk');
|
|
7
|
+
const { auditLog } = require('../core/audit-logger');
|
|
8
|
+
const { normalizeSetup, preflight, resolveWork } = require('./setup-context');
|
|
9
|
+
const { acquireWorkspace } = require('./setup-state');
|
|
10
|
+
const { ensureController, ensureGithubLogin, ensureProvider } = require('./setup-auth');
|
|
11
|
+
const { prepareWorkspace, validateExisting } = require('./setup-workspace');
|
|
12
|
+
const { prepareSession, attachSession } = require('./setup-session');
|
|
13
|
+
const { managedSetupOptions } = require('./setup-managed');
|
|
14
|
+
|
|
15
|
+
async function authenticate(context, options, setPhase) {
|
|
16
|
+
setPhase('controller-login');
|
|
17
|
+
logger.log('Connecting to AI Fabrix...');
|
|
18
|
+
await ensureController(context);
|
|
19
|
+
if (context.fullName) {
|
|
20
|
+
setPhase('github-login');
|
|
21
|
+
ensureGithubLogin(options);
|
|
22
|
+
}
|
|
23
|
+
if (context.code === 'cursor') return null;
|
|
24
|
+
setPhase('agent-login');
|
|
25
|
+
return ensureProvider(context, options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function prepareOnboarding(context, progress, options) {
|
|
29
|
+
const correlationId = randomUUID();
|
|
30
|
+
let provider;
|
|
31
|
+
let session;
|
|
32
|
+
let phase = 'workspace-check';
|
|
33
|
+
try {
|
|
34
|
+
validateExisting(context, progress, options);
|
|
35
|
+
provider = await authenticate(context, options, value => {
|
|
36
|
+
phase = value;
|
|
37
|
+
});
|
|
38
|
+
phase = 'workspace';
|
|
39
|
+
prepareWorkspace(context, progress, options);
|
|
40
|
+
if (!options.managed || context.code === 'claude') {
|
|
41
|
+
phase = 'agent-start';
|
|
42
|
+
session = prepareSession(context, provider, options);
|
|
43
|
+
}
|
|
44
|
+
await auditLog('AUDIT', 'Agent workspace onboarding ready', { correlationId, path: context.target, agent: context.code });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
await auditLog('ERROR', 'Agent workspace onboarding failed', { correlationId, phase });
|
|
47
|
+
throw error;
|
|
48
|
+
} finally {
|
|
49
|
+
progress.release();
|
|
50
|
+
}
|
|
51
|
+
return { session, provider };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function runSetup(options = {}) {
|
|
55
|
+
const input = normalizeSetup(options);
|
|
56
|
+
preflight(input, options);
|
|
57
|
+
const context = await resolveWork(input, options);
|
|
58
|
+
const progress = acquireWorkspace(context);
|
|
59
|
+
const { session, provider } = await prepareOnboarding(context, progress, options);
|
|
60
|
+
logger.log(formatSuccessLine(`Workspace ready: ${context.target}`));
|
|
61
|
+
if (options.managed) {
|
|
62
|
+
const host = options.remoteHost || new URL(context.controller).hostname;
|
|
63
|
+
if (context.code === 'claude') {
|
|
64
|
+
logger.log(`Claude Code running in tmux session ${session}. Attach: tmux attach -t ${session}`);
|
|
65
|
+
logger.log(`Next: Open Claude Code Remote Control on ${host}, or attach to ${session}.`);
|
|
66
|
+
return { path: context.target, code: context.code, session };
|
|
67
|
+
}
|
|
68
|
+
const next = {
|
|
69
|
+
cursor: `In Cursor, connect with Remote SSH to ${host}, then open ${context.target}.`,
|
|
70
|
+
codex: `In Codex, add the SSH project ${context.target} on ${host}.`
|
|
71
|
+
}[context.code];
|
|
72
|
+
logger.log(`Next: ${next} Say "Hi" to begin.`);
|
|
73
|
+
} else {
|
|
74
|
+
attachSession(session, provider, options);
|
|
75
|
+
}
|
|
76
|
+
return { path: context.target, code: context.code, session };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function runManagedOnboarding(options = {}) {
|
|
80
|
+
const managed = await managedSetupOptions(options);
|
|
81
|
+
return runSetup({ ...options, ...managed });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { runSetup, runManagedOnboarding };
|
package/lib/agent-kit/start.js
CHANGED
|
@@ -129,7 +129,7 @@ function buildTmuxSpec(p) {
|
|
|
129
129
|
const remote = p.app === 'claude' && p.remoteControl;
|
|
130
130
|
return {
|
|
131
131
|
bin: 'tmux',
|
|
132
|
-
args: [...mode, spec.bin, ...extra],
|
|
132
|
+
args: [...mode, ...(p.binary ? ['env', p.binary] : [spec.bin]), ...extra],
|
|
133
133
|
session: p.session,
|
|
134
134
|
appBin: spec.bin,
|
|
135
135
|
needTmux: true,
|
|
@@ -287,16 +287,17 @@ function resolveStartContext(options) {
|
|
|
287
287
|
const cwd = resolveGitRoot(options.cwd || process.cwd());
|
|
288
288
|
const session = resolveSessionName(options.name, cwd);
|
|
289
289
|
const remoteControlPrefix = hostUserPrefix(options);
|
|
290
|
-
const remoteControlName = resolveRemoteControlName(remoteControlPrefix, session);
|
|
290
|
+
const remoteControlName = options.remoteControlName || resolveRemoteControlName(remoteControlPrefix, session);
|
|
291
291
|
const remoteControl = options.remoteControl !== false;
|
|
292
292
|
const spec = buildLaunchSpec({
|
|
293
293
|
app, cwd, session, remoteControlPrefix, remoteControlName,
|
|
294
|
-
detach: Boolean(options.detach), remoteControl
|
|
294
|
+
detach: Boolean(options.detach), remoteControl, binary: options.binary
|
|
295
295
|
});
|
|
296
296
|
spec.cwd = cwd;
|
|
297
297
|
const remoteSpec = app === 'codex' && remoteControl
|
|
298
298
|
? buildCodexRemoteSpec(cwd)
|
|
299
299
|
: undefined;
|
|
300
|
+
if (remoteSpec && options.binary) remoteSpec.bin = options.binary;
|
|
300
301
|
return { app, cwd, session, remoteControl, spec, remoteSpec };
|
|
301
302
|
}
|
|
302
303
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
-
const { execFileSync } = require('child_process');
|
|
8
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
9
9
|
|
|
10
10
|
const LIST_FORMAT = [
|
|
11
11
|
'#{session_name}',
|
|
@@ -66,7 +66,7 @@ function isNoTmuxServer(error) {
|
|
|
66
66
|
.filter(Boolean)
|
|
67
67
|
.map(String)
|
|
68
68
|
.join(' ');
|
|
69
|
-
return /no server running|failed to connect to server/i.test(message);
|
|
69
|
+
return /no server running|failed to connect to server|error connecting to .*\/tmux-\d+\/.*No such file or directory/i.test(message);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
@@ -150,6 +150,22 @@ function runKill(options) {
|
|
|
150
150
|
return { command: 'kill', session, killed: true };
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
function runAttach(options) {
|
|
154
|
+
const exec = options.execFileSync || execFileSync;
|
|
155
|
+
const session = normalizeExactSessionName(options.name);
|
|
156
|
+
requireTmux(exec);
|
|
157
|
+
if (!tmuxSessionExists(session, exec)) {
|
|
158
|
+
throw new Error(`tmux session "${session}" is not running. Run: aifabrix agent-kit list`);
|
|
159
|
+
}
|
|
160
|
+
const insideTmux = Boolean((options.env || process.env).TMUX);
|
|
161
|
+
const args = [insideTmux ? 'switch-client' : 'attach-session', '-t', `=${session}`];
|
|
162
|
+
const result = (options.spawnSync || spawnSync)('tmux', args, { stdio: 'inherit' });
|
|
163
|
+
if (!result || result.error || result.signal || result.status !== 0) {
|
|
164
|
+
throw new Error(`Could not attach to tmux session "${session}".`);
|
|
165
|
+
}
|
|
166
|
+
return { command: 'attach', session, attached: true };
|
|
167
|
+
}
|
|
168
|
+
|
|
153
169
|
module.exports = {
|
|
154
170
|
LIST_FORMAT,
|
|
155
171
|
hasOnPath,
|
|
@@ -159,5 +175,6 @@ module.exports = {
|
|
|
159
175
|
tmuxSessionExists,
|
|
160
176
|
parseSessionLine,
|
|
161
177
|
runList,
|
|
162
|
-
runKill
|
|
178
|
+
runKill,
|
|
179
|
+
runAttach
|
|
163
180
|
};
|
package/lib/api/dev.api.js
CHANGED
|
@@ -198,7 +198,7 @@ async function listUsers(serverUrl, clientCertPem, serverCaPem) {
|
|
|
198
198
|
* @requiresPermission {BuilderServer} Client certificate (X-Client-Cert)
|
|
199
199
|
* @param {string} serverUrl - Builder Server base URL
|
|
200
200
|
* @param {string} clientCertPem - PEM client certificate
|
|
201
|
-
* @param {Object} body - CreateUserDto: developerId, name, email, optional groups (admin | secret-manager | secret-reader | developer | docker)
|
|
201
|
+
* @param {Object} body - CreateUserDto: developerId, name, email, optional groups (admin | secret-manager | secret-reader | developer | docker | integrator)
|
|
202
202
|
* @param {string} [serverCaPem] - Dev root CA PEM
|
|
203
203
|
* @returns {Promise<Object>} UserResponseDto
|
|
204
204
|
*/
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-token health proof through the public Miso SDK transport.
|
|
3
|
+
* @fileoverview Never reads or returns the application token itself.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* POST /api/v1/auth/validate-client-token
|
|
9
|
+
* @requiresPermission {Controller} Client token
|
|
10
|
+
* @param {Object} client Initialized runtime client
|
|
11
|
+
* @param {AbortSignal} signal Probe cancellation
|
|
12
|
+
* @returns {Promise<void>} Resolves only for an authenticated application
|
|
13
|
+
*/
|
|
14
|
+
async function validateMisoApplicationToken(client, signal) {
|
|
15
|
+
const response = await client.requestWithAuthStrategy(
|
|
16
|
+
'POST', '/api/v1/auth/validate-client-token', { methods: ['client-token'] },
|
|
17
|
+
{}, { signal, timeout: 3000, maxRedirects: 0 }
|
|
18
|
+
);
|
|
19
|
+
if (response?.success === false || response?.data?.authenticated !== true) {
|
|
20
|
+
const error = new Error('Miso application authentication failed');
|
|
21
|
+
error.statusCode = 401;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { validateMisoApplicationToken };
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* @property {string} aifabrix-secrets - Local filesystem path or https URL for shared secrets (file vs remote API)
|
|
31
31
|
* @property {string} aifabrix-env-config - Env config path
|
|
32
32
|
* @property {string} remote-server - Builder-server base URL
|
|
33
|
-
* @property {string} docker-endpoint - Docker API endpoint
|
|
33
|
+
* @property {string} [docker-endpoint] - Docker API endpoint; omit for SSH-only/integrator users
|
|
34
34
|
* @property {string} [developer-root-domain] - App hostname root (e.g. aifabrix.dev → dev01.aifabrix.dev)
|
|
35
35
|
* @property {boolean} [docker-tls-skip-verify] - When true, Docker uses DOCKER_TLS_VERIFY=0 even if ca.pem exists (client mTLS unchanged)
|
|
36
36
|
* @property {string} sync-ssh-user - SSH user for Mutagen
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
* @property {string} createdAt - ISO 8601
|
|
47
47
|
* @property {boolean} certificateIssued - Whether cert was issued
|
|
48
48
|
* @property {string} [certificateValidNotAfter] - Cert validity end (optional)
|
|
49
|
-
* @property {string[]} groups - Access groups (admin, secret-manager, secret-reader, developer, docker)
|
|
49
|
+
* @property {string[]} groups - Access groups (admin, secret-manager, secret-reader, developer, docker, integrator)
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
52
|
/**
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
* @property {string} developerId - Unique developer ID (numeric string)
|
|
56
56
|
* @property {string} name - Display name
|
|
57
57
|
* @property {string} email - Email
|
|
58
|
-
* @property {string[]} [groups] - Default [developer]; tokens: admin, secret-manager, secret-reader, developer, docker
|
|
58
|
+
* @property {string[]} [groups] - Default [developer]; tokens: admin, secret-manager, secret-reader, developer, docker, integrator
|
|
59
59
|
*/
|
|
60
60
|
|
|
61
61
|
/**
|
package/lib/app/deploy.js
CHANGED
|
@@ -18,7 +18,6 @@ const {
|
|
|
18
18
|
|
|
19
19
|
const fs = require('fs').promises;
|
|
20
20
|
const chalk = require('chalk');
|
|
21
|
-
const pushUtils = require('../deployment/push');
|
|
22
21
|
|
|
23
22
|
const SEP = chalk.gray('────────────────────────────────────────');
|
|
24
23
|
const logger = require('../utils/logger');
|
|
@@ -56,106 +55,6 @@ function validateAppName(appName) {
|
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
/**
|
|
60
|
-
* Validates push prerequisites
|
|
61
|
-
* @async
|
|
62
|
-
* @function validatePushPrerequisites
|
|
63
|
-
* @param {string} appName - Application name
|
|
64
|
-
* @param {string} registry - Registry URL
|
|
65
|
-
* @throws {Error} If prerequisites are not met
|
|
66
|
-
*/
|
|
67
|
-
async function validatePushPrerequisites(appName, registry) {
|
|
68
|
-
if (!pushUtils.validateRegistryURL(registry)) {
|
|
69
|
-
throw new Error(`Invalid registry URL format: ${registry}. Expected format: *.azurecr.io`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!await pushUtils.checkLocalImageExists(appName, 'latest')) {
|
|
73
|
-
throw new Error(`Docker image ${appName}:latest not found locally.\nRun 'aifabrix build ${appName}' first`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!await pushUtils.checkAzureCLIInstalled()) {
|
|
77
|
-
throw new Error('Azure CLI is not installed. Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Executes push operations
|
|
83
|
-
* @async
|
|
84
|
-
* @function executePush
|
|
85
|
-
* @param {string} appName - Application name
|
|
86
|
-
* @param {string} registry - Registry URL
|
|
87
|
-
* @param {string[]} tags - Tags to push
|
|
88
|
-
* @throws {Error} If push fails
|
|
89
|
-
*/
|
|
90
|
-
async function executePush(appName, registry, tags) {
|
|
91
|
-
if (await pushUtils.checkACRAuthentication(registry)) {
|
|
92
|
-
logger.log(formatSuccessLine(`Already authenticated with ${registry}`));
|
|
93
|
-
} else {
|
|
94
|
-
await pushUtils.authenticateACR(registry);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
await Promise.all(tags.map(async(tag) => {
|
|
98
|
-
await pushUtils.tagImage(`${appName}:latest`, `${registry}/${appName}:${tag}`);
|
|
99
|
-
await pushUtils.pushImage(`${registry}/${appName}:${tag}`, registry);
|
|
100
|
-
}));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Verifies push result
|
|
105
|
-
* @function verifyPushResult
|
|
106
|
-
* @param {string[]} tags - Tags that were pushed
|
|
107
|
-
* @param {string} registry - Registry URL
|
|
108
|
-
* @param {string} appName - Application name
|
|
109
|
-
*/
|
|
110
|
-
function verifyPushResult(tags, registry, appName) {
|
|
111
|
-
logger.log(formatSuccessParagraph(`Successfully pushed ${tags.length} tag(s) to ${registry}`));
|
|
112
|
-
logger.log(chalk.gray(`Image: ${registry}/${appName}:*`));
|
|
113
|
-
logger.log(chalk.gray(`Tags: ${tags.join(', ')}`));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Pushes application image to Azure Container Registry
|
|
118
|
-
* @async
|
|
119
|
-
* @function pushApp
|
|
120
|
-
* @param {string} appName - Name of the application
|
|
121
|
-
* @param {Object} options - Push options (registry, tag)
|
|
122
|
-
* @returns {Promise<void>} Resolves when push is complete
|
|
123
|
-
*/
|
|
124
|
-
async function pushApp(appName, options = {}) {
|
|
125
|
-
try {
|
|
126
|
-
validateAppName(appName);
|
|
127
|
-
|
|
128
|
-
const { getBuilderPath, resolveApplicationConfigPath } = require('../utils/paths');
|
|
129
|
-
const { loadConfigFile } = require('../utils/config-format');
|
|
130
|
-
const builderPath = getBuilderPath(appName);
|
|
131
|
-
let config;
|
|
132
|
-
try {
|
|
133
|
-
const configPath = resolveApplicationConfigPath(builderPath);
|
|
134
|
-
config = loadConfigFile(configPath);
|
|
135
|
-
} catch (error) {
|
|
136
|
-
throw new Error(`Failed to load configuration: ${error.message}\nRun 'aifabrix create ${appName}' first`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const registry = options.registry || config.image?.registry;
|
|
140
|
-
if (!registry) {
|
|
141
|
-
throw new Error('Registry URL is required. Provide via --registry flag or configure in application.yaml under image.registry');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!/^[^.]+\.azurecr\.io$/.test(registry)) {
|
|
145
|
-
throw new Error(`Invalid ACR URL format: ${registry}. Expected format: *.azurecr.io`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const tags = options.tag ? options.tag.split(',').map(t => t.trim()) : ['latest'];
|
|
149
|
-
|
|
150
|
-
await validatePushPrerequisites(appName, registry);
|
|
151
|
-
await executePush(appName, registry, tags);
|
|
152
|
-
verifyPushResult(tags, registry, appName);
|
|
153
|
-
|
|
154
|
-
} catch (error) {
|
|
155
|
-
throw new Error(`Failed to push application: ${error.message}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
58
|
/**
|
|
160
59
|
* Generates and validates deployment manifest
|
|
161
60
|
* @async
|
|
@@ -492,7 +391,6 @@ async function deployApp(appName, options = {}) {
|
|
|
492
391
|
}
|
|
493
392
|
|
|
494
393
|
module.exports = {
|
|
495
|
-
pushApp,
|
|
496
394
|
deployApp,
|
|
497
395
|
validateAppName,
|
|
498
396
|
validateImageIsPullable
|
package/lib/cli/index.js
CHANGED
|
@@ -41,6 +41,8 @@ const { setupChannelCommands } = require('../commands/channel');
|
|
|
41
41
|
const { setupManifestCommands } = require('../commands/manifest');
|
|
42
42
|
const { setupNpmCommands } = require('./setup-npm');
|
|
43
43
|
const { setupListCommand } = require('./setup-list');
|
|
44
|
+
const { setupHelpCommand } = require('./setup-help');
|
|
45
|
+
const { setupOnboardingCommand } = require('./setup-onboarding');
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
48
|
* Dev, secrets, parameters, and platform commands (keeps setupCommands under statement limit).
|
|
@@ -101,6 +103,8 @@ function setupExtendedCliCommands(program) {
|
|
|
101
103
|
* @param {Command} program - Commander program instance
|
|
102
104
|
*/
|
|
103
105
|
function setupCommands(program) {
|
|
106
|
+
setupHelpCommand(program);
|
|
107
|
+
setupOnboardingCommand(program);
|
|
104
108
|
setupCoreCliCommands(program);
|
|
105
109
|
setupExtendedCliCommands(program);
|
|
106
110
|
setupDevAndPlatformCommands(program);
|
package/lib/cli/setup-dev.js
CHANGED
|
@@ -44,8 +44,11 @@ const DEV_ADD_HELP_AFTER = `
|
|
|
44
44
|
Examples:
|
|
45
45
|
$ aifabrix dev add --developer-id 02 --name "Jane Doe" --email jane@example.com
|
|
46
46
|
$ aifabrix dev add --developer-id 03 --name "Build admin" --email admin@example.com --groups admin,developer
|
|
47
|
-
$ aifabrix dev add --developer-id 04 --name "
|
|
48
|
-
$ aifabrix dev add --developer-id 05 --name "Integrator" --email integrator@example.com --groups
|
|
47
|
+
$ aifabrix dev add --developer-id 04 --name "Docker developer" --email docker-dev@example.com --groups developer,docker
|
|
48
|
+
$ aifabrix dev add --developer-id 05 --name "Integrator" --email integrator@example.com --groups integrator
|
|
49
|
+
|
|
50
|
+
Groups: integrator is for SSH onboarding without Docker. Use developer,docker for host Docker access (root-equivalent; grant sparingly).
|
|
51
|
+
When --groups is supplied, it replaces the default developer group; developer is not added automatically.
|
|
49
52
|
|
|
50
53
|
Requires a configured remote Builder Server and an admin client certificate (same machine setup as dev list / dev pin). After add, run dev pin <id> once to create a PIN for aifabrix dev init.
|
|
51
54
|
`;
|
|
@@ -318,7 +321,7 @@ function setupDevListAddCommands(dev) {
|
|
|
318
321
|
.requiredOption('--developer-id <id>', 'Unique id, digits only (e.g. 02); used with dev init --developer-id')
|
|
319
322
|
.requiredOption('--name <name>', 'Display name')
|
|
320
323
|
.requiredOption('--email <email>', 'Email address')
|
|
321
|
-
.option('--groups <items>', 'Comma-separated roles: admin, secret-manager, secret-reader, developer, docker', 'developer')
|
|
324
|
+
.option('--groups <items>', 'Comma-separated roles: admin, secret-manager, secret-reader, developer, docker, integrator', 'developer')
|
|
322
325
|
.option('--no-provision', 'Create user in database only (skip host provision job)')
|
|
323
326
|
.option('--builder-version <ver>', 'Builder CLI version for provision job')
|
|
324
327
|
.option('--admin-email <email>', 'Platform admin email for provision job')
|
|
@@ -373,7 +376,7 @@ function setupDevUpdatePinDeleteCommands(dev) {
|
|
|
373
376
|
.option('--developer-id <id>', 'Developer ID (same as dev add)')
|
|
374
377
|
.option('--name <name>', 'Display name')
|
|
375
378
|
.option('--email <email>', 'Email address')
|
|
376
|
-
.option('--groups <items>', 'Comma-separated groups (admin, secret-manager, secret-reader, developer, docker)')
|
|
379
|
+
.option('--groups <items>', 'Comma-separated groups (admin, secret-manager, secret-reader, developer, docker, integrator)')
|
|
377
380
|
.addHelpText('after', DEV_UPDATE_HELP_AFTER)
|
|
378
381
|
.action(async(developerId, options) => {
|
|
379
382
|
try {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** Read-only onboarding guide; command reference remains available through --help. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const ONBOARDING_GUIDE = `Welcome to AI Fabrix
|
|
5
|
+
|
|
6
|
+
On a managed workspace host with provisioned config, run:
|
|
7
|
+
|
|
8
|
+
af onboarding
|
|
9
|
+
af onboarding --coding claude
|
|
10
|
+
af onboarding --coding codex
|
|
11
|
+
af onboarding --coding cursor
|
|
12
|
+
af onboarding --coding claude --name sales-workspace
|
|
13
|
+
af onboarding --coding claude --name finance-workspace
|
|
14
|
+
af onboarding --repo example-org/example-workspace
|
|
15
|
+
af onboarding --repo my-org/new-repo
|
|
16
|
+
|
|
17
|
+
This prepares the coding platform selected by your organisation, or the one
|
|
18
|
+
you pass for this run. Claude starts in a detached tmux session; Codex and
|
|
19
|
+
Cursor use their own SSH clients. It does not install the local AI Fabrix platform.
|
|
20
|
+
Use --name <folder> for separate workspaces; each Claude workspace has its own
|
|
21
|
+
tmux session. Use --repo owner/name to clone an accessible GitHub repository
|
|
22
|
+
or create a new private one. agent-kit remote create creates a new GitHub
|
|
23
|
+
repository for the current local workspace; --name sets that remote name.
|
|
24
|
+
|
|
25
|
+
Start your integration workspace with Codex:
|
|
26
|
+
|
|
27
|
+
aifabrix agent-kit setup <controller-url>
|
|
28
|
+
|
|
29
|
+
Ask your administrator for the Controller URL.
|
|
30
|
+
Setup guides you through sign-in, installs the Agent Kit,
|
|
31
|
+
and starts your coding agent.
|
|
32
|
+
|
|
33
|
+
Run setup in a Linux/macOS terminal with Builder, Git and tmux installed.
|
|
34
|
+
On Windows, connect to your organisation's Builder Server first.
|
|
35
|
+
Your files stay under the AI Fabrix work directory on the machine running setup.
|
|
36
|
+
No GitHub account or Docker is required for the default local workspace.
|
|
37
|
+
|
|
38
|
+
Optional:
|
|
39
|
+
Use GitHub (requires gh):
|
|
40
|
+
aifabrix agent-kit setup <controller-url> --repo owner/project
|
|
41
|
+
|
|
42
|
+
Use Claude:
|
|
43
|
+
aifabrix agent-kit setup <controller-url> --code claude
|
|
44
|
+
|
|
45
|
+
Already have a project?
|
|
46
|
+
Open its Git repository folder and install the Agent Kit:
|
|
47
|
+
aifabrix agent-kit install --host codex
|
|
48
|
+
|
|
49
|
+
Using your organisation's Builder Server?
|
|
50
|
+
On your own computer, use the onboarding command your administrator gives you:
|
|
51
|
+
aifabrix dev init --developer-id <id> --server <builder-server-url> --pin <pin>
|
|
52
|
+
|
|
53
|
+
Use the SSH command printed after successful onboarding to connect.
|
|
54
|
+
Once connected to a managed workspace, run af onboarding.
|
|
55
|
+
For a portable server without managed config, use agent-kit setup above.
|
|
56
|
+
Integrator accounts use the integrator group; Docker is not needed.
|
|
57
|
+
|
|
58
|
+
Controller URL: connects your workspace to AI Fabrix.
|
|
59
|
+
Builder Server URL: connects your computer to the remote workspace host.
|
|
60
|
+
These are different services; ask your administrator for the correct URLs.
|
|
61
|
+
|
|
62
|
+
Explore:
|
|
63
|
+
aifabrix --help
|
|
64
|
+
aifabrix agent-kit --help
|
|
65
|
+
aifabrix <command> --help
|
|
66
|
+
|
|
67
|
+
This guide does not sign you in, install software, or change configuration.
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
function setupHelpCommand(program) {
|
|
71
|
+
program.addHelpCommand(false);
|
|
72
|
+
program.command('help [command]')
|
|
73
|
+
.description('Show the onboarding guide, or help for a command')
|
|
74
|
+
.addHelpText('after', '\nExamples:\n $ aifabrix help\n $ aifabrix help agent-kit\n')
|
|
75
|
+
.action((name, options, command) => {
|
|
76
|
+
if (!name) {
|
|
77
|
+
command.configureOutput().writeOut(ONBOARDING_GUIDE);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const target = program.commands.find(item => item.name() === name || item.aliases().includes(name));
|
|
81
|
+
if (!target) command.error(`Unknown command '${name}'. Run aifabrix --help to list commands.`);
|
|
82
|
+
target.outputHelp();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { setupHelpCommand, ONBOARDING_GUIDE };
|