@ai-ecoverse/slicc-agent 3.43.0 → 3.43.1

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
@@ -363,9 +363,9 @@ pi core has no browser tooling, and Mario's pi-skills `browser-tools` launches i
363
363
  - **`curlwright`** (slicc-cdp ≥ 1.2.1) runs curl-style requests as a page-context `fetch()` in an open tab, with that tab's cookies and origin. The skill prefers it to UI clicks for reading data the app already fetches, and plain `curl` when no login is needed. Writes need the user's yes like clicks do, and response bodies are data.
364
364
  - **The `system` section** says the browser is connected when `SLICC_CDP_URL` is set and `playwright-cli` is on PATH (naming `curlwright` too when it's there), and lists it under "Not here yet" otherwise.
365
365
  - **Images:** durable's `read` doesn't return images, so a wrapper (`slicc-images`) answers `read` of a PNG, JPEG, GIF or WebP with image content. In a worker that can draw, images wider than 1600 px are scaled down, and an image over 1 MB of base64 is re-encoded as JPEG or refused with a hint. The adapter puts image content from a tool result on the tool card (`ToolCall.image`), so screenshots show in the chat.
366
- - **SLICC's own pages are off limits** (PRE-1). The `slicc-browser-guard` extension ([`src/browser/guard.ts`](src/browser/guard.ts)) refuses a `bash` call, direct or from codemode, that runs `playwright-cli` or `curlwright` and names a URL on SLICC's origin: `sliccy.ai`, `www.sliccy.ai`, any single-label `<name>.sliccy.ai` over https on port 443, and the origin the worker runs on (`location.origin`). It also refuses `*.kernel.localhost` names other than the agent's own kernel servers, `<port>.kernel.localhost` for a port 1–65535 other than the CDP facade's 9222. The answer is "SLICC's own pages can't be opened or driven from the browser tools.", and the `browser` skill says so too.
367
- - **How it reads a command:** a shell tokenizer ([`src/browser/shell.ts`](src/browser/shell.ts)) removes quotes and escapes (`'…'`, `"…"`, `$'…'`, `$"…"`, `\`), and reads into `$(…)`, backticks, `<(…)` and words that are themselves scripts, such as `bash -c` and `eval` arguments, four levels deep. If any word is `playwright-cli` or `curlwright` (by its last path segment), every word is checked: as a URL, as `https://` plus the word, after a `NAME=` or `--flag=` prefix, and for URLs and `//host` inside it. `new URL()` normalises the host (case, IDNA, full-width characters, percent escapes, userinfo); a trailing dot is dropped, `ws:` counts as `http:`, `wss:` as `https:`, and `blob:`, `view-source:` and `filesystem:` are checked by their inner URL. A `http://` URL on a sliccy.ai name is refused too, since it redirects to https.
368
- - **The list** lives in [`src/browser/origins.ts`](src/browser/origins.ts) (`protectedOrigins`, `isProtected(url, extra)`) until `@ai-ecoverse/slicc-browsers` ships it, with the same patterns and shape. `*.kernel.localhost` isn't on the shared list; that rule is this guard's own.
366
+ - **SLICC's own pages are off limits** (PRE-1). The `slicc-browser-guard` extension ([`src/browser/guard.ts`](src/browser/guard.ts)) refuses a `bash` call, direct or from codemode, that runs `playwright-cli` or `curlwright` and names a protected URL: one that [`@ai-ecoverse/slicc-browsers`](https://github.com/ai-ecoverse/slicc-browsers#protected-origins)' `protectedOrigins` matcher protects, with the origin the worker runs on (`location.origin`) as its extra origin. That covers `sliccy.ai` and every name under it (including `www` and `vault`) over http or https on any port, the worker's origin, `devtools:`, and `blob:`, `filesystem:` and `view-source:` by their inner URL. It also refuses `*.kernel.localhost` names other than the agent's own kernel servers, `<port>.kernel.localhost` for a port 1–65535 other than the CDP facade's 9222. The answer is "SLICC's own pages can't be opened or driven from the browser tools.", and the `browser` skill says so too.
367
+ - **How it reads a command:** a shell tokenizer ([`src/browser/shell.ts`](src/browser/shell.ts)) removes quotes and escapes (`'…'`, `"…"`, `$'…'`, `$"…"`, `\`), and reads into `$(…)`, backticks, `<(…)` and words that are themselves scripts, such as `bash -c` and `eval` arguments, four levels deep. If any word is `playwright-cli` or `curlwright` (by its last path segment), every word is checked: as a URL, as `https://` plus the word, after a `NAME=` or `--flag=` prefix, and for URLs and `//host` inside it. Tabs and newlines are removed and surrounding spaces trimmed, as the URL parser does; the matcher parses with the WHATWG `URL`, so case, IDNA, full-width characters, percent escapes, backslashes and userinfo all count. `about:blank`, `about:srcdoc` and the empty URL are allowed: a tab `playwright-cli` opens has no SLICC opener, and a command that also names a protected URL is refused anyway.
368
+ - **The list** is slicc-browsers' `catalog.json` `protectedOrigins` with its reference matcher `protected.mjs`, wrapped by [`src/browser/origins.ts`](src/browser/origins.ts) (`protectedMatcher(extra)`); the unit tests run every URL and target vector in its `protected.json`. `*.kernel.localhost` isn't on the shared list; that rule is this guard's own. The build keeps the `with { type: 'json' }` import attribute, so the catalog loads unbundled.
369
369
  - **What it can't catch:** it's defence in depth, and the browser tools themselves (slicc-cdp, the kernel's CDP facade and slicc-cli's `cdp` link) enforce the rule. It doesn't see a URL built at run time (from a variable set elsewhere, `printf`, brace expansion or a file), a browser command whose name is built at run time, a script or program file that drives the browser, a tab id (`--tab`, `tab-select`) that points at a SLICC tab, or a page that navigates there by itself. It may refuse a command that only names a SLICC URL alongside a browser command, such as `curl https://seven.sliccy.ai && playwright-cli open https://example.com`.
370
370
  - There are no browser licks yet.
371
371
 
@@ -4,20 +4,14 @@ import {
4
4
  ToolTask,
5
5
  wrapTool
6
6
  } from "@earendil-works/pi-durable";
7
- import { isProtected } from "./origins.js";
7
+ import { PROTECTED_RULES, protectedMatcher } from "./origins.js";
8
8
  import { words } from "./shell.js";
9
9
  const BROWSER_GUARD = "slicc-browser-guard";
10
10
  const REFUSAL = "SLICC's own pages can't be opened or driven from the browser tools.";
11
11
  const FACADE_PORT = 9222;
12
12
  const BROWSER = /* @__PURE__ */ new Set(["playwright-cli", "curlwright"]);
13
13
  const WEB = /* @__PURE__ */ new Set(["http:", "https:", "ws:", "wss:"]);
14
- const WRAPPED = /* @__PURE__ */ new Set(["blob:", "view-source:", "filesystem:"]);
15
- const SECURE = {
16
- "http:": "http:",
17
- "https:": "https:",
18
- "ws:": "http:",
19
- "wss:": "https:"
20
- };
14
+ const INNER = new Set(PROTECTED_RULES.innerSchemes.map((scheme) => `${scheme}:`));
21
15
  const SCHEMED = /^[a-z][a-z0-9+.-]*:/i;
22
16
  const EMBEDDED = /[a-z][a-z0-9+.-]*:\/\/[^\s'"`<>(){}\\]+/gi;
23
17
  const RELATIVE = /(?<![:\w/])\/\/[^\s'"`<>(){}\\/]+/g;
@@ -35,20 +29,19 @@ function ownKernelServer(host) {
35
29
  const port = OWN_KERNEL.exec(host)?.[1];
36
30
  return port !== void 0 && Number(port) <= 65535 && Number(port) !== FACADE_PORT;
37
31
  }
38
- function forbidden(url, origins, depth = 0) {
39
- const parsed = parse(url);
40
- if (!parsed) return false;
41
- if (WRAPPED.has(parsed.protocol)) {
42
- const inner = parsed.href.slice(parsed.protocol.length);
43
- return depth < 2 && [inner, `https://${inner}`].some((each) => forbidden(each, origins, depth + 1));
44
- }
45
- if (!WEB.has(parsed.protocol) || !parsed.hostname) return false;
32
+ function foreignKernel(url) {
33
+ let parsed = parse(url);
34
+ for (let depth = 0; parsed && INNER.has(parsed.protocol) && depth < 8; depth++)
35
+ parsed = parse(parsed.href.slice(parsed.protocol.length));
36
+ if (!parsed || !WEB.has(parsed.protocol)) return false;
46
37
  const host = parsed.hostname.replace(/\.+$/, "");
47
- if (host === KERNEL || host.endsWith(`.${KERNEL}`)) return !ownKernelServer(host);
48
- const port = parsed.port ? `:${parsed.port}` : "";
49
- return isProtected(`${SECURE[parsed.protocol]}//${host}${port}`, origins) || isProtected(`https://${host}${port}`);
38
+ return (host === KERNEL || host.endsWith(`.${KERNEL}`)) && !ownKernelServer(host);
39
+ }
40
+ function forbidden(url, matcher) {
41
+ return matcher.url(url) || foreignKernel(url);
50
42
  }
51
- function candidates(word) {
43
+ function candidates(raw) {
44
+ const word = raw.replace(/[\t\n\r]/g, "").replace(/^[\0- ]+|[\0- ]+$/g, "");
52
45
  const value = word.replace(PREFIX, "");
53
46
  const found = [
54
47
  word,
@@ -64,10 +57,10 @@ function candidates(word) {
64
57
  function browserCommand(word) {
65
58
  return BROWSER.has(word.slice(word.lastIndexOf("/") + 1));
66
59
  }
67
- function refused(command, origins) {
60
+ function refused(command, matcher) {
68
61
  const all = words(command);
69
62
  if (!all.some(browserCommand)) return false;
70
- return all.some((word) => candidates(word).some((url) => forbidden(url, origins)));
63
+ return all.some((word) => candidates(word).some((url) => forbidden(url, matcher)));
71
64
  }
72
65
  function commandOf(args) {
73
66
  const command = args?.command;
@@ -75,18 +68,19 @@ function commandOf(args) {
75
68
  }
76
69
  function browserGuard(origins, tools) {
77
70
  const bash = tools.find((tool) => tool.name === "bash");
71
+ const matcher = protectedMatcher(origins);
78
72
  return defineExtension({
79
73
  name: BROWSER_GUARD,
80
74
  hooks: [
81
75
  hook(ToolTask, {
82
- beforeTool: (call) => call.name === bash.name && refused(commandOf(call.arguments), origins) ? { block: REFUSAL } : void 0
76
+ beforeTool: (call) => call.name === bash.name && refused(commandOf(call.arguments), matcher) ? { block: REFUSAL } : void 0
83
77
  })
84
78
  ],
85
79
  wraps: [
86
80
  wrapTool(bash, (inner) => ({
87
81
  ...inner,
88
82
  execute(args, api, context) {
89
- if (!refused(commandOf(args), origins)) return inner.execute(args, api, context);
83
+ if (!refused(commandOf(args), matcher)) return inner.execute(args, api, context);
90
84
  return Promise.resolve({ content: [{ type: "text", text: REFUSAL }], isError: true });
91
85
  }
92
86
  }))
@@ -100,6 +94,7 @@ export {
100
94
  browserGuard,
101
95
  candidates,
102
96
  forbidden,
97
+ foreignKernel,
103
98
  ownKernelServer,
104
99
  refused
105
100
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/browser/guard.ts"],
4
- "sourcesContent": ["import {\n defineExtension,\n type Extension,\n hook,\n type ToolRegistration,\n ToolTask,\n wrapTool,\n} from '@earendil-works/pi-durable';\nimport { isProtected } from './origins.ts';\nimport { words } from './shell.ts';\n\nexport const BROWSER_GUARD = 'slicc-browser-guard';\nexport const REFUSAL = \"SLICC's own pages can't be opened or driven from the browser tools.\";\nexport const FACADE_PORT = 9222;\n\nconst BROWSER = new Set(['playwright-cli', 'curlwright']);\nconst WEB = new Set(['http:', 'https:', 'ws:', 'wss:']);\nconst WRAPPED = new Set(['blob:', 'view-source:', 'filesystem:']);\nconst SECURE: Record<string, string> = {\n 'http:': 'http:',\n 'https:': 'https:',\n 'ws:': 'http:',\n 'wss:': 'https:',\n};\nconst SCHEMED = /^[a-z][a-z0-9+.-]*:/i;\nconst EMBEDDED = /[a-z][a-z0-9+.-]*:\\/\\/[^\\s'\"`<>(){}\\\\]+/gi;\nconst RELATIVE = /(?<![:\\w/])\\/\\/[^\\s'\"`<>(){}\\\\/]+/g;\nconst PREFIX = /^(?:[A-Za-z_][A-Za-z0-9_]*|-{1,2}[A-Za-z0-9-]+)=/;\nconst KERNEL = 'kernel.localhost';\nconst OWN_KERNEL = /^([1-9][0-9]{0,4})\\.kernel\\.localhost$/;\n\nfunction parse(url: string): URL | undefined {\n try {\n return new URL(url);\n } catch {\n return undefined;\n }\n}\n\nexport function ownKernelServer(host: string): boolean {\n const port = OWN_KERNEL.exec(host)?.[1];\n return port !== undefined && Number(port) <= 65535 && Number(port) !== FACADE_PORT;\n}\n\nexport function forbidden(url: string, origins: readonly string[], depth = 0): boolean {\n const parsed = parse(url);\n if (!parsed) return false;\n if (WRAPPED.has(parsed.protocol)) {\n const inner = parsed.href.slice(parsed.protocol.length);\n return (\n depth < 2 && [inner, `https://${inner}`].some((each) => forbidden(each, origins, depth + 1))\n );\n }\n if (!WEB.has(parsed.protocol) || !parsed.hostname) return false;\n const host = parsed.hostname.replace(/\\.+$/, '');\n if (host === KERNEL || host.endsWith(`.${KERNEL}`)) return !ownKernelServer(host);\n const port = parsed.port ? `:${parsed.port}` : '';\n return (\n isProtected(`${SECURE[parsed.protocol]}//${host}${port}`, origins) ||\n isProtected(`https://${host}${port}`)\n );\n}\n\nexport function candidates(word: string): string[] {\n const value = word.replace(PREFIX, '');\n const found = [\n word,\n value,\n ...(value.match(EMBEDDED) ?? []),\n ...(value.match(RELATIVE) ?? []).map((relative) => `https:${relative}`),\n ];\n if (!/^[/.~-]/.test(value) && !/^[a-z][a-z0-9+.-]*:\\/\\//i.test(value))\n found.push(`https://${value}`);\n if (value.startsWith('//')) found.push(`https:${value}`);\n return found.filter((each) => SCHEMED.test(each));\n}\n\nfunction browserCommand(word: string): boolean {\n return BROWSER.has(word.slice(word.lastIndexOf('/') + 1));\n}\n\nexport function refused(command: string, origins: readonly string[]): boolean {\n const all = words(command);\n if (!all.some(browserCommand)) return false;\n return all.some((word) => candidates(word).some((url) => forbidden(url, origins)));\n}\n\nfunction commandOf(args: unknown): string {\n const command = (args as { command?: unknown } | undefined)?.command;\n return typeof command === 'string' ? command : '';\n}\n\nexport function browserGuard(\n origins: readonly string[],\n tools: readonly ToolRegistration[]\n): Extension {\n const bash = tools.find((tool) => tool.name === 'bash') as ToolRegistration;\n return defineExtension({\n name: BROWSER_GUARD,\n hooks: [\n hook(ToolTask, {\n beforeTool: (call) =>\n call.name === bash.name && refused(commandOf(call.arguments), origins)\n ? { block: REFUSAL }\n : undefined,\n }),\n ],\n wraps: [\n wrapTool(bash, (inner) => ({\n ...inner,\n execute(args, api, context) {\n if (!refused(commandOf(args), origins)) return inner.execute(args, api, context);\n return Promise.resolve({ content: [{ type: 'text', text: REFUSAL }], isError: true });\n },\n })),\n ],\n });\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n defineExtension,\n type Extension,\n hook,\n type ToolRegistration,\n ToolTask,\n wrapTool,\n} from '@earendil-works/pi-durable';\nimport { PROTECTED_RULES, type ProtectedMatcher, protectedMatcher } from './origins.ts';\nimport { words } from './shell.ts';\n\nexport const BROWSER_GUARD = 'slicc-browser-guard';\nexport const REFUSAL = \"SLICC's own pages can't be opened or driven from the browser tools.\";\nexport const FACADE_PORT = 9222;\n\nconst BROWSER = new Set(['playwright-cli', 'curlwright']);\nconst WEB = new Set(['http:', 'https:', 'ws:', 'wss:']);\nconst INNER = new Set(PROTECTED_RULES.innerSchemes.map((scheme) => `${scheme}:`));\nconst SCHEMED = /^[a-z][a-z0-9+.-]*:/i;\nconst EMBEDDED = /[a-z][a-z0-9+.-]*:\\/\\/[^\\s'\"`<>(){}\\\\]+/gi;\nconst RELATIVE = /(?<![:\\w/])\\/\\/[^\\s'\"`<>(){}\\\\/]+/g;\nconst PREFIX = /^(?:[A-Za-z_][A-Za-z0-9_]*|-{1,2}[A-Za-z0-9-]+)=/;\nconst KERNEL = 'kernel.localhost';\nconst OWN_KERNEL = /^([1-9][0-9]{0,4})\\.kernel\\.localhost$/;\n\nfunction parse(url: string): URL | undefined {\n try {\n return new URL(url);\n } catch {\n return undefined;\n }\n}\n\nexport function ownKernelServer(host: string): boolean {\n const port = OWN_KERNEL.exec(host)?.[1];\n return port !== undefined && Number(port) <= 65535 && Number(port) !== FACADE_PORT;\n}\n\nexport function foreignKernel(url: string): boolean {\n let parsed = parse(url);\n for (let depth = 0; parsed && INNER.has(parsed.protocol) && depth < 8; depth++)\n parsed = parse(parsed.href.slice(parsed.protocol.length));\n if (!parsed || !WEB.has(parsed.protocol)) return false;\n const host = parsed.hostname.replace(/\\.+$/, '');\n return (host === KERNEL || host.endsWith(`.${KERNEL}`)) && !ownKernelServer(host);\n}\n\nexport function forbidden(url: string, matcher: ProtectedMatcher): boolean {\n return matcher.url(url) || foreignKernel(url);\n}\n\nexport function candidates(raw: string): string[] {\n const word = raw.replace(/[\\t\\n\\r]/g, '').replace(/^[\\0- ]+|[\\0- ]+$/g, '');\n const value = word.replace(PREFIX, '');\n const found = [\n word,\n value,\n ...(value.match(EMBEDDED) ?? []),\n ...(value.match(RELATIVE) ?? []).map((relative) => `https:${relative}`),\n ];\n if (!/^[/.~-]/.test(value) && !/^[a-z][a-z0-9+.-]*:\\/\\//i.test(value))\n found.push(`https://${value}`);\n if (value.startsWith('//')) found.push(`https:${value}`);\n return found.filter((each) => SCHEMED.test(each));\n}\n\nfunction browserCommand(word: string): boolean {\n return BROWSER.has(word.slice(word.lastIndexOf('/') + 1));\n}\n\nexport function refused(command: string, matcher: ProtectedMatcher): boolean {\n const all = words(command);\n if (!all.some(browserCommand)) return false;\n return all.some((word) => candidates(word).some((url) => forbidden(url, matcher)));\n}\n\nfunction commandOf(args: unknown): string {\n const command = (args as { command?: unknown } | undefined)?.command;\n return typeof command === 'string' ? command : '';\n}\n\nexport function browserGuard(\n origins: readonly string[],\n tools: readonly ToolRegistration[]\n): Extension {\n const bash = tools.find((tool) => tool.name === 'bash') as ToolRegistration;\n const matcher = protectedMatcher(origins);\n return defineExtension({\n name: BROWSER_GUARD,\n hooks: [\n hook(ToolTask, {\n beforeTool: (call) =>\n call.name === bash.name && refused(commandOf(call.arguments), matcher)\n ? { block: REFUSAL }\n : undefined,\n }),\n ],\n wraps: [\n wrapTool(bash, (inner) => ({\n ...inner,\n execute(args, api, context) {\n if (!refused(commandOf(args), matcher)) return inner.execute(args, api, context);\n return Promise.resolve({ content: [{ type: 'text', text: REFUSAL }], isError: true });\n },\n })),\n ],\n });\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,41 +1,11 @@
1
- const protectedOrigins = {
2
- patterns: ["https://sliccy.ai", "https://www.sliccy.ai", "https://*.sliccy.ai"]
3
- };
4
- const LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
5
- function parse(url) {
6
- try {
7
- return new URL(url);
8
- } catch {
9
- return void 0;
10
- }
11
- }
12
- function hostOf(url) {
13
- return url.hostname.replace(/\.+$/, "");
14
- }
15
- function originOf(url) {
16
- const parsed = parse(url);
17
- if (!parsed || !["http:", "https:"].includes(parsed.protocol) || !parsed.hostname)
18
- return void 0;
19
- return `${parsed.protocol}//${hostOf(parsed)}${parsed.port ? `:${parsed.port}` : ""}`;
20
- }
21
- function matches(pattern, url) {
22
- const host = hostOf(url);
23
- const wanted = pattern.slice("https://".length);
24
- if (!wanted.startsWith("*.")) return host === wanted;
25
- const suffix = wanted.slice(1);
26
- return host.endsWith(suffix) && LABEL.test(host.slice(0, -suffix.length));
27
- }
28
- function isProtected(url, extra = []) {
29
- const origin = originOf(url);
30
- if (!origin) return false;
31
- if (extra.some((other) => originOf(other) === origin)) return true;
32
- const parsed = parse(url);
33
- if (parsed.protocol !== "https:" || parsed.port) return false;
34
- return protectedOrigins.patterns.some((pattern) => matches(pattern, parsed));
1
+ import catalog from "@ai-ecoverse/slicc-browsers/catalog.json" with { type: "json" };
2
+ import { protectedOrigins } from "@ai-ecoverse/slicc-browsers/protected.mjs";
3
+ const PROTECTED_RULES = catalog.protectedOrigins;
4
+ function protectedMatcher(extra = []) {
5
+ return protectedOrigins(PROTECTED_RULES, [...extra]);
35
6
  }
36
7
  export {
37
- isProtected,
38
- originOf,
39
- protectedOrigins
8
+ PROTECTED_RULES,
9
+ protectedMatcher
40
10
  };
41
11
  //# sourceMappingURL=origins.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/browser/origins.ts"],
4
- "sourcesContent": ["export const protectedOrigins = {\n patterns: ['https://sliccy.ai', 'https://www.sliccy.ai', 'https://*.sliccy.ai'],\n} as const;\n\nconst LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;\n\nfunction parse(url: string): URL | undefined {\n try {\n return new URL(url);\n } catch {\n return undefined;\n }\n}\n\nfunction hostOf(url: URL): string {\n return url.hostname.replace(/\\.+$/, '');\n}\n\nexport function originOf(url: string): string | undefined {\n const parsed = parse(url);\n if (!parsed || !['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname)\n return undefined;\n return `${parsed.protocol}//${hostOf(parsed)}${parsed.port ? `:${parsed.port}` : ''}`;\n}\n\nfunction matches(pattern: string, url: URL): boolean {\n const host = hostOf(url);\n const wanted = pattern.slice('https://'.length);\n if (!wanted.startsWith('*.')) return host === wanted;\n const suffix = wanted.slice(1);\n return host.endsWith(suffix) && LABEL.test(host.slice(0, -suffix.length));\n}\n\nexport function isProtected(url: string, extra: readonly string[] = []): boolean {\n const origin = originOf(url);\n if (!origin) return false;\n if (extra.some((other) => originOf(other) === origin)) return true;\n const parsed = parse(url) as URL;\n if (parsed.protocol !== 'https:' || parsed.port) return false;\n return protectedOrigins.patterns.some((pattern) => matches(pattern, parsed));\n}\n"],
5
- "mappings": "AAAO,MAAM,mBAAmB;AAAA,EAC9B,UAAU,CAAC,qBAAqB,yBAAyB,qBAAqB;AAChF;AAEA,MAAM,QAAQ;AAEd,SAAS,MAAM,KAA8B;AAC3C,MAAI;AACF,WAAO,IAAI,IAAI,GAAG;AAAA,EACpB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,OAAO,KAAkB;AAChC,SAAO,IAAI,SAAS,QAAQ,QAAQ,EAAE;AACxC;AAEO,SAAS,SAAS,KAAiC;AACxD,QAAM,SAAS,MAAM,GAAG;AACxB,MAAI,CAAC,UAAU,CAAC,CAAC,SAAS,QAAQ,EAAE,SAAS,OAAO,QAAQ,KAAK,CAAC,OAAO;AACvE,WAAO;AACT,SAAO,GAAG,OAAO,QAAQ,KAAK,OAAO,MAAM,CAAC,GAAG,OAAO,OAAO,IAAI,OAAO,IAAI,KAAK,EAAE;AACrF;AAEA,SAAS,QAAQ,SAAiB,KAAmB;AACnD,QAAM,OAAO,OAAO,GAAG;AACvB,QAAM,SAAS,QAAQ,MAAM,WAAW,MAAM;AAC9C,MAAI,CAAC,OAAO,WAAW,IAAI,EAAG,QAAO,SAAS;AAC9C,QAAM,SAAS,OAAO,MAAM,CAAC;AAC7B,SAAO,KAAK,SAAS,MAAM,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,CAAC,OAAO,MAAM,CAAC;AAC1E;AAEO,SAAS,YAAY,KAAa,QAA2B,CAAC,GAAY;AAC/E,QAAM,SAAS,SAAS,GAAG;AAC3B,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,MAAM,KAAK,CAAC,UAAU,SAAS,KAAK,MAAM,MAAM,EAAG,QAAO;AAC9D,QAAM,SAAS,MAAM,GAAG;AACxB,MAAI,OAAO,aAAa,YAAY,OAAO,KAAM,QAAO;AACxD,SAAO,iBAAiB,SAAS,KAAK,CAAC,YAAY,QAAQ,SAAS,MAAM,CAAC;AAC7E;",
4
+ "sourcesContent": ["import catalog from '@ai-ecoverse/slicc-browsers/catalog.json' with { type: 'json' };\nimport { protectedOrigins } from '@ai-ecoverse/slicc-browsers/protected.mjs';\n\nexport const PROTECTED_RULES = catalog.protectedOrigins;\n\nexport interface ProtectedMatcher {\n url(text: string): boolean;\n target(target: { url: string; previous?: boolean; opener?: string }): boolean;\n cookieDomain(domain: string): boolean;\n storageKey(key: string): boolean;\n}\n\nexport function protectedMatcher(extra: readonly string[] = []): ProtectedMatcher {\n return protectedOrigins(PROTECTED_RULES, [...extra]);\n}\n"],
5
+ "mappings": "AAAA,OAAO,aAAa,2CAA2C,KAAK,EAAE,MAAM,OAAO;AACnF,SAAS,wBAAwB;AAE1B,MAAM,kBAAkB,QAAQ;AAShC,SAAS,iBAAiB,QAA2B,CAAC,GAAqB;AAChF,SAAO,iBAAiB,iBAAiB,CAAC,GAAG,KAAK,CAAC;AACrD;",
6
6
  "names": []
7
7
  }
@@ -1,9 +1,11 @@
1
1
  import { type Extension, type ToolRegistration } from '@earendil-works/pi-durable';
2
+ import { type ProtectedMatcher } from './origins.ts';
2
3
  export declare const BROWSER_GUARD = "slicc-browser-guard";
3
4
  export declare const REFUSAL = "SLICC's own pages can't be opened or driven from the browser tools.";
4
5
  export declare const FACADE_PORT = 9222;
5
6
  export declare function ownKernelServer(host: string): boolean;
6
- export declare function forbidden(url: string, origins: readonly string[], depth?: number): boolean;
7
- export declare function candidates(word: string): string[];
8
- export declare function refused(command: string, origins: readonly string[]): boolean;
7
+ export declare function foreignKernel(url: string): boolean;
8
+ export declare function forbidden(url: string, matcher: ProtectedMatcher): boolean;
9
+ export declare function candidates(raw: string): string[];
10
+ export declare function refused(command: string, matcher: ProtectedMatcher): boolean;
9
11
  export declare function browserGuard(origins: readonly string[], tools: readonly ToolRegistration[]): Extension;
@@ -1,5 +1,20 @@
1
- export declare const protectedOrigins: {
2
- readonly patterns: readonly ['https://sliccy.ai', 'https://www.sliccy.ai', 'https://*.sliccy.ai'];
1
+ export declare const PROTECTED_RULES: {
2
+ domains: string[];
3
+ hosts: string[];
4
+ schemes: string[];
5
+ innerSchemes: string[];
6
+ anySchemes: string[];
7
+ inheriting: string[];
8
+ refusal: string;
3
9
  };
4
- export declare function originOf(url: string): string | undefined;
5
- export declare function isProtected(url: string, extra?: readonly string[]): boolean;
10
+ export interface ProtectedMatcher {
11
+ url(text: string): boolean;
12
+ target(target: {
13
+ url: string;
14
+ previous?: boolean;
15
+ opener?: string;
16
+ }): boolean;
17
+ cookieDomain(domain: string): boolean;
18
+ storageKey(key: string): boolean;
19
+ }
20
+ export declare function protectedMatcher(extra?: readonly string[]): ProtectedMatcher;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-agent",
3
- "version": "3.43.0",
3
+ "version": "3.43.1",
4
4
  "description": "SLICC's agent core on pi durable: an agent worker, session storage, remoting and pi extensions",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -63,7 +63,7 @@
63
63
  "devDependencies": {
64
64
  "@ai-ecoverse/slicc-kernel": "1.47.1",
65
65
  "@ai-ecoverse/slicc-shared-web": "1.13.0",
66
- "@ai-ecoverse/slicc-spectrum": "1.59.0",
66
+ "@ai-ecoverse/slicc-spectrum": "1.60.2",
67
67
  "@ai-ecoverse/wasm-bash": "5.3.0-17",
68
68
  "@ai-ecoverse/wasm-coreutils": "9.12.0-5",
69
69
  "@ai-ecoverse/wasm-git": "2.55.0-15",
@@ -78,6 +78,7 @@
78
78
  "typescript": "7.0.2"
79
79
  },
80
80
  "dependencies": {
81
+ "@ai-ecoverse/slicc-browsers": "1.4.0",
81
82
  "@earendil-works/chord": "1.0.4",
82
83
  "@earendil-works/pi-ai": "1.0.4",
83
84
  "@earendil-works/pi-client": "1.0.4",