@the-open-engine/zeroshot 6.8.2 → 6.8.3
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/package.json
CHANGED
package/src/orchestrator.js
CHANGED
|
@@ -3472,7 +3472,7 @@ Continue from where you left off. Review your previous output to understand what
|
|
|
3472
3472
|
|
|
3473
3473
|
// Phase 2: Build mock cluster config with proposed agents
|
|
3474
3474
|
// Collect all agents that would exist after operations complete
|
|
3475
|
-
const existingAgentConfigs = cluster
|
|
3475
|
+
const existingAgentConfigs = this._prepareExistingAgentConfigsForValidation(cluster);
|
|
3476
3476
|
const proposedAgentConfigs = this._buildProposedAgentConfigs(existingAgentConfigs, operations);
|
|
3477
3477
|
|
|
3478
3478
|
// Phase 3: Validate proposed cluster config
|
|
@@ -3542,16 +3542,22 @@ Continue from where you left off. Review your previous output to understand what
|
|
|
3542
3542
|
if (op.action === 'add_agents' && op.agents) {
|
|
3543
3543
|
for (const agentConfig of op.agents) {
|
|
3544
3544
|
const existingIdx = proposedAgentConfigs.findIndex((a) => a.id === agentConfig.id);
|
|
3545
|
+
const proposedAgentConfig = JSON.parse(JSON.stringify(agentConfig));
|
|
3545
3546
|
if (existingIdx === -1) {
|
|
3546
|
-
proposedAgentConfigs.push(
|
|
3547
|
+
proposedAgentConfigs.push(proposedAgentConfig);
|
|
3548
|
+
} else {
|
|
3549
|
+
proposedAgentConfigs[existingIdx] = proposedAgentConfig;
|
|
3547
3550
|
}
|
|
3548
3551
|
}
|
|
3549
3552
|
} else if (op.action === 'load_config' && op.config) {
|
|
3550
3553
|
const loadedAgentConfigs = this._resolveLoadConfigAgents(op.config);
|
|
3551
3554
|
for (const agentConfig of loadedAgentConfigs) {
|
|
3552
3555
|
const existingIdx = proposedAgentConfigs.findIndex((a) => a.id === agentConfig.id);
|
|
3556
|
+
const proposedAgentConfig = JSON.parse(JSON.stringify(agentConfig));
|
|
3553
3557
|
if (existingIdx === -1) {
|
|
3554
|
-
proposedAgentConfigs.push(
|
|
3558
|
+
proposedAgentConfigs.push(proposedAgentConfig);
|
|
3559
|
+
} else {
|
|
3560
|
+
proposedAgentConfigs[existingIdx] = proposedAgentConfig;
|
|
3555
3561
|
}
|
|
3556
3562
|
}
|
|
3557
3563
|
} else if (op.action === 'remove_agents' && op.agentIds) {
|
|
@@ -3572,38 +3578,42 @@ Continue from where you left off. Review your previous output to understand what
|
|
|
3572
3578
|
return proposedAgentConfigs;
|
|
3573
3579
|
}
|
|
3574
3580
|
|
|
3581
|
+
_prepareExistingAgentConfigsForValidation(cluster) {
|
|
3582
|
+
const existingAgentConfigs = JSON.parse(JSON.stringify(cluster?.config?.agents || []));
|
|
3583
|
+
|
|
3584
|
+
// The CLI validates --model before startup, then materializes it into every
|
|
3585
|
+
// initial agent config and persists the override separately on the cluster.
|
|
3586
|
+
// Only those already-accepted configs have that provenance. Strip their
|
|
3587
|
+
// runtime model projection before validating a later operation chain; new
|
|
3588
|
+
// add/load/update payloads stay untouched and must satisfy the authored
|
|
3589
|
+
// modelLevel-only policy even when their value equals the active override.
|
|
3590
|
+
if (cluster?.modelOverride) {
|
|
3591
|
+
for (const agentConfig of existingAgentConfigs) {
|
|
3592
|
+
delete agentConfig.model;
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
return existingAgentConfigs;
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3575
3599
|
_resolveLoadConfigAgents(config) {
|
|
3600
|
+
return this._resolveLoadConfig(config).loadedConfig.agents;
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
_resolveLoadConfig(config) {
|
|
3576
3604
|
if (!config) {
|
|
3577
3605
|
throw new Error('load_config operation missing config');
|
|
3578
3606
|
}
|
|
3579
3607
|
|
|
3580
3608
|
const templatesDir = path.join(__dirname, '..', 'cluster-templates');
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
// Parameterized template - resolve with TemplateResolver
|
|
3584
|
-
if (typeof config === 'object' && config.base) {
|
|
3585
|
-
const { base, params } = config;
|
|
3586
|
-
const resolver = new TemplateResolver(templatesDir);
|
|
3587
|
-
loadedConfig = resolver.resolve(base, params || {});
|
|
3588
|
-
} else if (typeof config === 'string') {
|
|
3589
|
-
// Static config - load directly from file
|
|
3590
|
-
const configPath = path.join(templatesDir, `${config}.json`);
|
|
3591
|
-
if (!fs.existsSync(configPath)) {
|
|
3592
|
-
throw new Error(`Config not found: ${config} (looked in ${configPath})`);
|
|
3593
|
-
}
|
|
3594
|
-
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
3595
|
-
loadedConfig = JSON.parse(configContent);
|
|
3596
|
-
} else {
|
|
3597
|
-
throw new Error(
|
|
3598
|
-
`Invalid config format: expected string or {base, params}, got ${typeof config}`
|
|
3599
|
-
);
|
|
3600
|
-
}
|
|
3609
|
+
const resolver = new TemplateResolver(templatesDir);
|
|
3610
|
+
const resolvedConfig = resolver.resolveConfigReference(config);
|
|
3601
3611
|
|
|
3602
|
-
if (!loadedConfig.agents || !Array.isArray(loadedConfig.agents)) {
|
|
3612
|
+
if (!resolvedConfig.loadedConfig.agents || !Array.isArray(resolvedConfig.loadedConfig.agents)) {
|
|
3603
3613
|
throw new Error(`Config has no agents array`);
|
|
3604
3614
|
}
|
|
3605
3615
|
|
|
3606
|
-
return
|
|
3616
|
+
return resolvedConfig;
|
|
3607
3617
|
}
|
|
3608
3618
|
|
|
3609
3619
|
_hasCompletionHandler(agentConfigs) {
|
|
@@ -3964,44 +3974,17 @@ Continue from where you left off. Review your previous output to understand what
|
|
|
3964
3974
|
*/
|
|
3965
3975
|
async _opLoadConfig(cluster, op, context) {
|
|
3966
3976
|
const { config } = op;
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
}
|
|
3977
|
+
const resolvedConfig = this._resolveLoadConfig(config);
|
|
3978
|
+
const loadedConfig = resolvedConfig.loadedConfig;
|
|
3970
3979
|
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
// Parameterized template - resolve with TemplateResolver
|
|
3977
|
-
const { base, params } = config;
|
|
3978
|
-
this._log(` Loading parameterized template: ${base}`);
|
|
3979
|
-
this._log(` Params: ${JSON.stringify(params)}`);
|
|
3980
|
-
|
|
3981
|
-
const resolver = new TemplateResolver(templatesDir);
|
|
3982
|
-
loadedConfig = resolver.resolve(base, params);
|
|
3983
|
-
|
|
3984
|
-
this._log(` ✓ Resolved template: ${base} → ${loadedConfig.agents?.length || 0} agent(s)`);
|
|
3985
|
-
} else if (typeof config === 'string') {
|
|
3986
|
-
// Static config - load directly from file
|
|
3987
|
-
const configPath = path.join(templatesDir, `${config}.json`);
|
|
3988
|
-
|
|
3989
|
-
if (!fs.existsSync(configPath)) {
|
|
3990
|
-
throw new Error(`Config not found: ${config} (looked in ${configPath})`);
|
|
3991
|
-
}
|
|
3992
|
-
|
|
3993
|
-
this._log(` Loading static config: ${config}`);
|
|
3994
|
-
|
|
3995
|
-
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
3996
|
-
loadedConfig = JSON.parse(configContent);
|
|
3997
|
-
} else {
|
|
3998
|
-
throw new Error(
|
|
3999
|
-
`Invalid config format: expected string or {base, params}, got ${typeof config}`
|
|
3980
|
+
if (resolvedConfig.kind === 'parameterized') {
|
|
3981
|
+
this._log(` Loading parameterized template: ${resolvedConfig.name}`);
|
|
3982
|
+
this._log(` Params: ${JSON.stringify(resolvedConfig.params)}`);
|
|
3983
|
+
this._log(
|
|
3984
|
+
` ✓ Resolved template: ${resolvedConfig.name} → ${loadedConfig.agents.length} agent(s)`
|
|
4000
3985
|
);
|
|
4001
|
-
}
|
|
4002
|
-
|
|
4003
|
-
if (!loadedConfig.agents || !Array.isArray(loadedConfig.agents)) {
|
|
4004
|
-
throw new Error(`Config has no agents array`);
|
|
3986
|
+
} else {
|
|
3987
|
+
this._log(` Loading static config: ${resolvedConfig.name}`);
|
|
4005
3988
|
}
|
|
4006
3989
|
|
|
4007
3990
|
this._log(` Found ${loadedConfig.agents.length} agent(s)`);
|
package/src/template-resolver.js
CHANGED
|
@@ -16,6 +16,46 @@ const fs = require('fs');
|
|
|
16
16
|
const path = require('path');
|
|
17
17
|
|
|
18
18
|
const COMPARISON_OPERATORS = ['==', '!=', '<=', '>=', '<', '>'];
|
|
19
|
+
const TEMPLATE_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
20
|
+
|
|
21
|
+
function invalidTemplateNameError(label, name) {
|
|
22
|
+
return new Error(
|
|
23
|
+
`Invalid ${label} name ${JSON.stringify(name)}: use only letters, numbers, hyphens, and underscores`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isPathContained(root, target) {
|
|
28
|
+
const relativePath = path.relative(root, target);
|
|
29
|
+
return (
|
|
30
|
+
relativePath !== '..' &&
|
|
31
|
+
!relativePath.startsWith(`..${path.sep}`) &&
|
|
32
|
+
!path.isAbsolute(relativePath)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveNamedTemplatePath(directory, name, label) {
|
|
37
|
+
if (typeof name !== 'string' || !TEMPLATE_NAME_PATTERN.test(name)) {
|
|
38
|
+
throw invalidTemplateNameError(label, name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const templateRoot = path.resolve(directory);
|
|
42
|
+
const templatePath = path.resolve(templateRoot, `${name}.json`);
|
|
43
|
+
if (!isPathContained(templateRoot, templatePath)) {
|
|
44
|
+
throw invalidTemplateNameError(label, name);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return templatePath;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function canonicalizeNamedTemplatePath(directory, templatePath, name, label) {
|
|
51
|
+
const canonicalRoot = fs.realpathSync(directory);
|
|
52
|
+
const canonicalTemplatePath = fs.realpathSync(templatePath);
|
|
53
|
+
if (!isPathContained(canonicalRoot, canonicalTemplatePath)) {
|
|
54
|
+
throw invalidTemplateNameError(label, name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return canonicalTemplatePath;
|
|
58
|
+
}
|
|
19
59
|
|
|
20
60
|
function isIdentifierChar(char) {
|
|
21
61
|
if (!char) return false;
|
|
@@ -97,6 +137,55 @@ class TemplateResolver {
|
|
|
97
137
|
this.baseTemplatesDir = path.join(templatesDir, 'base-templates');
|
|
98
138
|
}
|
|
99
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Load a static or parameterized cluster config from a safe named reference.
|
|
142
|
+
* Names are identifiers, never filesystem paths.
|
|
143
|
+
*
|
|
144
|
+
* @param {string | {base: string, params?: Object}} config
|
|
145
|
+
* @returns {{kind: 'static' | 'parameterized', name: string, params: Object | null, loadedConfig: Object}}
|
|
146
|
+
*/
|
|
147
|
+
resolveConfigReference(config) {
|
|
148
|
+
if (typeof config === 'string') {
|
|
149
|
+
const configPath = resolveNamedTemplatePath(this.templatesDir, config, 'config');
|
|
150
|
+
if (!fs.existsSync(configPath)) {
|
|
151
|
+
throw new Error(`Config not found: ${config} (looked in ${configPath})`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const canonicalConfigPath = canonicalizeNamedTemplatePath(
|
|
155
|
+
this.templatesDir,
|
|
156
|
+
configPath,
|
|
157
|
+
config,
|
|
158
|
+
'config'
|
|
159
|
+
);
|
|
160
|
+
return {
|
|
161
|
+
kind: 'static',
|
|
162
|
+
name: config,
|
|
163
|
+
params: null,
|
|
164
|
+
loadedConfig: JSON.parse(fs.readFileSync(canonicalConfigPath, 'utf8')),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
config &&
|
|
170
|
+
typeof config === 'object' &&
|
|
171
|
+
!Array.isArray(config) &&
|
|
172
|
+
Object.prototype.hasOwnProperty.call(config, 'base')
|
|
173
|
+
) {
|
|
174
|
+
const { base, params } = config;
|
|
175
|
+
const resolvedParams = params || {};
|
|
176
|
+
return {
|
|
177
|
+
kind: 'parameterized',
|
|
178
|
+
name: base,
|
|
179
|
+
params: resolvedParams,
|
|
180
|
+
loadedConfig: this.resolve(base, resolvedParams),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw new Error(
|
|
185
|
+
`Invalid config format: expected string or {base, params}, got ${typeof config}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
100
189
|
/**
|
|
101
190
|
* Resolve a template with parameters
|
|
102
191
|
* @param {string} baseName - Name of base template (without .json)
|
|
@@ -105,12 +194,18 @@ class TemplateResolver {
|
|
|
105
194
|
*/
|
|
106
195
|
resolve(baseName, params) {
|
|
107
196
|
// Load base template
|
|
108
|
-
const templatePath =
|
|
197
|
+
const templatePath = resolveNamedTemplatePath(this.baseTemplatesDir, baseName, 'base template');
|
|
109
198
|
if (!fs.existsSync(templatePath)) {
|
|
110
199
|
throw new Error(`Base template not found: ${baseName} (looked in ${templatePath})`);
|
|
111
200
|
}
|
|
112
201
|
|
|
113
|
-
const
|
|
202
|
+
const canonicalTemplatePath = canonicalizeNamedTemplatePath(
|
|
203
|
+
this.baseTemplatesDir,
|
|
204
|
+
templatePath,
|
|
205
|
+
baseName,
|
|
206
|
+
'base template'
|
|
207
|
+
);
|
|
208
|
+
const templateJson = fs.readFileSync(canonicalTemplatePath, 'utf8');
|
|
114
209
|
const template = JSON.parse(templateJson);
|
|
115
210
|
|
|
116
211
|
return this.resolveTemplate(template, params);
|
|
@@ -409,11 +504,17 @@ class TemplateResolver {
|
|
|
409
504
|
* @returns {any}
|
|
410
505
|
*/
|
|
411
506
|
getTemplateInfo(baseName) {
|
|
412
|
-
const templatePath =
|
|
507
|
+
const templatePath = resolveNamedTemplatePath(this.baseTemplatesDir, baseName, 'base template');
|
|
413
508
|
if (!fs.existsSync(templatePath)) {
|
|
414
509
|
return null;
|
|
415
510
|
}
|
|
416
|
-
const
|
|
511
|
+
const canonicalTemplatePath = canonicalizeNamedTemplatePath(
|
|
512
|
+
this.baseTemplatesDir,
|
|
513
|
+
templatePath,
|
|
514
|
+
baseName,
|
|
515
|
+
'base template'
|
|
516
|
+
);
|
|
517
|
+
const template = JSON.parse(fs.readFileSync(canonicalTemplatePath, 'utf8'));
|
|
417
518
|
return {
|
|
418
519
|
name: template.name,
|
|
419
520
|
description: template.description,
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
const fs = require('node:fs');
|
|
2
|
-
const path = require('node:path');
|
|
3
|
-
|
|
4
1
|
const Ledger = require('../ledger');
|
|
5
2
|
const MessageBus = require('../message-bus');
|
|
6
3
|
const LogicEngine = require('../logic-engine');
|
|
@@ -227,16 +224,8 @@ function parseOperations(raw) {
|
|
|
227
224
|
}
|
|
228
225
|
|
|
229
226
|
function resolveConfigOperation({ configOp, templatesDir }) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return resolver.resolve(configOp.base, configOp.params || {});
|
|
233
|
-
}
|
|
234
|
-
if (typeof configOp === 'string') {
|
|
235
|
-
const configPath = path.join(templatesDir, `${configOp}.json`);
|
|
236
|
-
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
237
|
-
return JSON.parse(configContent);
|
|
238
|
-
}
|
|
239
|
-
throw new Error(`Unsupported load_config payload: ${JSON.stringify(configOp)}`);
|
|
227
|
+
const resolver = new TemplateResolver(templatesDir);
|
|
228
|
+
return resolver.resolveConfigReference(configOp).loadedConfig;
|
|
240
229
|
}
|
|
241
230
|
|
|
242
231
|
function applyClusterOperation({ state, messageBus, operation, sourceMessage, templatesDir }) {
|