relic-mcp 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +67 -1
  4. package/dist/cover/cover.js +42 -15
  5. package/dist/cover/frame.html +21 -2
  6. package/dist/cover/styles.css +547 -26
  7. package/dist/native-secure-open.d.ts +1 -1
  8. package/dist/native-secure-open.js +8 -9
  9. package/dist/program/index.html +4 -0
  10. package/dist/program/index.js +35403 -0
  11. package/dist/relic-mcp.js +6076 -676
  12. package/mcp-servers.json +2 -2
  13. package/native/build.mjs +164 -0
  14. package/native/secure-enclave/relic-se.swift +243 -0
  15. package/native/secure-open.c +39 -39
  16. package/native/secure-open.d.ts +1 -1
  17. package/package.json +2 -2
  18. package/prebuilds/darwin-arm64/build-info.json +2 -2
  19. package/prebuilds/darwin-arm64/relic-se +0 -0
  20. package/prebuilds/darwin-arm64/relic-se-build-info.json +44 -0
  21. package/prebuilds/darwin-arm64/secure_open.node +0 -0
  22. package/prebuilds/darwin-x64/build-info.json +2 -2
  23. package/prebuilds/darwin-x64/relic-se +0 -0
  24. package/prebuilds/darwin-x64/relic-se-build-info.json +44 -0
  25. package/prebuilds/darwin-x64/secure_open.node +0 -0
  26. package/prebuilds/linux-arm64-gnu/build-info.json +2 -2
  27. package/prebuilds/linux-arm64-gnu/secure_open.node +0 -0
  28. package/prebuilds/linux-x64-gnu/build-info.json +2 -2
  29. package/prebuilds/linux-x64-gnu/secure_open.node +0 -0
  30. package/scripts/prove-packed-native.mjs +42 -3
  31. package/skills/relic/SKILL.md +39 -0
  32. package/src/comments.ts +138 -24
  33. package/src/cover/chrome.ts +46 -18
  34. package/src/cover/inputs.ts +1 -1
  35. package/src/decisions.ts +13 -7
  36. package/src/device.ts +1196 -0
  37. package/src/files.ts +2 -2
  38. package/src/index.ts +25 -0
  39. package/src/native-secure-open.ts +13 -13
  40. package/src/otio-bundle.ts +411 -144
  41. package/src/private.ts +1322 -0
  42. package/src/program/browser.ts +124 -0
  43. package/src/program/build.ts +1339 -0
  44. package/src/program/confine.ts +162 -0
  45. package/src/program/jail-probe.ts +192 -0
  46. package/src/program/policy.ts +128 -0
  47. package/src/program/ramdisk.ts +371 -0
  48. package/src/program/render.ts +793 -0
  49. package/src/program/report.ts +92 -0
  50. package/src/publish.ts +136 -9
  51. package/src/republish.ts +65 -4
  52. package/src/secure-enclave.ts +228 -0
  53. package/src/server.ts +588 -12
  54. package/src/share.ts +35 -1
  55. package/src/state.ts +201 -47
@@ -10,7 +10,7 @@
10
10
  {
11
11
  "name": "relic",
12
12
  "description": "Publish a file from your machine as an encrypted, shareable link. The agent encrypts locally, uploads only ciphertext, and hands back a URL whose fragment holds the key, so the service stores something it cannot read.",
13
- "version": "0.15.0",
13
+ "version": "0.16.0",
14
14
  "source": "./",
15
15
  "author": {
16
16
  "name": "The Bushido Collective",
@@ -21,6 +21,6 @@
21
21
  }
22
22
  ],
23
23
  "metadata": {
24
- "version": "0.15.0"
24
+ "version": "0.16.0"
25
25
  }
26
26
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relic",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Publish a file from your machine as an encrypted, shareable link. The agent encrypts locally, uploads only ciphertext, and hands back a URL whose fragment holds the key, so the service stores something it cannot read.",
5
5
  "mcpServers": "./mcp-servers.json",
