@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.
- package/package.json +1 -1
- package/src/parser/index.js +187 -258
- package/src/runtime/RuntimeAPI.js +641 -67
|
@@ -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.
|
|
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
|
-
//
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
-
//
|
|
1013
|
-
const eqMatch = cond.match(/^(?:\{(
|
|
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
|
-
//
|
|
1020
|
-
const
|
|
1021
|
-
if (
|
|
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(/^\{(
|
|
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(/^\{(
|
|
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
|
-
//
|
|
1030
|
-
|
|
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
|
-
|
|
1763
|
-
|
|
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
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
const
|
|
1775
|
-
|
|
1776
|
-
)
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1845
|
-
|
|
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
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
if (!
|
|
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(`⏭️
|
|
2373
|
+
console.log(`⏭️ Skipped emit due to missing semantic symbols`);
|
|
1886
2374
|
}
|
|
1887
2375
|
break;
|
|
1888
2376
|
}
|
|
1889
|
-
|
|
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
|
|
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
|
-
|
|
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(`⏭️
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|