@aifabrix/builder 2.58.0 → 2.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/agent-kit/codex-app-server.js +44 -0
- package/lib/agent-kit/git-identity.js +180 -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 +87 -0
- package/lib/agent-kit/start.js +48 -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/role-assistant-test-job.api.js +60 -0
- package/lib/api/types/dev.types.js +3 -3
- package/lib/api/work-search.api.js +13 -6
- package/lib/app/deploy.js +8 -103
- package/lib/app/show-display.js +1 -0
- package/lib/app/show-online.js +15 -0
- package/lib/build/standard-docker-build.js +4 -1
- package/lib/cli/index.js +4 -0
- package/lib/cli/setup-app.js +14 -1
- 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/cli/setup-utility.js +24 -1
- 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/role-assistant.js +7 -0
- 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/core/env-platform-expand.js +93 -0
- package/lib/core/secrets-env-content.js +37 -3
- 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 +13 -1
- 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 +10 -7
- 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/builders.js +17 -0
- package/lib/generator/helpers.js +23 -2
- package/lib/generator/index.js +33 -18
- 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/role-assistant/test-cases-search.js +3 -0
- package/lib/role-assistant/test-job-runner.js +125 -0
- package/lib/role-assistant/test-runner-search.js +44 -2
- package/lib/schema/application-schema.json +193 -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 +140 -34
- package/lib/schema/infra.parameter.yaml +30 -29
- package/lib/schema/infrastructure-schema.json +133 -14
- package/lib/schema/installation-environment.schema.json +1 -1
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/compose-generate-docker-compose.js +12 -0
- 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-build.js +29 -8
- package/lib/utils/docker-daemon-tls-ca.js +2 -2
- package/lib/utils/docker-manifest-public-port.js +36 -0
- package/lib/utils/error-formatters/validation-errors.js +25 -5
- package/lib/utils/help-builder.js +4 -3
- package/lib/utils/platform-resolution.js +226 -0
- package/lib/utils/resolve-docker-image-ref.js +8 -3
- package/lib/utils/token-manager.js +29 -36
- package/package.json +4 -3
- package/templates/README.md +2 -1
- package/templates/agent-kit/agent-kit.yaml +4 -1
- package/templates/agent-kit/instructions/AGENTKIT.md +40 -3
- package/templates/agent-kit/instructions/root.AGENTS.md +42 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +33 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/scripts/delivery-verdict.js +176 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -0
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +10 -3
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +21 -0
- package/templates/agent-kit/skills/shared/feedback.md +31 -0
- package/templates/agent-kit/skills/shared/hosts.md +35 -7
- package/templates/agent-kit/skills/shared/status.md +85 -0
- package/templates/agent-kit/workspace/BUILDER_IMPROVEMENT_FINDINGS.md +27 -0
- package/templates/agent-kit/workspace/README.md +14 -1
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/dataplane/application.yaml +30 -2
- package/templates/applications/dataplane/env.template +37 -4
- package/templates/applications/miso-controller/application.yaml +24 -1
- package/templates/applications/miso-controller/env.template +22 -35
- package/templates/external-system/external-datasource.yaml.hbs +7 -6
- package/templates/marketplace/main.json +281 -75
- package/templates/python/Dockerfile.hbs +2 -0
- package/templates/typescript/Dockerfile.hbs +2 -0
- package/templates/typescript/docker-compose.hbs +12 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Read the provisioned user config for zero-argument managed onboarding. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('../internal/fs-real-sync');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const yaml = require('js-yaml');
|
|
7
|
+
const config = require('../core/config');
|
|
8
|
+
const { normalizeSetup } = require('./setup-context');
|
|
9
|
+
|
|
10
|
+
function assertConfigFile(file) {
|
|
11
|
+
let stat;
|
|
12
|
+
try {
|
|
13
|
+
stat = fs.lstatSync(file);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT') {
|
|
16
|
+
throw new Error('Managed config.yaml is missing. Ask your administrator to provision this workspace.');
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
21
|
+
throw new Error('Managed config.yaml must be a regular file. Ask your administrator to repair it.');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function assertManagedKeys(file) {
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = yaml.load(fs.readFileSync(file, 'utf8'));
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error('Managed config.yaml is invalid. Ask your administrator to repair it.');
|
|
31
|
+
}
|
|
32
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw) ||
|
|
33
|
+
!['controller', 'environment', 'codingPlatform'].every(key => Object.hasOwn(raw, key))) {
|
|
34
|
+
throw new Error('Managed config.yaml needs controller, environment, and codingPlatform. Ask your administrator to repair it.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertManagedOverrides(options) {
|
|
39
|
+
if (options.code !== undefined || options.controllerUrl !== undefined) {
|
|
40
|
+
throw new Error('Managed onboarding uses the provisioned Controller; --code and --controller overrides are not allowed.');
|
|
41
|
+
}
|
|
42
|
+
if (options.repo !== undefined && options.name !== undefined) {
|
|
43
|
+
throw new Error('Use either --repo owner/name or --name folder for managed onboarding.');
|
|
44
|
+
}
|
|
45
|
+
if (options.coding !== undefined && !['claude', 'codex', 'cursor'].includes(options.coding)) {
|
|
46
|
+
throw new Error('Use --coding claude, codex, or cursor.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function assertManagedValues(values) {
|
|
51
|
+
if (!values || typeof values !== 'object' || typeof values.controller !== 'string' ||
|
|
52
|
+
typeof values.environment !== 'string' || !values.environment.trim() ||
|
|
53
|
+
!['codex', 'claude', 'cursor'].includes(values.codingPlatform)) {
|
|
54
|
+
throw new Error('Managed config.yaml needs controller, environment, and codingPlatform. Ask your administrator to repair it.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function managedSetupOptions(options = {}) {
|
|
59
|
+
assertManagedOverrides(options);
|
|
60
|
+
const file = options.configFile || config.CONFIG_FILE;
|
|
61
|
+
assertConfigFile(file);
|
|
62
|
+
assertManagedKeys(file);
|
|
63
|
+
const values = await (options.getConfig || config.getConfig)();
|
|
64
|
+
assertManagedValues(values);
|
|
65
|
+
let context;
|
|
66
|
+
try {
|
|
67
|
+
context = normalizeSetup({ controllerUrl: values.controller,
|
|
68
|
+
code: options.coding || values.codingPlatform, managed: true });
|
|
69
|
+
} catch {
|
|
70
|
+
throw new Error('Managed config.yaml has an invalid Controller URL or coding platform. Ask your administrator to repair it.');
|
|
71
|
+
}
|
|
72
|
+
const sshHost = values['sync-ssh-host'];
|
|
73
|
+
const remoteHost = typeof sshHost === 'string' && /^[a-z0-9.-]+$/i.test(sshHost)
|
|
74
|
+
? sshHost : os.hostname();
|
|
75
|
+
return { controllerUrl: context.controller, code: context.code,
|
|
76
|
+
remoteHost, managed: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { managedSetupOptions, assertConfigFile, assertManagedKeys };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Process boundary for onboarding: argv only and no credential-bearing errors. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
function capture(bin, args, options = {}) {
|
|
7
|
+
return (options.execFileSync || execFileSync)(bin, args, {
|
|
8
|
+
cwd: options.cwd,
|
|
9
|
+
env: options.env || process.env,
|
|
10
|
+
encoding: 'utf8',
|
|
11
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function checked(bin, args, message, options = {}) {
|
|
16
|
+
try {
|
|
17
|
+
return capture(bin, args, options);
|
|
18
|
+
} catch {
|
|
19
|
+
throw new Error(message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function interactive(bin, args, message, options = {}) {
|
|
24
|
+
const result = (options.spawnSync || spawnSync)(bin, args, {
|
|
25
|
+
cwd: options.cwd,
|
|
26
|
+
env: options.env || process.env,
|
|
27
|
+
stdio: 'inherit'
|
|
28
|
+
});
|
|
29
|
+
if (!result || result.error || result.signal || result.status !== 0) {
|
|
30
|
+
throw new Error(message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function available(bin, options = {}) {
|
|
35
|
+
try {
|
|
36
|
+
return String(capture('which', [bin], options)).trim();
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { capture, checked, interactive, available };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Start or reattach only to a session belonging to this workspace and selected agent. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { createHash } = require('crypto');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
7
|
+
const { runStart, hostUserPrefix, resolveRemoteControlName, resolveSessionName } = require('./start');
|
|
8
|
+
const { tmuxSessionExists } = require('./tmux-sessions');
|
|
9
|
+
const { checked, interactive } = require('./setup-process');
|
|
10
|
+
|
|
11
|
+
function sessionIdentity(context, options = {}) {
|
|
12
|
+
const key = `${context.target}\n${context.controller}\n${context.code}`;
|
|
13
|
+
const hash = createHash('sha256').update(key).digest('hex').slice(0, 16);
|
|
14
|
+
const legacyName = `aifabrix-${context.code}-${hash}`;
|
|
15
|
+
const name = options.managed && context.code === 'claude'
|
|
16
|
+
? resolveSessionName(undefined, context.target) : legacyName;
|
|
17
|
+
return { name, key, legacyName };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function verifySession(name, key, options) {
|
|
21
|
+
const saved = String(checked('tmux', ['show-options', '-v', '-t', name, '@aifabrix-setup'],
|
|
22
|
+
'Cannot verify existing agent session ownership.', options)).trim();
|
|
23
|
+
if (saved !== key) throw new Error('Existing session ownership does not match. No session was replaced.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function prepareSession(context, provider, options = {}) {
|
|
27
|
+
const { name, key, legacyName } = sessionIdentity(context, options);
|
|
28
|
+
const exec = (bin, args, extra) => (options.execFileSync || execFileSync)(bin, args, { ...extra, env: provider.env });
|
|
29
|
+
const spawn = (bin, args, extra) => (options.spawnSync || spawnSync)(bin, args, { ...extra, env: provider.env });
|
|
30
|
+
const opts = { ...options, env: provider.env };
|
|
31
|
+
if (tmuxSessionExists(name, exec)) {
|
|
32
|
+
verifySession(name, key, opts);
|
|
33
|
+
} else if (name !== legacyName && tmuxSessionExists(legacyName, exec)) {
|
|
34
|
+
verifySession(legacyName, key, opts);
|
|
35
|
+
checked('tmux', ['rename-session', '-t', legacyName, name],
|
|
36
|
+
'Could not rename the existing agent session. Inspect agent-kit list before retrying.', opts);
|
|
37
|
+
} else {
|
|
38
|
+
const remoteControlName = context.code === 'claude'
|
|
39
|
+
? resolveRemoteControlName(hostUserPrefix(options), path.basename(context.target)) : undefined;
|
|
40
|
+
runStart({ app: context.code, cwd: context.target, name, detach: true, skipApproveAll: true,
|
|
41
|
+
binary: provider.binary, remoteControlName, execFileSync: exec, spawnSync: spawn });
|
|
42
|
+
checked('tmux', ['set-option', '-t', name, '@aifabrix-setup', key],
|
|
43
|
+
'Agent started but its session could not be marked. Inspect agent-kit list before retrying.', opts);
|
|
44
|
+
}
|
|
45
|
+
return name;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function attachSession(name, provider, options = {}) {
|
|
49
|
+
interactive('tmux', ['attach-session', '-t', `=${name}`],
|
|
50
|
+
'Could not attach the agent session. Inspect aifabrix agent-kit list.', { ...options, env: provider.env });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { prepareSession, attachSession, sessionIdentity };
|
|
@@ -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,87 @@
|
|
|
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 { ensureGitIdentity } = require('./git-identity');
|
|
12
|
+
const { prepareWorkspace, validateExisting } = require('./setup-workspace');
|
|
13
|
+
const { prepareSession, attachSession } = require('./setup-session');
|
|
14
|
+
const { managedSetupOptions } = require('./setup-managed');
|
|
15
|
+
|
|
16
|
+
async function authenticate(context, options, setPhase) {
|
|
17
|
+
setPhase('controller-login');
|
|
18
|
+
logger.log('Connecting to AI Fabrix...');
|
|
19
|
+
await ensureController(context);
|
|
20
|
+
if (context.fullName) {
|
|
21
|
+
setPhase('github-login');
|
|
22
|
+
ensureGithubLogin(options);
|
|
23
|
+
// Commits need an author. The login token supplies the default; the user can change it.
|
|
24
|
+
await ensureGitIdentity({ ...options, interactive: options.interactive !== false });
|
|
25
|
+
}
|
|
26
|
+
if (context.code === 'cursor') return null;
|
|
27
|
+
setPhase('agent-login');
|
|
28
|
+
return ensureProvider(context, options);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function prepareOnboarding(context, progress, options) {
|
|
32
|
+
const correlationId = randomUUID();
|
|
33
|
+
let provider;
|
|
34
|
+
let session;
|
|
35
|
+
let phase = 'workspace-check';
|
|
36
|
+
try {
|
|
37
|
+
validateExisting(context, progress, options);
|
|
38
|
+
provider = await authenticate(context, options, value => {
|
|
39
|
+
phase = value;
|
|
40
|
+
});
|
|
41
|
+
phase = 'workspace';
|
|
42
|
+
prepareWorkspace(context, progress, options);
|
|
43
|
+
if (!options.managed || context.code === 'claude') {
|
|
44
|
+
phase = 'agent-start';
|
|
45
|
+
session = prepareSession(context, provider, options);
|
|
46
|
+
}
|
|
47
|
+
await auditLog('AUDIT', 'Agent workspace onboarding ready', { correlationId, path: context.target, agent: context.code });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
await auditLog('ERROR', 'Agent workspace onboarding failed', { correlationId, phase });
|
|
50
|
+
throw error;
|
|
51
|
+
} finally {
|
|
52
|
+
progress.release();
|
|
53
|
+
}
|
|
54
|
+
return { session, provider };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function runSetup(options = {}) {
|
|
58
|
+
const input = normalizeSetup(options);
|
|
59
|
+
preflight(input, options);
|
|
60
|
+
const context = await resolveWork(input, options);
|
|
61
|
+
const progress = acquireWorkspace(context);
|
|
62
|
+
const { session, provider } = await prepareOnboarding(context, progress, options);
|
|
63
|
+
logger.log(formatSuccessLine(`Workspace ready: ${context.target}`));
|
|
64
|
+
if (options.managed) {
|
|
65
|
+
const host = options.remoteHost || new URL(context.controller).hostname;
|
|
66
|
+
if (context.code === 'claude') {
|
|
67
|
+
logger.log(`Claude Code running in tmux session ${session}. Attach: tmux attach -t ${session}`);
|
|
68
|
+
logger.log(`Next: Open Claude Code Remote Control on ${host}, or attach to ${session}.`);
|
|
69
|
+
return { path: context.target, code: context.code, session };
|
|
70
|
+
}
|
|
71
|
+
const next = {
|
|
72
|
+
cursor: `In Cursor, connect with Remote SSH to ${host}, then open ${context.target}.`,
|
|
73
|
+
codex: `In Codex, add the SSH project ${context.target} on ${host}.`
|
|
74
|
+
}[context.code];
|
|
75
|
+
logger.log(`Next: ${next} Say "Hi" to begin.`);
|
|
76
|
+
} else {
|
|
77
|
+
attachSession(session, provider, options);
|
|
78
|
+
}
|
|
79
|
+
return { path: context.target, code: context.code, session };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function runManagedOnboarding(options = {}) {
|
|
83
|
+
const managed = await managedSetupOptions(options);
|
|
84
|
+
return runSetup({ ...options, ...managed });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { runSetup, runManagedOnboarding };
|
package/lib/agent-kit/start.js
CHANGED
|
@@ -116,6 +116,27 @@ function claudeInnerArgs(p) {
|
|
|
116
116
|
]);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* POSIX single-quote escaping for a value sent to a shell via tmux send-keys.
|
|
121
|
+
* @param {string} value - Raw value
|
|
122
|
+
* @returns {string} Quoted value
|
|
123
|
+
*/
|
|
124
|
+
function shellQuote(value) {
|
|
125
|
+
const escaped = String(value).split('\'').join('\'\\\'\'');
|
|
126
|
+
return `'${escaped}'`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Codex authenticates inside the session, not in the window that launched it. Running it as the
|
|
131
|
+
* tmux session command makes the session end with the sign-in process, so the shell owns the
|
|
132
|
+
* window and codex is started inside it with send-keys.
|
|
133
|
+
* @param {object} p - Start parameters
|
|
134
|
+
* @returns {string} Shell command to run inside the session
|
|
135
|
+
*/
|
|
136
|
+
function codexStartCommand(p) {
|
|
137
|
+
return p.binary ? `exec ${shellQuote(p.binary)}` : 'exec codex';
|
|
138
|
+
}
|
|
139
|
+
|
|
119
140
|
/**
|
|
120
141
|
* @param {object} p
|
|
121
142
|
* @returns {object}
|
|
@@ -127,12 +148,17 @@ function buildTmuxSpec(p) {
|
|
|
127
148
|
? ['new-session', '-d', '-s', p.session, '-c', p.cwd]
|
|
128
149
|
: ['new-session', '-s', p.session, '-c', p.cwd];
|
|
129
150
|
const remote = p.app === 'claude' && p.remoteControl;
|
|
151
|
+
// codex: shell owns the window, codex starts inside it (see codexStartCommand).
|
|
152
|
+
const launchArgs = p.app === 'codex'
|
|
153
|
+
? [...mode]
|
|
154
|
+
: [...mode, ...(p.binary ? ['env', p.binary] : [spec.bin]), ...extra];
|
|
130
155
|
return {
|
|
131
156
|
bin: 'tmux',
|
|
132
|
-
args:
|
|
157
|
+
args: launchArgs,
|
|
133
158
|
session: p.session,
|
|
134
159
|
appBin: spec.bin,
|
|
135
160
|
needTmux: true,
|
|
161
|
+
innerCommand: p.app === 'codex' ? codexStartCommand(p) : undefined,
|
|
136
162
|
remoteControlName: remote ? p.remoteControlName : undefined,
|
|
137
163
|
remoteControlPrefix: remote ? p.remoteControlPrefix : undefined
|
|
138
164
|
};
|
|
@@ -287,16 +313,17 @@ function resolveStartContext(options) {
|
|
|
287
313
|
const cwd = resolveGitRoot(options.cwd || process.cwd());
|
|
288
314
|
const session = resolveSessionName(options.name, cwd);
|
|
289
315
|
const remoteControlPrefix = hostUserPrefix(options);
|
|
290
|
-
const remoteControlName = resolveRemoteControlName(remoteControlPrefix, session);
|
|
316
|
+
const remoteControlName = options.remoteControlName || resolveRemoteControlName(remoteControlPrefix, session);
|
|
291
317
|
const remoteControl = options.remoteControl !== false;
|
|
292
318
|
const spec = buildLaunchSpec({
|
|
293
319
|
app, cwd, session, remoteControlPrefix, remoteControlName,
|
|
294
|
-
detach: Boolean(options.detach), remoteControl
|
|
320
|
+
detach: Boolean(options.detach), remoteControl, binary: options.binary
|
|
295
321
|
});
|
|
296
322
|
spec.cwd = cwd;
|
|
297
323
|
const remoteSpec = app === 'codex' && remoteControl
|
|
298
324
|
? buildCodexRemoteSpec(cwd)
|
|
299
325
|
: undefined;
|
|
326
|
+
if (remoteSpec && options.binary) remoteSpec.bin = options.binary;
|
|
300
327
|
return { app, cwd, session, remoteControl, spec, remoteSpec };
|
|
301
328
|
}
|
|
302
329
|
|
|
@@ -332,6 +359,22 @@ function executeStart(options, context, execute) {
|
|
|
332
359
|
startCodexRemote(options, context.remoteSpec);
|
|
333
360
|
}
|
|
334
361
|
spawnLaunch(options, context.spec);
|
|
362
|
+
sendInnerCommand(options, context.spec);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Starts the agent inside an existing session window. Only codex uses this; the session must exist
|
|
367
|
+
* first, which is why it runs after the launch.
|
|
368
|
+
* @param {object} options - Start options
|
|
369
|
+
* @param {object} spec - Launch spec
|
|
370
|
+
* @returns {void}
|
|
371
|
+
*/
|
|
372
|
+
function sendInnerCommand(options, spec) {
|
|
373
|
+
if (!spec.innerCommand) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const exec = options.execFileSync || execFileSync;
|
|
377
|
+
exec('tmux', ['send-keys', '-t', `=${spec.session}`, spec.innerCommand, 'Enter'], { stdio: 'inherit' });
|
|
335
378
|
}
|
|
336
379
|
|
|
337
380
|
/**
|
|
@@ -362,6 +405,8 @@ function runStart(options) {
|
|
|
362
405
|
|
|
363
406
|
module.exports = {
|
|
364
407
|
START_APPS,
|
|
408
|
+
codexStartCommand,
|
|
409
|
+
sendInnerCommand,
|
|
365
410
|
normalizeStartApp,
|
|
366
411
|
sanitizeSessionName,
|
|
367
412
|
resolveSessionName,
|
|
@@ -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 };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dataplane asynchronous Role Assistant test job API.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Enqueue and poll published Role Assistant cases
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { createDataplaneApiClient } = require('./index');
|
|
10
|
+
const { throwIfApiFailed } = require('./api-response-errors');
|
|
11
|
+
const { unwrapApiData } = require('../utils/external-system-readiness-core');
|
|
12
|
+
|
|
13
|
+
function jobPath(systemIdOrKey, suffix = '') {
|
|
14
|
+
const key = encodeURIComponent(String(systemIdOrKey || '').trim());
|
|
15
|
+
return `/api/v1/external/systems/${key}/role-assistant-tests/jobs${suffix}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function unwrapJob(res) {
|
|
19
|
+
const value = unwrapApiData(res);
|
|
20
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && value.data) {
|
|
21
|
+
return value.data;
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function enqueueRoleAssistantTestJob(
|
|
27
|
+
dataplaneUrl,
|
|
28
|
+
authConfig,
|
|
29
|
+
systemIdOrKey,
|
|
30
|
+
body
|
|
31
|
+
) {
|
|
32
|
+
const client = createDataplaneApiClient(dataplaneUrl, authConfig);
|
|
33
|
+
const path = jobPath(systemIdOrKey);
|
|
34
|
+
const res = await client.post(path, { body, timeoutMs: 60000 });
|
|
35
|
+
throwIfApiFailed(res, {
|
|
36
|
+
endpointUrl: `${String(dataplaneUrl || '').replace(/\/$/, '')}${path}`
|
|
37
|
+
});
|
|
38
|
+
return unwrapJob(res);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function getRoleAssistantTestJob(
|
|
42
|
+
dataplaneUrl,
|
|
43
|
+
authConfig,
|
|
44
|
+
systemIdOrKey,
|
|
45
|
+
jobId
|
|
46
|
+
) {
|
|
47
|
+
const suffix = `/${encodeURIComponent(String(jobId || '').trim())}`;
|
|
48
|
+
const client = createDataplaneApiClient(dataplaneUrl, authConfig);
|
|
49
|
+
const path = jobPath(systemIdOrKey, suffix);
|
|
50
|
+
const res = await client.get(path, { timeoutMs: 60000 });
|
|
51
|
+
throwIfApiFailed(res, {
|
|
52
|
+
endpointUrl: `${String(dataplaneUrl || '').replace(/\/$/, '')}${path}`
|
|
53
|
+
});
|
|
54
|
+
return unwrapJob(res);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
enqueueRoleAssistantTestJob,
|
|
59
|
+
getRoleAssistantTestJob
|
|
60
|
+
};
|