@o-lang/olang 1.3.0-alpha → 1.4.0-alpha.1

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.
@@ -3,7 +3,7 @@ const path = require('path');
3
3
  const crypto = require('crypto'); // ✅ CRYPTOGRAPHIC AUDIT LOGS
4
4
 
5
5
  // ✅ O-Lang Kernel Version (Safety Logic & Governance Rules)
6
- const KERNEL_VERSION = '1.3.0-alpha'; // 🔁 Bumped: PII redaction engine added
6
+ const KERNEL_VERSION = '1.4.0-alpha.1'; // 🔁 Bumped: PII redaction engine added
7
7
 
8
8
  // ─────────────────────────────────────────────────────────────────────────────
9
9
  // ✅ NEW v1.3.0 — SEPARATED PATTERN SETS
@@ -997,37 +997,129 @@ class RuntimeAPI {
997
997
  return path.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), obj);
998
998
  }
999
999
 
1000
+ /**
1001
+ * evaluateCondition(cond, ctx)
1002
+ *
1003
+ * Governance Features:
1004
+ * 1. Quote-Aware Parsing: Prevents splitting on "or"/"and" inside quoted strings.
1005
+ * 2. Strict Equality: Uses === to prevent type-coercion safety bypasses.
1006
+ * 3. Comprehensive Operators: Supports gte, lte, contains, not equals.
1007
+ * 4. Auditability: Warns on unrecognised syntax to prevent silent failures.
1008
+ */
1000
1009
  evaluateCondition(cond, ctx) {
1010
+ if (!cond) return false;
1001
1011
  cond = cond.trim();
1002
1012
 
1003
- // 1. Handle Logical OR (|| or 'or')
1004
- if (/\|\||\bor\b/i.test(cond)) {
1005
- return cond.split(/\|\||\bor\b/i).some(c => this.evaluateCondition(c.trim(), ctx));
1013
+ // ── Helper: split on logical operators OUTSIDE quoted strings ────────────
1014
+ const splitOutsideQuotes = (str, regex) => {
1015
+ const parts = [];
1016
+ let current = '';
1017
+ let inQuote = false;
1018
+ let quoteChar = '';
1019
+
1020
+ for (let i = 0; i < str.length; i++) {
1021
+ const ch = str[i];
1022
+
1023
+ // Handle quote toggling
1024
+ if (!inQuote && (ch === '"' || ch === "'")) {
1025
+ inQuote = true;
1026
+ quoteChar = ch;
1027
+ current += ch;
1028
+ } else if (inQuote && ch === quoteChar) {
1029
+ // Check for escaped quote? For now, simple toggle.
1030
+ inQuote = false;
1031
+ quoteChar = '';
1032
+ current += ch;
1033
+ } else if (!inQuote) {
1034
+ // Check for operator match at current position
1035
+ const remaining = str.slice(i);
1036
+ const m = remaining.match(regex);
1037
+ if (m && m.index === 0) {
1038
+ parts.push(current);
1039
+ current = '';
1040
+ i += m[0].length - 1;
1041
+ continue;
1042
+ } else {
1043
+ current += ch;
1044
+ }
1045
+ } else {
1046
+ current += ch;
1047
+ }
1048
+ }
1049
+ parts.push(current);
1050
+ return parts.map(p => p.trim()).filter(Boolean);
1051
+ };
1052
+
1053
+ // ── 1. Logical OR ─────────────────────────────────────────────────────────
1054
+ // (?!\s+equal) prevents splitting on "or" in "greater than or equal"
1055
+ const orParts = splitOutsideQuotes(cond, /^(\|\||\bor\b(?!\s+equal))/i);
1056
+ if (orParts.length > 1) {
1057
+ return orParts.some(c => this.evaluateCondition(c.trim(), ctx));
1006
1058
  }
1007
- // ✅ 2. Handle Logical AND (&& or 'and')
1008
- if (/&&|\band\b/i.test(cond)) {
1009
- return cond.split(/&&|\band\b/i).every(c => this.evaluateCondition(c.trim(), ctx));
1059
+
1060
+ // ── 2. Logical AND ────────────────────────────────────────────────────────
1061
+ const andParts = splitOutsideQuotes(cond, /^(&&|\band\b)/i);
1062
+ if (andParts.length > 1) {
1063
+ return andParts.every(c => this.evaluateCondition(c.trim(), ctx));
1010
1064
  }
1011
1065
 
1012
- // 3. Handle == or === (works with or without {})
1013
- const eqMatch = cond.match(/^(?:\{(.+)\}|(\w+))\s*===?\s*"(.*)"$/);
1066
+ // ── 3. Strict equality: {var} === "value" or {var} == "value" ────────────
1067
+ const eqMatch = cond.match(/^(?:\{(.+?)\}|(\w[\w.]*?))\s*===?\s*"(.*)"$/);
1014
1068
  if (eqMatch) {
1015
1069
  const key = eqMatch[1] || eqMatch[2];
1016
1070
  return this.getNested(ctx, key) === eqMatch[3];
1017
1071
  }
1018
1072
 
1019
- // 4. Keep original O-Lang syntax
1020
- const oldEq = cond.match(/^\{(.+)\}\s+equals\s+"(.*)"$/);
1021
- if (oldEq) return this.getNested(ctx, oldEq[1]) == oldEq[2];
1073
+ // ── 4. Not equals: {var} != "value" or {var} !== "value" ─────────────────
1074
+ const neqMatch = cond.match(/^(?:\{(.+?)\}|(\w[\w.]*?))\s*!==?\s*"(.*)"$/);
1075
+ if (neqMatch) {
1076
+ const key = neqMatch[1] || neqMatch[2];
1077
+ return this.getNested(ctx, key) !== neqMatch[3];
1078
+ }
1079
+
1080
+ // ── 5. O-Lang keyword: {var} equals "value" (strict) ─────────────────────
1081
+ const oldEq = cond.match(/^\{(.+?)\}\s+equals\s+"(.*)"$/);
1082
+ if (oldEq) return this.getNested(ctx, oldEq[1]) === oldEq[2];
1083
+
1084
+ // ── 6. O-Lang keyword: {var} not equals "value" ───────────────────────────
1085
+ const notEq = cond.match(/^\{(.+?)\}\s+not\s+equals\s+"(.*)"$/);
1086
+ if (notEq) return this.getNested(ctx, notEq[1]) !== notEq[2];
1087
+
1088
+ // ── 7. Contains: {var} contains "value" ──────────────────────────────────
1089
+ const containsMatch = cond.match(/^\{(.+?)\}\s+contains\s+"(.*)"$/);
1090
+ if (containsMatch) {
1091
+ const value = this.getNested(ctx, containsMatch[1]);
1092
+ const target = containsMatch[2];
1093
+ if (Array.isArray(value)) return value.includes(target);
1094
+ if (typeof value === 'string') return value.includes(target);
1095
+ return false;
1096
+ }
1097
+
1098
+ // ── 8. Numeric comparisons (GTE/LTE before GT/LT) ────────────────────────
1099
+ const gte = cond.match(/^\{(.+?)\}\s+greater than or equal\s+(\d+\.?\d*)$/);
1100
+ if (gte) return parseFloat(this.getNested(ctx, gte[1])) >= parseFloat(gte[2]);
1101
+
1102
+ const lte = cond.match(/^\{(.+?)\}\s+less than or equal\s+(\d+\.?\d*)$/);
1103
+ if (lte) return parseFloat(this.getNested(ctx, lte[1])) <= parseFloat(lte[2]);
1022
1104
 
1023
- const gt = cond.match(/^\{(.+)\}\s+greater than\s+(\d+\.?\d*)$/);
1105
+ const gt = cond.match(/^\{(.+?)\}\s+greater than\s+(\d+\.?\d*)$/);
1024
1106
  if (gt) return parseFloat(this.getNested(ctx, gt[1])) > parseFloat(gt[2]);
1025
1107
 
1026
- const lt = cond.match(/^\{(.+)\}\s+less than\s+(\d+\.?\d*)$/);
1108
+ const lt = cond.match(/^\{(.+?)\}\s+less than\s+(\d+\.?\d*)$/);
1027
1109
  if (lt) return parseFloat(this.getNested(ctx, lt[1])) < parseFloat(lt[2]);
1028
1110
 
1029
- // Fallback: truthy check
1030
- return Boolean(this.getNested(ctx, cond.replace(/\{|\}/g, '')));
1111
+ // ── 9. Truthy fallback — warn so authors know it fired ───────────────────
1112
+ const fallbackKey = cond.replace(/^\{|\}$/g, '');
1113
+ const fallbackValue = this.getNested(ctx, fallbackKey);
1114
+
1115
+ this.addWarning(
1116
+ `evaluateCondition: unrecognised condition syntax "${cond}" — ` +
1117
+ `falling back to truthy check on "${fallbackKey}" ` +
1118
+ `(value: ${JSON.stringify(fallbackValue)}). ` +
1119
+ `If this is unintentional, check your condition syntax.`
1120
+ );
1121
+
1122
+ return Boolean(fallbackValue);
1031
1123
  }
1032
1124
 
1033
1125
  mathFunctions = {
@@ -1758,111 +1850,496 @@ class RuntimeAPI {
1758
1850
  break;
1759
1851
  }
1760
1852
 
1853
+ // ─────────────────────────────────────────────────────────────────────────────
1854
+ // IF / ELSE-IF / ELSE
1855
+ //
1856
+ // Governance Features:
1857
+ // 1. Exclusive Branching: Only one branch executes (if -> else-if -> else).
1858
+ // 2. Semantic Validation: Checks symbols in conditions before evaluation.
1859
+ // 3. Auditability: Logs which condition was evaluated and which branch fired.
1860
+ // ─────────────────────────────────────────────────────────────────────────────
1861
+
1761
1862
  case 'if': {
1762
- if (this.evaluateCondition(step.condition, this.context)) {
1763
- for (const s of step.body) await this.executeStep(s, agentResolver);
1863
+ // 1. Validate all symbols referenced in the main condition
1864
+ const condSymbols = step.condition ? step.condition.match(/\{([^\}]+)\}/g) || [] : [];
1865
+ let symbolsValid = true;
1866
+
1867
+ for (const sym of condSymbols) {
1868
+ const key = sym.replace(/[{}]/g, '');
1869
+ if (!this._requireSemantic(key, 'if condition')) {
1870
+ symbolsValid = false;
1871
+ }
1872
+ }
1873
+
1874
+ if (!symbolsValid) {
1875
+ this._createAuditEntry('condition_skipped', {
1876
+ condition: step.condition,
1877
+ reason: 'One or more symbols missing in context',
1878
+ severity: 'warn'
1879
+ });
1880
+ break;
1764
1881
  }
1882
+
1883
+ // 2. Evaluate main if condition
1884
+ const mainPassed = this.evaluateCondition(step.condition, this.context);
1885
+
1886
+ this._createAuditEntry('condition_evaluated', {
1887
+ condition: step.condition,
1888
+ passed: mainPassed,
1889
+ branch: 'if',
1890
+ severity: 'info'
1891
+ });
1892
+
1893
+ if (mainPassed) {
1894
+ if (step.body && Array.isArray(step.body)) {
1895
+ for (const s of step.body) await this.executeStep(s, agentResolver);
1896
+ }
1897
+ break; // Exit after successful if
1898
+ }
1899
+
1900
+ // 3. else-if chain — stop at first match
1901
+ if (step.elseIf && Array.isArray(step.elseIf)) {
1902
+ let elseIfFired = false;
1903
+ for (const branch of step.elseIf) {
1904
+ // Validate symbols for else-if branch
1905
+ const branchSymbols = branch.condition ? branch.condition.match(/\{([^\}]+)\}/g) || [] : [];
1906
+ let branchSymbolsValid = true;
1907
+ for (const sym of branchSymbols) {
1908
+ const key = sym.replace(/[{}]/g, '');
1909
+ if (!this._requireSemantic(key, 'else-if condition')) {
1910
+ branchSymbolsValid = false;
1911
+ }
1912
+ }
1913
+
1914
+ if (!branchSymbolsValid) {
1915
+ this._createAuditEntry('condition_skipped', {
1916
+ condition: branch.condition,
1917
+ reason: 'One or more symbols missing in context',
1918
+ severity: 'warn'
1919
+ });
1920
+ continue; // Skip this else-if, try next
1921
+ }
1922
+
1923
+ const branchPassed = this.evaluateCondition(branch.condition, this.context);
1924
+
1925
+ this._createAuditEntry('condition_evaluated', {
1926
+ condition: branch.condition,
1927
+ passed: branchPassed,
1928
+ branch: 'else-if',
1929
+ severity: 'info'
1930
+ });
1931
+
1932
+ if (branchPassed) {
1933
+ if (branch.body && Array.isArray(branch.body)) {
1934
+ for (const s of branch.body) await this.executeStep(s, agentResolver);
1935
+ }
1936
+ elseIfFired = true;
1937
+ break;
1938
+ }
1939
+ }
1940
+ if (elseIfFired) break;
1941
+ }
1942
+
1943
+ // 4. else fallback
1944
+ if (step.elseBranch && Array.isArray(step.elseBranch)) {
1945
+ this._createAuditEntry('condition_evaluated', {
1946
+ condition: 'else',
1947
+ passed: true,
1948
+ branch: 'else',
1949
+ severity: 'info'
1950
+ });
1951
+ for (const s of step.elseBranch) await this.executeStep(s, agentResolver);
1952
+ }
1953
+
1765
1954
  break;
1766
1955
  }
1767
1956
 
1957
+ // ─────────────────────────────────────────────────────────────────────────────
1958
+ // PARALLEL
1959
+ //
1960
+ // Bugs fixed:
1961
+ // 1. Shared this.context race condition — all branches wrote to the same
1962
+ // object concurrently. Branch 2 restoring the snapshot overwrote whatever
1963
+ // branch 1 had just saved, so result_a was lost by the time it was read.
1964
+ // Fix: each branch gets its own context via Object.create(this), which
1965
+ // prototype-links to the parent (sharing allowedResolvers, auditLog,
1966
+ // events, verbose) but has its own context property that shadows the
1967
+ // parent's. Branches never touch this.context directly.
1968
+ //
1969
+ // 2. Promise.all → silent failure swallowing. A single rejection cancelled
1970
+ // all siblings. Fix: buildStepPromise catches internally and returns a
1971
+ // structured outcome, so Promise.all always resolves with the full set.
1972
+ //
1973
+ // 3. timed_out not always written. Fix: always written after settlement —
1974
+ // true on timeout, false on clean completion.
1975
+ //
1976
+ // 4. Losing Promise.race branch kept mutating this.context after the
1977
+ // workflow moved on. Fix: branches write to branchRuntime.context only;
1978
+ // this.context is only touched during the final merge step.
1979
+ // ─────────────────────────────────────────────────────────────────────────────
1980
+
1768
1981
  case 'parallel': {
1769
1982
  const { steps, timeout } = step;
1983
+
1984
+ if (!steps || !Array.isArray(steps) || steps.length === 0) {
1985
+ this.addWarning('Parallel step contains no sub-steps. Skipping.');
1986
+ break;
1987
+ }
1988
+
1989
+ // Snapshot context before any branch runs.
1990
+ // Every branch reads from this — not from each other.
1991
+ const contextSnapshot = { ...this.context };
1992
+
1993
+ // Build one promise per sub-step.
1994
+ //
1995
+ // Each branch runs against a prototype-linked clone of the runtime.
1996
+ // Object.create(this) shares: allowedResolvers, auditLog, events,
1997
+ // verbose, resources, agentMap — everything a step needs to execute.
1998
+ // But branchRuntime.context is its own property that shadows this.context,
1999
+ // so concurrent writes never collide.
2000
+ //
2001
+ // Errors are caught here and returned as structured outcomes so that
2002
+ // Promise.all always resolves with the full result set — one failure
2003
+ // does not cancel sibling branches.
2004
+ const buildStepPromise = async (s, index) => {
2005
+ const branchRuntime = Object.create(this);
2006
+ branchRuntime.context = { ...contextSnapshot };
2007
+
2008
+ try {
2009
+ await branchRuntime.executeStep(s, agentResolver);
2010
+
2011
+ const saveKey = s.saveAs || null;
2012
+ const value = saveKey ? branchRuntime.context[saveKey] : undefined;
2013
+
2014
+ return {
2015
+ status: 'fulfilled',
2016
+ index,
2017
+ saveAs: saveKey,
2018
+ value,
2019
+ stepType: s.type
2020
+ };
2021
+ } catch (error) {
2022
+ return {
2023
+ status: 'rejected',
2024
+ index,
2025
+ reason: error.message || String(error),
2026
+ stepType: s.type
2027
+ };
2028
+ }
2029
+ };
2030
+
2031
+ const runAllSteps = () =>
2032
+ Promise.all(steps.map((s, i) => buildStepPromise(s, i)));
2033
+
2034
+ let settledResults;
2035
+
1770
2036
  if (timeout !== undefined && timeout > 0) {
1771
- const timeoutPromise = new Promise(resolve => {
1772
- setTimeout(() => resolve({ timedOut: true }), timeout);
1773
- });
1774
- const parallelPromise = Promise.all(
1775
- steps.map(s => this.executeStep(s, agentResolver))
1776
- ).then(() => ({ timedOut: false }));
1777
- const result = await Promise.race([timeoutPromise, parallelPromise]);
1778
- this.context.timed_out = result.timedOut;
1779
- if (result.timedOut) {
1780
- this.emit('parallel_timeout', { duration: timeout, steps: steps.length });
2037
+ // Race: all steps vs timeout sentinel.
2038
+ // runAllSteps never rejects (errors caught inside buildStepPromise),
2039
+ // so Promise.race resolves with either the results array or null.
2040
+ const timeoutPromise = new Promise(resolve =>
2041
+ setTimeout(() => resolve(null), timeout)
2042
+ );
2043
+
2044
+ settledResults = await Promise.race([runAllSteps(), timeoutPromise]);
2045
+
2046
+ if (settledResults === null) {
2047
+ // Timeout won. Restore snapshot + mark timed_out.
2048
+ // Do NOT merge partial results — we cannot know which branches
2049
+ // completed cleanly before the cutoff.
2050
+ this.context = { ...contextSnapshot, timed_out: true };
2051
+
2052
+ this.emit('parallel_timeout', {
2053
+ duration: timeout,
2054
+ steps_count: steps.length
2055
+ });
2056
+
1781
2057
  if (this.verbose) {
1782
2058
  console.log(`⏰ Parallel execution timed out after ${timeout}ms`);
1783
2059
  }
2060
+
2061
+ this._createAuditEntry('parallel_timeout', {
2062
+ timeout_ms: timeout,
2063
+ steps_count: steps.length,
2064
+ severity: 'warn'
2065
+ });
2066
+
2067
+ break;
1784
2068
  }
2069
+
1785
2070
  } else {
1786
- await Promise.all(steps.map(s => this.executeStep(s, agentResolver)));
1787
- this.context.timed_out = false;
2071
+ settledResults = await runAllSteps();
1788
2072
  }
2073
+
2074
+ // Merge results back into this.context.
2075
+ // Start from the snapshot so pre-parallel state is the clean base,
2076
+ // then layer each branch's saveAs result on top.
2077
+ this.context = { ...contextSnapshot, timed_out: false };
2078
+
2079
+ for (const outcome of settledResults) {
2080
+ if (outcome.status === 'fulfilled') {
2081
+ const { saveAs, value } = outcome;
2082
+ if (saveAs !== null && value !== undefined) {
2083
+ this.context[saveAs] = value;
2084
+ }
2085
+ } else {
2086
+ const { reason, index } = outcome;
2087
+ this.addWarning(`Parallel step [index ${index}] failed: ${reason}`);
2088
+
2089
+ this.emit('parallel_step_failed', {
2090
+ index,
2091
+ reason,
2092
+ stepType: outcome.stepType
2093
+ });
2094
+
2095
+ this._createAuditEntry('parallel_step_failed', {
2096
+ step_index: index,
2097
+ reason,
2098
+ severity: 'high'
2099
+ });
2100
+ }
2101
+ }
2102
+
1789
2103
  break;
1790
2104
  }
1791
2105
 
2106
+ // ─────────────────────────────────────────────────────────────────────────────
2107
+ // ESCALATION
2108
+ //
2109
+ // Governance Features:
2110
+ // 1. Scope Safety: Fixes ReferenceError by declaring levelSteps in outer scope.
2111
+ // 2. Auditability: Logs level start, completion, timeout, and final outcome.
2112
+ // 3. Determinism: Ensures timed-out levels don't corrupt context.
2113
+ // ─────────────────────────────────────────────────────────────────────────────
2114
+
1792
2115
  case 'escalation': {
1793
2116
  const { levels } = step;
2117
+ const { parseBlock } = require('../parser');
2118
+
1794
2119
  let finalResult = null;
1795
- let currentTimeout = 0;
1796
2120
  let completedLevel = null;
2121
+
1797
2122
  for (const level of levels) {
2123
+ // Fix: Declare levelSteps in outer block scope to avoid ReferenceError
2124
+ // in the timed-out branch.
2125
+ const levelSteps = parseBlock(level.steps);
2126
+
2127
+ this._createAuditEntry('escalation_level_started', {
2128
+ level: level.levelNumber,
2129
+ timeout_ms: level.timeout,
2130
+ steps_count: levelSteps.length,
2131
+ severity: 'info'
2132
+ });
2133
+
1798
2134
  if (level.timeout === 0) {
1799
- const levelSteps = require('./parser').parseBlock(level.steps);
2135
+ // Immediate level — execute sequentially
1800
2136
  for (const levelStep of levelSteps) {
1801
2137
  await this.executeStep(levelStep, agentResolver);
1802
2138
  }
2139
+
2140
+ // Check if result was saved
1803
2141
  if (levelSteps.length > 0) {
1804
2142
  const lastStep = levelSteps[levelSteps.length - 1];
1805
2143
  if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
1806
2144
  finalResult = this.context[lastStep.saveAs];
1807
2145
  completedLevel = level.levelNumber;
1808
- break;
2146
+
2147
+ this._createAuditEntry('escalation_level_completed', {
2148
+ level: level.levelNumber,
2149
+ timed_out: false,
2150
+ severity: 'info'
2151
+ });
2152
+ break; // Escalation complete
1809
2153
  }
1810
2154
  }
2155
+
1811
2156
  } else {
1812
- currentTimeout += level.timeout;
1813
- const timeoutPromise = new Promise(resolve => {
1814
- setTimeout(() => resolve({ timedOut: true }), level.timeout);
1815
- });
2157
+ // Timed level
2158
+ const timeoutPromise = new Promise(resolve =>
2159
+ setTimeout(() => resolve({ timedOut: true }), level.timeout)
2160
+ );
2161
+
1816
2162
  const levelPromise = (async () => {
1817
- const levelSteps = require('./parser').parseBlock(level.steps);
1818
2163
  for (const levelStep of levelSteps) {
1819
2164
  await this.executeStep(levelStep, agentResolver);
1820
2165
  }
1821
2166
  return { timedOut: false };
1822
2167
  })();
2168
+
1823
2169
  const result = await Promise.race([timeoutPromise, levelPromise]);
2170
+
1824
2171
  if (!result.timedOut) {
1825
- if (levelSteps && levelSteps.length > 0) {
2172
+ // Level completed within time
2173
+ if (levelSteps.length > 0) {
1826
2174
  const lastStep = levelSteps[levelSteps.length - 1];
1827
2175
  if (lastStep.saveAs && this.context[lastStep.saveAs] !== undefined) {
1828
2176
  finalResult = this.context[lastStep.saveAs];
1829
2177
  completedLevel = level.levelNumber;
1830
- break;
2178
+
2179
+ this._createAuditEntry('escalation_level_completed', {
2180
+ level: level.levelNumber,
2181
+ timed_out: false,
2182
+ severity: 'info'
2183
+ });
2184
+ break; // Escalation complete
1831
2185
  }
1832
2186
  }
2187
+ } else {
2188
+ // Level timed out
2189
+ this._createAuditEntry('escalation_level_timeout', {
2190
+ level: level.levelNumber,
2191
+ timeout_ms: level.timeout,
2192
+ severity: 'warn'
2193
+ });
2194
+
2195
+ if (this.verbose) {
2196
+ console.log(
2197
+ `⏰ Escalation level ${level.levelNumber} timed out ` +
2198
+ `after ${level.timeout}ms — trying next level`
2199
+ );
2200
+ }
2201
+ // Continue to next level
1833
2202
  }
1834
2203
  }
1835
2204
  }
2205
+
2206
+ // Final context state
1836
2207
  this.context.escalation_completed = finalResult !== null;
1837
2208
  this.context.timed_out = finalResult === null;
1838
2209
  if (completedLevel !== null) {
1839
2210
  this.context.escalation_level = completedLevel;
1840
2211
  }
1841
- break;
1842
- }
1843
2212
 
1844
- case 'connect': {
1845
- this.resources[step.resource] = step.endpoint;
2213
+ this._createAuditEntry('escalation_outcome', {
2214
+ completed: finalResult !== null,
2215
+ completed_at_level: completedLevel,
2216
+ timed_out: finalResult === null,
2217
+ severity: finalResult !== null ? 'info' : 'warn'
2218
+ });
2219
+
1846
2220
  break;
1847
2221
  }
1848
2222
 
2223
+ case 'connect': {
2224
+ if (!step.resource || !step.endpoint) {
2225
+ this.addWarning('Connect step missing "resource" or "endpoint". Skipping.');
2226
+ break;
2227
+ }
2228
+
2229
+ // Only validate URL format for url-type connects.
2230
+ // Resolver-type endpoints are package names (@o-lang/kyc-resolver)
2231
+ // and are not valid URLs — do not run them through new URL().
2232
+ if (!step.targetType || step.targetType === 'url') {
2233
+ try {
2234
+ new URL(step.endpoint);
2235
+ } catch (e) {
2236
+ this.addWarning(`Connect: Invalid endpoint URL for "${step.resource}": ${step.endpoint}`);
2237
+ break;
2238
+ }
2239
+ }
2240
+
2241
+ this.resources[step.resource] = step.endpoint;
2242
+
2243
+ this._createAuditEntry('resource_connected', {
2244
+ resource: step.resource,
2245
+ target_type: step.targetType || 'url',
2246
+ endpoint_masked: step.endpoint.replace(/\/\/[^@]+@/, '//***@'),
2247
+ severity: 'info'
2248
+ });
2249
+
2250
+ if (this.verbose) {
2251
+ console.log(`🔗 Connected "${step.resource}" → ${step.endpoint}`);
2252
+ }
2253
+ break;
2254
+ }
2255
+ // ─────────────────────────────────────────────────────────────────────────────
2256
+ // AGENT_USE
2257
+ // Maps a logical agent name (e.g., "support_bot") to a registered resource.
2258
+ // ─────────────────────────────────────────────────────────────────────────────
1849
2259
  case 'agent_use': {
2260
+ if (!step.logicalName || !step.resource) {
2261
+ this.addWarning('Agent_use step missing "logicalName" or "resource". Skipping.');
2262
+ break;
2263
+ }
2264
+
2265
+ // Optional: Validate that the resource was previously connected
2266
+ if (!this.resources[step.resource]) {
2267
+ this.addWarning(`Agent_use: Resource "${step.resource}" has not been connected yet.`);
2268
+ }
2269
+
1850
2270
  this.agentMap[step.logicalName] = step.resource;
2271
+
2272
+ // ✅ AUDIT LOG: Agent Mapping
2273
+ this._createAuditEntry('agent_mapped', {
2274
+ logical_name: step.logicalName,
2275
+ resource: step.resource,
2276
+ severity: 'info'
2277
+ });
2278
+
2279
+ if (this.verbose) {
2280
+ console.log(`🤖 Mapped agent "${step.logicalName}" to resource "${step.resource}"`);
2281
+ }
1851
2282
  break;
1852
2283
  }
2284
+ // ─────────────────────────────────────────────────────────────────────────────
2285
+ // DEBRIEF
2286
+ //
2287
+ // Governance Features:
2288
+ // 1. Semantic Validation: Ensures all {symbols} exist before emitting.
2289
+ // 2. Interpolation: Agents receive resolved values, not raw templates.
2290
+ // 3. Auditability: Every debrief is logged for compliance tracing.
2291
+ // ─────────────────────────────────────────────────────────────────────────────
1853
2292
 
1854
2293
  case 'debrief': {
1855
- if (step.message.includes('{')) {
1856
- const symbols = step.message.match(/\{([^\}]+)\}/g) || [];
1857
- for (const symbolMatch of symbols) {
1858
- const symbol = symbolMatch.replace(/[{}]/g, '');
1859
- this._requireSemantic(symbol, 'debrief');
2294
+ const messageTemplate = step.message;
2295
+
2296
+ // 1. Validate all referenced symbols exist in context
2297
+ if (messageTemplate && messageTemplate.includes('{')) {
2298
+ const symbolMatches = messageTemplate.match(/\{([^\}]+)\}/g) || [];
2299
+
2300
+ // Check every symbol. _requireSemantic will emit 'semantic_violation'
2301
+ // if a symbol is missing, but we also need to stop execution here.
2302
+ const allPresent = symbolMatches.every(sym => {
2303
+ const key = sym.replace(/[{}]/g, '');
2304
+ return this._requireSemantic(key, 'debrief');
2305
+ });
2306
+
2307
+ if (!allPresent) {
2308
+ if (this.verbose) {
2309
+ console.log(`⏭️ Debrief skipped — one or more symbols missing in context`);
2310
+ }
2311
+ // Break early. Do not emit incomplete data.
2312
+ break;
1860
2313
  }
1861
2314
  }
1862
- this.emit('debrief', { agent: step.agent, message: step.message });
2315
+
2316
+ // 2. Interpolate — agent receives the real value, not the template
2317
+ const interpolatedMessage = this._safeInterpolate(
2318
+ messageTemplate,
2319
+ this.context,
2320
+ 'debrief message'
2321
+ );
2322
+
2323
+ // 3. Audit trail — every agent message must be traceable
2324
+ this._createAuditEntry('debrief_emitted', {
2325
+ agent: step.agent,
2326
+ message_length: interpolatedMessage ? interpolatedMessage.length : 0,
2327
+ symbols_resolved: (messageTemplate.match(/\{([^\}]+)\}/g) || []).length,
2328
+ severity: 'info'
2329
+ });
2330
+
2331
+ this.emit('debrief', {
2332
+ agent: step.agent,
2333
+ message: interpolatedMessage
2334
+ });
2335
+
2336
+ if (this.verbose) {
2337
+ console.log(`📨 Debrief → agent "${step.agent}": ${interpolatedMessage}`);
2338
+ }
1863
2339
  break;
1864
2340
  }
1865
2341
 
2342
+
1866
2343
  case 'prompt': {
1867
2344
  if (this.verbose) {
1868
2345
  console.log(`❓ Prompt: ${step.question}`);
@@ -1870,56 +2347,153 @@ class RuntimeAPI {
1870
2347
  break;
1871
2348
  }
1872
2349
 
2350
+ // ─────────────────────────────────────────────────────────────────────────────
2351
+ // EMIT
2352
+ //
2353
+ // Governance Features:
2354
+ // 1. Semantic Validation: Stops at first missing symbol to reduce noise.
2355
+ // 2. Auditability: External events are logged with payload metadata.
2356
+ // 3. Consistency: Uses same interpolation logic as debrief.
2357
+ // ─────────────────────────────────────────────────────────────────────────────
2358
+
1873
2359
  case 'emit': {
1874
2360
  const payloadTemplate = step.payload;
2361
+
2362
+ // Extract unique symbols from the payload template
1875
2363
  const symbols = [...new Set(payloadTemplate.match(/\{([^\}]+)\}/g) || [])];
1876
- let shouldEmit = true;
1877
- for (const symbolMatch of symbols) {
1878
- const symbol = symbolMatch.replace(/[{}]/g, '');
1879
- if (!this._requireSemantic(symbol, 'emit')) {
1880
- shouldEmit = false;
1881
- }
1882
- }
1883
- if (!shouldEmit) {
2364
+
2365
+ // Validate all symbols, stop at first missing one
2366
+ const allPresent = symbols.every(sym => {
2367
+ const key = sym.replace(/[{}]/g, '');
2368
+ return this._requireSemantic(key, 'emit');
2369
+ });
2370
+
2371
+ if (!allPresent) {
1884
2372
  if (this.verbose) {
1885
- console.log(`⏭️ Skipped emit due to missing semantic symbols`);
2373
+ console.log(`⏭️ Skipped emit due to missing semantic symbols`);
1886
2374
  }
1887
2375
  break;
1888
2376
  }
1889
- const payload = this._safeInterpolate(step.payload, this.context, 'emit payload');
2377
+
2378
+ // Interpolate the payload
2379
+ const payload = this._safeInterpolate(
2380
+ payloadTemplate,
2381
+ this.context,
2382
+ 'emit payload'
2383
+ );
2384
+
2385
+ // ✅ AUDIT LOG: External event emission
2386
+ this._createAuditEntry('event_emitted', {
2387
+ event: step.event,
2388
+ payload_length: payload ? payload.length : 0,
2389
+ symbols_resolved: symbols.length,
2390
+ severity: 'info'
2391
+ });
2392
+
1890
2393
  this.emit(step.event, {
1891
- payload: payload,
1892
- workflow: this.context.workflow_name,
2394
+ payload,
2395
+ workflow: this.context.workflow_name || 'unknown',
1893
2396
  timestamp: new Date().toISOString()
1894
2397
  });
2398
+
1895
2399
  if (this.verbose) {
1896
2400
  console.log(`📤 Emit event "${step.event}" with payload: ${payload}`);
1897
2401
  }
1898
2402
  break;
1899
2403
  }
1900
2404
 
2405
+ // ─────────────────────────────────────────────────────────────────────────────
2406
+ // PERSIST
2407
+ //
2408
+ // Governance Features:
2409
+ // 1. Data Integrity: Prevents silent corruption of objects into "[object Object]".
2410
+ // 2. Auditability: Logs path, format, and variable name (without raw values).
2411
+ // 3. Safety: Validates semantic existence before attempting I/O.
2412
+ // ─────────────────────────────────────────────────────────────────────────────
2413
+
1901
2414
  case 'persist': {
1902
- if (!this._requireSemantic(step.variable, 'persist')) {
2415
+ const fs = require('fs');
2416
+ const path = require('path');
2417
+
2418
+ // 1. Semantic Guard: Ensure the variable exists in context
2419
+ if (!step.variable || !this._requireSemantic(step.variable, 'persist')) {
1903
2420
  if (this.verbose) {
1904
- console.log(`⏭️ Skipped persist for undefined "${step.variable}"`);
2421
+ console.log(`⏭️ Skipped persist for undefined "${step.variable}"`);
1905
2422
  }
1906
2423
  break;
1907
2424
  }
2425
+
1908
2426
  const sourceValue = this.context[step.variable];
2427
+
2428
+ // Resolve absolute path to prevent directory traversal attacks or ambiguity
1909
2429
  const outputPath = path.resolve(process.cwd(), step.target);
1910
2430
  const outputDir = path.dirname(outputPath);
2431
+
2432
+ // Ensure directory exists
1911
2433
  if (!fs.existsSync(outputDir)) {
1912
- fs.mkdirSync(outputDir, { recursive: true });
2434
+ try {
2435
+ fs.mkdirSync(outputDir, { recursive: true });
2436
+ } catch (e) {
2437
+ this.addWarning(`persist: failed to create directory "${outputDir}": ${e.message}`);
2438
+ break;
2439
+ }
1913
2440
  }
2441
+
2442
+ // 2. Serialize with safe fallback for object → non-JSON targets
1914
2443
  let content;
2444
+ const isObject = sourceValue !== null && typeof sourceValue === 'object';
2445
+ let formatUsed = 'string';
2446
+
1915
2447
  if (step.target.endsWith('.json')) {
2448
+ // Standard JSON serialization
1916
2449
  content = JSON.stringify(sourceValue, null, 2);
2450
+ formatUsed = 'json';
2451
+ } else if (isObject) {
2452
+ // CRITICAL FIX: Prevents "[object Object]" data corruption.
2453
+ // If user tries to save an object to .txt/.csv, we coerce to JSON
2454
+ // so the data remains recoverable, but we warn them.
2455
+ this.addWarning(
2456
+ `persist: "${step.variable}" is an object but target "${step.target}" is not .json. ` +
2457
+ `Writing as JSON to prevent data loss. Rename target to .json or select a specific field.`
2458
+ );
2459
+ content = JSON.stringify(sourceValue, null, 2);
2460
+ formatUsed = 'json-fallback';
1917
2461
  } else {
2462
+ // Primitive values (string, number, boolean)
1918
2463
  content = String(sourceValue);
2464
+ formatUsed = 'string';
1919
2465
  }
1920
- fs.writeFileSync(outputPath, content, 'utf8');
2466
+
2467
+ // 3. Write with error surfacing
2468
+ try {
2469
+ fs.writeFileSync(outputPath, content, 'utf8');
2470
+ } catch (e) {
2471
+ this.addWarning(`persist: failed to write "${step.variable}" to "${step.target}": ${e.message}`);
2472
+
2473
+ // ✅ AUDIT LOG: Failed Write
2474
+ this._createAuditEntry('persist_failed', {
2475
+ variable: step.variable,
2476
+ target: step.target,
2477
+ error: e.message,
2478
+ severity: 'high'
2479
+ });
2480
+ break;
2481
+ }
2482
+
2483
+ // 4. ✅ AUDIT LOG: Successful Write
2484
+ // Note: We do NOT log the actual value content to protect PII.
2485
+ // We log metadata only.
2486
+ this._createAuditEntry('context_persisted', {
2487
+ variable: step.variable,
2488
+ target: step.target,
2489
+ format: formatUsed,
2490
+ value_type: isObject ? 'object' : typeof sourceValue,
2491
+ byte_length: Buffer.byteLength(content, 'utf8'),
2492
+ severity: 'info'
2493
+ });
2494
+
1921
2495
  if (this.verbose) {
1922
- console.log(`💾 Persisted "${step.variable}" to ${step.target}`);
2496
+ console.log(`💾 Persisted "${step.variable}" to ${step.target} (${formatUsed})`);
1923
2497
  }
1924
2498
  break;
1925
2499
  }