@ai-ecoverse/slicc-spectrum 1.110.0 → 1.111.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.
package/README.md CHANGED
@@ -560,10 +560,10 @@ A rejection shows the error card with its sentence (a check without a `preview`
560
560
 
561
561
  A SLICC sprinkle (`.shtml`), as a panel or, with `inline`, as a dip in the chat that grows to fit, even when it loads out of view (the frame measures itself on load, after icons and fonts arrive, and on every resize).
562
562
  A dip has a tab-like handle: drag it into the dock (or onto a rail, or use its open-as-panel button, `data-action="detach-sprinkle"`, an extra-small action button whose tap target pads out to 24 × 24 px on a coarse pointer while it looks the same) and it lives on as a sprinkle panel, with a note in the chat where it was and a **Show** button (`data-action="show-sprinkle"`) that brings the panel forward; which dips are out is saved in `slicc-ui.dips`.
563
- It runs in a sandboxed frame (`allow-scripts` only) with SLICC's sprinkle theme (the `--s2-*` tokens and `.sprinkle-*` classes), the app's fonts, and Lucide icons (`<i data-lucide>`, `LucideIcons.render()`).
564
- It talks back through `slicc.lick({ action, data })`, which reaches the owning cone as a `sprinkle` lick, and, when the port has `call`, reads files and keeps state (see [Sprinkle bridge](#sprinkle-bridge)).
563
+ It runs in a sandboxed frame (`allow-scripts allow-forms`) with SLICC's sprinkle theme (the `--s2-*` tokens and `.sprinkle-*` classes), the app's fonts, and Lucide icons (`<i data-lucide>`, `LucideIcons.render()`).
564
+ It talks back through `slicc.lick({ action, data })`, which reaches the owning cone as a `sprinkle` lick, and, when the port has `call`, reads files, keeps state and runs its own `cgi-bin/` scripts (see [Sprinkle bridge](#sprinkle-bridge)).
565
565
  Panel sprinkles sit at the top of the right rail with their Lucide icon.
566
- On a guest's page (the model's `seat` set), every sprinkle, inline or panel, is view only: its frame is inert, it says "View only on this device", and its licks and `setState` calls go nowhere.
566
+ On a guest's page (the model's `seat` set), every sprinkle, inline or panel, is view only: its frame is inert, it says "View only on this device", its licks and `setState` calls go nowhere, and its `cgi-bin/` answers every request with a `403`.
567
567
 
568
568
  #### `<slicc-network>`
569
569
 
@@ -714,6 +714,25 @@ A refusal or a missing file rejects with an `Error`.
714
714
  There is no `exec`: a sprinkle that needs a command asks its cone with `slicc.lick()`.
715
715
  Without `call`, none of the four exist, so guard them (`typeof slicc.readFile === 'function'`) and fall back.
716
716
 
717
+ #### `cgi-bin/`
718
+
719
+ A sprinkle's `fetch()` of a URL under `new URL('cgi-bin/', document.baseURI)` runs the sprinkle's own script through the port's `cgi`, so `fetch('cgi-bin/items')` answers with what `<sprinkle>/cgi-bin/items` prints, CGI/1.1 style.
720
+ Every other URL goes to the frame's own `fetch`, unchanged.
721
+ The answer is a real `Response`: `status`, `statusText`, `headers`, `url`, and a body that streams as the script prints, so `json()`, `text()` and `body.getReader()` all work; a `HEAD` and a `101`, `103`, `204`, `205` or `304` have no body.
722
+ The request goes as `{ method, path, query, headers, body }`, as the `Request` constructor made it (a string body gets `text/plain;charset=UTF-8`, a `FormData` its multipart boundary), with `Accept: */*` and the browser's `Accept-Language` when the sprinkle set none; `path` is what follows `cgi-bin/`, still percent-encoded, `query` has no `?`, and the body is base64.
723
+ A `301`, `302`, `303`, `307` or `308` with `Location` is followed as `fetch` follows redirects (a `303`, or a `301` or `302` after a `POST`, turns into a `GET` without a body), up to 20; `redirect: 'manual'` returns the redirect itself and `redirect: 'error'` rejects.
724
+ Aborting the `fetch` (its `signal`) or cancelling the body stops the script (`cgiClose`); a failed `cgi` or `cgiRead` rejects with a `TypeError`, as a network error does.
725
+
726
+ A form never navigates the frame: the bridge cancels every `submit` (except `method="dialog"`) and `form.submit()`.
727
+ A form whose action (or its submitter's `formaction`) is under `cgi-bin/` goes the same way, with its `formmethod`, and as `enctype` says: `application/x-www-form-urlencoded` by default, `multipart/form-data` or `text/plain`; a `GET` form replaces the action's query.
728
+ The bridge cancels the browser's own `submit` before any page handler runs and stops it there, then dispatches a copy (a `SubmitEvent` with the same `submitter`, so `isTrusted` is `false`) to the form, so stopping the copy's propagation can't navigate the frame.
729
+ A handler that calls `preventDefault()` on that copy, on the form or above it, or an `onsubmit` that returns `false`, keeps the bridge from sending it.
730
+ The form then gets a cancelable `slicc-cgi` event with `detail.response` (or `detail.error`); unless it's cancelled, an answer with `Location` rebuilds the sprinkle, with its latest state.
731
+
732
+ The element sends `cgi` with its own sprinkle id, never one from the frame, posts the head as soon as the port answers, then calls `cgiRead` until `done`.
733
+ A guest seat gets a `403` for every request, a frame without `call` a `404`, and a read-only, watch-only or frozen frame may `GET` and `HEAD`; any other method gets a `403` without reaching the port.
734
+ The port does every other check (paths, ownership, limits).
735
+
717
736
  ### Panel command
718
737
 
719
738
  `panelCommand(app, argv, caller)`, exported from `dist/slicc-ui.js`, runs `panel` for an agent or a terminal, so the agent can arrange the UI.
@@ -833,8 +852,9 @@ Sprinkles (`inline` ones show as dips in the chat, not in the rail) and `send`,
833
852
  The rail groups sprinkles by their `agentId`: the selected cone's come first, then the other cones' in the agents list's order, each group set apart by a line (`.rail .cones`) and each button carrying `data-cone`.
834
853
  A sprinkle of another cone reads `{cone name} / {title}` in its tooltip, its label and the tab it opens; the name comes from the agents list, so give each sprinkle an `id` that stays stable when a cone is renamed, such as `{coneId}/{name}`.
835
854
  The optional `call(id, method, args)` serves the [sprinkle bridge](#sprinkle-bridge) for sprinkle `id`: `readFile` and `exists` (`[path]`) resolve with a string and a boolean, `getState` (`[]`) with the saved value or `null`, and `setState` (`[value]`) once it is saved.
836
- The adapter owns path policy and keeps state per sprinkle; `<slicc-sprinkle>` awaits `getState` before it builds a frame, mirrors what the frame saves, and forwards only these four methods from its own frame.
837
- The dummy keeps state in memory and maps `/shared/x` to `/home/x` in its files, refusing anything outside `/home`.
855
+ `cgi` (`[{ method, path, query, headers, body }]`, with `headers` as `[name, value]` pairs and `body` base64 or `null`) resolves once the script's CGI headers are in, with `{ status, statusText, headers, body, done, stream }`; `cgiRead` (`[stream]`) long-polls for `{ body, done }`, and `cgiClose` (`[stream]`) stops the script (see [`cgi-bin/`](#cgi-bin)).
856
+ The adapter owns path policy and keeps state per sprinkle; `<slicc-sprinkle>` awaits `getState` before it builds a frame, mirrors what the frame saves, and forwards only the first four methods from its own frame's `slicc-call`; the `cgi` ones only through its `fetch` and forms.
857
+ The dummy keeps state in memory and maps `/shared/x` to `/home/x` in its files, refusing anything outside `/home`; it answers every `cgi` with a canned `200` `application/json` (`X-Slicc-Cgi: dummy`) that echoes the method, path, query, content type and body, in two chunks.
838
858
 
839
859
  #### `tray`
840
860
 
@@ -4908,11 +4908,29 @@ function homePath(path) {
4908
4908
  throw new Error(`EACCES: ${text} is outside /home`);
4909
4909
  return mapped;
4910
4910
  }
4911
+ var base64 = (bytes) => btoa(String.fromCharCode(...bytes));
4912
+ var unbase64 = (text) => new TextDecoder().decode(Uint8Array.from(atob(text), (char) => char.charCodeAt(0)));
4913
+ function cannedCgi(request) {
4914
+ const given = request ?? {};
4915
+ const headers = Array.isArray(given.headers) ? given.headers : [];
4916
+ const type = headers.find(([name2]) => String(name2).toLowerCase() === "content-type");
4917
+ return `${JSON.stringify({
4918
+ ok: true,
4919
+ method: given.method ?? "GET",
4920
+ path: given.path ?? "",
4921
+ query: given.query ?? "",
4922
+ type: type?.[1] ?? null,
4923
+ body: typeof given.body === "string" ? unbase64(given.body) : null
4924
+ })}
4925
+ `;
4926
+ }
4911
4927
  var DummySprinkles = class extends Emitter {
4912
4928
  #sprinkles;
4913
4929
  #agent;
4914
4930
  #files;
4915
4931
  #state = /* @__PURE__ */ new Map();
4932
+ #streams = /* @__PURE__ */ new Map();
4933
+ #next = 0;
4916
4934
  constructor(sprinkles2, agent, files3) {
4917
4935
  super();
4918
4936
  this.#sprinkles = sprinkles2.map((sprinkle) => ({ ...sprinkle }));
@@ -4940,10 +4958,39 @@ var DummySprinkles = class extends Emitter {
4940
4958
  this.#state.set(id, structuredClone(args[0] ?? null));
4941
4959
  return void 0;
4942
4960
  }
4961
+ if (method === "cgi") return this.#cgi(args[0]);
4962
+ if (method === "cgiRead") return this.#read(String(args[0]));
4963
+ if (method === "cgiClose") {
4964
+ this.#streams.delete(String(args[0]));
4965
+ return null;
4966
+ }
4943
4967
  const path = homePath(args[0]);
4944
4968
  if (method === "exists") return (await this.#files.list()).some((entry) => entry.path === path);
4945
4969
  return this.#files.read(path);
4946
4970
  }
4971
+ #cgi(request) {
4972
+ const bytes = new TextEncoder().encode(cannedCgi(request));
4973
+ const half = Math.ceil(bytes.length / 2);
4974
+ const stream = `dummy-cgi-${++this.#next}`;
4975
+ this.#streams.set(stream, bytes.subarray(half));
4976
+ return {
4977
+ status: 200,
4978
+ statusText: "OK",
4979
+ headers: [
4980
+ ["Content-Type", "application/json"],
4981
+ ["X-Slicc-Cgi", "dummy"]
4982
+ ],
4983
+ body: base64(bytes.subarray(0, half)),
4984
+ done: false,
4985
+ stream
4986
+ };
4987
+ }
4988
+ #read(stream) {
4989
+ const rest = this.#streams.get(stream);
4990
+ if (rest === void 0) throw new Error(`there is no cgi-bin stream ${stream}`);
4991
+ this.#streams.delete(stream);
4992
+ return { body: base64(rest), done: true };
4993
+ }
4947
4994
  };
4948
4995
 
4949
4996
  // src/dummy/tabs.ts