beatrina 0.8.7 → 0.9.43

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 (111) hide show
  1. package/NOTICES +1 -1
  2. package/README.md +6 -0
  3. package/beatrina_V0.9.43.html +1885 -0
  4. package/beatrina_V0.9.43.html.inputs.json +1 -0
  5. package/bin/beatrina.mjs +152 -24
  6. package/bin/browser.mjs +31 -0
  7. package/bin/cli.mjs +64 -5
  8. package/bin/doctor-rows.mjs +31 -0
  9. package/bin/failsafe.mjs +8 -3
  10. package/bin/finder.mjs +74 -0
  11. package/bin/lsquery.swift +55 -0
  12. package/bin/pages.mjs +81 -0
  13. package/bin/python-setup.mjs +207 -0
  14. package/bin/runtime-dirs.mjs +27 -0
  15. package/bin/sessions.mjs +30 -10
  16. package/bin/shortcut.mjs +250 -42
  17. package/bin/update-check.mjs +2 -2
  18. package/build-info.json +1 -1
  19. package/engines/js/worker.mjs +6 -3
  20. package/engines/python/adapter.py +14 -13
  21. package/engines/python/analyze.py +4 -2
  22. package/engines/python/bootstrap.py +32 -31
  23. package/engines/python/debugger.py +10 -6
  24. package/engines/python/engine.json +1 -1
  25. package/engines/python/handoff.py +4 -2
  26. package/engines/python/worker.py +39 -10
  27. package/engines/r/engine.json +1 -1
  28. package/engines/r/handoff.R +4 -3
  29. package/failsafe/ai-policy.R +16 -16
  30. package/failsafe/ai-store.R +6 -6
  31. package/failsafe/cite.R +11 -11
  32. package/failsafe/journal.R +11 -11
  33. package/failsafe/plugins.R +27 -27
  34. package/failsafe/serve.R +241 -241
  35. package/host/ai-authority.mjs +55 -0
  36. package/host/ai-policy.mjs +48 -26
  37. package/host/bundle.mjs +194 -0
  38. package/host/deployment.mjs +25 -18
  39. package/host/engine-js.mjs +12 -17
  40. package/host/engine-pool.mjs +48 -6
  41. package/host/engine-python.mjs +80 -31
  42. package/host/engine-r.mjs +20 -21
  43. package/host/engine-stdio.mjs +35 -10
  44. package/host/env-names.mjs +148 -0
  45. package/host/gateway-token.mjs +51 -0
  46. package/host/journal-store.mjs +10 -9
  47. package/host/main.mjs +193 -77
  48. package/host/payload.mjs +131 -0
  49. package/host/planes/README.md +1 -1
  50. package/host/planes/ai-store.mjs +6 -5
  51. package/host/planes/ai.mjs +49 -29
  52. package/host/planes/analyze.mjs +6 -5
  53. package/host/planes/bundle.mjs +344 -0
  54. package/host/planes/choose.mjs +404 -0
  55. package/host/planes/cite.mjs +18 -17
  56. package/host/planes/files.mjs +0 -0
  57. package/host/planes/jobs.mjs +43 -20
  58. package/host/planes/latex.mjs +15 -5
  59. package/host/planes/mcp.mjs +80 -91
  60. package/host/planes/pair.mjs +24 -24
  61. package/host/planes/pipe-term.mjs +2 -2
  62. package/host/planes/plugins.mjs +1 -1
  63. package/host/planes/recent-documents.mjs +170 -0
  64. package/host/planes/sessions.mjs +226 -82
  65. package/host/planes/settings.mjs +4 -4
  66. package/host/planes/terminal.mjs +13 -11
  67. package/host/planes/test-file.mjs +2 -2
  68. package/host/planes/update.mjs +6 -13
  69. package/host/plugin-store.mjs +35 -52
  70. package/host/recent-documents.mjs +124 -0
  71. package/host/runtime-dir.mjs +47 -0
  72. package/host/server.mjs +75 -18
  73. package/host/session-keep.mjs +1 -1
  74. package/host/settings.mjs +85 -37
  75. package/host/update-record.mjs +3 -2
  76. package/host/user-dirs.mjs +60 -18
  77. package/host/which.mjs +39 -0
  78. package/host/windows-runtime.mjs +6 -3
  79. package/host/worker-plane.mjs +34 -6
  80. package/host/ws.mjs +9 -2
  81. package/host/zip.mjs +237 -0
  82. package/kernel/analyze.R +1 -1
  83. package/{check → kernel/check}/acceptance.mjs +1 -1
  84. package/kernel/check/knit-file.mjs +19639 -0
  85. package/{check → kernel/check}/session.mjs +49 -8
  86. package/kernel/deployment.R +20 -20
  87. package/kernel/examples/NOTICE.md +1 -1
  88. package/kernel/fileio.R +6 -6
  89. package/kernel/index.html +2 -2
  90. package/kernel/job-run.R +83 -22
  91. package/kernel/jobs.R +28 -12
  92. package/kernel/kernel-version +1 -1
  93. package/kernel/kernel.R +18 -18
  94. package/kernel/knitr-run.R +50 -9
  95. package/kernel/latex.R +128 -30
  96. package/kernel/mcp/{carmar-mcp.mjs → beatrina-mcp.mjs} +249 -59
  97. package/kernel/notebook-page.R +7 -7
  98. package/kernel/project.R +10 -10
  99. package/kernel/settings.R +105 -56
  100. package/kernel/sniff.R +4 -4
  101. package/kernel/typst.R +121 -0
  102. package/kernel/worker.R +522 -119
  103. package/kernel/workspace-keep.R +3 -3
  104. package/lib/agent-authoring-contract.js +28 -19
  105. package/lib/cell-kinds.js +3 -3
  106. package/lib/engine-labels.js +4 -4
  107. package/menu/Beatrina Menu.app/Contents/Info.plist +14 -0
  108. package/menu/Beatrina Menu.app/Contents/MacOS/Beatrina Menu +0 -0
  109. package/menu/Beatrina Menu.app/Contents/_CodeSignature/CodeResources +115 -0
  110. package/package.json +4 -3
  111. package/carmar_V0.8.7.html +0 -1522
