babavoss 0.0.1 → 0.0.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.
Files changed (158) hide show
  1. package/NOTICE +7 -0
  2. package/bin/voss.ts +48 -0
  3. package/gui/babavoss-web.js +234 -0
  4. package/gui/chunk-5hpp1ypv.js +11710 -0
  5. package/gui/chunk-9rq662fd.js +8627 -0
  6. package/gui/chunk-k3cm8j6r.js +433 -0
  7. package/gui/chunk-pk09y98y.js +50 -0
  8. package/gui/chunk-smz02qa6.js +186 -0
  9. package/gui/chunk-wwqypxre.js +49 -0
  10. package/gui/gui.js +2015 -0
  11. package/gui/react-compiler-runtime.js +39 -0
  12. package/gui/react-dom-client.js +21 -0
  13. package/gui/react-dom.js +44 -0
  14. package/gui/react-jsx-runtime.js +19 -0
  15. package/gui/react.js +105 -0
  16. package/gui/theme.css +3053 -0
  17. package/index.ts +15 -0
  18. package/package.json +50 -4
  19. package/src/baba/check.ts +90 -0
  20. package/src/baba/config.ts +261 -0
  21. package/src/baba/find.ts +12 -0
  22. package/src/baba/init.ts +176 -0
  23. package/src/baba/node.ts +483 -0
  24. package/src/baba/project.ts +63 -0
  25. package/src/baba/registry.ts +35 -0
  26. package/src/baba/worker.ts +55 -0
  27. package/src/bench/index.ts +6 -0
  28. package/src/bench/measure.ts +132 -0
  29. package/src/bench/scenarios.ts +136 -0
  30. package/src/build/builder.ts +74 -0
  31. package/src/build/failure.ts +78 -0
  32. package/src/build/guard.ts +85 -0
  33. package/src/build/mdx-register.ts +3 -0
  34. package/src/build/mdx.ts +38 -0
  35. package/src/build/project.ts +38 -0
  36. package/src/build/views.ts +146 -0
  37. package/src/builder/main.ts +29 -0
  38. package/src/desktop/bob.ts +76 -0
  39. package/src/desktop/desktop.css +111 -0
  40. package/src/desktop/icons.ts +50 -0
  41. package/src/desktop/index.ts +323 -0
  42. package/src/desktop/routes.ts +98 -0
  43. package/src/desktop/view.tsx +673 -0
  44. package/src/door/core.ts +384 -0
  45. package/src/ecs/baba.ts +431 -0
  46. package/src/ecs/codec.ts +334 -0
  47. package/src/ecs/handles.ts +91 -0
  48. package/src/ecs/replica.ts +150 -0
  49. package/src/ecs/runtime.ts +603 -0
  50. package/src/ecs/scheduler.ts +75 -0
  51. package/src/ecs/snapshot.ts +102 -0
  52. package/src/ecs/state.ts +759 -0
  53. package/src/ecs/system.ts +256 -0
  54. package/src/ecs/table.ts +420 -0
  55. package/src/ecs/testbed.ts +97 -0
  56. package/src/exec/host.ts +177 -0
  57. package/src/exec/main.ts +98 -0
  58. package/src/exec/watch.ts +7 -0
  59. package/src/exec/wire.ts +29 -0
  60. package/src/generated/build.ts +4 -0
  61. package/src/gui/css.d.ts +1 -0
  62. package/src/gui/gui.tsx +245 -0
  63. package/src/gui/index.ts +51 -0
  64. package/src/gui/inspector.tsx +47 -0
  65. package/src/gui/levels.tsx +73 -0
  66. package/src/gui/promptware.tsx +68 -0
  67. package/src/gui/runner.tsx +118 -0
  68. package/src/gui/theme.css +498 -0
  69. package/src/gui/theme.ts +25 -0
  70. package/src/gui/wizard.tsx +227 -0
  71. package/src/guide/add-a-desktop.mdx +100 -0
  72. package/src/guide/compose-an-interface.mdx +84 -0
  73. package/src/guide/index.ts +13 -0
  74. package/src/guide/reach-outside.mdx +112 -0
  75. package/src/guide/spec-a-system.mdx +93 -0
  76. package/src/guide/systems-together.mdx +72 -0
  77. package/src/guide/write-a-system.mdx +183 -0
  78. package/src/guide/write-promptware.mdx +90 -0
  79. package/src/http/server.ts +310 -0
  80. package/src/kernel/build.ts +21 -0
  81. package/src/kernel/builder.ts +105 -0
  82. package/src/kernel/children.ts +117 -0
  83. package/src/kernel/context.ts +90 -0
  84. package/src/kernel/lock.ts +46 -0
  85. package/src/kernel/names.ts +14 -0
  86. package/src/kernel/schema.ts +130 -0
  87. package/src/kernel/where.ts +12 -0
  88. package/src/kit/index.ts +232 -0
  89. package/src/maker/system.ts +213 -0
  90. package/src/mcp/daemon.ts +61 -0
  91. package/src/mcp/main.ts +208 -0
  92. package/src/mcp/rpc.ts +64 -0
  93. package/src/mcp/tools.ts +125 -0
  94. package/src/prompt/evals.ts +42 -0
  95. package/src/prompt/index.ts +242 -0
  96. package/src/prompt/jsx-dev-runtime.ts +1 -0
  97. package/src/prompt/jsx-runtime.ts +49 -0
  98. package/src/prompt/mdx.d.ts +1 -0
  99. package/src/promptware/compile.ts +183 -0
  100. package/src/promptware/define.ts +16 -0
  101. package/src/promptware/disk.ts +72 -0
  102. package/src/promptware/markdown.d.ts +6 -0
  103. package/src/promptware/sync.ts +437 -0
  104. package/src/promptware/system.ts +215 -0
  105. package/src/runtime/bridge.ts +85 -0
  106. package/src/runtime/connect.ts +54 -0
  107. package/src/runtime/env.ts +35 -0
  108. package/src/runtime/harness.ts +80 -0
  109. package/src/runtime/main.ts +119 -0
  110. package/src/runtime/worker.ts +33 -0
  111. package/src/server/edge.ts +332 -0
  112. package/src/server/main.ts +45 -0
  113. package/src/server/messages.ts +97 -0
  114. package/src/server/protocol.ts +37 -0
  115. package/src/services/args.ts +45 -0
  116. package/src/services/exec.ts +69 -0
  117. package/src/services/fs.ts +139 -0
  118. package/src/services/http.ts +30 -0
  119. package/src/services/index.ts +113 -0
  120. package/src/services/secrets.ts +18 -0
  121. package/src/shell/address.ts +21 -0
  122. package/src/shell/args.ts +219 -0
  123. package/src/shell/client.ts +107 -0
  124. package/src/shell/codes.ts +26 -0
  125. package/src/shell/positional.ts +20 -0
  126. package/src/shell/run.ts +470 -0
  127. package/src/shell/service.ts +167 -0
  128. package/src/shell/state.ts +204 -0
  129. package/src/spec/adapters.ts +72 -0
  130. package/src/spec/diff.ts +26 -0
  131. package/src/spec/files.ts +17 -0
  132. package/src/spec/index.ts +155 -0
  133. package/src/spec/run.ts +97 -0
  134. package/src/spec/take.ts +54 -0
  135. package/src/test/index.ts +8 -0
  136. package/src/test/prove.ts +56 -0
  137. package/src/test/records.ts +23 -0
  138. package/src/test/specs.ts +56 -0
  139. package/src/test/steps.ts +100 -0
  140. package/src/test/voss-dir.ts +17 -0
  141. package/src/transport/messages.ts +110 -0
  142. package/src/transport/transport.ts +62 -0
  143. package/src/wall/probe.ts +67 -0
  144. package/src/wall/profile.ts +103 -0
  145. package/src/wall/spawn.ts +59 -0
  146. package/src/web/app.tsx +53 -0
  147. package/src/web/core.tsx +140 -0
  148. package/src/web/form.ts +155 -0
  149. package/src/web/hooks.ts +135 -0
  150. package/src/web/index.tsx +17 -0
  151. package/src/web/list.ts +19 -0
  152. package/src/web/maker.tsx +766 -0
  153. package/src/web/objects.tsx +213 -0
  154. package/src/web/socket.ts +84 -0
  155. package/src/web/state.tsx +69 -0
  156. package/src/web/store.ts +221 -0
  157. package/src/web/ui.tsx +135 -0
  158. package/README.md +0 -5