6
6
  "author": {
package/README.md CHANGED
@@ -100,7 +100,10 @@ created `0600` inside a `0700` directory, and redirectable with
100
100
  Copying the state file to another machine moves the ability with it.
101
101
  - **The file holds the key and the token.** They are never printed, logged,
102
102
  or returned by any tool result. The service stores only a SHA-256 of the
103
- token, so it cannot reconstruct either one.
103
+ token, so it cannot reconstruct either one. On this machine the version keys
104
+ are plaintext in the file. Other users cannot read it (`0600`), but any
105
+ process running as this user can, so the file is protected only from other
106
+ users and from the service.
104
107
  - **Losing the file changes nothing for existing links**; it only ends that
105
108
  machine's ability to update those relics.
106
109
  - **A takedown is permanent.** A removed relic answers `relic_removed`
@@ -210,6 +213,66 @@ flags (`--host-resolver-rules=MAP * ~NOTFOUND`,
210
213
  `--webrtc-ip-handling-policy=disable_non_proxied_udp`, `--disable-quic`) so no
211
214
  relic plaintext leaves the machine during rendering.
212
215
 
216
+ ### OTIO programs
217
+
218
+ Publishing or republishing an `.otioz` archive, or a `.otiod` directory packaged
219
+ into one, also renders each OpenTimelineIO timeline to an H.264/AAC MP4 in the
220
+ same local headless browser shell. The render runs in memory: the browser context is
221
+ off-the-record, the HTTP cache is disabled, and the page can reach only the
222
+ archive's own media through the virtual origin
223
+ (`https://program.relic.invalid`). The MP4s and a `relic/program.json` manifest
224
+ go into the archive before encryption, so the program is encrypted like the
225
+ source. `content.otio` and every media entry keep their bytes.
226
+
227
+ A timeline that cannot be rendered is published focus-only, and the publish
228
+ result says why for each one: no browser, no H.264/AAC encoder, an unsupported
229
+ effect, codec, transition, schema, or timing, missing media, or the size cap.
230
+ The result's `program` field reports the status and every timeline. A bare
231
+ `.otio` file has no archive to carry a program and publishes as given. No
232
+ browser or encoder is installed or downloaded, and nothing is sent to a remote
233
+ renderer.
234
+
235
+ Programs render only on macOS in this version. On Linux no browser starts, and
236
+ every OTIO timeline is published focus-only with the reason "program rendering is
237
+ confined only on macOS in this version, so this timeline has no program".
238
+
239
+ On macOS each publish creates a private RAM volume and runs the headless shell
240
+ under one deny-default macOS sandbox profile that the kernel applies to every
241
+ process the shell starts. Chromium's own sandbox is off, since it cannot start
242
+ inside that profile, so the profile is the containment. The profile allows
243
+ writes only to the RAM volume; reads only of the system libraries, fonts, and
244
+ the shell's own directory; no network; no home directory; and executing only the
245
+ shell. GPU and hardware video decoding are disabled. Before every launch a probe
246
+ runs unconfined,
247
+ which must succeed, and then confined, where each outside write, read, exec and
248
+ connection must be refused and the inside write must succeed. Any deviation, or
249
+ a volume that cannot be created, means no browser starts and every timeline is
250
+ focus-only with that reason. The browser gets a minimal environment: `HOME`,
251
+ temporary and cache directories inside the volume, and a fixed `PATH`. Nothing
252
+ else is inherited.
253
+
254
+ After rendering, the process unmounts and detaches its volume and removes only
255
+ the device and directory it created. SIGINT and SIGTERM abort active renders and
256
+ wait for browser and volume cleanup before exit. SIGKILL, power loss, and other
257
+ forced termination cannot run cleanup; the RAM mount may persist until system
258
+ cleanup or reboot. If storage this process made cannot be released, the publish
259
+ is refused. Reasons in the archive are fixed sentences,
260
+ never local paths or environment values. The archive always carries its
261
+ manifest, so a reader never falls back to a full composite the publisher did not
262
+ select. An archive that already contains `relic/` entries, a bad
263
+ `RELIC_PROGRAM_MAX_BYTES`, an unsafe entry name, or a manifest that cannot fit
264
+ the cap is refused with an exact reason rather than published without one.
265
+
266
+ What the sandbox does not cover: with Chromium's sandbox off, one profile covers
267
+ every process the shell starts, so an exploit in a media decoder reaches
268
+ everything that profile allows, though nothing beyond it. Seatbelt does not filter
269
+ every system call, and its profile language is private and deprecated, so a
270
+ future macOS release may change it; the per-launch probe then fails closed. The
271
+ macOS crash reporter sits outside the profile and can write crash details,
272
+ including strings a compromised process chose, to `~/Library/Logs/DiagnosticReports`.
273
+ No claim is made about swap. Anything readable inside the volume, such as the
274
+ host name, can appear in the published MP4.
275
+
213
276
  ## Tools
214
277
 
215
278
  | Tool | Input | Notes |
@@ -238,6 +301,9 @@ relic plaintext leaves the machine during rendering.
238
301
  | `RELIC_MCP_PORT`, `RELIC_MCP_HOST` | HTTP bind. Defaults to `127.0.0.1:7333`. |
239
302
  | `RELIC_MCP_ALLOWED_ORIGINS` | Comma-separated `Origin` allowlist for HTTP. |
240
303
  | `RELIC_CHROME_PATH` | Explicit path to Chrome, Chromium, or Edge binary for cover rendering. Set to `none` or `disabled` to skip covers. |
304
+ | `RELIC_OTIO_CHROME_PATH` | Path to a `chrome-headless-shell` build whose file layout matches the r1228 headless shell, for OTIO program rendering on macOS. Unset, the newest one under `~/.omp/owned-shells` is used. Any other file or layout is refused. Full Chrome is never used for programs. Set to `none` or `disabled` to publish OTIO without programs. |
305
+ | `RELIC_PROGRAM_MAX_BYTES` | Lowers the program budget for the whole archive, never raises it. Must be a positive integer. A timeline that does not fit is focus-only with the reason `size-cap-exceeded`. |
306
+ | `RELIC_PROGRAM_ENCODER_OFF` | `1` makes the page report its H.264/AAC encoder as absent. Used by proofs, to exercise the focus-only path. |
241
307
  | `RELIC_FFMPEG_PATH` | Path to ffmpeg binary for generating video frame and audio waveform/artwork cover images. |
242
308
 
243
309
  ## Protocol
@@ -1302,7 +1302,7 @@ function inline(source) {
1302
1302
  });
