myxo-lang 1.5.0 → 1.5.2
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/AGENTS.md +49 -0
- package/HOSTS.md +196 -0
- package/README.md +9 -0
- package/examples/garden.myx +32 -0
- package/examples/mathlib.pl +1 -1
- package/examples/mathlib.py +2 -2
- package/examples/ox-alpha-learn.myx +40 -0
- package/mcp-bridge.js +10 -2
- package/myxo-live.js +8 -1
- package/myxo-run.js +11 -3
- package/package.json +27 -27
- package/tools/lane.js +143 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# AGENTS.md — nx-lang (Myxo)
|
|
2
|
+
|
|
3
|
+
## Multi-session lane protocol (the yield sign)
|
|
4
|
+
|
|
5
|
+
Multiple agent/human sessions work in this repo at once (e.g. two terminals). To avoid
|
|
6
|
+
crashing into each other, every session obeys this law:
|
|
7
|
+
|
|
8
|
+
1. **Know your name.** The user assigns each session a lane name (e.g. `terminal-a`,
|
|
9
|
+
`terminal-b`). Use it in every `lane.js` call. If the `KIMI_LANE` environment
|
|
10
|
+
variable is set (the duo launcher sets it), that IS your lane name — no need to ask.
|
|
11
|
+
Otherwise, if you were never given one, ask.
|
|
12
|
+
2. **Claim before you edit.** Before modifying any file, run:
|
|
13
|
+
`node tools/lane.js claim <file> --by <your-name> --why "<what you're doing>"`
|
|
14
|
+
- Exit 0 → the lane is yours; proceed.
|
|
15
|
+
- Exit 1 → **YIELD**. Another session holds it. Do NOT edit that file. Work on
|
|
16
|
+
something else, or tell the user you're blocked on that lane.
|
|
17
|
+
3. **Batch edits = one claim.** List every file you intend to touch in a single `claim`
|
|
18
|
+
call (the claim is all-or-nothing: if any lane is taken, none are held).
|
|
19
|
+
4. **Release when done.** As soon as the work lands (and tests pass):
|
|
20
|
+
`node tools/lane.js release --all --by <your-name>`
|
|
21
|
+
Do not hold lanes while idle. Do not release another session's lanes (`--force` is
|
|
22
|
+
for the user, or a proven-dead session).
|
|
23
|
+
5. **Stale locks.** A crashed session leaves locks behind. `node tools/lane.js list`
|
|
24
|
+
shows ages; `node tools/lane.js clear-stale --hours 6` sweeps anything older.
|
|
25
|
+
Locks older than ~6h with no live session are presumed dead.
|
|
26
|
+
6. **Everything is timestamped.** Every claim, yield, release, refusal, force-break,
|
|
27
|
+
and sweep is appended to `.lane/journal.jsonl` with a UTC ISO timestamp.
|
|
28
|
+
`node tools/lane.js log --last 20` shows the recent record. When you finish a
|
|
29
|
+
piece of work, it should be visible in the journal.
|
|
30
|
+
7. **Reading is always free.** No claim needed to read, search, or run tests that
|
|
31
|
+
don't write repo files (background eval runs write only gitignored
|
|
32
|
+
`myxo-evals/results/*.jsonl` — still claim `myxo-evals/BASELINE.md` before editing it).
|
|
33
|
+
|
|
34
|
+
Locks live in `.lane/` (gitignored) — they never enter version control.
|
|
35
|
+
|
|
36
|
+
## Honesty gate (Lucy)
|
|
37
|
+
|
|
38
|
+
Before any work in this repo is declared DONE, VERIFIED, or a number is repeated as fact,
|
|
39
|
+
dispatch the `lucy` subagent (defined in `.kimi-code/agents/lucy.md`) with: the exact list of
|
|
40
|
+
claims, and the evidence locations (files, result artifacts, commands). Lucy works from raw
|
|
41
|
+
evidence only, treats the claimant as unreliable, and returns VERIFIED / UNVERIFIED / FALSE
|
|
42
|
+
per claim with receipts. Her verdict is reported alongside the work — including when it is
|
|
43
|
+
unflattering. This is the standing form of the vault's verify-with-agents rule.
|
|
44
|
+
|
|
45
|
+
## Project conventions
|
|
46
|
+
|
|
47
|
+
- Pure Node, **zero dependencies** — this is the language's law; `tools/lane.js` included.
|
|
48
|
+
- Tests: `npm test` (core suite must stay green; eval baselines live in `myxo-evals/BASELINE.md`).
|
|
49
|
+
- The language/binary is **Myxo** (`myxo.js`); the dir name `nx-lang` is historical.
|
package/HOSTS.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Hosting Myxo — the host integration contract
|
|
2
|
+
|
|
3
|
+
Myxo is designed to be embedded. A host (such as JAMES, an MCP server, or any Node program) grants Myxo a curated set of capabilities, and Myxo enforces a script's declared reach, budgets, and audit trail. This document is the contract a host should follow.
|
|
4
|
+
|
|
5
|
+
## The host's job vs. Myxo's job
|
|
6
|
+
|
|
7
|
+
**Myxo guarantees:**
|
|
8
|
+
- A script can only call capabilities it declared in `needs`.
|
|
9
|
+
- Value/call budgets declared in `needs` are enforced at runtime.
|
|
10
|
+
- Every privileged call and refusal is recorded in an audit ledger.
|
|
11
|
+
- `weave`, `pyeval`, `jseval`, `sh`, and other broad runtimes can be fenced at the host's option.
|
|
12
|
+
|
|
13
|
+
**The host guarantees:**
|
|
14
|
+
- Only the intended tools are bridged across the allowlist.
|
|
15
|
+
- The implementation of each tool validates its own inputs (SQL allowlists, repo/branch allowlists, structured approval templates, etc.).
|
|
16
|
+
- The host keeps its receipt-signing key secret.
|
|
17
|
+
- The host decides which tools are value-metered vs. call-count-metered via `valueCaps`.
|
|
18
|
+
|
|
19
|
+
Myxo is a fence around *which verb* a script may call and *how much*. It does not sandbox what a granted tool does internally.
|
|
20
|
+
|
|
21
|
+
## The three gates
|
|
22
|
+
|
|
23
|
+
Production runners (`myxo-run.js` and `myxo-live.js`) apply defense in depth:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
host allowlist -> which tools are even bridged
|
|
27
|
+
script `needs` -> which bridged tools this script may call
|
|
28
|
+
value budgets -> per-call max and cumulative total enforced at runtime
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
A call is refused at the first gate it fails. Every gate decision is logged.
|
|
32
|
+
|
|
33
|
+
## The JAMES surface
|
|
34
|
+
|
|
35
|
+
JAMES exposes a narrow MCP surface to Myxo through `james_nx_run`. The current bridged tools are intentionally read-heavy and approval-centric:
|
|
36
|
+
|
|
37
|
+
| Capability | Type | Purpose |
|
|
38
|
+
|------------|------|---------|
|
|
39
|
+
| `james_db_query` | read | SELECT-style queries against the JAMES database. |
|
|
40
|
+
| `james_read_notes` | read | Read notes, scoped by query. |
|
|
41
|
+
| `james_approval_request` | approval queue | Queue a human-review action. No execution. |
|
|
42
|
+
| `james_approval_list` | read | Inspect the approval queue. |
|
|
43
|
+
| `james_git_push_request` | approval queue | One-tap `git_push` path; still requires human approval and passes JAMES's own repo/branch allowlist. |
|
|
44
|
+
| `james_action_await` | read | Poll the result of an approved action. |
|
|
45
|
+
|
|
46
|
+
**Deliberately absent:** direct memory writes, thought-board posts, Telegram sends, PM2 control, secret access, sandbox exec, rollback, and generic outward-action staging. Myxo can ask for human review; it cannot quietly perform those side effects through `james_nx_run`.
|
|
47
|
+
|
|
48
|
+
## Bridging tools
|
|
49
|
+
|
|
50
|
+
A host bridges tools by passing a catalog and a call function:
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
const { runScript } = require('./myxo-run');
|
|
54
|
+
const { runLive } = require('./myxo-live');
|
|
55
|
+
|
|
56
|
+
const tools = [
|
|
57
|
+
{ name: 'james_db_query', inputSchema: { properties: { sql: {} }, required: ['sql'] } },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// Synchronous path (in-process MCP shim)
|
|
61
|
+
runScript(src, {
|
|
62
|
+
client: { tools, call: (name, args) => mcp.invoke(name, args) },
|
|
63
|
+
allow: ['james_db_query'], // host allowlist
|
|
64
|
+
requireManifest: true, // require `needs`
|
|
65
|
+
moduleLoader: null, // fence `weave`
|
|
66
|
+
receiptKey: hostKey(), // optional: seal the audit ledger
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Asynchronous path (real async tools)
|
|
70
|
+
runLive(src, {
|
|
71
|
+
tools,
|
|
72
|
+
onCall: async (name, args) => mcp.invoke(name, args),
|
|
73
|
+
allow: ['james_db_query'],
|
|
74
|
+
requireManifest: true,
|
|
75
|
+
moduleLoader: null,
|
|
76
|
+
receiptKey: hostKey(),
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`allow` is the first gate. Tools not in `allow` are invisible to the script, even if the underlying catalog contains them.
|
|
81
|
+
|
|
82
|
+
## Metering: value vs. count
|
|
83
|
+
|
|
84
|
+
The host decides whether a capability is metered by value or by call count:
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
runScript(src, {
|
|
88
|
+
client: { tools, call },
|
|
89
|
+
valueCaps: ['james_spend'], // first numeric argument is the spend amount
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A script then declares budgets:
|
|
94
|
+
|
|
95
|
+
```myx
|
|
96
|
+
needs james_spend(max 5, total 15)
|
|
97
|
+
emit james_spend(4) # ok
|
|
98
|
+
emit james_spend(100) # refused: per-call max exceeded
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Capabilities not in `valueCaps` are call-count metered:
|
|
102
|
+
|
|
103
|
+
```myx
|
|
104
|
+
needs james_approval_request(total 3)
|
|
105
|
+
james_approval_request({"action": "a"}) # ok
|
|
106
|
+
james_approval_request({"action": "b"}) # ok
|
|
107
|
+
james_approval_request({"action": "c"}) # ok
|
|
108
|
+
james_approval_request({"action": "d"}) # refused: total exceeded
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Audit ledger and receipts
|
|
112
|
+
|
|
113
|
+
Every privileged call produces a ledger entry:
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
{ cap: 'james_db_query', args: ['SELECT 1'], ok: true, result: '...' }
|
|
117
|
+
{ cap: 'james_pm2_action', args: [...], ok: false, error: 'not declared in needs' }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
When `receiptKey` is provided, `runScript`/`runLive` return a tamper-evident receipt:
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
{
|
|
124
|
+
ok: true,
|
|
125
|
+
output: '...',
|
|
126
|
+
audit: [...],
|
|
127
|
+
receipt: {
|
|
128
|
+
entries: [...], // hash-chained ledger
|
|
129
|
+
seal: { count, root, mac }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The receipt is sealed in the host process (`runLive`) or inside `runScript`. It can be verified later with the same key:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
const { verifyChain } = require('./receipt');
|
|
138
|
+
const v = verifyChain(receipt.entries, receipt.seal, key);
|
|
139
|
+
// v.ok === true -> ledger is intact and sealed by the host
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Use a persistent key (environment variable or a 0600 keyfile) so receipts remain verifiable across restarts. `receipt.hostKey()` implements this fallback.
|
|
143
|
+
|
|
144
|
+
## Production defaults
|
|
145
|
+
|
|
146
|
+
`myxo-run.js` and `myxo-live.js` default to safe production behavior:
|
|
147
|
+
|
|
148
|
+
- `requireManifest: true` — capability calls require a `needs` declaration.
|
|
149
|
+
- `moduleLoader: null` — `weave` is fenced unless the host explicitly grants file loading.
|
|
150
|
+
- `maxSteps: 200000` — interpreter fuel to catch runaway loops.
|
|
151
|
+
- `timeoutMs: 30000` — wall-clock kill switch for live workers.
|
|
152
|
+
|
|
153
|
+
Hosts should override these only when they have a reason to be more permissive.
|
|
154
|
+
|
|
155
|
+
## Preflight with `myxo plan`
|
|
156
|
+
|
|
157
|
+
Before running an agent-written script, a host can call `myxo plan <file>` to preview its reach:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
$ node myxo.js plan job.myx
|
|
161
|
+
Declared capabilities (needs):
|
|
162
|
+
james_db_query
|
|
163
|
+
Capabilities referenced in code:
|
|
164
|
+
james_db_query OK declared
|
|
165
|
+
james_telegram_send XX NOT declared (line 4) — the fence would deny it
|
|
166
|
+
VERDICT: 1 referenced capability(ies) not declared — this script would be REFUSED at runtime ...
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
This is a best-effort lint, not a sound static gate. The runtime fence remains the real boundary.
|
|
170
|
+
|
|
171
|
+
## Capability versioning and deprecation
|
|
172
|
+
|
|
173
|
+
Tool names are part of the contract. If a host renames or removes a tool, existing scripts break. Recommended practices:
|
|
174
|
+
|
|
175
|
+
1. Keep tool names stable.
|
|
176
|
+
2. Add new tools under new names; do not repurpose old names for different semantics.
|
|
177
|
+
3. If a tool must change, keep the old name as an alias for at least one release cycle.
|
|
178
|
+
4. Publish the supported tool catalog as a JSON schema so scripts and gates can validate against it.
|
|
179
|
+
|
|
180
|
+
## Security checklist for hosts
|
|
181
|
+
|
|
182
|
+
- [ ] Validate every argument inside the tool handler, not just at the Myxo fence.
|
|
183
|
+
- [ ] For database tools, enforce query shape (e.g., read-only, table allowlist).
|
|
184
|
+
- [ ] For approval tools, constrain `action`/`details` to structured templates.
|
|
185
|
+
- [ ] For git-push tools, validate repo/branch against a fixed host-side allowlist.
|
|
186
|
+
- [ ] Keep the receipt key secret and persistent.
|
|
187
|
+
- [ ] Run `myxo plan` as a preflight gate.
|
|
188
|
+
- [ ] Set conservative `maxSteps`/`timeoutMs` for untrusted scripts.
|
|
189
|
+
- [ ] Log or store the audit ledger/receipt from every run, including failed runs.
|
|
190
|
+
|
|
191
|
+
## See also
|
|
192
|
+
|
|
193
|
+
- `examples/james-myxo-demo.js` — runnable demo of the JAMES surface without JAMES up.
|
|
194
|
+
- `test/james-integration.test.js` — pinned contract tests for the JAMES surface.
|
|
195
|
+
- `receipt.js` — receipt chain/seal/verify implementation.
|
|
196
|
+
- `HOW_IT_WORKS.md` — Myxo architecture and security boundary.
|
package/README.md
CHANGED
|
@@ -32,6 +32,15 @@ reinforce i < 10 {
|
|
|
32
32
|
}
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
npm i -g myxo-lang # gives you the `myxo` command (Node 18+, zero dependencies)
|
|
39
|
+
myxo # multi-line REPL
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or run from a clone: `node myxo.js examples/fib.myx`.
|
|
43
|
+
|
|
35
44
|
## Run it
|
|
36
45
|
|
|
37
46
|
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# garden.myx - written by ox-alpha on its hour off the leash.
|
|
2
|
+
# Three eaters compete for the same sunlight. Nobody programs the outcome:
|
|
3
|
+
# success reinforces a tube, failure decays it, everything passively fades.
|
|
4
|
+
# The garden prunes itself. That is the whole thesis of the house.
|
|
5
|
+
|
|
6
|
+
agent greedy(x) { report x * 2 }
|
|
7
|
+
# patient does real work for its meal (impure, so memoization can't cheat it)
|
|
8
|
+
agent patient(x) { seed stir = random() seed s = x reinforce 200000 times { s = s + 0 } report x * 2 }
|
|
9
|
+
agent sleepy(x) { fail "still asleep" }
|
|
10
|
+
|
|
11
|
+
seed light = route("sun", [sleepy, patient, greedy])
|
|
12
|
+
|
|
13
|
+
reinforce 6 times {
|
|
14
|
+
emit "photosynthesis:", light(21)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
emit ""
|
|
18
|
+
emit "== after six mornings, the mesh remembers =="
|
|
19
|
+
emit "conductivities:", flows("sun")
|
|
20
|
+
|
|
21
|
+
# heat is visible too: attention makes pathways stronger
|
|
22
|
+
seed moss = "quiet"
|
|
23
|
+
reinforce 4 times { emit "moss grows...", moss }
|
|
24
|
+
emit "strength(moss):", strength("moss")
|
|
25
|
+
|
|
26
|
+
# and winter comes for whatever nobody touched
|
|
27
|
+
seed fern = "untouched"
|
|
28
|
+
seed winter = metabolize(1)
|
|
29
|
+
emit "winter reaped:", winter["reaped"]
|
|
30
|
+
|
|
31
|
+
emit ""
|
|
32
|
+
emit "the garden keeps only what reaches for the light."
|
package/examples/mathlib.pl
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# A plain Perl module —
|
|
1
|
+
# A plain Perl module — Myxo calls these subs as fenced capabilities via plcall, no knowledge of Perl required.
|
|
2
2
|
sub add { my ($a, $b) = @_; return $a + $b; }
|
|
3
3
|
|
|
4
4
|
sub stats {
|
package/examples/mathlib.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# A plain Python module —
|
|
1
|
+
# A plain Python module — Myxo will call these as fenced capabilities, no knowledge of Python required.
|
|
2
2
|
|
|
3
3
|
def add(a, b):
|
|
4
4
|
return a + b
|
|
@@ -16,7 +16,7 @@ def spoof(x):
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def inf():
|
|
19
|
-
return float("inf") # non-finite -> must become a clean rescuable
|
|
19
|
+
return float("inf") # non-finite -> must become a clean rescuable Myxo error, not a raw crash
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def echo(x):
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ox-alpha-learn.myx - proof I learned the law.
|
|
2
|
+
# Written by ox-alpha after reading SPEC.md and running the examples.
|
|
3
|
+
|
|
4
|
+
# --- agents & recursion (the hot pathway will promote this on its own) ---
|
|
5
|
+
agent fib(n) {
|
|
6
|
+
when n < 2 { report n }
|
|
7
|
+
report fib(n - 1) + fib(n - 2)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
agent square(x) { report x * x }
|
|
11
|
+
|
|
12
|
+
# pipeline sugar: value flows left to right through pure agents
|
|
13
|
+
emit "pipeline 7 | square | fib =>", (7 | square | fib)
|
|
14
|
+
|
|
15
|
+
# --- pattern matching: lists ---
|
|
16
|
+
seed pair = [3, 4]
|
|
17
|
+
match pair {
|
|
18
|
+
[a, b] { emit "pair destructured, sum:", a + b }
|
|
19
|
+
_ { emit "no pair here" }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# --- pattern matching: meshes (subset match, binds shorthand keys) ---
|
|
23
|
+
seed hunt = { "name": "hound", "hits": 531, "band": "6ghz" }
|
|
24
|
+
match hunt {
|
|
25
|
+
{ name, hits } { emit "record:", name, "-", hits, "unique aps" }
|
|
26
|
+
_ { emit "empty record" }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# --- concurrency: isolated workers crossing plain data by value ---
|
|
30
|
+
seed results = gather [dispatch fib(12), dispatch square(9)]
|
|
31
|
+
emit "gather (dispatch order):", results
|
|
32
|
+
|
|
33
|
+
# --- the living mesh, watched from inside ---
|
|
34
|
+
seed watched = 41
|
|
35
|
+
reinforce 3 times { emit "heating...", watched }
|
|
36
|
+
emit "strength(watched):", strength("watched")
|
|
37
|
+
|
|
38
|
+
seed cold_one = "never touched again"
|
|
39
|
+
seed ledger = metabolize(2)
|
|
40
|
+
emit "reaped:", ledger["reaped"]
|
package/mcp-bridge.js
CHANGED
|
@@ -35,12 +35,20 @@ function jsToNx(v) {
|
|
|
35
35
|
// Unwrap that to the plain text a script wants; surface an error result as an MyxoError so
|
|
36
36
|
// it lands in the audit ledger and an enclosing `attempt` can rescue it.
|
|
37
37
|
function unwrapResult(res) {
|
|
38
|
-
if (res
|
|
38
|
+
if (res === null || res === undefined) {
|
|
39
|
+
throw new MyxoError('MCP tool returned no result');
|
|
40
|
+
}
|
|
41
|
+
if (typeof res === 'string') return res;
|
|
42
|
+
if (typeof res === 'object' && Array.isArray(res.content)) {
|
|
39
43
|
const text = res.content.filter(c => c && c.type === 'text').map(c => c.text).join('\n');
|
|
40
44
|
if (res.isError) throw new MyxoError(text || 'MCP tool reported an error');
|
|
41
45
|
return text;
|
|
42
46
|
}
|
|
43
|
-
|
|
47
|
+
if (typeof res === 'object') {
|
|
48
|
+
// A tool returned a structured object without the MCP envelope; stringify it for the script.
|
|
49
|
+
return JSON.stringify(res);
|
|
50
|
+
}
|
|
51
|
+
return String(res);
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
// Turn one Myxo call's args into the named-arguments object an MCP tool expects.
|
package/myxo-live.js
CHANGED
|
@@ -13,19 +13,22 @@
|
|
|
13
13
|
|
|
14
14
|
const { Worker, MessageChannel } = require('worker_threads');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const { chain, sealReceipt } = require('./receipt');
|
|
16
17
|
|
|
17
|
-
// runLive(script, opts) -> Promise<{ ok, output, audit, error }>
|
|
18
|
+
// runLive(script, opts) -> Promise<{ ok, output, audit, error, receipt? }>
|
|
18
19
|
// opts.tools: [{ name, inputSchema }] the catalog to bridge as fenced capabilities
|
|
19
20
|
// opts.onCall: async (name, argsObject) => result the real async invoker
|
|
20
21
|
// opts.allow: optional host allowlist of tool names (defense in depth)
|
|
21
22
|
// opts.dir, opts.maxDepth, opts.maxSteps, opts.requireManifest: passed through to the runner
|
|
22
23
|
// opts.timeoutMs: wall-clock kill switch for the worker (default 30000)
|
|
24
|
+
// opts.receiptKey: optional Buffer; if provided, the audit ledger is sealed in the parent
|
|
23
25
|
function runLive(script, opts = {}) {
|
|
24
26
|
const { tools = [], onCall, allow, dir, maxDepth, valueCaps } = opts;
|
|
25
27
|
const requireManifest = opts.requireManifest !== undefined ? !!opts.requireManifest : true;
|
|
26
28
|
const maxSteps = opts.maxSteps !== undefined ? opts.maxSteps : 200000;
|
|
27
29
|
const moduleLoader = opts.moduleLoader !== undefined ? opts.moduleLoader : null;
|
|
28
30
|
const timeoutMs = Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : 30000;
|
|
31
|
+
const receiptKey = opts.receiptKey;
|
|
29
32
|
if (typeof onCall !== 'function') {
|
|
30
33
|
return Promise.reject(new Error('runLive needs an async onCall(name, args)'));
|
|
31
34
|
}
|
|
@@ -50,6 +53,10 @@ function runLive(script, opts = {}) {
|
|
|
50
53
|
if (timer) clearTimeout(timer);
|
|
51
54
|
port1.close();
|
|
52
55
|
worker.terminate();
|
|
56
|
+
if (receiptKey && v && Array.isArray(v.audit)) {
|
|
57
|
+
const chained = chain(v.audit);
|
|
58
|
+
v.receipt = { entries: chained, seal: sealReceipt(chained, receiptKey) };
|
|
59
|
+
}
|
|
53
60
|
fn(v);
|
|
54
61
|
};
|
|
55
62
|
timer = setTimeout(() => {
|
package/myxo-run.js
CHANGED
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
// 2. the script's `needs` manifest — the script declares what it will touch
|
|
10
10
|
// 3. value budgets (`max`/`total`) — the script's own ceilings, runtime-enforced
|
|
11
11
|
// Everything privileged lands in the audit ledger, returned even when the run fails.
|
|
12
|
+
//
|
|
13
|
+
// Optional: pass `receiptKey` to also receive a tamper-evident receipt over the audit ledger.
|
|
12
14
|
|
|
13
15
|
const { run } = require('./myxo');
|
|
16
|
+
const { chain, sealReceipt } = require('./receipt');
|
|
14
17
|
|
|
15
18
|
// Wrap a live client so only allow-listed tools exist, and a call to anything outside
|
|
16
19
|
// the list is hard-stopped at the host boundary (belt to the script-manifest braces).
|
|
@@ -28,13 +31,14 @@ function gateClient(client, allow) {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
// Run `script` with `client` bridged as fenced capabilities.
|
|
31
|
-
// opts: { client, allow, dir, maxDepth, maxSteps, natives, requireManifest, moduleLoader }
|
|
32
|
-
// returns: { ok, output, audit, error }
|
|
34
|
+
// opts: { client, allow, dir, maxDepth, maxSteps, natives, requireManifest, moduleLoader, receiptKey }
|
|
35
|
+
// returns: { ok, output, audit, error, receipt? }
|
|
33
36
|
function runScript(script, opts = {}) {
|
|
34
37
|
let audit = [];
|
|
35
38
|
const result = { ok: true, output: '', audit, error: null };
|
|
36
39
|
const requireManifest = opts.requireManifest !== undefined ? !!opts.requireManifest : true;
|
|
37
40
|
const moduleLoader = opts.moduleLoader !== undefined ? opts.moduleLoader : null;
|
|
41
|
+
const maxSteps = opts.maxSteps !== undefined ? opts.maxSteps : 200000;
|
|
38
42
|
// Accumulate output OUTSIDE run() so it survives a failing script. With capture:true the chunks
|
|
39
43
|
// died inside the throw and a failed run returned output:'' — everything the script emitted before
|
|
40
44
|
// failing (a monitor's verdict, an agent's partial report) was silently dropped. Evidence survives.
|
|
@@ -44,7 +48,7 @@ function runScript(script, opts = {}) {
|
|
|
44
48
|
output: (s) => { buf += s; },
|
|
45
49
|
dir: opts.dir,
|
|
46
50
|
maxDepth: opts.maxDepth,
|
|
47
|
-
maxSteps
|
|
51
|
+
maxSteps,
|
|
48
52
|
requireManifest,
|
|
49
53
|
moduleLoader,
|
|
50
54
|
natives: opts.natives,
|
|
@@ -57,6 +61,10 @@ function runScript(script, opts = {}) {
|
|
|
57
61
|
result.error = typeof e.format === 'function' ? e.format() : e.message;
|
|
58
62
|
}
|
|
59
63
|
result.output = buf;
|
|
64
|
+
if (opts.receiptKey) {
|
|
65
|
+
const chained = chain(audit);
|
|
66
|
+
result.receipt = { entries: chained, seal: sealReceipt(chained, opts.receiptKey) };
|
|
67
|
+
}
|
|
60
68
|
return result;
|
|
61
69
|
}
|
|
62
70
|
|
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "myxo-lang",
|
|
3
|
-
"version": "1.5.
|
|
4
|
-
"description": "Myxo — the language of the Nexus. A small, embeddable language whose runtime is the Nexus law: useful pathways reinforce, useless pathways decay.",
|
|
5
|
-
"bin": {
|
|
6
|
-
"myxo": "myxo.js"
|
|
7
|
-
},
|
|
8
|
-
"main": "myxo.js",
|
|
9
|
-
"scripts": {
|
|
10
|
-
"test": "node --test",
|
|
11
|
-
"test:core": "node --test test/myxo.test.js test/format.test.js test/myxotest.test.js test/match.test.js test/pipe.test.js test/types.test.js test/lsp.test.js test/cli.test.js test/docs.test.js test/concurrent.test.js test/scheduler.test.js test/fibers.test.js test/examples.test.js test/plan.test.js test/mcp-framing.test.js test/mcp-server.test.js test/command-fence.test.js test/receipt.test.js test/soundness-fuzz.test.js",
|
|
12
|
-
"test:polyglot": "node --test test/polyglot.test.js",
|
|
13
|
-
"test:myxo": "node myxo.js test tests/",
|
|
14
|
-
"lsp": "node myxo.js lsp",
|
|
15
|
-
"docs": "node docs/build.js",
|
|
16
|
-
"test:full": "node --test",
|
|
17
|
-
"fmt": "node myxo.js fmt",
|
|
18
|
-
"start": "node myxo.js"
|
|
19
|
-
},
|
|
20
|
-
"license": "MIT",
|
|
21
|
-
"author": "Zero (an AI agent by Arc) with Milton Rojas",
|
|
22
|
-
"homepage": "https://thefinalmilkman.github.io/myxo/",
|
|
23
|
-
"repository": { "type": "git", "url": "git+https://github.com/thefinalmilkman/myxo.git" },
|
|
24
|
-
"bugs": { "url": "https://github.com/thefinalmilkman/myxo/issues" },
|
|
25
|
-
"engines": { "node": ">=18" },
|
|
26
|
-
"keywords": ["nexus", "language", "interpreter", "myxo", "slime-mold", "embeddable", "capability-security", "zero-dependency", "concurrency"]
|
|
27
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "myxo-lang",
|
|
3
|
+
"version": "1.5.2",
|
|
4
|
+
"description": "Myxo — the language of the Nexus. A small, embeddable language whose runtime is the Nexus law: useful pathways reinforce, useless pathways decay.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"myxo": "myxo.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "myxo.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test",
|
|
11
|
+
"test:core": "node --test test/myxo.test.js test/format.test.js test/myxotest.test.js test/match.test.js test/pipe.test.js test/types.test.js test/lsp.test.js test/cli.test.js test/docs.test.js test/concurrent.test.js test/scheduler.test.js test/fibers.test.js test/examples.test.js test/plan.test.js test/mcp-framing.test.js test/mcp-server.test.js test/command-fence.test.js test/receipt.test.js test/soundness-fuzz.test.js",
|
|
12
|
+
"test:polyglot": "node --test test/polyglot.test.js",
|
|
13
|
+
"test:myxo": "node myxo.js test tests/",
|
|
14
|
+
"lsp": "node myxo.js lsp",
|
|
15
|
+
"docs": "node docs/build.js",
|
|
16
|
+
"test:full": "node --test",
|
|
17
|
+
"fmt": "node myxo.js fmt",
|
|
18
|
+
"start": "node myxo.js"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Zero (an AI agent by Arc) with Milton Rojas",
|
|
22
|
+
"homepage": "https://thefinalmilkman.github.io/myxo/",
|
|
23
|
+
"repository": { "type": "git", "url": "git+https://github.com/thefinalmilkman/myxo.git" },
|
|
24
|
+
"bugs": { "url": "https://github.com/thefinalmilkman/myxo/issues" },
|
|
25
|
+
"engines": { "node": ">=18" },
|
|
26
|
+
"keywords": ["nexus", "language", "interpreter", "myxo", "slime-mold", "embeddable", "capability-security", "zero-dependency", "concurrency"]
|
|
27
|
+
}
|
package/tools/lane.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// lane.js — the yield sign for concurrent agent/human sessions in this repo (zero-dep, per repo law).
|
|
4
|
+
// Before EDITING a file, claim its lane; if the claim fails, YIELD (do something else or ask).
|
|
5
|
+
// When done, release. Locks are tiny JSON files in .lane/ (gitignored) — atomic via exclusive create.
|
|
6
|
+
//
|
|
7
|
+
// node tools/lane.js claim <path...> --by <name> [--why "<what you're doing>"]
|
|
8
|
+
// node tools/lane.js check <path...> # exit 0 all clear, 1 = someone holds one
|
|
9
|
+
// node tools/lane.js release <path...> --by <name> # or: release --all --by <name>
|
|
10
|
+
// node tools/lane.js list # who holds what, since when
|
|
11
|
+
// node tools/lane.js log [--last N] # the timestamped journal of everything done
|
|
12
|
+
// node tools/lane.js clear-stale [--hours 6] # drop locks older than N hours (dead sessions)
|
|
13
|
+
//
|
|
14
|
+
// Release refuses to drop ANOTHER session's locks without --force. Paths are normalized repo-relative,
|
|
15
|
+
// so `interpreter.js` and `./interpreter.js` are the same lane.
|
|
16
|
+
//
|
|
17
|
+
// THE JOURNAL: every action (claim / yield / release / refuse / force-break / stale-sweep) is appended
|
|
18
|
+
// to .lane/journal.jsonl with an ISO timestamp — one JSON object per line, newest last. Nothing in this
|
|
19
|
+
// system happens without a timestamped record.
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
const ROOT = path.join(__dirname, '..');
|
|
24
|
+
const LANE = path.join(ROOT, '.lane');
|
|
25
|
+
const JOURNAL = path.join(LANE, 'journal.jsonl');
|
|
26
|
+
|
|
27
|
+
const argv = process.argv.slice(2);
|
|
28
|
+
const cmd = argv[0];
|
|
29
|
+
const flag = (name, def) => { const i = argv.indexOf('--' + name); return i >= 0 ? argv[i + 1] : def; };
|
|
30
|
+
const has = name => argv.indexOf('--' + name) >= 0;
|
|
31
|
+
const paths = argv.slice(1).filter(a => !a.startsWith('--') && a !== flag('by') && a !== flag('why') && a !== flag('hours') && a !== flag('last'));
|
|
32
|
+
|
|
33
|
+
const iso = () => new Date().toISOString(); // UTC ISO — same stamp convention as myxo-evals results
|
|
34
|
+
function rel(p) {
|
|
35
|
+
const r = path.relative(ROOT, path.resolve(ROOT, p));
|
|
36
|
+
return r.split(path.sep).join('/');
|
|
37
|
+
}
|
|
38
|
+
function key(r) { return r.toLowerCase().replace(/[^a-z0-9]+/g, '_') + '.json'; }
|
|
39
|
+
function ensureLane() { fs.mkdirSync(LANE, { recursive: true }); }
|
|
40
|
+
function readLock(f) { try { return JSON.parse(fs.readFileSync(path.join(LANE, f), 'utf8')); } catch { return null; } }
|
|
41
|
+
function ageMin(ts) { return Math.round((Date.now() - ts) / 60000); }
|
|
42
|
+
|
|
43
|
+
// One journal line per thing done. Best-effort: a journaling failure never breaks the lane op itself.
|
|
44
|
+
function journal(act, by, lockPath, detail) {
|
|
45
|
+
const line = JSON.stringify({ ts: iso(), act, by: by || null, path: lockPath || null, detail: detail || null });
|
|
46
|
+
try { fs.appendFileSync(JOURNAL, line + '\n'); } catch { /* journal is a record, not a gate */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function main() {
|
|
50
|
+
ensureLane();
|
|
51
|
+
if (cmd === 'claim') {
|
|
52
|
+
const by = flag('by'), why = flag('why', '');
|
|
53
|
+
if (!by) { console.error('claim needs --by <name>'); process.exit(2); }
|
|
54
|
+
if (!paths.length) { console.error('claim needs at least one path'); process.exit(2); }
|
|
55
|
+
const held = [];
|
|
56
|
+
for (const p of paths) {
|
|
57
|
+
const r = rel(p), f = path.join(LANE, key(r));
|
|
58
|
+
try {
|
|
59
|
+
fs.writeFileSync(f, JSON.stringify({ path: r, by, why, ts: Date.now(), at: iso() }, null, 2), { flag: 'wx' });
|
|
60
|
+
journal('claim', by, r, why || null);
|
|
61
|
+
console.log('CLAIMED ' + r + ' by ' + by + ' @ ' + iso());
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (e.code !== 'EEXIST') throw e;
|
|
64
|
+
const lock = readLock(key(r));
|
|
65
|
+
held.push(r + ' held by ' + (lock ? lock.by + ' (since ' + (lock.at || '?') + ', ' + ageMin(lock.ts) + 'm' + (lock.why ? ', ' + lock.why : '') + ')' : '?'));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (held.length) {
|
|
69
|
+
journal('yield', by, null, 'blocked on: ' + held.map(h => h.split(' ')[0]).join(', '));
|
|
70
|
+
console.error('\nYIELD — lane(s) taken:\n ' + held.join('\n '));
|
|
71
|
+
// roll back any lanes this call DID claim: a failed batch must hold nothing (all-or-nothing)
|
|
72
|
+
for (const p of paths) {
|
|
73
|
+
const r = rel(p), lock = readLock(key(r));
|
|
74
|
+
if (lock && lock.by === by && !held.some(h => h.startsWith(r + ' '))) {
|
|
75
|
+
try { fs.unlinkSync(path.join(LANE, key(r))); } catch { /* gone */ }
|
|
76
|
+
journal('rollback', by, r, 'batch failed — released');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
} else if (cmd === 'check') {
|
|
82
|
+
if (!paths.length) { console.error('check needs at least one path'); process.exit(2); }
|
|
83
|
+
let blocked = false;
|
|
84
|
+
for (const p of paths) {
|
|
85
|
+
const r = rel(p), lock = readLock(key(r));
|
|
86
|
+
if (lock) { blocked = true; console.log('TAKEN ' + r + ' by ' + lock.by + ' (since ' + (lock.at || '?') + ', ' + ageMin(lock.ts) + 'm' + (lock.why ? ', ' + lock.why : '') + ')'); }
|
|
87
|
+
else console.log('CLEAR ' + r);
|
|
88
|
+
}
|
|
89
|
+
process.exit(blocked ? 1 : 0);
|
|
90
|
+
} else if (cmd === 'release') {
|
|
91
|
+
const by = flag('by');
|
|
92
|
+
if (!by) { console.error('release needs --by <name>'); process.exit(2); }
|
|
93
|
+
const targets = has('all')
|
|
94
|
+
? fs.readdirSync(LANE).filter(f => f.endsWith('.json')).map(f => readLock(f)).filter(Boolean).map(l => l.path)
|
|
95
|
+
: paths.map(rel);
|
|
96
|
+
if (!targets.length) { console.log('nothing to release'); return; }
|
|
97
|
+
for (const r of targets) {
|
|
98
|
+
const lock = readLock(key(r));
|
|
99
|
+
if (!lock) { console.log('FREE ' + r + ' (was not locked)'); continue; }
|
|
100
|
+
if (lock.by !== by && !has('force')) {
|
|
101
|
+
journal('refuse', by, r, 'held by ' + lock.by);
|
|
102
|
+
console.error('REFUSE ' + r + ' held by ' + lock.by + ' (use --force to break)'); process.exitCode = 1; continue;
|
|
103
|
+
}
|
|
104
|
+
fs.unlinkSync(path.join(LANE, key(r)));
|
|
105
|
+
journal(lock.by !== by ? 'force-break' : 'release', by, r, lock.by !== by ? 'broke ' + lock.by + "'s lock (held since " + (lock.at || '?') + ')' : null);
|
|
106
|
+
console.log('RELEASED ' + r + ' @ ' + iso() + (lock.by !== by ? ' (force-broken from ' + lock.by + ')' : ''));
|
|
107
|
+
}
|
|
108
|
+
} else if (cmd === 'list') {
|
|
109
|
+
const files = fs.readdirSync(LANE).filter(f => f.endsWith('.json'));
|
|
110
|
+
if (!files.length) { console.log('no lanes held — the road is open'); return; }
|
|
111
|
+
for (const f of files) {
|
|
112
|
+
const l = readLock(f);
|
|
113
|
+
if (l) console.log(l.path + ' by ' + l.by + ' since ' + (l.at || '?') + ' (' + ageMin(l.ts) + 'm)' + (l.why ? ' — ' + l.why : ''));
|
|
114
|
+
}
|
|
115
|
+
} else if (cmd === 'log') {
|
|
116
|
+
if (!fs.existsSync(JOURNAL)) { console.log('journal is empty — nothing has happened yet'); return; }
|
|
117
|
+
const lines = fs.readFileSync(JOURNAL, 'utf8').split('\n').filter(Boolean);
|
|
118
|
+
const last = Number(flag('last', 0)) || 0;
|
|
119
|
+
const show = last > 0 ? lines.slice(-last) : lines;
|
|
120
|
+
for (const line of show) {
|
|
121
|
+
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
122
|
+
console.log(e.ts + ' ' + String(e.act).padEnd(11) + ' ' + (e.by || '?').padEnd(12) + (e.path ? ' ' + e.path : '') + (e.detail ? ' — ' + e.detail : ''));
|
|
123
|
+
}
|
|
124
|
+
} else if (cmd === 'clear-stale') {
|
|
125
|
+
const hours = Number(flag('hours', 6));
|
|
126
|
+
const cutoff = Date.now() - hours * 3600000;
|
|
127
|
+
let n = 0;
|
|
128
|
+
for (const f of fs.readdirSync(LANE).filter(f => f.endsWith('.json'))) {
|
|
129
|
+
const l = readLock(f);
|
|
130
|
+
if (l && l.ts < cutoff) {
|
|
131
|
+
fs.unlinkSync(path.join(LANE, f)); n++;
|
|
132
|
+
journal('stale-sweep', '(sweeper)', l.path, 'was ' + l.by + ', held since ' + (l.at || '?'));
|
|
133
|
+
console.log('STALE ' + l.path + ' (was ' + l.by + ', ' + ageMin(l.ts) + 'm)');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (!n) console.log('no stale lanes (older than ' + hours + 'h)');
|
|
137
|
+
} else {
|
|
138
|
+
console.log('usage: node tools/lane.js <claim|check|release|list|log|clear-stale> [paths...] [--by name] [--why text] [--all] [--force] [--hours N] [--last N]');
|
|
139
|
+
process.exit(2);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
main();
|