@@ -0,0 +1,177 @@
1
+ // The kernel owns records until OS cleanup finishes, including after the
2
+ // runtime stops listening. References are scoped to one system and generation.
3
+ import { createServer, type Socket } from "node:net";
4
+ import { mkdtemp, writeFile, rm } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { ServiceError, type Who, type ExecEvent } from "../kernel/context.ts";
7
+ import { roleCommand } from "../runtime/connect.ts";
8
+ import { receive, send, CHUNK, FRAME, type FromRunner, type Command } from "./wire.ts";
9
+
10
+ const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
11
+ const xml = (s: string) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
12
+ function alive(group: number) { try { process.kill(-group, 0); return true; } catch (e) { if ((e as NodeJS.ErrnoException).code === "ESRCH") return false; if ((e as NodeJS.ErrnoException).code === "EPERM") return true; throw e; } }
13
+ async function launchctl(args: string[]) {
14
+ const p = Bun.spawn(["/bin/launchctl", ...args], { stdin: "ignore", stdout: "pipe", stderr: "pipe", timeout: 10_000 });
15
+ const [out, err, code] = await Promise.all([new Response(p.stdout).text(), new Response(p.stderr).text(), p.exited]);
16
+ return { code, text: out + err };
17
+ }
18
+ interface Execution { who: Who; signal: AbortSignal; write(input: string | null, signal: AbortSignal): Promise<void> }
19
+ export class ExecutionHost {
20
+ private records = new Map<string, Execution>();
21
+ async write(who: Who, ref: string, input: string | null, signal: AbortSignal): Promise<void> {
22
+ const r = this.records.get(ref);
23
+ if (!r || r.signal.aborted) throw new ServiceError("notfound", "execution is no longer running");
24
+ if (r.who.project !== who.project || r.who.system !== who.system || r.who.generation !== who.generation) throw new ServiceError("denied", "execution belongs to another system or generation");
25
+ if (input !== null && Buffer.byteLength(input) > CHUNK) throw new ServiceError("args", `stdin write exceeds ${CHUNK} bytes`);
26
+ await r.write(input, signal);
27
+ }
28
+ async *stream(who: Who, command: Command, signal: AbortSignal): AsyncGenerator<ExecEvent> {
29
+ if (signal.aborted) throw new ServiceError("failed", "execution cancelled");
30
+ if (process.platform !== "darwin") throw new ServiceError("denied", "supervised execution currently requires macOS launchd");
31
+ if (Buffer.byteLength(JSON.stringify({ type: "start", command: { ...command, input: undefined } })) + 1 > FRAME) throw new ServiceError("args", "execution arguments and environment exceed 128 KiB");
32
+ const dir = await mkdtemp("/tmp/voss-exec-");
33
+ const ref = crypto.randomUUID();
34
+ const token = crypto.randomUUID();
35
+ const label = `org.babavoss.exec.${ref}`;
36
+ const domain = `gui/${process.getuid!()}`;
37
+ const target = `${domain}/${label}`;
38
+ let socket: Socket | undefined, runner = 0, closed = false, bootstrapped = false;
39
+ let error: Error | undefined;
40
+ let wake: (() => void) | undefined;
41
+ let exit: Extract<FromRunner, { type: "exit" }> | undefined;
42
+ const queue: ExecEvent[] = [];
43
+ let outputPending = false;
44
+ let nextWrite = 0;
45
+ let initialized = false;
46
+ const writes = new Map<number, { resolve(): void; reject(e: Error): void }>();
47
+ const fail = (e: Error) => { error ??= e; wake?.(); };
48
+ const stop = () => { try { if (socket && !socket.destroyed) send(socket, { type: "stop" }); } catch {} wake?.(); };
49
+ signal.addEventListener("abort", stop, { once: true });
50
+ const writeRaw = (input: string | null) => new Promise<void>((resolve, reject) => {
51
+ const id = ++nextWrite;
52
+ writes.set(id, { resolve, reject });
53
+ try { send(socket!, { type: "write", id, input }); }
54
+ catch (e) { writes.delete(id); reject(e); }
55
+ });
56
+ const server = createServer((s) => {
57
+ if (socket) { s.destroy(); return; }
58
+ let authenticated = false;
59
+ receive<FromRunner>(s, (m) => {
60
+ if (!authenticated) {
61
+ if (m.type !== "hello" || m.token !== token || !Number.isSafeInteger(m.pid) || m.pid < 2) { s.destroy(); return; }
62
+ authenticated = true; socket = s; runner = m.pid;
63
+ if (signal.aborted) stop(); else send(s, { type: "start", command: { ...command, input: undefined } });
64
+ return;
65
+ }
66
+ if (m.type === "started") {
67
+ initialized = command.interactive && !command.input;
68
+ queue.push({ type: "started", process: ref, pid: m.pid });
69
+ // Pump input concurrently with output. Each write is acknowledged;
70
+ // even large initial input never makes an oversized control frame.
71
+ void (async () => {
72
+ const input = command.input ?? "";
73
+ for (let i = 0; i < input.length;) {
74
+ let end = Math.min(i + 4096, input.length);
75
+ const last = input.charCodeAt(end - 1);
76
+ if (end < input.length && last >= 0xd800 && last <= 0xdbff) end--;
77
+ await writeRaw(input.slice(i, end)); i = end;
78
+ }
79
+ if (!command.interactive) await writeRaw(null);
80
+ initialized = true;
81
+ })().catch(() => {
82
+ // Programs may close stdin without consuming all input (e.g.
83
+ // `true`). Their exit status remains authoritative. A lost runner
84
+ // still fails the stream independently if no exit was received.
85
+ initialized = true;
86
+ });
87
+ }
88
+ else if (m.type === "output") {
89
+ if (outputPending || m.data.length > CHUNK * 2) { fail(new Error("execution exceeded output credit")); stop(); return; }
90
+ outputPending = true;
91
+ queue.push({ type: m.stream, data: new Uint8Array(Buffer.from(m.data, "base64")) });
92
+ } else if (m.type === "exit") exit = m;
93
+ else if (m.type === "error") fail(new Error(m.message));
94
+ else if (m.type === "written") { const w = writes.get(m.id); writes.delete(m.id); if (m.error) w?.reject(new Error(m.error)); else w?.resolve(); }
95
+ wake?.();
96
+ }, (e) => {
97
+ // Once the exit is in hand the runner may close the socket while the
98
+ // host is still streaming initial input to it (a program like `true`
99
+ // exits at once): that EPIPE is not the execution's failure.
100
+ if (exit) { closed = true; wake?.(); } else fail(e);
101
+ });
102
+ s.on("close", () => { if (!authenticated) return; closed = true; for (const w of writes.values()) w.reject(new Error("execution closed")); writes.clear(); wake?.(); });
103
+ });
104
+ let cleanupError: Error | undefined;
105
+ try {
106
+ await new Promise<void>((resolve, reject) => { server.once("error", reject); server.listen(join(dir, "s"), resolve); });
107
+ const args = [...roleCommand("--exec"), join(dir, "s"), token];
108
+ const plist = `<?xml version="1.0"?><plist version="1.0"><dict><key>Label</key><string>${label}</string><key>ProgramArguments</key><array>${args.map((a) => `<string>${xml(a)}</string>`).join("")}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><false/><key>AbandonProcessGroup</key><false/><key>ExitTimeOut</key><integer>1</integer></dict></plist>`;
109
+ await writeFile(join(dir, "job.plist"), plist, { mode: 0o600 });
110
+ if (signal.aborted) throw new Error("execution cancelled");
111
+ const started = await launchctl(["bootstrap", domain, join(dir, "job.plist")]);
112
+ if (started.code !== 0) throw new Error(`launchd could not supervise execution: ${started.text.trim()}`);
113
+ bootstrapped = true;
114
+ const startup = setTimeout(() => { fail(new Error("execution runner did not connect")); stop(); }, 10_000);
115
+ const write = async (input: string | null, caller: AbortSignal) => {
116
+ if (!command.interactive) throw new ServiceError("args", "execution stdin is not interactive");
117
+ if (caller.aborted || signal.aborted || !socket || closed || exit) throw new ServiceError("failed", "stdin write cancelled or execution closed");
118
+ // One write in flight: systems serialize commands; no hidden unbounded queue.
119
+ if (!initialized || writes.size) throw new ServiceError("failed", "another stdin write is pending");
120
+ const id = ++nextWrite;
121
+ await new Promise<void>((resolve, reject) => {
122
+ const abort = () => { reject(new Error("stdin write cancelled; delivery may have occurred")); };
123
+ caller.addEventListener("abort", abort, { once: true });
124
+ writes.set(id, { resolve: () => { caller.removeEventListener("abort", abort); resolve(); }, reject: (e) => { caller.removeEventListener("abort", abort); reject(e); } });
125
+ send(socket!, { type: "write", id, input });
126
+ });
127
+ };
128
+ this.records.set(ref, { who, signal, write });
129
+ try {
130
+ for (;;) {
131
+ if (signal.aborted) throw new Error("execution cancelled");
132
+ if (error) throw error;
133
+ const event = queue.shift();
134
+ if (event) {
135
+ if (event.type === "started") clearTimeout(startup);
136
+ yield event;
137
+ if (event.type === "stdout" || event.type === "stderr") { outputPending = false; if (socket && !socket.destroyed) send(socket, { type: "ack" }); }
138
+ continue;
139
+ }
140
+ if (closed) { if (!exit) throw new Error("execution runner exited unexpectedly"); break; }
141
+ await new Promise<void>((r) => { wake = r; }); wake = undefined;
142
+ }
143
+ } finally { clearTimeout(startup); }
144
+ } finally {
145
+ this.records.delete(ref);
146
+ signal.removeEventListener("abort", stop);
147
+ stop();
148
+ // The runner/sentinel terminate their own current group. The host only
149
+ // observes group disappearance; it never signals a saved numeric PID.
150
+ if (runner) {
151
+ const until = Date.now() + 15_000;
152
+ while (alive(runner) && Date.now() < until) await delay(20);
153
+ if (alive(runner)) cleanupError = new Error(`execution group ${runner} did not stop`);
154
+ }
155
+ if (bootstrapped && !cleanupError) {
156
+ // Boot the job out and confirm it is gone (`print` answers 113 for an
157
+ // unknown service). launchd can be slow to tear a job down, and a
158
+ // `bootout` the helper's timeout cut short reports nothing, so the
159
+ // confirmation is polled for a bounded time, with the bootout retried.
160
+ let out = await launchctl(["bootout", target]);
161
+ const until = Date.now() + 15_000;
162
+ for (;;) {
163
+ const check = await launchctl(["print", target]);
164
+ if (check.code === 113) break;
165
+ if (Date.now() > until) { cleanupError = new Error(`could not confirm launchd deregistration: ${out.text.trim() || `bootout exited ${out.code}, print exited ${check.code}`}`); break; }
166
+ await delay(250);
167
+ out = await launchctl(["bootout", target]);
168
+ }
169
+ }
170
+ socket?.destroy();
171
+ await new Promise<void>((r) => server.close(() => r()));
172
+ if (!cleanupError) await rm(dir, { recursive: true, force: true });
173
+ if (cleanupError) { cleanupError.name = "ExecutionCleanupError"; throw cleanupError; }
174
+ }
175
+ if (exit) yield { type: "exit", code: exit.code, signal: exit.signal };
176
+ }
177
+ }
@@ -0,0 +1,98 @@
1
+ // Internal launchd job. Runner and sentinel monitor one another in the
2
+ // workload group; either can kill its own group without saved PID signalling.
3
+ import { connect } from "node:net";
4
+ import { roleCommand } from "../runtime/connect.ts";
5
+ import { CHUNK, receive, send, type ToRunner, type Command } from "./wire.ts";
6
+
7
+ const [path, token] = process.argv.slice(3);
8
+ if (!path || !token) process.exit(2);
9
+ const socket = connect(path);
10
+ let child: ReturnType<typeof Bun.spawn<"pipe", "pipe", "pipe">> | null = null;
11
+ let stopping = false;
12
+ let credit: (() => void) | null = null;
13
+ let outputTail = Promise.resolve();
14
+ let inputTail = Promise.resolve();
15
+ const startup = setTimeout(() => stop(), 10_000);
16
+ let deadline: ReturnType<typeof setTimeout> | undefined;
17
+ // Stay alive through graceful group termination to relay the outcome.
18
+ process.on("SIGTERM", () => {});
19
+ process.on("SIGINT", () => stop());
20
+ function terminateGroup() { try { process.kill(0, "SIGTERM"); } catch {} }
21
+ function stop() {
22
+ if (stopping) return;
23
+ stopping = true;
24
+ clearTimeout(startup); clearTimeout(deadline);
25
+ terminateGroup();
26
+ credit?.(); credit = null;
27
+ setTimeout(force, 250);
28
+ }
29
+ function force(): never {
30
+ // Includes runner, sentinel and workload, selected by current membership.
31
+ try { process.kill(0, "SIGKILL"); } catch {}
32
+ process.exit(1);
33
+ }
34
+ function fail(e: unknown) {
35
+ try { send(socket, { type: "error", message: e instanceof Error ? e.message : String(e) }); } catch {}
36
+ stop();
37
+ }
38
+ socket.on("connect", () => send(socket, { type: "hello", token, pid: process.pid }));
39
+ socket.on("close", stop);
40
+ receive<ToRunner>(socket, (m) => {
41
+ if (m.type === "ack") { credit?.(); credit = null; }
42
+ else if (m.type === "stop") stop();
43
+ else if (m.type === "start") {
44
+ if (child || stopping) return fail(new Error("execution already started or stopped"));
45
+ void run(m.command).catch(fail);
46
+ } else if (m.type === "write") {
47
+ inputTail = inputTail.then(async () => {
48
+ try {
49
+ if (!child || stopping) throw new Error("execution is closed");
50
+ if (m.input === null) await child.stdin.end();
51
+ else { child.stdin.write(m.input); await child.stdin.flush(); }
52
+ send(socket, { type: "written", id: m.id });
53
+ } catch (e) { send(socket, { type: "written", id: m.id, error: String(e) }); }
54
+ }).catch(fail);
55
+ }
56
+ }, fail);
57
+ async function output(stream: "stdout" | "stderr", bytes: ReadableStream<Uint8Array>) {
58
+ for await (const chunk of bytes) {
59
+ for (let i = 0; i < chunk.length && !stopping; i += CHUNK) {
60
+ const data = Buffer.from(chunk.subarray(i, i + CHUNK)).toString("base64");
61
+ const next = outputTail.then(async () => {
62
+ if (stopping) return;
63
+ await new Promise<void>((resolve) => { credit = resolve; send(socket, { type: "output", stream, data }); });
64
+ });
65
+ outputTail = next;
66
+ await next;
67
+ }
68
+ if (stopping) break;
69
+ }
70
+ }
71
+ async function run(c: Command) {
72
+ const sentinel = Bun.spawn(roleCommand("--exec-watch"), { stdin: "pipe", stdout: "pipe", stderr: "ignore", env: {} });
73
+ // Do not launch user code until the sentinel ignores group TERM and is
74
+ // watching the pipe. If it dies at any point, the runner cleans the group.
75
+ void sentinel.exited.then(stop);
76
+ const reader = sentinel.stdout.getReader();
77
+ const ready = await reader.read();
78
+ if (ready.done || new TextDecoder().decode(ready.value) !== "ready\n" || stopping) throw new Error("execution sentinel did not become ready");
79
+ reader.releaseLock();
80
+ child = Bun.spawn([c.program, ...c.args], { cwd: c.cwd, env: c.env, stdin: "pipe", stdout: "pipe", stderr: "pipe" });
81
+ clearTimeout(startup);
82
+ send(socket, { type: "started", pid: child.pid });
83
+ if (c.timeout !== null) deadline = setTimeout(() => fail(new Error(`execution timed out after ${c.timeout} ms`)), c.timeout);
84
+ const reads = Promise.all([output("stdout", child.stdout), output("stderr", child.stderr)]);
85
+ void reads.catch(fail);
86
+ // Initial input and EOF arrive on the acknowledged stdin channel too.
87
+ const code = await child.exited;
88
+ clearTimeout(deadline);
89
+ // A foreground command owns its descendants; they may not keep pipes or
90
+ // locks after it exits. Give them a bounded chance to close cleanly.
91
+ terminateGroup();
92
+ const drain = setTimeout(() => fail(new Error("execution output did not drain after program exit")), 5000);
93
+ await reads;
94
+ clearTimeout(drain);
95
+ if (stopping) return;
96
+ send(socket, { type: "exit", code, signal: child.signalCode ?? null });
97
+ socket.end(force);
98
+ }
@@ -0,0 +1,7 @@
1
+ // Same-group sentinel. The runner alone holds this pipe's write end. Signal
2
+ // our CURRENT group on EOF, never a persisted PID that could have been reused.
3
+ process.on("SIGTERM", () => {});
4
+ process.on("SIGINT", () => {});
5
+ process.stdout.write("ready\n");
6
+ await Bun.stdin.text();
7
+ process.kill(0, "SIGKILL");
@@ -0,0 +1,29 @@
1
+ // A bounded, acknowledged channel. One output chunk may be outstanding;
2
+ // control (cancel, stdin, exit) never waits behind output credit.
3
+ import type { Socket } from "node:net";
4
+ export const CHUNK = 16 * 1024;
5
+ export const FRAME = 128 * 1024;
6
+ export interface Command { program: string; args: string[]; cwd: string; env: Record<string, string>; input?: string; interactive: boolean; timeout: number | null }
7
+ export type ToRunner = { type: "start"; command: Command } | { type: "ack" } | { type: "stop" } | { type: "write"; id: number; input: string | null };
8
+ export type FromRunner = { type: "hello"; token: string; pid: number } | { type: "started"; pid: number } | { type: "output"; stream: "stdout" | "stderr"; data: string } | { type: "written"; id: number; error?: string } | { type: "exit"; code: number; signal: string | null } | { type: "error"; message: string };
9
+ export function receive<T>(socket: Socket, onMessage: (m: T) => void, onError: (e: Error) => void): void {
10
+ let buffer = "";
11
+ socket.setEncoding("utf8");
12
+ socket.on("data", (chunk: string) => {
13
+ buffer += chunk;
14
+ if (buffer.length > FRAME && !buffer.includes("\n")) { onError(new Error("execution frame exceeds limit")); return; }
15
+ for (;;) {
16
+ const end = buffer.indexOf("\n");
17
+ if (end < 0) break;
18
+ if (end > FRAME) { onError(new Error("execution frame exceeds limit")); return; }
19
+ const line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
20
+ try { onMessage(JSON.parse(line) as T); } catch (e) { onError(e instanceof Error ? e : new Error(String(e))); return; }
21
+ }
22
+ });
23
+ socket.on("error", onError);
24
+ }
25
+ export function send(socket: Socket, m: ToRunner | FromRunner): void {
26
+ const line = JSON.stringify(m) + "\n";
27
+ if (Buffer.byteLength(line) > FRAME) throw new Error("execution frame exceeds limit");
28
+ socket.write(line);
29
+ }
@@ -0,0 +1,4 @@
1
+ // The commit and the moment a package was built from, stamped by the pack
2
+ // of a release (tools/release/pack.ts) and restored after it. From a
3
+ // checkout both are empty: the tree is the build.
4
+ export const build = {"sha":"5a0a15d","at":"2026-10-08T18:17:26.925Z"};
@@ -0,0 +1 @@
1
+ declare module "*.css" {}
@@ -0,0 +1,245 @@
1
+ // voss's GUI: voss's shell at every level. At the root, over page state: the
2
+ // babas and the New baba wizard. At /NAME/ over the baba,
3
+ // its apps and voss's own, the Maker, the state, the contract, the
4
+ // promptware and the settings, as windows. One bundle, one React, served by the
5
+ // kernel's server child.
6
+ import "./theme.css";
7
+ import { Component, StrictMode, useEffect, useMemo, useState, type ComponentType, type ReactNode } from "react";
8
+ import { createRoot } from "react-dom/client";
9
+ import { ScopeProvider, apiHeaders, formatFailure, Maker, Desktop, useResource, useBaba, type AppProps, type ViewFailure } from "babavoss/web";
10
+ import { isApps, type Interface } from "../web/core.tsx";
11
+ import { desktop, themes, type DesktopSystem } from "../desktop/index.ts";
12
+ import type { AppSpec } from "../ecs/baba.ts";
13
+ import type { Described } from "../baba/node.ts";
14
+ import { Inspector } from "./inspector.tsx";
15
+ import { Runner } from "./runner.tsx";
16
+ import { Promptware } from "./promptware.tsx";
17
+ import { Button } from "../web/ui.tsx";
18
+ import { Root } from "./levels.tsx";
19
+ import type { Voss } from "../server/messages.ts";
20
+ import { tellApart } from "../kernel/names.ts";
21
+
22
+ const scope = document.querySelector('meta[name="voss-scope"]')?.getAttribute("content") ?? "";
23
+
24
+
25
+ async function describe(name: string, since: number, signal?: AbortSignal): Promise<Described> {
26
+ const r = await fetch(`/api/describe/${encodeURIComponent(name)}${since ? `?since=${since}` : ""}`, { signal, headers: apiHeaders() });
27
+ const d = await r.json() as Described & { error?: { message: string } };
28
+ if (!r.ok) throw new Error(d.error?.message ?? r.statusText);
29
+ return d;
30
+ }
31
+
32
+ /** Describes the baba, then keeps asking "since the generation I have": the answer comes when it changes. */
33
+ function useDescribed(name: string): { d: Described | null; failed: string | null } {
34
+ const [d, setD] = useState<Described | null>(null);
35
+ const [failed, setFailed] = useState<string | null>(null);
36
+ useEffect(() => {
37
+ const ctl = new AbortController();
38
+ (async () => {
39
+ let since = 0;
40
+ while (!ctl.signal.aborted) {
41
+ try {
42
+ const next = await describe(name, since, ctl.signal);
43
+ since = next.gen;
44
+ setD(next);
45
+ setFailed(null);
46
+ } catch (e) {
47
+ if (ctl.signal.aborted) return;
48
+ setFailed(e instanceof Error ? e.message : String(e));
49
+ since = 0;
50
+ await new Promise((r) => setTimeout(r, 2000));
51
+ }
52
+ }
53
+ })();
54
+ return () => ctl.abort();
55
+ }, [name]);
56
+ return { d, failed };
57
+ }
58
+
59
+ // ---- voss itself -----------------------------------------------------------
60
+
61
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
62
+
63
+ /**
64
+ * A newer voss installed: this one is stale and holds every change on disk
65
+ * until the restart. The bar says so on every page and offers the restart;
66
+ * after it, the page comes back on the new daemon, whose token is new too.
67
+ */
68
+ function Stale() {
69
+ const [b, setB] = useState<Voss | null>(null);
70
+ const [restarting, setRestarting] = useState(false);
71
+ useEffect(() => {
72
+ let live = true;
73
+ const tick = async () => {
74
+ try { const r = await fetch("/api/voss", { headers: apiHeaders() }); if (r.ok) { const j = await r.json() as Voss; if (live) setB(j); } } catch { /* asked again */ }
75
+ };
76
+ void tick();
77
+ const t = setInterval(() => void tick(), 10_000);
78
+ return () => { live = false; clearInterval(t); };
79
+ }, []);
80
+ if (!b?.stale || !b.installed) return null;
81
+ const [installed, running] = tellApart(b.installed, b);
82
+ const restart = async () => {
83
+ setRestarting(true);
84
+ try { await fetch("/api/voss/restart", { method: "POST", headers: { "content-type": "application/json", ...apiHeaders() }, body: "{}" }); } catch { /* it may already be going */ }
85
+ const until = Date.now() + 90_000;
86
+ while (Date.now() < until) {
87
+ await sleep(1000);
88
+ try {
89
+ const r = await fetch("/api/voss", { headers: apiHeaders() });
90
+ if (r.status === 401) { location.reload(); return; } // the new daemon, with its new token
91
+ if (r.ok && (await r.json() as Voss).pid !== b.pid) { location.reload(); return; }
92
+ } catch { /* down for the moment */ }
93
+ }
94
+ setRestarting(false);
95
+ };
96
+ return (
97
+ <div className="gui-stale" role="status">
98
+ {restarting ? <span>Restarting voss…</span> : <>
99
+ <span>voss <b>{installed}</b> is installed; this page runs on <b>{running}</b>. Changes on disk wait for the restart.</span>
100
+ <Button onClick={() => void restart()}>Restart to update</Button>
101
+ </>}
102
+ </div>
103
+ );
104
+ }
105
+
106
+ // ---- the baba's page -----------------------------------------------------
107
+
108
+ /** voss's pages before they were apps, and the app each is now. */
109
+ const moved: Record<string, string> = { inspect: "state", world: "state", run: "contract", proofs: "maker" };
110
+
111
+ /** A baba's page is voss's shell over it, always: its apps, or its one view, then voss's own. Its address is `/NAME/APP/PATH`. */
112
+ function Host() {
113
+ const { d, failed } = useDescribed(scope);
114
+ const parts = location.pathname.split("/").filter(Boolean);
115
+ // An old page's address opens the app it is now, unless the baba has an app by that name.
116
+ const to = d && parts.length === 2 && !(d.manifest?.apps ?? []).some((a) => a.key === parts[1]) ? moved[parts[1]!] : undefined;
117
+ const [, moveOn] = useState(0);
118
+ useEffect(() => { if (to) { history.replaceState(null, "", `/${encodeURIComponent(scope)}/${to}${location.search}${location.hash}`); moveOn((n) => n + 1); } }, [to]);
119
+ if (to) return null;
120
+ if (!d) return <div className="gui-page"><p className="bv-muted">{failed ? `${scope} did not answer: ${failed}` : "…"}</p></div>;
121
+ if (!d.manifest) return <div className="gui-page"><p className="bv-error">{d.title} is not running.{d.error ? ` ${d.error}` : ""}</p></div>;
122
+ return <ShellPage d={d} />;
123
+ }
124
+
125
+ function Failure({ f }: { f: ViewFailure }) {
126
+ return <div className="gui-notice" role="alert"><strong>The interface did not {f.stage === "build" ? "build" : f.stage === "load" ? "load" : "render"}.</strong><pre className="gui-failure">{formatFailure(f)}</pre></div>;
127
+ }
128
+
129
+ /** The interface module at a generation: a component per app, or the old way a shell of its own; the stylesheets its build produced. */
130
+ interface LoadedView { View: ComponentType | null; apps: Record<string, ComponentType<AppProps>>; css: string[] }
131
+ const loaded = new Map<number, LoadedView>();
132
+
133
+ async function loadView(name: string, v: number): Promise<LoadedView> {
134
+ const have = loaded.get(v);
135
+ if (have) return have;
136
+ const base = `/view/${encodeURIComponent(name)}/${v}/`;
137
+ const res = await fetch(base, { headers: apiHeaders() });
138
+ const index = await res.json() as { entry?: string; css?: string[]; error?: { message: string } };
139
+ if (!res.ok || !index.entry) throw new Error(index.error?.message ?? res.statusText);
140
+ const mod = await import(/* @vite-ignore */ base + index.entry);
141
+ const def = (mod.default ?? mod.view) as Interface | undefined;
142
+ if (!def || (!isApps(def) && typeof def.shell !== "function")) throw new Error("interface.tsx has no default export made by compose()");
143
+ const css = (index.css ?? []).map((p) => base + p);
144
+ const l: LoadedView = isApps(def) ? { View: null, apps: def.apps, css } : { View: def.shell, apps: {}, css };
145
+ loaded.set(v, l);
146
+ return l;
147
+ }
148
+
149
+ /** The interface at the baba's current generation, swapped only once the new one has loaded; its failure, if it has one. */
150
+ function useInterface(d: Described): { view: LoadedView | null; failure: ViewFailure | null } {
151
+ const v = d.view && !d.view.error ? d.view.v : undefined;
152
+ const [view, setView] = useState<LoadedView | null>(() => (v !== undefined ? loaded.get(v) ?? null : null));
153
+ const [failure, setFailure] = useState<ViewFailure | null>(null);
154
+ useEffect(() => {
155
+ if (v === undefined) return;
156
+ let live = true;
157
+ loadView(d.name, v).then((l) => { if (live) { setView(l); setFailure(null); } }, (e) => { if (live) setFailure({ stage: "load", message: e instanceof Error ? e.message : String(e) }); });
158
+ return () => { live = false; };
159
+ }, [d.name, v]);
160
+ // The interface's stylesheets, as links.
161
+ useEffect(() => {
162
+ if (!view) return;
163
+ const links = view.css.map((href) => { const l = document.createElement("link"); l.rel = "stylesheet"; l.href = href; document.head.appendChild(l); return l; });
164
+ return () => { for (const l of links) l.remove(); };
165
+ }, [view]);
166
+ return { view, failure: d.view?.error ?? failure };
167
+ }
168
+
169
+ /** The shell's system as this baba runs it, from its manifest: the same apps. */
170
+ const useShellSystem = (apps: readonly AppSpec[] | undefined) => {
171
+ const key = JSON.stringify(apps ?? []);
172
+ return useMemo(() => desktop({ apps: apps ?? [] }), [key]);
173
+ };
174
+
175
+ /**
176
+ * voss's shell over a baba: a window per app opened. An app draws the component the interface gave it,
177
+ * the one view draws an old interface whole, and voss's own apps draw voss's, unless the interface gave
178
+ * one of them its own. While the interface fails, each of the baba's apps says why; voss's still work.
179
+ */
180
+ function ShellPage({ d }: { d: Described }) {
181
+ const { view, failure } = useInterface(d);
182
+ const system = useShellSystem(d.manifest?.apps);
183
+ const manifest = d.manifest!;
184
+ const components = useMemo(() => {
185
+ const pending = () => failure ? <Failure f={failure} /> : <p className="desktop-empty bv-muted">…</p>;
186
+ const out: Record<string, ComponentType<AppProps>> = {
187
+ // The Maker with no specs: the interface gives voss's maker app its own component, with the baba's specs and its apps, to replace this.
188
+ maker: ({ path, navigate, setTitle }) => <Maker specs={[]} title={d.title} path={path} navigate={navigate} setTitle={setTitle} />,
189
+ state: () => <section className="gui-app">{d.error && <p className="bv-error">The code did not load; the last generation serves: {d.error}</p>}<Inspector manifest={manifest} /></section>,
190
+ contract: () => <section className="gui-app"><Runner manifest={manifest} /></section>,
191
+ promptware: () => <section className="gui-app"><Promptware manifest={manifest} /></section>,
192
+ settings: Settings,
193
+ };
194
+ for (const sc of manifest.apps ?? []) {
195
+ if (sc.key in out) continue;
196
+ out[sc.key] = sc.key === "view" && !view?.apps[sc.key]
197
+ ? (view?.View ? (view.View as ComponentType<AppProps>) : !d.view ? () => <p className="desktop-empty bv-muted">This baba has no interface.tsx yet: its apps are voss's.</p> : pending)
198
+ : pending;
199
+ }
200
+ for (const [k, v] of Object.entries(view?.apps ?? {})) out[k] = v;
201
+ return out;
202
+ }, [view, failure, system, manifest, d.error, d.view, d.title]);
203
+ return (
204
+ <div className="gui-view" key={view ? "loaded" : "loading"}>
205
+ <ViewBoundary fallback={(f) => <div className="gui-page"><Failure f={f} /></div>}>
206
+ <Desktop system={system} apps={components} title={d.title} root={{ label: "voss", href: "/" }} />
207
+ </ViewBoundary>
208
+ </div>
209
+ );
210
+ }
211
+
212
+ /** voss's settings for this baba: the theme its pages share, in its state. */
213
+ function Settings() {
214
+ const system = useMemo(() => desktop({}), []);
215
+ const current = useResource(system.desktopSettings)?.theme ?? "system";
216
+ const { fire } = useBaba<DesktopSystem>();
217
+ return (
218
+ <section className="gui-app">
219
+ <h2>Theme</h2>
220
+ <p className="bv-muted">The theme every page of this baba shows, kept in its state.</p>
221
+ <div className="gui-choices" role="radiogroup" aria-label="Theme">
222
+ {themes.map((t) => <Button key={t} variant={t === current ? "default" : "quiet"} role="radio" aria-checked={t === current} onClick={() => void fire("desktop-settings", { theme: t })}>{t}</Button>)}
223
+ </div>
224
+ <h2>voss</h2>
225
+ <p><a href="/">All babas</a></p>
226
+ </section>
227
+ );
228
+ }
229
+
230
+ class ViewBoundary extends Component<{ children: ReactNode; fallback: (f: ViewFailure) => ReactNode }, { failure: ViewFailure | null }> {
231
+ override state = { failure: null as ViewFailure | null };
232
+ static getDerivedStateFromError(err: unknown) {
233
+ return { failure: { stage: "render" as const, message: err instanceof Error ? err.message : String(err), detail: err instanceof Error ? err.stack : undefined } };
234
+ }
235
+ override render() {
236
+ return this.state.failure ? this.props.fallback(this.state.failure) : this.props.children;
237
+ }
238
+ }
239
+
240
+ createRoot(document.getElementById("root")!).render(
241
+ <StrictMode>
242
+ {scope ? <ScopeProvider value={scope}><Host /></ScopeProvider> : <Root />}
243
+ <Stale />
244
+ </StrictMode>,
245
+ );
@@ -0,0 +1,51 @@
1
+ // voss's GUI: its page, its build with the shared modules. The picker at
2
+ // the root, a baba's page at /NAME/ with its view, and voss's inspector and
3
+ // runner over any baba.
4
+ import { dirname, join } from "node:path";
5
+ import { rm, rename } from "node:fs/promises";
6
+ import { early } from "./theme.ts";
7
+ import { shared, names, resolver } from "../build/views.ts";
8
+
9
+ const here = dirname(Bun.fileURLToPath(import.meta.url));
10
+
11
+ /** Builds the GUI and the shared modules into outdir. One React on the page. */
12
+ export async function buildGui(outdir: string): Promise<void> {
13
+ await rm(outdir, { recursive: true, force: true });
14
+ // Each shared module gets an entry that names every export, written in
15
+ // memory: a CommonJS package like React cannot be re-exported with `export *`.
16
+ const files: Record<string, string> = {};
17
+ for (const [name, file] of Object.entries(shared)) {
18
+ const mod = await import(names[name] ?? name);
19
+ const keys = Object.keys(mod).filter((k) => k !== "default" && /^[A-Za-z_$][\w$]*$/.test(k));
20
+ files[join(here, "shared", file.replace(/\.js$/, ".ts"))] =
21
+ `import * as m from ${JSON.stringify(name)};\nexport default m.default ?? m;\n` +
22
+ keys.map((k) => `export const ${k} = m.${k};`).join("\n") + "\n";
23
+ }
24
+ const r = await Bun.build({
25
+ entrypoints: [join(here, "gui.tsx"), ...Object.keys(files)], files,
26
+ outdir, target: "browser", format: "esm", splitting: true,
27
+ naming: "[name].[ext]", plugins: [resolver],
28
+ jsx: { development: false },
29
+ define: { "process.env.NODE_ENV": '"production"' },
30
+ });
31
+ if (!r.success) throw new Error(r.logs.map(String).join("\n"));
32
+ const css = r.outputs.filter((o) => o.path.endsWith(".css"));
33
+ if (css.length !== 1) throw new Error(`the GUI build emitted ${css.length} stylesheets, expected one`);
34
+ await rename(css[0]!.path, join(outdir, "theme.css"));
35
+ }
36
+
37
+ /** The import map, as the page carries it: the shared names to the modules built with the GUI. */
38
+ export const importMap = () => JSON.stringify({ imports: Object.fromEntries(Object.entries(shared).map(([n, f]) => [n, `/assets/${f}`])) });
39
+
40
+ /** The page of a scope: the theme before the first paint, the import map, the GUI. */
41
+ export function page(scope: string | null): string {
42
+ const meta = scope ? `<meta name="voss-scope" content="${escape(scope)}">` : "";
43
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
44
+ <title>${scope ? escape(scope) : "voss"}</title>${meta}<script>${early}</script><link rel="stylesheet" href="/assets/theme.css">
45
+ <script type="importmap">${importMap()}</script></head>
46
+ <body><div id="root"></div><script type="module" src="/assets/gui.js"></script></body></html>`;
47
+ }
48
+
49
+ function escape(s: string): string {
50
+ return s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
51
+ }