myxo-lang 1.5.0

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 (58) hide show
  1. package/CONCURRENCY.md +207 -0
  2. package/HOW_IT_WORKS.md +235 -0
  3. package/INDEPENDENCE.md +24 -0
  4. package/MYXO_PROMPT.md +139 -0
  5. package/README.md +494 -0
  6. package/ROADMAP.md +200 -0
  7. package/SPEC.md +181 -0
  8. package/VISION.md +249 -0
  9. package/builtins.js +361 -0
  10. package/command-fence.js +75 -0
  11. package/errors.js +45 -0
  12. package/examples/agent.myx +31 -0
  13. package/examples/fenced-agent.js +78 -0
  14. package/examples/fib.myx +11 -0
  15. package/examples/fibers.myx +57 -0
  16. package/examples/flow-routing.myx +12 -0
  17. package/examples/geo.myx +12 -0
  18. package/examples/hello.myx +2 -0
  19. package/examples/host.js +30 -0
  20. package/examples/james-myxo-demo.js +60 -0
  21. package/examples/living-mesh.myx +22 -0
  22. package/examples/match.myx +18 -0
  23. package/examples/mathlib.js +8 -0
  24. package/examples/mathlib.pl +11 -0
  25. package/examples/mathlib.py +23 -0
  26. package/examples/mcp-host.js +37 -0
  27. package/examples/nexus-mesh.myx +22 -0
  28. package/examples/nexus.myx +29 -0
  29. package/examples/ouroboros.myx +2 -0
  30. package/examples/outward-gate.myx +23 -0
  31. package/examples/physarum.myx +75 -0
  32. package/examples/polyglot-host.js +17 -0
  33. package/examples/polyglot.myx +11 -0
  34. package/examples/resilient.myx +28 -0
  35. package/examples/scheduler.myx +46 -0
  36. package/examples/the-law.myx +27 -0
  37. package/examples/use-geo.myx +9 -0
  38. package/format.js +206 -0
  39. package/interpreter.js +1092 -0
  40. package/lexer.js +173 -0
  41. package/mcp-bridge.js +77 -0
  42. package/mcp-framing.js +34 -0
  43. package/mcp-server.js +57 -0
  44. package/myxo-concurrent.js +110 -0
  45. package/myxo-live-worker.js +30 -0
  46. package/myxo-live.js +83 -0
  47. package/myxo-lsp.js +226 -0
  48. package/myxo-par-worker.js +39 -0
  49. package/myxo-plan.js +207 -0
  50. package/myxo-run.js +63 -0
  51. package/myxo.js +296 -0
  52. package/package.json +27 -0
  53. package/parser.js +662 -0
  54. package/polyglot-host.js +39 -0
  55. package/polyglot.js +191 -0
  56. package/receipt.js +71 -0
  57. package/std.myx +88 -0
  58. package/tools/memo-fuzz.js +224 -0
