@o-lang/olang 1.0.15 → 1.0.17

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.
Files changed (3) hide show
  1. package/cli.js +45 -33
  2. package/package.json +1 -1
  3. package/src/parser.js +72 -93
package/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  const { Command } = require('commander');
3
3
  const { parse } = require('./src/parser');
4
4
  const { execute } = require('./src/runtime');
@@ -22,7 +22,6 @@ function ensureOlExtension(filename) {
22
22
  */
23
23
  async function defaultMockResolver(action, context) {
24
24
  if (!action || typeof action !== 'string') return `[Unhandled: ${String(action)}]`;
25
-
26
25
  if (action.startsWith('Search for ')) {
27
26
  return {
28
27
  title: "HR Policy 2025",
@@ -30,45 +29,38 @@ async function defaultMockResolver(action, context) {
30
29
  url: "mock://hr-policy"
31
30
  };
32
31
  }
33
-
34
32
  if (action.startsWith('Ask ')) {
35
- return "✅ [Mock] Summarized for demonstration.";
33
+ return " [Mock] Summarized for demonstration.";
36
34
  }
37
-
38
35
  if (action.startsWith('Notify ')) {
39
36
  const recipient = action.match(/Notify (\S+)/)?.[1] || 'user@example.com';
40
- return `📬 Notification sent to ${recipient}`;
37
+ return ` 📬 Notification sent to ${recipient}`;
41
38
  }
42
-
43
39
  if (action.startsWith('Debrief ') || action.startsWith('Evolve ')) {
44
40
  console.log(`[O-Lang] ${action}`);
45
41
  return 'Acknowledged';
46
42
  }
47
-
48
43
  return `[Unhandled: ${action}]`;
49
44
  }
45
+ defaultMockResolver.resolverName = 'defaultMockResolver';
50
46
 
51
47
  /**
52
48
  * Built-in Math Resolver
53
49
  */
54
50
  async function builtInMathResolver(action, context) {
55
51
  if (!action || typeof action !== 'string') return null;
56
-
57
52
  const a = action.replace(/\{([^\}]+)\}/g, (_, k) => {
58
53
  const v = context[k.trim()];
59
54
  return v !== undefined ? v : `{${k}}`;
60
55
  });
61
-
62
56
  let m;
63
57
  m = a.match(/^add\(([^,]+),\s*([^)]+)\)$/i); if (m) return parseFloat(m[1]) + parseFloat(m[2]);
64
58
  m = a.match(/^subtract\(([^,]+),\s*([^)]+)\)$/i); if (m) return parseFloat(m[1]) - parseFloat(m[2]);
65
59
  m = a.match(/^multiply\(([^,]+),\s*([^)]+)\)$/i); if (m) return parseFloat(m[1]) * parseFloat(m[2]);
66
60
  m = a.match(/^divide\(([^,]+),\s*([^)]+)\)$/i); if (m) return parseFloat(m[1]) / parseFloat(m[2]);
67
61
  m = a.match(/^sum\(\s*\[([^\]]+)\]\s*\)$/i); if (m) return m[1].split(',').map(s => parseFloat(s.trim())).reduce((s, v) => s + v, 0);
68
-
69
62
  return null;
70
63
  }
71
- // Add resolver metadata so workflow policy recognizes it
72
64
  builtInMathResolver.resolverName = 'builtInMathResolver';
73
65
 
74
66
  /**
@@ -84,7 +76,7 @@ function createResolverChain(resolvers, verbose = false) {
84
76
  context[`__resolver_${i}`] = res;
85
77
  lastResult = res;
86
78
  } catch (e) {
87
- console.error(`❌ Resolver ${i} failed for action "${action}":`, e.message);
79
+ console.error(` Resolver ${i} failed for action "${action}":`, e.message);
88
80
  }
89
81
  }
90
82
  if (verbose) console.log(`[Resolver Chain] action="${action}" lastResult=`, lastResult);
@@ -96,17 +88,16 @@ function createResolverChain(resolvers, verbose = false) {
96
88
 
97
89
  function loadSingleResolver(specifier) {
98
90
  if (!specifier) return defaultMockResolver;
99
-
100
91
  try {
101
92
  const resolver = require(specifier);
102
93
  if (typeof resolver !== 'function') throw new Error(`Resolver must export a function`);
103
- console.log(`📦 Loaded resolver: ${specifier}`);
94
+ console.log(` 📦 Loaded resolver: ${specifier}`);
104
95
  return resolver;
105
96
  } catch (e1) {
106
97
  try {
107
98
  const absolutePath = path.resolve(process.cwd(), specifier);
108
99
  const resolver = require(absolutePath);
109
- console.log(`📁 Loaded resolver: ${absolutePath}`);
100
+ console.log(` 📁 Loaded resolver: ${absolutePath}`);
110
101
  return resolver;
111
102
  } catch (e2) {
112
103
  throw new Error(
@@ -117,16 +108,40 @@ function loadSingleResolver(specifier) {
117
108
  }
118
109
 
119
110
  /**
120
- * loadResolverChain: include built-in math resolver first, then user resolvers, then default mock resolver
111
+ * POLICY-AWARE resolver loader: only includes resolvers in allowedResolvers
121
112
  */
