UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

245 lines • 10.3 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { StreamRefusal } from "../rs/errors.js"; import { MovableStream } from "../rs/stream.js"; import { createAsyncContext } from "../utils/async_context.js"; import { compareCodePoints } from "../utils/sort.js"; const ENC = new TextEncoder(); // Each list a `network` object takes, and the rule layer it fills. const LISTS = { allowed_domains: 'allow', denied_domains: 'deny' }; const SHAPE = '{allowed_domains: [...], denied_domains: [...], routes: {...}}'; // A route's end: a bracketed address or a name, and a port. const ENDPOINT = /^(?:\[([^\]]+)\]|([^:\s/[\]]+)):(\d+)$/; /** * A route's end, `host:port` (an IPv6 address bracketed), as the host and * the port a connection names them: the host in lower case, without its * brackets or a trailing dot. Throws for anything else. */ function endpoint(text) { const match = ENDPOINT.exec(text); const port = Number(match?.[3]); if (match === null || !(port > 0 && port < 65536)) { throw new Error(`network: route ${JSON.stringify(text)} is not host:port`); } const host = (match[1] ?? match[2] ?? '').toLowerCase(); return [host.endsWith('.') ? host.slice(0, -1) : host, port]; } function key(host, port) { return `${host}\0${String(port)}`; } /** * Whether a workspace may open one connection: its `network` setting, the * process's ceiling and the metadata block, each a layer mirage-rs compiles * once and every connection must clear. Throws when an entry is not an * address, a range or a host name. Mirrors Python's `NetworkRule`. */ export class NetworkRule { layers; metadata; routes; stream; targets = new Map(); constructor( // What it was built from, which a narrower rule builds on. layers, metadata = false, // Where a connection to `host:port` goes instead, judged there. routes = {}) { this.layers = layers; this.metadata = metadata; this.routes = routes; try { this.stream = new MovableStream('network', { layers: [...layers], metadata }); } catch (error) { if (error instanceof StreamRefusal) throw new Error(error.message); throw error; } const sources = new Map(); for (const [from, to] of Object.entries(routes)) { const source = key(...endpoint(from)); const named = sources.get(source); if (named !== undefined) { throw new Error(`network: routes ${named} and ${from} name the same end`); } sources.set(source, from); this.targets.set(source, endpoint(to)); } for (const [from, to] of Object.entries(routes)) { if (this.targets.has(key(...endpoint(to)))) { throw new Error(`network: route ${from} goes to ${to}, which is routed too`); } } } /** * Where a connection to `host` on `port` goes instead: the host and the * port its route names, null for none. */ route(host, port) { const name = host.toLowerCase(); const bare = name.startsWith('[') && name.endsWith(']') ? name.slice(1, -1) : name; return this.targets.get(key(bare.endsWith('.') ? bare.slice(0, -1) : bare, port)) ?? null; } /** * Whether a connection to `address`, resolved for `host`, opens on `port`; * either name is empty and the port undefined when unknown. */ lets(host, address, port) { return this.stream.live.test(ENC.encode(`${host}\0${address}\0${port === undefined ? '' : String(port)}`)); } } function field(key, value) { if (key === 'routes') { return (typeof value === 'object' && value !== null && !Array.isArray(value) && Object.values(value).every((target) => typeof target === 'string')); } return key in LISTS && Array.isArray(value) && value.every((entry) => typeof entry === 'string'); } /** * The layer a `network` setting adds: none for true or routes alone, one * that lets nothing for false, and its allowed and denied lists for an * object. Throws for anything else. Mirrors Python's `network_layer`. */ export function networkLayer(setting) { if (setting === true) return null; if (setting === false) return { allow: [] }; if (typeof setting === 'object' && setting !== null && !Array.isArray(setting)) { const fields = Object.entries(setting); if (fields.length > 0 && fields.every(([key, value]) => field(key, value))) { const lists = fields.filter(([key]) => key in LISTS); if (lists.length === 0) return null; const layer = {}; for (const [key, entries] of lists) layer[LISTS[key] ?? key] = [...entries]; return layer; } } throw new Error(`network is true, false or ${SHAPE}, not ${JSON.stringify(setting)}`); } /** * The routes a `network` setting names, `{ "host:port": "host:port" }`; * none for true or false. Mirrors Python's `network_routes`. */ export function networkRoutes(setting) { return typeof setting === 'object' ? { ...setting.routes } : {}; } /** * `base`'s routes with `over`'s in place of any naming the same end, as a * session's profile routes over its workspace's. Mirrors Python's * `merge_routes`. */ export function mergeRoutes(base, over) { const named = new Set(Object.keys(over).map((from) => key(...endpoint(from)))); const kept = Object.entries(base).filter(([from]) => !named.has(key(...endpoint(from)))); return { ...Object.fromEntries(kept), ...over }; } /** * `base` held also to `setting`: its layer added and its routes over the * base's; `base` itself when the setting adds neither. Mirrors Python's * `narrowed`, which also starts watching; a TypeScript host watches where * it binds the rule. */ export function narrowed(base, setting) { const layer = networkLayer(setting); const routes = networkRoutes(setting); const layers = base?.layers ?? []; const held = base?.routes ?? {}; const known = layers.map((each) => JSON.stringify(each)); if ((layer === null || known.includes(JSON.stringify(layer))) && Object.entries(routes).every(([source, target]) => held[source] === target)) { return base; } return new NetworkRule([...layers, ...(layer === null ? [] : [layer])], base?.metadata ?? false, mergeRoutes(held, routes)); } /** * Whether two settings hold the same rule: their allowed and denied lists * name the same entries as written, in any order, `false` being an empty * allowed list and an empty denied list none at all, since it refuses * nothing, and their routes go from and to the same ends. An entry is * compared as written, since one spelling can be an address and another a * host name. */ export function sameNetwork(a, b) { const [left, right] = [a, b].map((setting) => { const layer = networkLayer(setting); const sorted = (entries) => entries === undefined ? null : [...new Set(entries)].sort(compareCodePoints); const denied = sorted(layer?.deny); const routes = Object.entries(networkRoutes(setting)) .map(([from, to]) => [key(...endpoint(from)), key(...endpoint(to))]) .sort(([a], [b]) => compareCodePoints(a ?? '', b ?? '')); return JSON.stringify([sorted(layer?.allow), denied?.length === 0 ? null : denied, routes]); }); return left === right; } /** * The rule a workspace's connections are judged by: its `network`, with the * cloud metadata addresses refused when `metadata` is set; null when nothing * limits or routes it. Mirrors Python's `network_rule`. */ export function networkRule(setting = true, metadata = false) { const layer = networkLayer(setting); const layers = layer === null ? [] : [layer]; const routes = networkRoutes(setting); if (layers.length === 0 && !metadata && Object.keys(routes).length === 0) return null; return new NetworkRule(layers, metadata, routes); } // The rule of the workspace a line or an op runs for, null for no limit. const rule = createAsyncContext(); /** * `fn` run under a workspace's network rule, which every connection it opens * must clear. Mirrors Python's `run_with_network`. */ export function runWithNetwork(bound, fn) { return Promise.resolve(rule.run(bound, fn)); } /** * Whether the workspace a call runs for may open a connection to `address`, * resolved for `host`, on `port`. A browser cannot tell one task's binding * from another's, so there every live workspace's rule must let it. Mirrors * Python's `network_lets`. */ export function networkLets(host, address, port) { for (const live of rule.liveStores()) { if (live !== null && !live.lets(host, address, port)) return false; } return true; } /** * Where the workspace a call runs for sends a connection to `host` on * `port` instead, null for nowhere else. A browser answers by the newest * live rule; its page hooks ask each live workspace. Mirrors Python's * `network_route`. */ export function networkRoute(host, port) { return networkBound()?.route(host, port) ?? null; } /** * The rule bound to the running call, null for none, which a watcher reads * to skip the rest and keys its pools by. A browser answers the newest live * one. Mirrors Python's `network_bound`. */ export function networkBound() { return (rule .liveStores() .filter((live) => live !== null) .at(-1) ?? null); } //# sourceMappingURL=network.js.map