package/myxo-lsp.js ADDED
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+ // myxo-lsp.js — a minimal Language Server for Myxo (stdio JSON-RPC, LSP 3.x).
3
+ // v1 scope: diagnostics (parse/syntax errors), hover (keyword + builtin docs), completion
4
+ // (keywords + builtins + TOP-LEVEL names in the file), and formatting (comment-preserving).
5
+ // The analysis core (diagnostics/hoverAt/completions/docNames/formatDoc) is pure and unit-tested;
6
+ // `handle` is a pure JSON-RPC dispatcher; `serve` is the thin stdio transport.
7
+ // HONEST scope: syntax/parse diagnostics ONLY — a parseable-but-semantically-broken file shows NO squiggles
8
+ // (type/runtime errors surface only at run time). Completion offers top-level names, NOT scope-aware. No
9
+ // go-to-definition / references / rename / document-symbols. Single-file; not yet run in a live editor.
10
+
11
+ const { parse } = require('./parser');
12
+ const { MyxoError } = require('./errors');
13
+ const { KEYWORDS } = require('./lexer');
14
+ const { formatSource } = require('./format');
15
+ const { makeInterpreter } = require('./myxo');
16
+
17
+ // ---- name tables -----------------------------------------------------------
18
+
19
+ let NAMES = null;
20
+ function builtinNames() { // builtins + self-hosted stdlib, straight from a booted interpreter
21
+ if (NAMES) return NAMES;
22
+ try { NAMES = [...makeInterpreter().globals.vars.keys()].sort(); }
23
+ catch { NAMES = []; }
24
+ return NAMES;
25
+ }
26
+
27
+ const KEYWORD_DOCS = {
28
+ seed: 'seed name = value — bind a pathway (seed name: Type = value to type it).',
29
+ decay: 'decay name — remove a pathway (or decay x[i] for a slot).',
30
+ emit: 'emit a, b, ... — print the values, space-joined.',
31
+ when: 'when cond { ... } otherwise { ... } — conditional.',
32
+ otherwise: 'otherwise { ... } / otherwise when ... — the else of a when.',
33
+ reinforce: 'reinforce cond { ... } (while) · reinforce N times { ... } (repeat).',
34
+ times: 'reinforce N times { ... } — repeat a block N times.',
35
+ for: 'for each x in iterable { ... } — iterate a list/mesh/string.',
36
+ each: 'for each x in iterable { ... }.',
37
+ in: 'for each x in iterable { ... }.',
38
+ agent: 'agent name(params) { body } — define an agent (closure). : Type for a return type.',
39
+ report: 'report expr — return a value from the enclosing agent.',
40
+ attempt: 'attempt { ... } rescue err { ... } — catch a failure (err is a mesh).',
41
+ rescue: 'rescue err { ... } — handle a failure from attempt.',
42
+ fail: 'fail expr — raise a failure an enclosing attempt can rescue.',
43
+ weave: 'weave "path" [as alias] — import a module strand (fenced).',
44
+ expose: 'expose name — make a pathway public for weaving.',
45
+ as: 'weave "path" as alias — namespace the imported names.',
46
+ needs: 'needs cap, cap(max N, total M) — declare/limit capabilities.',
47
+ live: 'live — boolean true.',
48
+ dead: 'dead — boolean false.',
49
+ void: 'void — the empty value (also a type name).',
50
+ and: 'a and b — returns a if dead, else b (value-returning, short-circuit).',
51
+ or: 'a or b — returns a if live, else b (so `x or default`).',
52
+ not: 'not x — boolean negation.',
53
+ test: 'test "name" { expect ... } — a test case (runs under `myxo test`).',
54
+ expect: 'expect e [is v | is not v | to fail | to fail with "s"] — an assertion.',
55
+ match: 'match subject { pattern { ... } _ { ... } } — pattern matching.',
56
+ };
57
+
58
+ // ---- pure analysis (unit-tested) -------------------------------------------
59
+
60
+ // Parse the text; return LSP-shaped diagnostics (0-based line/char). Syntax-level only.
61
+ function diagnostics(text) {
62
+ try { parse(text); return []; }
63
+ catch (e) {
64
+ const line = (typeof e.line === 'number' ? e.line : 1) - 1;
65
+ const col = (typeof e.col === 'number' ? e.col : 1) - 1;
66
+ return [{ line: Math.max(0, line), col: Math.max(0, col), message: e.message }];
67
+ }
68
+ }
69
+
70
+ const isWordChar = (c) => /[A-Za-z0-9_]/.test(c || '');
71
+
72
+ // The identifier/keyword token straddling a 0-based (line, character), or '' .
73
+ function wordAt(text, line, character) {
74
+ const lines = text.split('\n');
75
+ const src = lines[line];
76
+ if (src == null) return '';
77
+ let s = character, e = character;
78
+ while (s > 0 && isWordChar(src[s - 1])) s--;
79
+ while (e < src.length && isWordChar(src[e])) e++;
80
+ return src.slice(s, e);
81
+ }
82
+
83
+ function hoverAt(text, line, character) {
84
+ const w = wordAt(text, line, character);
85
+ if (!w) return null;
86
+ if (KEYWORD_DOCS[w]) return KEYWORD_DOCS[w];
87
+ if (builtinNames().includes(w)) return `${w} — Myxo builtin/stdlib agent.`;
88
+ return null;
89
+ }
90
+
91
+ // Names declared in this file: seeded pathways, agents, and destructured bindings.
92
+ function docNames(text) {
93
+ let ast;
94
+ try { ast = parse(text); } catch { return []; }
95
+ const out = new Set();
96
+ const fromPattern = (p) => {
97
+ if (!p) return;
98
+ if (p.type === 'PBind') out.add(p.name);
99
+ else if (p.type === 'PList') { p.elements.forEach(fromPattern); if (p.rest) out.add(p.rest); }
100
+ else if (p.type === 'PMesh') p.pairs.forEach(pr => fromPattern(pr.pattern));
101
+ };
102
+ for (const s of ast.body) {
103
+ if (s.type === 'Seed') out.add(s.name);
104
+ else if (s.type === 'Agent') out.add(s.name);
105
+ else if (s.type === 'SeedDestructure') fromPattern(s.pattern);
106
+ }
107
+ return [...out];
108
+ }
109
+
110
+ // LSP CompletionItemKind: Function=3, Variable=6, Keyword=14.
111
+ function completions(text) {
112
+ const items = [];
113
+ for (const n of docNames(text)) items.push({ label: n, kind: 6, detail: 'in this file' });
114
+ for (const k of KEYWORDS) items.push({ label: k, kind: 14, detail: 'keyword' });
115
+ for (const b of builtinNames()) items.push({ label: b, kind: 3, detail: 'builtin' });
116
+ const seen = new Set(), out = [];
117
+ for (const it of items) { if (!seen.has(it.label)) { seen.add(it.label); out.push(it); } } // doc names win
118
+ return out;
119
+ }
120
+
121
+ function formatDoc(text) {
122
+ try { return formatSource(text); } catch { return null; } // comments are preserved now; just don't reformat invalid source
123
+ }
124
+
125
+ // ---- JSON-RPC dispatch (pure: state + message -> { response?, notifications? }) ----
126
+
127
+ function lspDiagnostics(text) {
128
+ return diagnostics(text).map(d => ({
129
+ range: { start: { line: d.line, character: d.col }, end: { line: d.line, character: d.col + 1 } },
130
+ severity: 1, // Error
131
+ source: 'myxo',
132
+ message: d.message,
133
+ }));
134
+ }
135
+
136
+ function handle(state, msg) {
137
+ const { id, method, params } = msg;
138
+ const docs = state.docs;
139
+ const textOf = (p) => docs.get(p && p.textDocument && p.textDocument.uri);
140
+ const publish = (uri) => ({ method: 'textDocument/publishDiagnostics', params: { uri, diagnostics: lspDiagnostics(docs.get(uri) || '') } });
141
+
142
+ switch (method) {
143
+ case 'initialize':
144
+ return { response: { id, result: {
145
+ serverInfo: { name: 'myxo-lsp', version: '1.0' },
146
+ capabilities: {
147
+ textDocumentSync: 1, // full-document sync
148
+ hoverProvider: true,
149
+ completionProvider: { triggerCharacters: [] },
150
+ documentFormattingProvider: true,
151
+ } } } };
152
+ case 'initialized': return {};
153
+ case 'textDocument/didOpen': {
154
+ const uri = params.textDocument.uri;
155
+ docs.set(uri, params.textDocument.text || '');
156
+ return { notifications: [publish(uri)] };
157
+ }
158
+ case 'textDocument/didChange': {
159
+ const uri = params.textDocument.uri;
160
+ const last = params.contentChanges[params.contentChanges.length - 1];
161
+ docs.set(uri, last ? last.text : (docs.get(uri) || '')); // full sync: last change is the whole doc
162
+ return { notifications: [publish(uri)] };
163
+ }
164
+ case 'textDocument/didClose':
165
+ docs.delete(params.textDocument.uri);
166
+ return {};
167
+ case 'textDocument/hover': {
168
+ const h = hoverAt(textOf(params) || '', params.position.line, params.position.character);
169
+ return { response: { id, result: h ? { contents: { kind: 'plaintext', value: h } } : null } };
170
+ }
171
+ case 'textDocument/completion':
172
+ return { response: { id, result: { isIncomplete: false, items: completions(textOf(params) || '') } } };
173
+ case 'textDocument/formatting': {
174
+ const text = textOf(params) || '';
175
+ const f = formatDoc(text);
176
+ if (f == null || f === text) return { response: { id, result: [] } };
177
+ const lines = text.split('\n');
178
+ const end = { line: lines.length - 1, character: lines[lines.length - 1].length };
179
+ return { response: { id, result: [{ range: { start: { line: 0, character: 0 }, end }, newText: f }] } };
180
+ }
181
+ case 'shutdown': state.shuttingDown = true; return { response: { id, result: null } };
182
+ case 'exit': return { exit: true, code: state.shuttingDown ? 0 : 1 }; // LSP: exit before shutdown is an error
183
+ default:
184
+ return id != null ? { response: { id, error: { code: -32601, message: `method not found: ${method}` } } } : {};
185
+ }
186
+ }
187
+
188
+ // ---- stdio transport -------------------------------------------------------
189
+
190
+ function serve(input = process.stdin, output = process.stdout) {
191
+ const state = { docs: new Map() };
192
+ let buf = Buffer.alloc(0);
193
+ const send = (obj) => {
194
+ const body = Buffer.from(JSON.stringify({ jsonrpc: '2.0', ...obj }), 'utf8');
195
+ output.write(`Content-Length: ${body.length}\r\n\r\n`);
196
+ output.write(body);
197
+ };
198
+ input.on('data', (chunk) => {
199
+ buf = Buffer.concat([buf, chunk]);
200
+ for (;;) {
201
+ const headerEnd = buf.indexOf('\r\n\r\n');
202
+ if (headerEnd < 0) break;
203
+ const header = buf.slice(0, headerEnd).toString('utf8');
204
+ const m = /Content-Length:\s*(\d+)/i.exec(header);
205
+ if (!m) { buf = buf.slice(headerEnd + 4); continue; }
206
+ const len = parseInt(m[1], 10);
207
+ const start = headerEnd + 4;
208
+ if (buf.length < start + len) break; // wait for the full body
209
+ const body = buf.slice(start, start + len).toString('utf8');
210
+ buf = buf.slice(start + len);
211
+ let msg;
212
+ try { msg = JSON.parse(body); } catch { continue; }
213
+ let out;
214
+ try { out = handle(state, msg); } // a malformed/unexpected message must never kill the server
215
+ catch (e) {
216
+ if (msg && msg.id != null) send({ id: msg.id, error: { code: -32603, message: 'internal error: ' + e.message } });
217
+ continue;
218
+ }
219
+ if (out.response) send(out.response);
220
+ if (out.notifications) for (const n of out.notifications) send(n);
221
+ if (out.exit) process.exit(out.code || 0);
222
+ }
223
+ });
224
+ }
225
+
226
+ module.exports = { diagnostics, wordAt, hoverAt, docNames, completions, formatDoc, handle, serve, KEYWORD_DOCS };
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+ // myxo-par-worker.js — the worker half of Myxo parallelism. Receives ONE agent (its params + body AST) and its
3
+ // args, runs it in a fresh isolated interpreter, posts the JSON result back, and signals the shared barrier.
4
+ // Isolation is the point: a dispatched agent gets its own interpreter (builtins + stdlib), so there is no
5
+ // shared mutable state to race. It is therefore self-contained — it sees its params, the stdlib, and itself
6
+ // (recursion), but NOT other user agents or the parent's pathways.
7
+
8
+ const { workerData } = require('worker_threads');
9
+ if (!workerData) return; // this file is only meaningful when launched as a Worker; running it standalone is a no-op
10
+
11
+ // Pull only what we need to release the barrier FIRST, so that even a failure while loading the interpreter
12
+ // modules (a bad require) still ticks the counter from the `finally` — the parent must never hang on us.
13
+ const { name, params, body, argsJSON, counterSab, port } = workerData;
14
+ const counter = new Int32Array(counterSab);
15
+
16
+ let msg;
17
+ try {
18
+ const { makeInterpreter } = require('./myxo');
19
+ const { jsToNx, nxToJs } = require('./polyglot');
20
+ const { assertSerializable } = require('./myxo-concurrent');
21
+ const { performance } = require('perf_hooks');
22
+ const interp = makeInterpreter();
23
+ const agent = { __agent: true, name: name || 'task', params, body, closure: interp.globals };
24
+ interp.globals.define(name || 'task', agent, true); // define under its own name so recursion resolves
25
+ const args = JSON.parse(argsJSON).map(jsToNx);
26
+ const t0 = performance.now();
27
+ const result = interp.callValue(agent, args, 0);
28
+ const ms = performance.now() - t0; // pure compute time (excludes worker startup) — the scheduler's speed signal
29
+ assertSerializable(result, 'a gathered result'); // a task must return data, not code, to cross back
30
+ msg = { ok: true, resultJSON: JSON.stringify(nxToJs(result)), ms };
31
+ } catch (e) {
32
+ msg = { ok: false, error: e && e.message ? e.message : String(e) };
33
+ } finally {
34
+ // ALWAYS post-then-tick, on every path: the parent drains the message only after the barrier is satisfied,
35
+ // so posting before ticking guarantees the message is queued by the time a visible tick releases the wait.
36
+ try { port.postMessage(msg || { ok: false, error: 'worker produced no result' }); } catch {}
37
+ Atomics.add(counter, 0, 1);
38
+ Atomics.notify(counter, 0);
39
+ }
package/myxo-plan.js ADDED
@@ -0,0 +1,207 @@
1
+ 'use strict';
2
+ // myxo-plan.js — the capability preview. Given a script, report what it is ALLOWED to touch (its `needs` manifest)
3
+ // versus what it actually TRIES to touch (capabilities it calls). This is the approval surface for handing Myxo to
4
+ // an agent: a gate (or a human) can see a script's reach BEFORE running it, and catch fence mistakes statically —
5
+ // • referenced-but-undeclared -> the fence would DENY it at runtime (or, with no manifest, it runs ungoverned)
6
+ // • declared-but-unreferenced -> an over-grant to tighten
7
+ // Static + single-file: a "capability" is any called name that is neither a user `agent` nor a core builtin.
8
+ // Honest limit: names imported via `weave` aren't resolved, so an imported agent can look like a capability.
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { parse } = require('./parser');
13
+ const { builtins } = require('./builtins');
14
+
15
+ // Generic AST walk: visit every node (object with a string `type`), recursing through all properties.
16
+ function walk(node, visit) {
17
+ if (!node || typeof node !== 'object') return;
18
+ if (Array.isArray(node)) { for (const n of node) walk(n, visit); return; }
19
+ if (typeof node.type === 'string') visit(node);
20
+ for (const k of Object.keys(node)) { if (k !== 'type') walk(node[k], visit); }
21
+ }
22
+
23
+ function calleeName(n) {
24
+ if (!n) return null;
25
+ if (n.type === 'Call' && n.callee && n.callee.type === 'Identifier') return n.callee.name;
26
+ if (n.type === 'Pipe') return n.right && n.right.type === 'Call'
27
+ ? (n.right.callee && n.right.callee.type === 'Identifier' ? n.right.callee.name : null)
28
+ : (n.right && n.right.type === 'Identifier' ? n.right.name : null); // `x | f`
29
+ return null;
30
+ }
31
+
32
+ // The "safe core" in scope for every program: JS builtins + every agent the stdlib (std.myx) defines. We parse
33
+ // std.myx directly (NOT via makeInterpreter) to avoid a require cycle through myxo.js — that's what makes the stdlib
34
+ // (`map`/`filter`/`sum`/…) not look like host capabilities.
35
+ let SAFE_CACHE = null;
36
+ function safeCore() {
37
+ if (SAFE_CACHE) return SAFE_CACHE;
38
+ const safe = new Set(Object.keys(builtins()));
39
+ try {
40
+ walk(parse(fs.readFileSync(path.join(__dirname, 'std.myx'), 'utf8')), (n) => { if (n.type === 'Agent') safe.add(n.name); });
41
+ } catch { /* fall back to JS builtins only */ }
42
+ SAFE_CACHE = safe;
43
+ return safe;
44
+ }
45
+
46
+ // Names a pattern binds (so `match`/destructure binds shadow correctly and aren't seen as capabilities).
47
+ function patternBinds(pat, set) {
48
+ if (!pat || typeof pat !== 'object') return;
49
+ if (pat.type === 'PBind') set.add(pat.name);
50
+ else if (pat.type === 'PList') { for (const e of pat.elements) patternBinds(e, set); if (pat.rest) set.add(pat.rest); }
51
+ else if (pat.type === 'PMesh') { for (const pr of pat.pairs) patternBinds(pr.pattern, set); }
52
+ }
53
+
54
+ // Names DECLARED directly in a statement list (agents + seeds) — pre-collected so intra-scope references resolve
55
+ // regardless of order. A nested agent is NOT pulled up here; it only enters its enclosing scope, never the parent's.
56
+ function scopeDecls(stmts, set) {
57
+ for (const s of stmts) {
58
+ if (!s || typeof s !== 'object') continue;
59
+ if (s.type === 'Agent') set.add(s.name);
60
+ else if (s.type === 'Seed') set.add(s.name);
61
+ else if (s.type === 'SeedDestructure') patternBinds(s.pattern, set);
62
+ }
63
+ }
64
+
65
+ // Scope-aware capability detection with TWO resolution modes, mirroring the runtime (interpreter.js has NO hoisting):
66
+ // • IMMEDIATE code (run in statement order) resolves only against decls seen SO FAR — so a call placed BEFORE a
67
+ // same-named `agent` decl is NOT masked by it (closes the declaration-order decoy).
68
+ // • DEFERRED bodies (an agent runs when CALLED, later) resolve against the FULL scope — so mutual recursion and
69
+ // ordinary forward references still work.
70
+ // A called name not resolvable is a host capability.
71
+
72
+ // analyzeExpr: an expression runs immediately in `visible`; a deferred body (AgentExpr) sees the enclosing `full`
73
+ // scope (so forward refs resolve). `full` is threaded so the anon-agent path matches the named-agent path.
74
+ function analyzeExpr(node, visible, full, refs) {
75
+ if (!node || typeof node !== 'object') return;
76
+ if (Array.isArray(node)) { for (const n of node) analyzeExpr(n, visible, full, refs); return; }
77
+ if (node.type === 'Call' || node.type === 'Pipe') {
78
+ const name = calleeName(node);
79
+ if (name && !visible.has(name) && !refs.has(name)) refs.set(name, node.line || 0);
80
+ for (const k of Object.keys(node)) { if (k !== 'type') analyzeExpr(node[k], visible, full, refs); }
81
+ return;
82
+ }
83
+ if (node.type === 'AgentExpr') { // DEFERRED body: sees the full enclosing scope + params (like a named agent)
84
+ const child = new Set(full);
85
+ for (const p of node.params) { child.add(p.name); if (p.def) analyzeExpr(p.def, child, full, refs); }
86
+ analyzeBody(node.body, child, refs);
87
+ return;
88
+ }
89
+ for (const k of Object.keys(node)) { if (k !== 'type') analyzeExpr(node[k], visible, full, refs); }
90
+ }
91
+
92
+ // analyzeBody: a statement list. `running` grows as decls are seen (for immediate code); `full` = parent + ALL of
93
+ // this scope's decls (for deferred agent bodies / forward refs).
94
+ function analyzeBody(stmts, parentVisible, refs) {
95
+ const full = new Set(parentVisible); scopeDecls(stmts, full);
96
+ const running = new Set(parentVisible);
97
+ for (const s of stmts) {
98
+ analyzeStmt(s, running, full, refs);
99
+ if (s.type === 'Agent') running.add(s.name);
100
+ else if (s.type === 'Seed') running.add(s.name);
101
+ else if (s.type === 'SeedDestructure') patternBinds(s.pattern, running);
102
+ else if (s.type === 'Take') running.add(s.name);
103
+ }
104
+ }
105
+
106
+ function analyzeStmt(s, running, full, refs) {
107
+ switch (s.type) {
108
+ case 'Agent': { // DEFERRED body: sees the full enclosing scope (forward refs OK) + its params
109
+ const child = new Set(full);
110
+ for (const p of s.params) { child.add(p.name); if (p.def) analyzeExpr(p.def, child, full, refs); }
111
+ analyzeBody(s.body, child, refs);
112
+ return;
113
+ }
114
+ // immediate blocks: run in order; their own bodies resolve against `running` (decls so far) + block-local binds
115
+ case 'When':
116
+ analyzeExpr(s.cond, running, full, refs);
117
+ analyzeBody(s.thenBlock, running, refs);
118
+ if (s.elseBlock) analyzeBody(s.elseBlock, running, refs);
119
+ return;
120
+ case 'Reinforce': analyzeExpr(s.cond, running, full, refs); analyzeBody(s.body, running, refs); return;
121
+ case 'ReinforceTimes': analyzeExpr(s.count, running, full, refs); analyzeBody(s.body, running, refs); return;
122
+ case 'ForEach': { analyzeExpr(s.iterable, running, full, refs); analyzeBody(s.body, addAll(running, [s.varName]), refs); return; }
123
+ case 'Match': {
124
+ analyzeExpr(s.subject, running, full, refs);
125
+ for (const arm of s.arms) { const c = new Set(running); patternBinds(arm.pattern, c); analyzeBody(arm.body, c, refs); }
126
+ return;
127
+ }
128
+ case 'Attempt':
129
+ analyzeBody(s.tryBlock, running, refs);
130
+ analyzeBody(s.catchBlock, addAll(running, [s.errName]), refs);
131
+ return;
132
+ case 'Test': analyzeBody(s.body, running, refs); return;
133
+ default:
134
+ analyzeExpr(s, running, full, refs); // seed/emit/report/assign/decay/give/take/expression-statement -> immediate
135
+ return;
136
+ }
137
+ }
138
+
139
+ function addAll(set, names) { const c = new Set(set); for (const n of names) c.add(n); return c; }
140
+
141
+ // planScript(src) -> { needs:[{name,limit}], referenced:[{name,line,declared}], undeclared:[name], unused:[name], hasManifest }
142
+ function planScript(src) {
143
+ const ast = parse(src);
144
+ const needs = [];
145
+ walk(ast, (n) => { if (n.type === 'Needs') for (const it of n.items) needs.push(it); });
146
+ const declared = new Set(needs.map(x => x.name));
147
+
148
+ const refs = new Map(); // host-capability name -> first line
149
+ analyzeBody(ast.body, safeCore(), refs);
150
+
151
+ const referenced = [...refs.entries()]
152
+ .map(([name, line]) => ({ name, line, declared: declared.has(name) }))
153
+ .sort((a, b) => a.name.localeCompare(b.name));
154
+ const undeclared = referenced.filter(r => !r.declared).map(r => r.name);
155
+ const referencedSet = new Set(referenced.map(r => r.name));
156
+ const unused = [...declared].filter(n => !referencedSet.has(n)).sort();
157
+ return { needs, referenced, undeclared, unused, hasManifest: needs.length > 0 };
158
+ }
159
+
160
+ function fmtLimit(limit) {
161
+ if (!limit) return '';
162
+ const parts = [];
163
+ if (limit.max != null) parts.push(`max ${limit.max}`);
164
+ if (limit.total != null) parts.push(`total ${limit.total}`);
165
+ return parts.length ? ` (${parts.join(', ')})` : '';
166
+ }
167
+
168
+ // A readable report. Returns { text, ok } — ok=false means the script would be refused at runtime (undeclared use).
169
+ function formatPlan(plan, file) {
170
+ const L = [];
171
+ L.push(`myxo plan: ${file || '(script)'}`);
172
+ L.push('');
173
+ L.push('Declared capabilities (needs):');
174
+ if (plan.needs.length === 0) L.push(' (none)');
175
+ else for (const it of plan.needs) L.push(` ${it.name}${fmtLimit(it.limit)}`);
176
+ L.push('');
177
+ L.push('Capabilities referenced in code:');
178
+ if (plan.referenced.length === 0) L.push(' (none — this script reaches for nothing outside the language)');
179
+ else for (const r of plan.referenced) {
180
+ L.push(` ${r.name}${' '.repeat(Math.max(1, 16 - r.name.length))}${r.declared ? 'OK declared' : 'XX NOT declared (line ' + r.line + ') — the fence would deny it'}`);
181
+ }
182
+ if (plan.unused.length) {
183
+ L.push('');
184
+ L.push('Over-grants (declared but never used — tighten these):');
185
+ for (const n of plan.unused) L.push(` ${n}`);
186
+ }
187
+ L.push('');
188
+ let ok = true;
189
+ if (plan.undeclared.length) {
190
+ L.push(`VERDICT: ${plan.undeclared.length} referenced capability(ies) not declared — this script would be REFUSED at runtime (or run UNGOVERNED if the host requires no manifest).`);
191
+ ok = false;
192
+ } else if (!plan.hasManifest && plan.referenced.length) {
193
+ L.push('VERDICT: no `needs` manifest, but capabilities are referenced — they would run UNGOVERNED unless the host requires a manifest. Declare a `needs` to bound them.');
194
+ ok = false;
195
+ } else {
196
+ L.push('VERDICT: no undeclared capability reach detected (best-effort static preview). The runtime fence is the actual boundary.');
197
+ }
198
+ return { text: L.join('\n') + FOOTER, ok };
199
+ }
200
+
201
+ // Honest about what this is: a BEST-EFFORT static preview, NOT a sound security gate. The RUNTIME fence is what
202
+ // actually stops a capability call — `myxo plan` is a lint to SEE a script's likely reach and catch common mistakes.
203
+ // It cannot soundly resolve capability vs. user-agent names statically (that needs whole-program flow analysis),
204
+ // so the disclosed blind spots below can hide a real reach. Always back it with the fence; never approve on it alone.
205
+ const FOOTER = '\n\n(Best-effort static preview, NOT the boundary — the RUNTIME FENCE enforces. It can be fooled by: a user agent that SHARES A NAME with a host capability (it masks the capability here, but the fence still resolves by execution order); a capability passed as a VALUE then invoked elsewhere (aliased to a variable, or handed to a higher-order function like sort/map/route/schedule); a name pulled in via weave; or a host capability registered under a core-builtin name. Use it to see intent and catch mistakes — never approve on it alone.)';
206
+
207
+ module.exports = { planScript, formatPlan, walk };
package/myxo-run.js ADDED
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+ // myxo-run.js — the production entry for running an agent-written Myxo script behind the
3
+ // fence. This is what an MCP tool like `james_nx_run` calls: hand it a script and a
4
+ // live tool client, get back structured output + the audit ledger. It never throws —
5
+ // a tool handler wants a result object, not an exception.
6
+ //
7
+ // THREE gates, defense in depth:
8
+ // 1. host allowlist — the host decides which tools are even BRIDGED (absence > refusal)
9
+ // 2. the script's `needs` manifest — the script declares what it will touch
10
+ // 3. value budgets (`max`/`total`) — the script's own ceilings, runtime-enforced
11
+ // Everything privileged lands in the audit ledger, returned even when the run fails.
12
+
13
+ const { run } = require('./myxo');
14
+
15
+ // Wrap a live client so only allow-listed tools exist, and a call to anything outside
16
+ // the list is hard-stopped at the host boundary (belt to the script-manifest braces).
17
+ function gateClient(client, allow) {
18
+ if (!client) return undefined;
19
+ if (!Array.isArray(allow)) return client; // no allowlist -> bridge everything given
20
+ const set = new Set(allow);
21
+ return {
22
+ tools: (client.tools || []).filter(t => set.has(t.name)),
23
+ call(name, args) {
24
+ if (!set.has(name)) throw new Error(`tool '${name}' is not in the host allowlist`);
25
+ return client.call(name, args);
26
+ },
27
+ };
28
+ }
29
+
30
+ // Run `script` with `client` bridged as fenced capabilities.
31
+ // opts: { client, allow, dir, maxDepth, maxSteps, natives, requireManifest, moduleLoader }
32
+ // returns: { ok, output, audit, error }
33
+ function runScript(script, opts = {}) {
34
+ let audit = [];
35
+ const result = { ok: true, output: '', audit, error: null };
36
+ const requireManifest = opts.requireManifest !== undefined ? !!opts.requireManifest : true;
37
+ const moduleLoader = opts.moduleLoader !== undefined ? opts.moduleLoader : null;
38
+ // Accumulate output OUTSIDE run() so it survives a failing script. With capture:true the chunks
39
+ // died inside the throw and a failed run returned output:'' — everything the script emitted before
40
+ // failing (a monitor's verdict, an agent's partial report) was silently dropped. Evidence survives.
41
+ let buf = '';
42
+ try {
43
+ run(script, {
44
+ output: (s) => { buf += s; },
45
+ dir: opts.dir,
46
+ maxDepth: opts.maxDepth,
47
+ maxSteps: opts.maxSteps,
48
+ requireManifest,
49
+ moduleLoader,
50
+ natives: opts.natives,
51
+ valueCaps: opts.valueCaps, // caps the host meters by value (e.g. spend); everything else = call-count
52
+ mcp: gateClient(opts.client, opts.allow),
53
+ onAudit: (l) => { audit = result.audit = l; },
54
+ });
55
+ } catch (e) {
56
+ result.ok = false;
57
+ result.error = typeof e.format === 'function' ? e.format() : e.message;
58
+ }
59
+ result.output = buf;
60
+ return result;
61
+ }
62
+
63
+ module.exports = { runScript, gateClient };