1303
1303
  return out.split(SENTINEL).join("");
1304
1304
  }
1305
- function renderMarkdown(source) {
1305
+ function parseMarkdown(source) {
1306
1306
  const lines = source.replace(/\r\n?/g, `
1307
1307
  `).split(`
1308
1308
  `);
@@ -1312,7 +1312,7 @@ function renderMarkdown(source) {
1312
1312
  const flushParagraph = () => {
1313
1313
  if (paragraph.length === 0)
1314
1314
  return;
1315
- out.push(`<p>${inline(paragraph.join(" "))}</p>`);
1315
+ out.push({ kind: "paragraph", text: paragraph.join(" ") });
1316
1316
  paragraph = [];
1317
1317
  };
1318
1318
  while (index < lines.length) {
@@ -1328,22 +1328,24 @@ function renderMarkdown(source) {
1328
1328
  index++;
1329
1329
  }
1330
1330
  index++;
1331
- const attribute = language.length > 0 ? ` data-language="${escapeHtml(language)}"` : "";
1332
- out.push(`<pre${attribute}><code>${escapeHtml(body.join(`
1333
- `))}</code></pre>`);
1331
+ out.push({ kind: "code", language, text: body.join(`
1332
+ `) });
1334
1333
  continue;
1335
1334
  }
1336
1335
  const heading = /^(#{1,6})\s+(.*)$/.exec(line);
1337
1336
  if (heading !== null) {
1338
1337
  flushParagraph();
1339
- const level = heading[1].length;
1340
- out.push(`<h${level}>${inline(heading[2])}</h${level}>`);
1338
+ out.push({
1339
+ kind: "heading",
1340
+ level: heading[1].length,
1341
+ text: heading[2]
1342
+ });
1341
1343
  index++;
1342
1344
  continue;
1343
1345
  }
1344
1346
  if (/^\s*(-\s*-\s*-|\*\s*\*\s*\*|_\s*_\s*_)[-*_\s]*$/.test(line)) {
1345
1347
  flushParagraph();
1346
- out.push("<hr>");
1348
+ out.push({ kind: "rule" });
1347
1349
  index++;
1348
1350
  continue;
1349
1351
  }
@@ -1354,8 +1356,8 @@ function renderMarkdown(source) {
1354
1356
  quoted.push(lines[index].replace(/^\s*>\s?/, ""));
1355
1357
  index++;
1356
1358
  }
1357
- out.push(`<blockquote>${renderMarkdown(quoted.join(`
1358
- `))}</blockquote>`);
1359
+ out.push({ kind: "quote", blocks: parseMarkdown(quoted.join(`
1360
+ `)) });
1359
1361
  continue;
1360
1362
  }
1361
1363
  if (/^\s*([-*+]|\d+\.)\s+/.test(line)) {
@@ -1373,9 +1375,7 @@ function renderMarkdown(source) {
1373
1375
  }
1374
1376
  index++;
1375
1377
  }
1376
- const tag = ordered ? "ol" : "ul";
1377
- const body = items.map((item) => `<li>${inline(item)}</li>`).join("");
1378
- out.push(`<${tag}>${body}</${tag}>`);
1378
+ out.push({ kind: "list", ordered, items });
1379
1379
  continue;
1380
1380
  }
1381
1381
  if (line.includes("|") && isTableSeparator(lines[index + 1])) {
@@ -1387,7 +1387,7 @@ function renderMarkdown(source) {
1387
1387
  rows.push(splitRow(lines[index]));
1388
1388
  index++;
1389
1389
  }
1390
- out.push('<div class="md-scroll"><table><thead><tr>' + header.map((cell) => `<th>${inline(cell)}</th>`).join("") + "</tr></thead><tbody>" + rows.map((row) => `<tr>${row.map((cell) => `<td>${inline(cell)}</td>`).join("")}</tr>`).join("") + "</tbody></table></div>");
1390
+ out.push({ kind: "table", header, rows });
1391
1391
  continue;
1392
1392
  }
1393
1393
  if (line.trim().length === 0) {
@@ -1399,7 +1399,34 @@ function renderMarkdown(source) {
1399
1399
  index++;
1400
1400
  }
1401
1401
  flushParagraph();
1402
- return out.join(`
1402
+ return out;
1403
+ }
1404
+ function renderBlock(block) {
1405
+ switch (block.kind) {
1406
+ case "heading":
1407
+ return `<h${block.level}>${inline(block.text)}</h${block.level}>`;
1408
+ case "paragraph":
1409
+ return `<p>${inline(block.text)}</p>`;
1410
+ case "code": {
1411
+ const attribute = block.language.length > 0 ? ` data-language="${escapeHtml(block.language)}"` : "";
1412
+ return `<pre${attribute}><code>${escapeHtml(block.text)}</code></pre>`;
1413
+ }
1414
+ case "rule":
1415
+ return "<hr>";
1416
+ case "quote":
1417
+ return `<blockquote>${block.blocks.map(renderBlock).join(`
1418
+ `)}</blockquote>`;
1419
+ case "list": {
1420
+ const tag = block.ordered ? "ol" : "ul";
1421
+ const body = block.items.map((item) => `<li>${inline(item)}</li>`);
1422
+ return `<${tag}>${body.join("")}</${tag}>`;
1423
+ }
1424
+ case "table":
1425
+ return '<div class="md-scroll"><table><thead><tr>' + block.header.map((cell) => `<th>${inline(cell)}</th>`).join("") + "</tr></thead><tbody>" + block.rows.map((row) => `<tr>${row.map((cell) => `<td>${inline(cell)}</td>`).join("")}</tr>`).join("") + "</tbody></table></div>";
1426
+ }
1427
+ }
1428
+ function renderMarkdown(source) {
1429
+ return parseMarkdown(source).map(renderBlock).join(`
1403
1430
  `);
1404
1431
  }
1405
1432
  function isTableSeparator(line) {
@@ -11532,6 +11532,12 @@ var documentSequences = new WeakMap;
11532
11532
 
11533
11533
  // ../relic-format/src/otio-identity.ts
11534
11534
  var mediaWitnessCache = new WeakMap;
11535
+ // ../relic-format/src/otio-program.ts
11536
+ var MAX_OTIO_PROGRAM_MANIFEST_BYTES = 16 * 1024 * 1024;
11537
+ var OTIO_PROGRAM_MAX_BYTES = 100 * 1024 * 1024;
11538
+ var UTF8 = new TextEncoder;
11539
+ var MAX_FRAMES = Number.MAX_SAFE_INTEGER;
11540
+ var MAX_AUDIO_SAMPLES = Number.MAX_SAFE_INTEGER;
11535
11541
  // ../relic-format/src/share.ts
11536
11542
  var SHARE_PROOF_INFO = new TextEncoder().encode("relic/share-proof/v1");
11537
11543
  // ../relic-viewer/src/sandbox.ts
@@ -11881,12 +11887,25 @@ function createScrollportWatch(onChange) {
11881
11887
  }
11882
11888
  function concealMarks(marks, watch, live = true) {
11883
11889
  watch.sync(marks.map((mark) => mark.node));
11884
- const merged = new Map;
11890
+ if (live) {
11891
+ for (const { node, subject } of marks) {
11892
+ if (subject === node && node.getAttribute(CONCEALED_ATTRIBUTE) === "hidden")
11893
+ node.removeAttribute(CONCEALED_ATTRIBUTE);
11894
+ }
11895
+ }
11896
+ const measured = [];
11885
11897
  for (const { node, subject } of marks) {
11886
11898
  const id = node.getAttribute("data-comment-id");
11887
11899
  if (id === null || id === "")
11888
11900
  continue;
11889
- const concealment = !live || !isRendered(subject) ? "hidden" : !watch.inView(node) ? "offscreen" : "shown";
11901
+ measured.push({
11902
+ node,
11903
+ id,
11904
+ concealment: !live || !isRendered(subject) ? "hidden" : !watch.inView(node) ? "offscreen" : "shown"
11905
+ });
11906
+ }
11907
+ const merged = new Map;
11908
+ for (const { node, id, concealment } of measured) {
11890
11909
  setConcealed(node, concealment === "shown" ? undefined : concealment);
11891
11910
  const previous = merged.get(id);
11892
11911
  merged.set(id, mergeReason(previous, concealment));