package/host/zip.mjs ADDED
@@ -0,0 +1,237 @@
1
+ // zip.mjs — write and read standard ZIP archives, with Node's zlib and nothing else.
2
+ //
3
+ // The container under `.btrn` (docs/results-plan.md R2). A .btrn must open in
4
+ // every unzip there is — macOS Archive Utility, `unzip`, R's utils::unzip() —
5
+ // so this writes the plain, universal subset of the format and nothing clever:
6
+ // one local header + data per entry, a central directory, the end record.
7
+ // Method 0 (stored) or 8 (deflated); UTF-8 names (flag bit 11); no ZIP64 (a
8
+ // .btrn holds ≤ 100 MB of data by design — past that the data lives beside
9
+ // it), no encryption, no data descriptors. zlib.crc32 is Node ≥ 22.2.
10
+ //
11
+ // READING IS READING SOMETHING FROM ELSEWHERE, so the reader refuses before it
12
+ // trusts:
13
+ // · a name that is absolute, has a `..` segment, a drive letter, a
14
+ // backslash, a NUL, or is empty — the zip-slip family, refused by name;
15
+ // · the same name twice;
16
+ // · an encrypted entry, or a method other than 0/8;
17
+ // · more than `maxTotal` bytes uncompressed in all — the zip-bomb family,
18
+ // refused with its numbers BEFORE inflating (a per-entry ratio is
19
+ // optional: deflate tops out near 1032×, so it guards nothing by default);
20
+ // · an entry that inflates to more than it declared (zlib's
21
+ // maxOutputLength enforces the declared size), or whose CRC-32 differs.
22
+ // Every refusal is a thrown Error with `code` set, so callers can say which.
23
+
24
+ import zlib from "node:zlib";
25
+
26
+ const SIG_LOCAL = 0x04034b50;
27
+ const SIG_CENTRAL = 0x02014b50;
28
+ const SIG_END = 0x06054b50;
29
+ const FLAG_UTF8 = 0x0800;
30
+ const U32 = 0xffffffff;
31
+
32
+ /** Date → the two 16-bit MS-DOS fields (2-second resolution, 1980..2107). */
33
+ function dosTime(date) {
34
+ const d = date instanceof Date && !Number.isNaN(date.getTime()) ? date : new Date(1980, 0, 1);
35
+ const year = Math.min(2107, Math.max(1980, d.getFullYear()));
36
+ return {
37
+ time: (d.getHours() << 11) | (d.getMinutes() << 5) | Math.floor(d.getSeconds() / 2),
38
+ date: ((year - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate(),
39
+ };
40
+ }
41
+
42
+ const fail = (code, message) => Object.assign(new Error(message), { code });
43
+
44
+ /** The Info-ZIP Unicode Path extra field: version 1, CRC-32 of the header name, the UTF-8 name. */
45
+ function unicodePathExtra(nameBytes) {
46
+ const field = Buffer.alloc(4 + 5 + nameBytes.length);
47
+ field.writeUInt16LE(0x7075, 0);
48
+ field.writeUInt16LE(5 + nameBytes.length, 2);
49
+ field.writeUInt8(1, 4);
50
+ field.writeUInt32LE(zlib.crc32(nameBytes) >>> 0, 5);
51
+ nameBytes.copy(field, 9);
52
+ return field;
53
+ }
54
+
55
+ /**
56
+ * Is this an entry name a reader may use? Relative, forward slashes, no
57
+ * `..`/`.` segment, no drive, no NUL, not empty. A directory entry ends in `/`.
58
+ */
59
+ export function safeEntryName(name) {
60
+ const n = String(name ?? "");
61
+ if (!n || n.length > 1024 || /[\u0000\\]/.test(n)) return false;
62
+ if (n.startsWith("/") || /^[A-Za-z]:/.test(n)) return false;
63
+ const parts = n.replace(/\/$/, "").split("/");
64
+ return parts.every((p) => p !== "" && p !== "." && p !== "..");
65
+ }
66
+
67
+ /**
68
+ * Build a ZIP in memory.
69
+ *
70
+ * @param {Array<{name:string, data:Buffer|Uint8Array|string, compress?:boolean, date?:Date}>} entries
71
+ * `compress` false stores the bytes (right for PNG/JPEG, already compressed);
72
+ * the default deflates, and keeps the stored form when deflate does not help.
73
+ * @returns {Buffer}
74
+ */
75
+ export function writeZip(entries, { date = new Date() } = {}) {
76
+ if (!Array.isArray(entries)) throw fail("zip_input", "writeZip needs a list of entries");
77
+ if (entries.length > 0xfffe) throw fail("zip_too_many", `${entries.length} entries is more than a plain ZIP holds`);
78
+ const seen = new Set();
79
+ const locals = [];
80
+ const centrals = [];
81
+ let offset = 0;
82
+ for (const entry of entries) {
83
+ const name = String(entry.name ?? "");
84
+ if (!safeEntryName(name)) throw fail("zip_bad_name", `unsafe entry name: ${JSON.stringify(name)}`);
85
+ if (seen.has(name)) throw fail("zip_duplicate", `the entry ${name} appears twice`);
86
+ seen.add(name);
87
+ const raw = Buffer.isBuffer(entry.data) ? entry.data
88
+ : typeof entry.data === "string" ? Buffer.from(entry.data, "utf8") : Buffer.from(entry.data ?? []);
89
+ if (raw.length >= U32) throw fail("zip_too_large", `${name} is over 4 GB; a plain ZIP cannot hold it`);
90
+ const crc = zlib.crc32(raw) >>> 0;
91
+ let method = 0;
92
+ let body = raw;
93
+ if (entry.compress !== false && raw.length) {
94
+ const deflated = zlib.deflateRawSync(raw, { level: 6 });
95
+ if (deflated.length < raw.length) { method = 8; body = deflated; }
96
+ }
97
+ const nameBytes = Buffer.from(name, "utf8");
98
+ // A non-ASCII name also travels in the Info-ZIP Unicode Path field
99
+ // (0x7075): macOS's bundled unzip ignores the UTF-8 flag and mangles
100
+ // "ünïcode.csv" without it; readers that honour the flag ignore this.
101
+ const extra = /[^\x00-\x7f]/.test(name) ? unicodePathExtra(nameBytes) : Buffer.alloc(0);
102
+ const { time, date: dd } = dosTime(entry.date || date);
103
+ const local = Buffer.alloc(30);
104
+ local.writeUInt32LE(SIG_LOCAL, 0);
105
+ local.writeUInt16LE(20, 4);
106
+ local.writeUInt16LE(FLAG_UTF8, 6);
107
+ local.writeUInt16LE(method, 8);
108
+ local.writeUInt16LE(time, 10);
109
+ local.writeUInt16LE(dd, 12);
110
+ local.writeUInt32LE(crc, 14);
111
+ local.writeUInt32LE(body.length, 18);
112
+ local.writeUInt32LE(raw.length, 22);
113
+ local.writeUInt16LE(nameBytes.length, 26);
114
+ local.writeUInt16LE(extra.length, 28);
115
+ const central = Buffer.alloc(46);
116
+ central.writeUInt32LE(SIG_CENTRAL, 0);
117
+ // Made by UNIX (3), spec 2.0: a reader then takes the name's bytes as they
118
+ // are. Marked MS-DOS (0), macOS's bundled unzip (6.00, no Unicode support)
119
+ // translated every non-ASCII name from code page 437 and wrote garbage.
120
+ central.writeUInt16LE((3 << 8) | 20, 4);
121
+ central.writeUInt16LE(20, 6);
122
+ central.writeUInt16LE(FLAG_UTF8, 8);
123
+ central.writeUInt16LE(method, 10);
124
+ central.writeUInt16LE(time, 12);
125
+ central.writeUInt16LE(dd, 14);
126
+ central.writeUInt32LE(crc, 16);
127
+ central.writeUInt32LE(body.length, 20);
128
+ central.writeUInt32LE(raw.length, 24);
129
+ central.writeUInt16LE(nameBytes.length, 28);
130
+ central.writeUInt16LE(extra.length, 30);
131
+ central.writeUInt32LE(((name.endsWith("/") ? 0o40755 : 0o100644) << 16) >>> 0, 38); // rw-r--r-- (a dir rwxr-xr-x)
132
+ central.writeUInt32LE(offset, 42); // comment, disk, internal attrs: 0
133
+ locals.push(local, nameBytes, extra, body);
134
+ centrals.push(central, nameBytes, extra);
135
+ offset += local.length + nameBytes.length + extra.length + body.length;
136
+ if (offset >= U32) throw fail("zip_too_large", "the archive would pass 4 GB; a plain ZIP cannot hold it");
137
+ }
138
+ const cd = Buffer.concat(centrals);
139
+ const end = Buffer.alloc(22);
140
+ end.writeUInt32LE(SIG_END, 0);
141
+ end.writeUInt16LE(entries.length, 8);
142
+ end.writeUInt16LE(entries.length, 10);
143
+ end.writeUInt32LE(cd.length, 12);
144
+ end.writeUInt32LE(offset, 16);
145
+ return Buffer.concat([...locals, cd, end]);
146
+ }
147
+
148
+ /**
149
+ * The central directory: every entry's name, sizes, method and where its
150
+ * bytes are — nothing inflated. What `bundle_list` shows, and what readZip
151
+ * checks the caps against before touching any data.
152
+ *
153
+ * @param {Buffer} buf
154
+ * @returns {Array<{name:string, method:number, crc:number, csize:number, usize:number, offset:number, dir:boolean}>}
155
+ */
156
+ export function listZip(buf) {
157
+ if (!Buffer.isBuffer(buf) || buf.length < 22) throw fail("zip_not_zip", "not a ZIP archive (too short)");
158
+ let end = -1;
159
+ for (let i = buf.length - 22; i >= Math.max(0, buf.length - 22 - 0xffff); i -= 1) {
160
+ if (buf.readUInt32LE(i) === SIG_END) { end = i; break; }
161
+ }
162
+ if (end < 0) throw fail("zip_not_zip", "not a ZIP archive (no end record)");
163
+ const count = buf.readUInt16LE(end + 10);
164
+ const cdSize = buf.readUInt32LE(end + 12);
165
+ const cdOffset = buf.readUInt32LE(end + 16);
166
+ if (count === 0xffff || cdOffset === U32) throw fail("zip_zip64", "a ZIP64 archive — not a .btrn this version wrote");
167
+ if (cdOffset + cdSize > end) throw fail("zip_corrupt", "the central directory runs past the end record");
168
+ const out = [];
169
+ const names = new Set();
170
+ let p = cdOffset;
171
+ for (let k = 0; k < count; k += 1) {
172
+ if (p + 46 > end || buf.readUInt32LE(p) !== SIG_CENTRAL) throw fail("zip_corrupt", `central directory entry ${k + 1} is damaged`);
173
+ const flags = buf.readUInt16LE(p + 8);
174
+ const method = buf.readUInt16LE(p + 10);
175
+ const crc = buf.readUInt32LE(p + 16) >>> 0;
176
+ const csize = buf.readUInt32LE(p + 20);
177
+ const usize = buf.readUInt32LE(p + 24);
178
+ const nlen = buf.readUInt16LE(p + 28);
179
+ const xlen = buf.readUInt16LE(p + 30);
180
+ const clen = buf.readUInt16LE(p + 32);
181
+ const offset = buf.readUInt32LE(p + 42);
182
+ const name = buf.subarray(p + 46, p + 46 + nlen).toString((flags & FLAG_UTF8) ? "utf8" : "latin1");
183
+ if (!safeEntryName(name)) throw fail("zip_bad_name", `unsafe entry name in the archive: ${JSON.stringify(name)}`);
184
+ if (names.has(name)) throw fail("zip_duplicate", `the entry ${name} appears twice`);
185
+ names.add(name);
186
+ if (flags & 0x0001) throw fail("zip_encrypted", `${name} is encrypted`);
187
+ if (method !== 0 && method !== 8) throw fail("zip_method", `${name} uses compression method ${method}; only stored and deflated are read`);
188
+ out.push({ name, method, crc, csize, usize, offset, dir: name.endsWith("/") });
189
+ p += 46 + nlen + xlen + clen;
190
+ }
191
+ return out;
192
+ }
193
+
194
+ /**
195
+ * Read a ZIP: every file entry's bytes, verified.
196
+ *
197
+ * @param {Buffer} buf
198
+ * @param {{maxTotal?:number, maxRatio?:number, only?:(name:string)=>boolean}} [opts]
199
+ * the cap on the whole archive's uncompressed bytes (default 2 GB); an
200
+ * optional per-entry expansion cap (none by default: one layer of deflate
201
+ * cannot pass ~1032×, and an honest file of zeros reaches 1026×, so a ratio
202
+ * refuses real data while the total cap and the declared-size inflate are
203
+ * what actually stop a bomb); `only` reads a subset
204
+ * @returns {Map<string, Buffer>}
205
+ */
206
+ export function readZip(buf, { maxTotal = 2 * 1024 ** 3, maxRatio = Infinity, only = null } = {}) {
207
+ const entries = listZip(buf).filter((e) => !e.dir && (!only || only(e.name)));
208
+ const total = entries.reduce((a, e) => a + e.usize, 0);
209
+ if (total > maxTotal) throw fail("zip_bomb", `the archive expands to ${total} bytes, over the ${maxTotal}-byte cap`);
210
+ for (const e of entries) {
211
+ if (e.method === 8 && e.usize > Math.max(1024, e.csize * maxRatio)) {
212
+ throw fail("zip_bomb", `${e.name} claims to expand ${e.csize} → ${e.usize} bytes, over ${maxRatio}×`);
213
+ }
214
+ }
215
+ const files = new Map();
216
+ for (const e of entries) {
217
+ const p = e.offset;
218
+ if (p + 30 > buf.length || buf.readUInt32LE(p) !== SIG_LOCAL) throw fail("zip_corrupt", `${e.name}: its local header is damaged`);
219
+ const start = p + 30 + buf.readUInt16LE(p + 26) + buf.readUInt16LE(p + 28);
220
+ if (start + e.csize > buf.length) throw fail("zip_corrupt", `${e.name}: its data runs past the end`);
221
+ const body = buf.subarray(start, start + e.csize);
222
+ let data;
223
+ if (e.method === 0) {
224
+ if (e.csize !== e.usize) throw fail("zip_corrupt", `${e.name}: stored entry with two different sizes`);
225
+ data = Buffer.from(body);
226
+ } else {
227
+ try { data = zlib.inflateRawSync(body, { maxOutputLength: Math.max(1, e.usize) }); }
228
+ catch (err) { throw fail("zip_corrupt", `${e.name}: ${err.code === "ERR_BUFFER_TOO_LARGE" ? "inflates past its declared size" : "does not inflate"}`); }
229
+ if (data.length !== e.usize) throw fail("zip_corrupt", `${e.name}: inflated to ${data.length} bytes, declared ${e.usize}`);
230
+ }
231
+ if ((zlib.crc32(data) >>> 0) !== e.crc) throw fail("zip_crc", `${e.name}: CRC-32 does not match — the archive is damaged`);
232
+ files.set(e.name, data);
233
+ }
234
+ return files;
235
+ }
236
+
237
+ export default { writeZip, readZip, listZip, safeEntryName };
package/kernel/analyze.R CHANGED
@@ -44,7 +44,7 @@ suppressPackageStartupMessages(stopifnot(requireNamespace("jsonlite", quietly =
44
44
  # raise it knowingly. Clamped to [0, 60] so a typo cannot remove the bound —
45
45
  # and 0 is a real setting, meaning "scan nothing and report it truncated".
46
46
  ANALYZE_BUDGET <- local({
47
- v <- suppressWarnings(as.numeric(Sys.getenv("CARMAR_ANALYZE_BUDGET", "3")))
47
+ v <- suppressWarnings(as.numeric(Sys.getenv("BEATRINA_ANALYZE_BUDGET", "3")))
48
48
  if (!is.finite(v)) v <- 3
49
49
  max(0, min(v, 60))
50
50
  })
@@ -14,7 +14,7 @@
14
14
  //
15
15
  // Usage
16
16
  // node tools/beatrina/acceptance.mjs # this checkout's host
17
- // node tools/beatrina/acceptance.mjs --installed # the installed carmar package's host
17
+ // node tools/beatrina/acceptance.mjs --installed # the installed beatrina package's host
18
18
  // node tools/beatrina/acceptance.mjs --host=C:\path\to\host\main.mjs
19
19
  // node tools/beatrina/acceptance.mjs --supervisor=serve-r # the retired R supervisor, for comparison
20
20
  // options: --port=28900 --repeat=5 --json=out.json --junit=out.xml --only=stop,readline --log=host.log