122
- function loadResolverChain(specifiers, verbose = false) {
113
+ function loadResolverChain(specifiers, verbose = false, allowedResolvers = new Set()) {
123
114
  const userResolvers = specifiers?.map(loadSingleResolver) || [];
124
- const resolvers = [builtInMathResolver, ...userResolvers, defaultMockResolver];
115
+ const resolvers = [];
125
116
 
126
- if (!specifiers || specifiers.length === 0) {
127
- console.log('ℹ️ No resolver provided. Using built-in math + default mock resolver.');
117
+ // Only add builtInMathResolver if allowed
118
+ if (allowedResolvers.has('builtInMathResolver')) {
119
+ resolvers.push(builtInMathResolver);
120
+ }
121
+
122
+ // Add user resolvers only if their name is allowed
123
+ for (const r of userResolvers) {
124
+ const name = r.resolverName || r.name || 'unknown';
125
+ if (allowedResolvers.has(name)) {
126
+ resolvers.push(r);
127
+ } else if (verbose) {
128
+ console.warn(` ⚠️ Skipping disallowed user resolver: ${name}`);
129
+ }
130
+ }
131
+
132
+ // Only add defaultMockResolver if explicitly allowed
133
+ if (allowedResolvers.has('defaultMockResolver')) {
134
+ resolvers.push(defaultMockResolver);
135
+ }
136
+
137
+ if (resolvers.length === 0) {
138
+ if (verbose) {
139
+ console.warn(' ⚠️ No allowed resolvers loaded. Actions may fail.');
140
+ }
128
141
  } else {
129
- console.log(`📦 Loaded user resolvers: ${specifiers.join(', ')}`);
142
+ if (verbose) {
143
+ console.log(` ℹ️ Loaded allowed resolvers: ${resolvers.map(r => r.resolverName || 'anonymous').join(', ')}`);
144
+ }
130
145
  }
131
146
 
132
147
  return createResolverChain(resolvers, verbose);
@@ -136,7 +151,6 @@ function loadResolverChain(specifiers, verbose = false) {
136
151
  * CLI Setup
137
152
  */
138
153
  const program = new Command();
139
-
140
154
  program
141
155
  .name('olang')
142
156
  .description('O-Lang CLI: run .ol workflows with rule-enforced agent governance')
@@ -162,29 +176,27 @@ program
162
176
  .action(async (file, options) => {
163
177
  try {
164
178
  ensureOlExtension(file);
165
-
166
179
  const content = fs.readFileSync(file, 'utf8');
167
- const workflow = parse(content);
168
-
180
+ const workflow = parse(content, file);
169
181
  if (!workflow || typeof workflow !== 'object') {
170
- console.error('❌ Error: Parsed workflow is invalid or empty');
182
+ console.error(' Error: Parsed workflow is invalid or empty');
171
183
  process.exit(1);
172
184
  }
173
-
174
185
  if (options.verbose) {
175
- console.log('📄 Parsed Workflow:', JSON.stringify(workflow, null, 2));
186
+ console.log(' 📄 Parsed Workflow:', JSON.stringify(workflow, null, 2));
176
187
  }
177
188
 
178
- const resolver = loadResolverChain(options.resolver, options.verbose);
189
+ // Pass allowedResolvers to loadResolverChain
190
+ const allowedSet = new Set(workflow.allowedResolvers.map(r => r.trim()));
191
+ const resolver = loadResolverChain(options.resolver, options.verbose, allowedSet);
179
192
 
180
193
  const result = await execute(workflow, options.input, resolver, options.verbose);
181
-
182
194
  console.log('\n=== Workflow Result ===');
183
195
  console.log(JSON.stringify(result, null, 2));
184
196
  } catch (err) {
185
- console.error('❌ Error:', err.message);
197
+ console.error(' Error:', err.message);
186
198
  process.exit(1);
187
199
  }
188
200
  });
189
201
 
190
- program.parse(process.argv);
202
+ program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o-lang/olang",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "author": "Olalekan Ogundipe <info@workfily.com>",
5
5
  "description": "O-Lang: A governance language for user-directed, rule-enforced agent workflows",
6
6
  "main": "./src/index.js",
package/src/parser.js CHANGED
@@ -1,12 +1,9 @@
1
1
  // src/parser.js
2
-
3
2
  function parse(code, fileName = null) {
4
3
  if (fileName && !fileName.endsWith(".ol")) {
5
4
  throw new Error(`Expected .ol workflow, got: ${fileName}`);
6
5
  }
7
-
8
6
  const rawLines = code.split(/\r?\n/);
9
-
10
7
  const lines = rawLines
11
8
  .map(l => l.trim())
12
9
  .filter(l => l && !l.startsWith('#') && !l.startsWith('//'));
@@ -17,37 +14,41 @@ function parse(code, fileName = null) {
17
14
  steps: [],
18
15
  returnValues: [],
19
16
  allowedResolvers: [],
20
-
21
17
  resolverPolicy: {
22
18
  declared: [],
23
19
  autoInjected: [],
24
20
  used: [],
25
21
  warnings: []
26
22
  },
27
-
28
23
  __warnings: [],
29
24
  __requiresMath: false
30
25
  };
31
26
 
32
27
  let i = 0;
33
-
34
28
  while (i < lines.length) {
35
29
  let line = lines[i];
36
30
 
37
- const allowMatch = line.match(/^Allow resolvers\s*:\s*$/i);
31
+ // FIXED: Match both "Allow resolvers" and "Allowed resolvers"
32
+ const allowMatch = line.match(/^Allow(?:ed)?\s+resolvers\s*:\s*$/i);
38
33
  if (allowMatch) {
39
34
  i++;
40
- while (i < lines.length && !/^[A-Za-z]/.test(lines[i])) {
41
- const val = lines[i].trim();
42
- if (val) {
43
- workflow.allowedResolvers.push(val);
44
- workflow.resolverPolicy.declared.push(val);
35
+ // Read all subsequent non-empty lines until a new top-level section (starts with capital letter)
36
+ while (i < lines.length) {
37
+ const nextLine = lines[i].trim();
38
+ // Stop if line is empty or appears to be a new top-level keyword (e.g., Step, Workflow, Return)
39
+ if (nextLine === '' || /^[A-Z][a-z]/.test(nextLine)) {
40
+ break;
41
+ }
42
+ if (nextLine) {
43
+ workflow.allowedResolvers.push(nextLine);
44
+ workflow.resolverPolicy.declared.push(nextLine);
45
45
  }
46
46
  i++;
47
47
  }
48
48
  continue;
49
49
  }
50
50
 
51
+ // ---- Math step patterns (standalone) ----
51
52
  let mathAdd = line.match(/^Add\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i);
52
53
  if (mathAdd) {
53
54
  workflow.__requiresMath = true;
@@ -65,7 +66,7 @@ function parse(code, fileName = null) {
65
66
  workflow.__requiresMath = true;
66
67
  workflow.steps.push({
67
68
  type: 'calculate',
68
- expression: `subtract({${mathSub[2]}}, {${mathSub[1]}})`,
69
+ expression: `subtract({${mathAdd[2]}}, {${mathAdd[1]}})`,
69
70
  saveAs: mathSub[3].trim()
70
71
  });
71
72
  i++;
@@ -96,6 +97,7 @@ function parse(code, fileName = null) {
96
97
  continue;
97
98
  }
98
99
 
100
+ // ---- Workflow declaration ----
99
101
  const wfMatch = line.match(/^Workflow\s+"([^"]+)"(?:\s+with\s+(.+))?/i);
100
102
  if (wfMatch) {
101
103
  workflow.name = wfMatch[1];
@@ -106,96 +108,84 @@ function parse(code, fileName = null) {
106
108
  continue;
107
109
  }
108
110
 
109
- // ---------------------------
110
- // Return statement (updated: auto-detect math)
111
- // ---------------------------
112
- const returnMatch = line.match(/^Return\s+(.+)$/i);
113
- if (returnMatch) {
114
- const returns = returnMatch[1].split(',').map(v => v.trim());
115
- workflow.returnValues = returns;
116
-
117
- // --- Check if any return vars come from math steps ---
118
- for (const retVar of returns) {
119
- const producedByMath = workflow.steps.some(
120
- s => s.saveAs === retVar && s.type === 'calculate'
121
- );
122
- if (producedByMath) workflow.__requiresMath = true;
123
- }
124
-
125
- i++;
126
- continue;
127
- }
128
-
129
-
130
- // ---------------------------
131
- // Steps (updated: auto-detect math + saveAs)
132
- // ---------------------------
133
- const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
134
- if (stepMatch) {
135
- const stepNum = parseInt(stepMatch[1], 10);
136
- const raw = stepMatch[2].trim();
137
-
138
- // --- Detect math inside Step ---
139
- let mathDetected = null;
140
- let expr = '';
141
- let saveVar = null;
142
-
143
- const mathOps = [
144
- { re: /^Add\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'add' },
145
- { re: /^Subtract\s+\{(.+?)\}\s+from\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'subtract' },
146
- { re: /^Multiply\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'multiply' },
147
- { re: /^Divide\s+\{(.+?)\}\s+by\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'divide' }
148
- ];
149
-
150
- for (const op of mathOps) {
151
- const m = raw.match(op.re);
152
- if (m) {
153
- mathDetected = op.fn;
154
- saveVar = m[3].trim();
155
- if (op.fn === 'subtract') expr = `subtract({${m[2]}}, {${m[1]}})`;
156
- else expr = `${op.fn}({${m[1]}}, {${m[2]}})`;
157
- break;
111
+ // ---- Return statement (updated: auto-detect math) ----
112
+ const returnMatch = line.match(/^Return\s+(.+)$/i);
113
+ if (returnMatch) {
114
+ const returns = returnMatch[1].split(',').map(v => v.trim());
115
+ workflow.returnValues = returns;
116
+ // Check if any return vars come from math steps
117
+ for (const retVar of returns) {
118
+ const producedByMath = workflow.steps.some(
119
+ s => s.saveAs === retVar && s.type === 'calculate'
120
+ );
121
+ if (producedByMath) workflow.__requiresMath = true;
122
+ }
123
+ i++;
124
+ continue;
158
125
  }
159
- }
160
-
161
- if (mathDetected) workflow.__requiresMath = true;
162
-
163
- workflow.steps.push({
164
- type: mathDetected ? 'calculate' : 'action',
165
- stepNumber: stepNum,
166
- actionRaw: mathDetected ? null : raw,
167
- expression: mathDetected ? expr : undefined,
168
- saveAs: saveVar,
169
- constraints: {}
170
- });
171
126
 
172
- i++;
173
- continue;
174
- }
127
+ // ---- Step parsing (with inline math detection) ----
128
+ const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
129
+ if (stepMatch) {
130
+ const stepNum = parseInt(stepMatch[1], 10);
131
+ const raw = stepMatch[2].trim();
132
+
133
+ let mathDetected = null;
134
+ let expr = '';
135
+ let saveVar = null;
136
+ const mathOps = [
137
+ { re: /^Add\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'add' },
138
+ { re: /^Subtract\s+\{(.+?)\}\s+from\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'subtract' },
139
+ { re: /^Multiply\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'multiply' },
140
+ { re: /^Divide\s+\{(.+?)\}\s+by\s+\{(.+?)\}\s+Save as\s+(.+)$/i, fn: 'divide' }
141
+ ];
142
+ for (const op of mathOps) {
143
+ const m = raw.match(op.re);
144
+ if (m) {
145
+ mathDetected = op.fn;
146
+ saveVar = m[3].trim();
147
+ if (op.fn === 'subtract') {
148
+ expr = `subtract({${m[2]}}, {${m[1]}})`;
149
+ } else {
150
+ expr = `${op.fn}({${m[1]}}, {${m[2]}})`;
151
+ }
152
+ break;
153
+ }
154
+ }
155
+ if (mathDetected) workflow.__requiresMath = true;
156
+ workflow.steps.push({
157
+ type: mathDetected ? 'calculate' : 'action',
158
+ stepNumber: stepNum,
159
+ actionRaw: mathDetected ? null : raw,
160
+ expression: mathDetected ? expr : undefined,
161
+ saveAs: saveVar,
162
+ constraints: {}
163
+ });
164
+ i++;
165
+ continue;
166
+ }
175
167
 
168
+ // ---- Save as (for legacy or multi-line steps) ----
176
169
  const saveMatch = line.match(/^Save as\s+(.+)$/i);
177
170
  if (saveMatch && workflow.steps.length > 0) {
178
171
  const lastStep = workflow.steps[workflow.steps.length - 1];
179
172
  lastStep.saveAs = saveMatch[1].trim();
180
-
181
173
  if (lastStep.saveAs.match(/[A-Z][A-Za-z0-9_]*/)) {
182
174
  workflow.__requiresMath = true;
183
175
  }
184
-
185
176
  i++;
186
177
  continue;
187
178
  }
188
179
 
180
+ // ---- Constraint parsing ----
189
181
  const constraintMatch = line.match(/^Constraint:\s*(.+)$/i);
190
182
  if (constraintMatch && workflow.steps.length > 0) {
191
183
  const lastStep = workflow.steps[workflow.steps.length - 1];
192
184
  if (!lastStep.constraints) lastStep.constraints = {};
193
-
194
185
  const eq = constraintMatch[1].match(/^([^=]+)=\s*(.+)$/);
195
186
  if (eq) {
196
187
  let key = eq[1].trim();
197
188
  let value = eq[2].trim();
198
-
199
189
  if (value.startsWith('[') && value.endsWith(']')) {
200
190
  value = value.slice(1, -1).split(',').map(v =>
201
191
  v.trim().replace(/^"/, '').replace(/"$/, '')
@@ -205,7 +195,6 @@ if (stepMatch) {
205
195
  } else if (value.startsWith('"') && value.endsWith('"')) {
206
196
  value = value.slice(1, -1);
207
197
  }
208
-
209
198
  lastStep.constraints[key] = value;
210
199
  }
211
200
  i++;
@@ -220,7 +209,6 @@ if (stepMatch) {
220
209
  // ============================
221
210
  if (workflow.__requiresMath) {
222
211
  workflow.resolverPolicy.used.push('builtInMathResolver');
223
-
224
212
  if (!workflow.resolverPolicy.declared.includes('builtInMathResolver')) {
225
213
  workflow.resolverPolicy.autoInjected.push('builtInMathResolver');
226
214
  workflow.allowedResolvers.unshift('builtInMathResolver');
@@ -237,7 +225,6 @@ if (stepMatch) {
237
225
  }
238
226
 
239
227
  workflow.resolverPolicy.warnings = workflow.__warnings.slice();
240
-
241
228
  return workflow;
242
229
  }
243
230
 
@@ -247,7 +234,6 @@ if (stepMatch) {
247
234
  function parseBlock(lines) {
248
235
  const steps = [];
249
236
  let current = null;
250
-
251
237
  for (const line of lines) {
252
238
  const stepMatch = line.match(/^Step\s+(\d+)\s*:\s*(.+)$/i);
253
239
  if (stepMatch) {
@@ -261,37 +247,30 @@ function parseBlock(lines) {
261
247
  steps.push(current);
262
248
  continue;
263
249
  }
264
-
265
250
  const saveMatch = line.match(/^Save as\s+(.+)$/i);
266
251
  if (saveMatch && current) current.saveAs = saveMatch[1].trim();
267
-
268
252
  const debriefMatch = line.match(/^Debrief\s+(\w+)\s+with\s+"(.+)"$/i);
269
253
  if (debriefMatch) {
270
254
  steps.push({ type: 'debrief', agent: debriefMatch[1], message: debriefMatch[2] });
271
255
  }
272
-
273
256
  const evolveMatch = line.match(/^Evolve\s+(\w+)\s+using\s+feedback:\s+"(.+)"$/i);
274
257
  if (evolveMatch) {
275
258
  steps.push({ type: 'evolve', agent: evolveMatch[1], feedback: evolveMatch[2] });
276
259
  }
277
-
278
260
  const promptMatch = line.match(/^Prompt user to\s+"(.+)"$/i);
279
261
  if (promptMatch) {
280
262
  steps.push({ type: 'prompt', question: promptMatch[1], saveAs: null });
281
263
  }
282
-
283
264
  const useMatch = line.match(/^Use\s+(.+)$/i);
284
265
  if (useMatch) {
285
266
  steps.push({ type: 'use', tool: useMatch[1].trim(), saveAs: null, constraints: {} });
286
267
  }
287
-
288
268
  const askMatch = line.match(/^Ask\s+(.+)$/i);
289
269
  if (askMatch) {
290
270
  steps.push({ type: 'ask', target: askMatch[1].trim(), saveAs: null, constraints: {} });
291
271
  }
292
272
  }
293
-
294
273
  return steps;
295
274
  }
296
275
 
297
- module.exports = { parse };
276
+ module.exports = { parse };