fraim-hub 2.0.351 → 2.0.352
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/dist/src/ai-hub/configured-agent-child.js +172 -0
- package/dist/src/ai-hub/configured-agent-command.js +204 -162
- package/dist/src/ai-hub/configured-agent-runner.js +185 -0
- package/dist/src/ai-hub/configured-agents.js +95 -130
- package/dist/src/ai-hub/hosts.js +33 -25
- package/dist/src/ai-hub/server.js +30 -11
- package/dist/src/cli/utils/managed-agent-paths.js +25 -9
- package/package.json +2 -2
- package/public/ai-hub/script.js +46 -36
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
40
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
41
|
+
const node_child_process_1 = require("node:child_process");
|
|
42
|
+
const node_string_decoder_1 = require("node:string_decoder");
|
|
43
|
+
function report(stage, value = '') {
|
|
44
|
+
const control = process.env.FRAIM_CONFIGURED_CONTROL;
|
|
45
|
+
if (control)
|
|
46
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(control, stage), value, { mode: 0o600 });
|
|
47
|
+
}
|
|
48
|
+
/** Quote a native argument, then protect both cmd parsing passes of a %* forwarding wrapper. */
|
|
49
|
+
function forwardingCmdArgument(value) {
|
|
50
|
+
let quoted = '"';
|
|
51
|
+
let slashes = 0;
|
|
52
|
+
for (const character of value) {
|
|
53
|
+
if (character === '\\') {
|
|
54
|
+
slashes++;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
quoted += '\\'.repeat(character === '"' ? slashes * 2 + 1 : slashes) + character;
|
|
58
|
+
slashes = 0;
|
|
59
|
+
}
|
|
60
|
+
quoted += '\\'.repeat(slashes * 2) + '"';
|
|
61
|
+
return escapeCmd(escapeCmd(quoted));
|
|
62
|
+
}
|
|
63
|
+
function escapeCmd(value) { return value.replace(/([()\][%!^"`<>&|;, *?])/g, '^$1'); }
|
|
64
|
+
function cleanEnvironment() {
|
|
65
|
+
const env = { ...process.env };
|
|
66
|
+
const electronMode = env.FRAIM_CONFIGURED_ELECTRON_MODE;
|
|
67
|
+
for (const key of Object.keys(env))
|
|
68
|
+
if (key.startsWith('FRAIM_CONFIGURED_'))
|
|
69
|
+
delete env[key];
|
|
70
|
+
if (electronMode)
|
|
71
|
+
env.ELECTRON_RUN_AS_NODE = electronMode;
|
|
72
|
+
else
|
|
73
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
74
|
+
return env;
|
|
75
|
+
}
|
|
76
|
+
async function main() {
|
|
77
|
+
const mode = process.argv[2];
|
|
78
|
+
if (mode === '--fail') {
|
|
79
|
+
report('failure', ['preparation', 'resolution', 'invocation', 'migration'].includes(process.argv[3]) ? process.argv[3] : 'invocation');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const spec = JSON.parse(process.env.FRAIM_CONFIGURED_SPEC || '{}');
|
|
83
|
+
if (mode === '--shell-data') {
|
|
84
|
+
const shellPath = (value) => process.platform === 'win32' ? value.replace(/\\/g, '/') : value;
|
|
85
|
+
const fields = [shellPath(spec.target), spec.envPrefixText, shellPath(spec.setupScript?.command || ''), ...(spec.setupScript?.args || [])];
|
|
86
|
+
process.stdout.write(fields.join('\0') + '\0');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const env = cleanEnvironment();
|
|
90
|
+
let browser = { args: [] };
|
|
91
|
+
if (spec.browser && env.FRAIM_BROWSER_CDP_ENDPOINT) {
|
|
92
|
+
// Loaded only in the prepared child: CODEX_HOME and auth now belong to the selected account.
|
|
93
|
+
const { sharedBrowserHostConfig } = await Promise.resolve().then(() => __importStar(require('./hosts')));
|
|
94
|
+
browser = sharedBrowserHostConfig(spec.hostId, env);
|
|
95
|
+
if (spec.hostId === 'codex' && !browser.env?.CODEX_HOME)
|
|
96
|
+
throw new Error('Browser home preparation failed');
|
|
97
|
+
}
|
|
98
|
+
const args = spec.hostId === 'codex'
|
|
99
|
+
? [...spec.args, ...spec.extraArgs, ...browser.args]
|
|
100
|
+
: [...spec.args, ...browser.args, ...spec.extraArgs];
|
|
101
|
+
Object.assign(env, browser.env);
|
|
102
|
+
if (mode === '--prepare-shell') {
|
|
103
|
+
report('ready');
|
|
104
|
+
const updates = { ...browser.env, ELECTRON_RUN_AS_NODE: env.ELECTRON_RUN_AS_NODE || '' };
|
|
105
|
+
process.stdout.write(JSON.stringify({ args, env: updates }));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
let target = process.env.FRAIM_CONFIGURED_TARGET;
|
|
109
|
+
if (target && process.platform === 'win32' && spec.runner === 'bash' && !node_fs_1.default.existsSync(target) && node_fs_1.default.existsSync(`${target}.exe`))
|
|
110
|
+
target += '.exe';
|
|
111
|
+
if (!target || !node_fs_1.default.existsSync(target)) {
|
|
112
|
+
report('failure', 'resolution');
|
|
113
|
+
process.exitCode = 71;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
let command = target;
|
|
117
|
+
let commandArgs = args;
|
|
118
|
+
let windowsVerbatimArguments = false;
|
|
119
|
+
let shellTransport = false;
|
|
120
|
+
if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(target)) {
|
|
121
|
+
command = env.ComSpec || env.COMSPEC || 'cmd.exe';
|
|
122
|
+
commandArgs = ['/d', '/v:off', '/s', '/c', `"${[escapeCmd(target), ...args.map(forwardingCmdArgument)].join(' ')}"`];
|
|
123
|
+
windowsVerbatimArguments = true;
|
|
124
|
+
shellTransport = true;
|
|
125
|
+
}
|
|
126
|
+
else if (process.platform === 'win32' && spec.runner === 'bash' && !/\.(exe|com)$/i.test(target)) {
|
|
127
|
+
command = 'bash';
|
|
128
|
+
// MSYS parses apostrophes in separately supplied Windows argv. Transport one
|
|
129
|
+
// POSIX command string whose every data token is single-quoted for Bash.
|
|
130
|
+
const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
131
|
+
commandArgs = ['--noprofile', '--norc', '-c', `exec ${[target.replace(/\\/g, '/'), ...args].map(quote).join(' ')}`];
|
|
132
|
+
shellTransport = true;
|
|
133
|
+
}
|
|
134
|
+
const child = (0, node_child_process_1.spawn)(command, commandArgs, { env, cwd: process.cwd(), stdio: ['inherit', 'inherit', shellTransport ? 'pipe' : 'inherit'], windowsHide: true, windowsVerbatimArguments });
|
|
135
|
+
if (child.stderr) {
|
|
136
|
+
// A selected script can disappear between resolution and the shell opening it.
|
|
137
|
+
// Preserve diagnostics while withholding the selected account's local path.
|
|
138
|
+
const names = [...new Set([target, target.replace(/\\/g, '/'), target.replace(/\//g, '\\')])];
|
|
139
|
+
const pattern = new RegExp(names.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'gi');
|
|
140
|
+
const decoder = new node_string_decoder_1.StringDecoder('utf8');
|
|
141
|
+
let pending = '';
|
|
142
|
+
const publish = (text) => process.stderr.write(text.replace(pattern, '[configured target]'));
|
|
143
|
+
child.stderr.on('data', chunk => {
|
|
144
|
+
pending += decoder.write(chunk);
|
|
145
|
+
let boundary;
|
|
146
|
+
while ((boundary = pending.indexOf('\n')) >= 0) {
|
|
147
|
+
publish(pending.slice(0, boundary + 1));
|
|
148
|
+
pending = pending.slice(boundary + 1);
|
|
149
|
+
}
|
|
150
|
+
if (pending.length > 65536 + target.length) {
|
|
151
|
+
let boundary = 65536;
|
|
152
|
+
pattern.lastIndex = 0;
|
|
153
|
+
let match;
|
|
154
|
+
while ((match = pattern.exec(pending)) && match.index < boundary) {
|
|
155
|
+
if (match.index + match[0].length > boundary) {
|
|
156
|
+
boundary = match.index;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
publish(pending.slice(0, boundary));
|
|
161
|
+
pending = pending.slice(boundary);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
child.stderr.once('end', () => publish(pending + decoder.end()));
|
|
165
|
+
}
|
|
166
|
+
child.once('spawn', () => report('ready'));
|
|
167
|
+
child.once('error', () => { report('failure', 'invocation'); process.exitCode = 72; });
|
|
168
|
+
child.once('close', (code, signal) => { process.exitCode = signal ? 73 : code ?? 72; });
|
|
169
|
+
}
|
|
170
|
+
if (require.main === module) {
|
|
171
|
+
main().catch(() => { report('failure', 'preparation'); process.exitCode = 70; });
|
|
172
|
+
}
|
|
@@ -1,116 +1,196 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CONFIGURED_MIGRATION = exports.CONFIGURED_TROUBLESHOOTING = void 0;
|
|
6
4
|
exports.parseConfiguredCommand = parseConfiguredCommand;
|
|
7
5
|
exports.describeConfiguredCommandInvocationProblem = describeConfiguredCommandInvocationProblem;
|
|
8
|
-
exports.__setCommandResolutionProbeForTests = __setCommandResolutionProbeForTests;
|
|
9
|
-
exports.probeCommandResolvesNonInteractively = probeCommandResolvesNonInteractively;
|
|
10
6
|
exports.commandContainsLiteralSecret = commandContainsLiteralSecret;
|
|
11
7
|
exports.decorateHostPlanWithConfiguredAgent = decorateHostPlanWithConfiguredAgent;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
8
|
+
exports.CONFIGURED_TROUBLESHOOTING = 'Run troubleshoot-fraim with another working agent or in your IDE if this agent cannot start. Share only the profile ID, base agent and error category; do not share commands, environment values or credentials.';
|
|
9
|
+
exports.CONFIGURED_MIGRATION = 'This profile needs migration: Hub launches the base CLI after setup-only preparation. Use troubleshoot-fraim, then create-hub-configured-agent to extract and preserve the wrapper or profile function\'s account/environment setup. Do not replace its name and lose account selection. ' + exports.CONFIGURED_TROUBLESHOOTING;
|
|
10
|
+
const unsupportedSyntax = () => new Error('Configured command uses unsupported shell syntax. Use environment assignments followed by the base CLI and supported settings. ' + exports.CONFIGURED_MIGRATION);
|
|
11
|
+
// Explicit settings prevent alternate CLI spellings (attached short options,
|
|
12
|
+
// clusters, positionals and subcommands) from taking over Hub's session/protocol.
|
|
13
|
+
// Values are consumed with their option, never scanned as independent flags.
|
|
14
|
+
const SETTINGS = {
|
|
15
|
+
claude: ['--model', '-m', '--effort', '--permission-mode', '--allowedTools', '--disallowedTools', '--tools', '--add-dir', '--betas', '--settings', '--setting-sources', '--fallback-model', '--max-budget-usd'],
|
|
16
|
+
codex: ['--model', '-m', '--profile', '-p', '--config', '-c', '--sandbox'],
|
|
17
|
+
gemini: ['--model', '-m', '--approval-mode', '--allowed-tools', '--extensions', '--include-directories'],
|
|
18
|
+
copilot: ['--model', '--reasoning-effort', '--allow-tool', '--deny-tool', '--add-dir'],
|
|
19
|
+
};
|
|
20
|
+
// Prefixes are trusted user preparation, but only assignments belong here. Scan
|
|
21
|
+
// balanced quotes/subexpressions so a semicolon inside a secret-file expression
|
|
22
|
+
// cannot become an invocation boundary, and never evaluate an entire command.
|
|
23
|
+
function prefixValueEnd(text, start, runner) {
|
|
24
|
+
let quote = '';
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let i = start;
|
|
27
|
+
for (; i < text.length; i++) {
|
|
28
|
+
const char = text[i];
|
|
29
|
+
if (quote) {
|
|
30
|
+
if (char === quote) {
|
|
31
|
+
if (runner === 'powershell' && quote === "'" && text[i + 1] === "'") {
|
|
32
|
+
i++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
quote = '';
|
|
36
|
+
}
|
|
37
|
+
else if ((runner === 'powershell' && char === '`' && quote === '"') || (runner === 'bash' && char === '\\' && quote === '"')) {
|
|
38
|
+
if (++i >= text.length)
|
|
39
|
+
throw unsupportedSyntax();
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (char === "'" || char === '"') {
|
|
44
|
+
quote = char;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if ((runner === 'powershell' && char === '`') || (runner === 'bash' && char === '\\')) {
|
|
48
|
+
if (++i >= text.length)
|
|
49
|
+
throw unsupportedSyntax();
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (char === '(') {
|
|
53
|
+
if (runner === 'bash' && depth === 0 && text[i - 1] !== '$')
|
|
54
|
+
throw unsupportedSyntax();
|
|
55
|
+
depth++;
|
|
56
|
+
}
|
|
57
|
+
else if (char === ')') {
|
|
58
|
+
if (depth === 0)
|
|
59
|
+
throw unsupportedSyntax();
|
|
60
|
+
depth--;
|
|
61
|
+
}
|
|
62
|
+
else if (depth === 0) {
|
|
63
|
+
if (runner === 'powershell' && char === ';')
|
|
64
|
+
break;
|
|
65
|
+
if (/\s/.test(char)) {
|
|
66
|
+
if (runner === 'powershell') {
|
|
67
|
+
const afterPlus = text[i - 1] === '+';
|
|
68
|
+
while (/\s/.test(text[i] || '') && i < text.length)
|
|
69
|
+
i++;
|
|
70
|
+
if (text[i] === '+')
|
|
71
|
+
continue;
|
|
72
|
+
if (afterPlus && text[i] && text[i] !== ';') {
|
|
73
|
+
i--;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (text[i] !== ';')
|
|
77
|
+
throw unsupportedSyntax();
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (/[;&|<>`{}]/.test(char))
|
|
82
|
+
throw unsupportedSyntax();
|
|
83
|
+
}
|
|
54
84
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
85
|
+
if (quote || depth !== 0 || (i === start && runner === 'powershell'))
|
|
86
|
+
throw unsupportedSyntax();
|
|
87
|
+
if (runner === 'powershell' && text[i] !== ';')
|
|
88
|
+
throw unsupportedSyntax();
|
|
89
|
+
return i;
|
|
60
90
|
}
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const cached = probeCache.get(key);
|
|
75
|
-
if (cached && Date.now() - cached.at < PROBE_CACHE_TTL_MS)
|
|
76
|
-
return cached.result;
|
|
77
|
-
let result;
|
|
78
|
-
if (runner === 'powershell') {
|
|
79
|
-
if (process.platform !== 'win32') {
|
|
80
|
-
result = null;
|
|
91
|
+
function tokenizeInvocation(text, runner) {
|
|
92
|
+
const tokens = [];
|
|
93
|
+
let value = '';
|
|
94
|
+
let quote = '';
|
|
95
|
+
let started = false;
|
|
96
|
+
for (let i = 0; i < text.length; i++) {
|
|
97
|
+
const char = text[i];
|
|
98
|
+
if (!quote && /\s/.test(char)) {
|
|
99
|
+
if (started)
|
|
100
|
+
tokens.push(value);
|
|
101
|
+
value = '';
|
|
102
|
+
started = false;
|
|
103
|
+
continue;
|
|
81
104
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
started = true;
|
|
106
|
+
if (quote === "'") {
|
|
107
|
+
if (char === "'") {
|
|
108
|
+
if (runner === 'powershell' && text[i + 1] === "'") {
|
|
109
|
+
value += "'";
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
else
|
|
113
|
+
quote = '';
|
|
114
|
+
}
|
|
115
|
+
else
|
|
116
|
+
value += char;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (char === '"') {
|
|
120
|
+
quote = quote ? '' : '"';
|
|
121
|
+
continue;
|
|
89
122
|
}
|
|
123
|
+
if (!quote && char === "'") {
|
|
124
|
+
quote = "'";
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if ((runner === 'powershell' && char === '`') || (runner === 'bash' && char === '\\')) {
|
|
128
|
+
const next = text[++i];
|
|
129
|
+
if (next === undefined)
|
|
130
|
+
throw unsupportedSyntax();
|
|
131
|
+
if (runner === 'bash' && quote && !/["$`\\]/.test(next))
|
|
132
|
+
value += '\\';
|
|
133
|
+
if (runner === 'powershell' && /[0abefnrtuv]/i.test(next))
|
|
134
|
+
throw unsupportedSyntax();
|
|
135
|
+
value += next;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// Expanding variables, globbing, expressions and shell operators in the
|
|
139
|
+
// target/arguments would require evaluating a second command language.
|
|
140
|
+
// Reject those shapes rather than silently changing their meaning.
|
|
141
|
+
if (char === '$' || char === '`' || (!quote && /[;&|<>(){}*?\[\]#@~]/.test(char)))
|
|
142
|
+
throw unsupportedSyntax();
|
|
143
|
+
value += char;
|
|
90
144
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
145
|
+
if (quote)
|
|
146
|
+
throw unsupportedSyntax();
|
|
147
|
+
if (started)
|
|
148
|
+
tokens.push(value);
|
|
149
|
+
return tokens;
|
|
150
|
+
}
|
|
151
|
+
function parseConfiguredCommand(runner, commandText) {
|
|
152
|
+
const text = String(commandText || '').trim();
|
|
153
|
+
if (/[\r\n\0]/.test(text))
|
|
154
|
+
throw unsupportedSyntax();
|
|
155
|
+
let prefixEnd = 0;
|
|
156
|
+
while (prefixEnd < text.length) {
|
|
157
|
+
const assignment = text.slice(prefixEnd).match(runner === 'powershell'
|
|
158
|
+
? /^\$env:[A-Za-z_][A-Za-z0-9_]*\s*=\s*/i
|
|
159
|
+
: /^[A-Za-z_][A-Za-z0-9_]*=/);
|
|
160
|
+
if (!assignment)
|
|
161
|
+
break;
|
|
162
|
+
const valueEnd = prefixValueEnd(text, prefixEnd + assignment[0].length, runner);
|
|
163
|
+
prefixEnd = valueEnd + (runner === 'powershell' ? 1 : 0);
|
|
164
|
+
while (/\s/.test(text[prefixEnd] || '') && prefixEnd < text.length)
|
|
165
|
+
prefixEnd++;
|
|
102
166
|
}
|
|
103
|
-
|
|
104
|
-
|
|
167
|
+
let invocation = text.slice(prefixEnd).trim();
|
|
168
|
+
if (runner === 'powershell' && invocation.startsWith('&'))
|
|
169
|
+
invocation = invocation.slice(1).trimStart();
|
|
170
|
+
const tokens = tokenizeInvocation(invocation, runner);
|
|
171
|
+
return { envPrefixText: text.slice(0, prefixEnd).trim(), invocationHead: tokens[0] || '', extraArgs: tokens.slice(1) };
|
|
105
172
|
}
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
173
|
+
function describeConfiguredCommandInvocationProblem(parsed, baseHostId) {
|
|
174
|
+
if (!parsed.invocationHead)
|
|
175
|
+
return 'Configured command has no invocation after its environment prefix.';
|
|
176
|
+
if (parsed.invocationHead !== baseHostId)
|
|
177
|
+
return exports.CONFIGURED_MIGRATION;
|
|
178
|
+
const settings = SETTINGS[baseHostId] || [];
|
|
179
|
+
for (let i = 0; i < parsed.extraArgs.length; i++) {
|
|
180
|
+
const argument = parsed.extraArgs[i];
|
|
181
|
+
const option = argument.split('=')[0];
|
|
182
|
+
if (!settings.includes(option)) {
|
|
183
|
+
return 'Configured command contains an unsupported setting or conflicts with Hub protocol or session arguments. Hub supplies prompt, output and resume options, including short and attached forms. ' + exports.CONFIGURED_TROUBLESHOOTING;
|
|
184
|
+
}
|
|
185
|
+
if (argument.includes('=')) {
|
|
186
|
+
if (!argument.slice(argument.indexOf('=') + 1))
|
|
187
|
+
return 'Configured setting requires a value. ' + exports.CONFIGURED_TROUBLESHOOTING;
|
|
188
|
+
}
|
|
189
|
+
else if (!parsed.extraArgs[++i] || parsed.extraArgs[i].startsWith('-')) {
|
|
190
|
+
return 'Configured setting requires a value. ' + exports.CONFIGURED_TROUBLESHOOTING;
|
|
191
|
+
}
|
|
112
192
|
}
|
|
113
|
-
return
|
|
193
|
+
return null;
|
|
114
194
|
}
|
|
115
195
|
// Issue #1201 (R2.3): UX safety net only — flags what pattern-matches as a
|
|
116
196
|
// literal high-entropy secret pasted into the command field. Not a substitute
|
|
@@ -129,76 +209,38 @@ function commandContainsLiteralSecret(commandText) {
|
|
|
129
209
|
}
|
|
130
210
|
return false;
|
|
131
211
|
}
|
|
132
|
-
// The env-prefix assignment is evaluated as a bash "temporary environment"
|
|
133
|
-
// directly attached to the `exec` command on the same line (`VAR=$(...) exec
|
|
134
|
-
// "$@"`), not as a separate statement before it. A bare `VAR=value` on its
|
|
135
|
-
// own line only sets a local shell variable — it is NOT exported, so it
|
|
136
|
-
// would silently vanish from the real binary's environment once `exec`
|
|
137
|
-
// replaces this process image. Attaching it to the same command line is
|
|
138
|
-
// exactly what a terminal user's `VAR=$(...) codex ...` already does.
|
|
139
|
-
const BASH_LAUNCH_WRAPPER = [
|
|
140
|
-
'#!/usr/bin/env bash',
|
|
141
|
-
'set -e',
|
|
142
|
-
'if [ -n "$FRAIM_CONFIGURED_AGENT_ENV_PREFIX" ]; then',
|
|
143
|
-
' eval "$FRAIM_CONFIGURED_AGENT_ENV_PREFIX exec \\"\\$@\\""',
|
|
144
|
-
'else',
|
|
145
|
-
' exec "$@"',
|
|
146
|
-
'fi',
|
|
147
|
-
'',
|
|
148
|
-
].join('\n');
|
|
149
|
-
const POWERSHELL_LAUNCH_WRAPPER = [
|
|
150
|
-
'$ErrorActionPreference = "Stop"',
|
|
151
|
-
'if ($env:FRAIM_CONFIGURED_AGENT_ENV_PREFIX) {',
|
|
152
|
-
' Invoke-Expression $env:FRAIM_CONFIGURED_AGENT_ENV_PREFIX',
|
|
153
|
-
'}',
|
|
154
|
-
'$exe = $args[0]',
|
|
155
|
-
'if ($args.Length -gt 1) { $rest = $args[1..($args.Length - 1)] } else { $rest = @() }',
|
|
156
|
-
'& $exe @rest',
|
|
157
|
-
'exit $LASTEXITCODE',
|
|
158
|
-
'',
|
|
159
|
-
].join('\r\n');
|
|
160
|
-
// Writes the small, fixed wrapper script that lets a configured-agent's
|
|
161
|
-
// env-prefix (which may contain `$(...)`/`(...)` substitutions) resolve in a
|
|
162
|
-
// real shell at spawn time, then `exec`s Hub's own required invocation with
|
|
163
|
-
// the parsed extra CLI args appended. Content-checked so it self-heals like
|
|
164
|
-
// any other FRAIM-generated artifact (issue #1206 precedent) rather than
|
|
165
|
-
// silently going stale if regenerated with different logic.
|
|
166
|
-
//
|
|
167
|
-
// Security review (issue #1201): written under the per-user FRAIM home
|
|
168
|
-
// (`getUserFraimDirPath()`), NOT `os.tmpdir()`. A predictable filename in a
|
|
169
|
-
// shared, world-writable OS temp directory is a symlink-race target on
|
|
170
|
-
// multi-user machines — another local user could pre-place a symlink at the
|
|
171
|
-
// predictable path, and `fs.writeFileSync` would follow it and overwrite an
|
|
172
|
-
// arbitrary file the *victim* user can write to. The per-user FRAIM home is
|
|
173
|
-
// not shared across OS accounts, closing that class of attack.
|
|
174
|
-
function ensureConfiguredAgentLaunchWrapper(runner) {
|
|
175
|
-
const content = runner === 'bash' ? BASH_LAUNCH_WRAPPER : POWERSHELL_LAUNCH_WRAPPER;
|
|
176
|
-
const filePath = path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), runner === 'bash' ? 'hub-configured-agent-launch.sh' : 'hub-configured-agent-launch.ps1');
|
|
177
|
-
const existing = fs_1.default.existsSync(filePath) ? fs_1.default.readFileSync(filePath, 'utf8') : null;
|
|
178
|
-
if (existing !== content) {
|
|
179
|
-
fs_1.default.writeFileSync(filePath, content, { encoding: 'utf8', mode: 0o755 });
|
|
180
|
-
}
|
|
181
|
-
return filePath;
|
|
182
|
-
}
|
|
183
212
|
function decorateHostPlanWithConfiguredAgent(plan, agent, agentEnv = {}) {
|
|
184
213
|
if (!agent)
|
|
185
214
|
return plan;
|
|
215
|
+
if (!agent.enabled)
|
|
216
|
+
throw new Error('Configured agent is disabled. Enable this profile before starting it.');
|
|
186
217
|
const withEnv = { ...plan, env: { ...(plan.env || {}), ...agentEnv } };
|
|
187
|
-
if (!agent.command)
|
|
188
|
-
return withEnv;
|
|
189
|
-
const parsed = parseConfiguredCommand(agent.command.runner, agent.command.command);
|
|
190
|
-
if (!parsed.envPrefixText && parsed.extraArgs.length === 0)
|
|
218
|
+
if (!agent.command && !agent.setupScript)
|
|
191
219
|
return withEnv;
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const decoratedEnv = { ...withEnv.env, FRAIM_CONFIGURED_AGENT_ENV_PREFIX: parsed.envPrefixText };
|
|
195
|
-
if (agent.command.runner === 'bash') {
|
|
196
|
-
return { ...withEnv, command: 'bash', args: [wrapperPath, ...realInvocation], env: decoratedEnv };
|
|
220
|
+
if (agent.command && agent.setupScript && agent.command.runner !== agent.setupScript.runner) {
|
|
221
|
+
throw new Error('Configured command and setup script use different runners. Use the same runner for both preparation and launch.');
|
|
197
222
|
}
|
|
223
|
+
const runner = agent.command?.runner || agent.setupScript.runner;
|
|
224
|
+
const parsed = agent.command
|
|
225
|
+
? parseConfiguredCommand(runner, agent.command.command)
|
|
226
|
+
: { invocationHead: agent.baseHostId, envPrefixText: '', extraArgs: [] };
|
|
227
|
+
const problem = describeConfiguredCommandInvocationProblem(parsed, agent.baseHostId);
|
|
228
|
+
if (problem)
|
|
229
|
+
throw new Error(problem);
|
|
230
|
+
if (agent.setupScript && !agent.setupScript.command.trim())
|
|
231
|
+
throw new Error('Configured setup script is empty.');
|
|
198
232
|
return {
|
|
199
233
|
...withEnv,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
234
|
+
configuredInvocation: {
|
|
235
|
+
configuredAgentId: agent.id,
|
|
236
|
+
runner,
|
|
237
|
+
// The adapter owns the executable; settings never select a launch wrapper.
|
|
238
|
+
// Resolve it only after preparation, against that session's PATH.
|
|
239
|
+
target: plan.command,
|
|
240
|
+
envPrefixText: parsed.envPrefixText,
|
|
241
|
+
extraArgs: parsed.extraArgs,
|
|
242
|
+
...(agent.setupScript ? { setupScript: { ...agent.setupScript, args: [...(agent.setupScript.args || [])] } } : {}),
|
|
243
|
+
timeoutMs: agent.setupScript?.timeoutMs || agent.command?.timeoutMs || 30_000,
|
|
244
|
+
},
|
|
203
245
|
};
|
|
204
246
|
}
|