@the-open-engine/zeroshot 6.8.1 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-open-engine/zeroshot",
3
- "version": "6.8.1",
3
+ "version": "6.8.3",
4
4
  "description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
5
5
  "main": "src/orchestrator.js",
6
6
  "bin": {
@@ -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.config.agents || [];
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(agentConfig);
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(agentConfig);
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
- let loadedConfig;
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 loadedConfig.agents;
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
- if (!config) {
3968
- throw new Error('load_config operation missing config');
3969
- }
3977
+ const resolvedConfig = this._resolveLoadConfig(config);
3978
+ const loadedConfig = resolvedConfig.loadedConfig;
3970
3979
 
3971
- const templatesDir = path.join(__dirname, '..', 'cluster-templates');
3972
- let loadedConfig;
3973
-
3974
- // Check if config is parameterized ({ base, params }) or static (string)
3975
- if (typeof config === 'object' && config.base) {
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)`);
@@ -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 = path.join(this.baseTemplatesDir, `${baseName}.json`);
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 templateJson = fs.readFileSync(templatePath, 'utf8');
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 = path.join(this.baseTemplatesDir, `${baseName}.json`);
507
+ const templatePath = resolveNamedTemplatePath(this.baseTemplatesDir, baseName, 'base template');
413
508
  if (!fs.existsSync(templatePath)) {
414
509
  return null;
415
510
  }
416
- const template = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
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
- if (typeof configOp === 'object' && configOp?.base) {
231
- const resolver = new TemplateResolver(templatesDir);
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 }) {