posipaki 0.35.0 → 0.35.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"node.js","names":[],"sources":["../../src/remote/spawners/fifo-command.ts","../../src/remote/kit.ts","../../src/remote/spawners/fifo-argv.ts","../../src/remote/define-subprocess.ts","../../src/remote/host.ts","../../src/remote/gateway-client.ts"],"sourcesContent":["// ── FIFO client spawner (command) ──────────────────────────────────────────\n//\n// Environment-specific: spawns a command, creates the two named fifos, and\n// returns a frame Channel with the $proto handshake already validated.\n\nimport { spawn, execSync } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { unlink } from \"node:fs/promises\";\nimport { FifoUtf8NlineTransport } from \"../transports/fifo.js\";\nimport { json1Channel, VERSION } from \"../protocols/json1.js\";\nimport { isProto } from \"../channel.js\";\nimport type { Channel } from \"../channel.js\";\n\nexport function commandSpawner(command: string[]): () => Promise<Channel> {\n return async () => {\n const basePath = join(tmpdir(), `posipaki-${randomUUID()}`);\n\n execSync(`mkfifo \"${basePath}.in\"`);\n execSync(`mkfifo \"${basePath}.out\"`);\n\n const setup = FifoUtf8NlineTransport.beginConnect(basePath + \".in\", basePath + \".out\");\n\n const childCmd = [\n ...command,\n `--fifo-in=${basePath + \".in\"}`,\n `--fifo-out=${basePath + \".out\"}`,\n ];\n const childProc = spawn(childCmd[0], childCmd.slice(1), {\n cwd: process.cwd(),\n stdio: [\"inherit\", \"inherit\", \"inherit\"],\n });\n\n const transport = await setup.transport;\n const channel = json1Channel(transport);\n\n const protoFrame = await new Promise<Record<string, unknown>>((resolve) => {\n channel.onMessage((frame) => resolve(frame));\n });\n channel.removeHandler();\n if (!isProto(protoFrame) || protoFrame.$proto !== VERSION) {\n throw new Error(`unsupported protocol: ${JSON.stringify(protoFrame).slice(0, 50)}`);\n }\n\n let cleaned = false;\n const cleanup = () => {\n if (cleaned) return;\n cleaned = true;\n transport.close();\n unlink(basePath + \".in\").catch(() => {});\n unlink(basePath + \".out\").catch(() => {});\n };\n childProc.on(\"exit\", cleanup);\n\n return channel;\n };\n}\n","// ── Kit ────────────────────────────────────────────────────────────────────\n//\n// Getting a payload onto a host we know nothing about. A way in gives us a\n// shell; from there one script runs everywhere: probe what is really there, write\n// the kit only if it is not, and report every step as one machine-readable line.\n// The script always arrives on stdin, never in argv — and since stdin *is* the\n// script, the payload gets its own channel into the same environment (a second\n// `ssh`, an `exec` on a container that is already running).\n//\n// The script assumes POSIX sh and nothing else: `command -v`, `mkdir -p`,\n// `wc -c`, `base64 -d`. A broken environment therefore reaches the client as a\n// readable reason instead of a channel that just closes.\n//\n// A kit is named after the *host version* it serves — the app, the build its bytes came from\n// and the posipaki release that speaks to it — so two consumers, two builds or two releases\n// cannot land on each other's files, and the directory a kit sits in cannot disagree with the\n// identity the client then states on the wire. A payload that is started for a host version\n// other than its own refuses to serve (see `hostVersionAccepts`), so a name alone is never\n// mistaken for the build behind it.\n\nimport { createHash } from \"node:crypto\";\nimport { LIB_VERSION } from \"../version.js\";\n\n/** Runtimes a kit is happy to be run with, best first. A caller may override. */\nexport const DEFAULT_RUNTIMES = [\"node\", \"nodejs\", \"bun\"];\n\n/** Layout of a staged kit — the contract's own version, recorded in `version.json`. */\nexport const KIT_LAYOUT = 1;\n\n/** Where kits live on a host, relative to `$HOME`, unless the caller says otherwise. */\nexport const DEFAULT_KIT_PARENT = \"bin/posipaki\";\n\n/** The names a kit's two bundles get: the payload, and the gateway that relays to it. */\nexport const PAYLOAD_ARTIFACT = \"payload.js\";\nexport const GATEWAY_ARTIFACT = \"gateway.js\";\n\nexport interface KitFile {\n /** Name inside the kit directory, e.g. `payload.js`. */\n name: string;\n /** The file's bytes, base64 — what the script writes. */\n base64: string;\n /** Byte count, for the cheap presence check. */\n bytes: number;\n /** sha256 of the file, recorded in `version.json`. */\n sha256: string;\n}\n\n/** Who is staging this kit: the consumer's own name and build version. */\nexport interface KitApp {\n name: string;\n version: string;\n}\n\nexport interface Kit {\n /** Directory name: the host version this kit serves, as {@link hostVersion} renders it. */\n name: string;\n /** Everything the kit installs, `version.json` last. */\n files: KitFile[];\n /** Runtime candidates, in order; the first one found wins. */\n runtimes: string[];\n /** Directory the kit lands in, relative to the host's `$HOME`. */\n parent: string;\n /** The consumer this kit belongs to. */\n app: KitApp;\n}\n\nexport interface MakeKitOptions {\n /** Required: a kit with no owner cannot be told apart from another's. */\n app: KitApp;\n /** Defaults to {@link DEFAULT_RUNTIMES}. */\n runtimes?: string[];\n /** Defaults to {@link DEFAULT_KIT_PARENT}. */\n parent?: string;\n}\n\nexport type BootstrapReport =\n | { kind: \"ready\"; kitDir: string; runtime: string; staged: boolean }\n | { kind: \"error\"; reason: string };\n\n/** Hex sha256 of some content. */\nexport function sha256Hex(content: string | Uint8Array): string {\n return createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\n/** What may stand as one fact in a host version: no `@`, no whitespace, no `/`. */\nconst FACT = /^[A-Za-z0-9][A-Za-z0-9.+_-]*$/;\n\n/** The label posipaki's own field carries, so nobody has to guess which field it is. */\nconst POSIPAKI_FIELD = \"posipaki-\";\n\n/** A host version read back: what a consumer stated, and the posipaki that rendered it. */\nexport interface HostVersion extends KitApp {\n posipaki: string;\n}\n\n/** A name or version that could not stand as one fact in a host version. */\nexport class HostVersionError extends Error {}\n\n/**\n * The one value that says what a host is expected to run: whose payload it is, which build\n * those bytes came from, and the posipaki release that speaks to it —\n * `email-agent@1.0.0+d52ee13@posipaki-0.35.0`.\n *\n * One string rather than three flags because it is one fact: it names the kit directory, it\n * travels with every way in, and the payload compares it with its own before it serves. A\n * consumer states the two facts only it knows; posipaki renders its own release, so that\n * field cannot be got wrong. `@` separates, and no name or version may contain one.\n */\nexport function hostVersion(app: KitApp): string {\n const facts: [string, string][] = [\n [\"name\", app.name],\n [\"version\", app.version],\n ];\n for (const [what, value] of facts) {\n if (!FACT.test(value)) {\n throw new HostVersionError(\n `an app ${what} must match ${FACT.source}: ${JSON.stringify(value)}`,\n );\n }\n }\n return `${app.name}@${app.version}@${POSIPAKI_FIELD}${LIB_VERSION}`;\n}\n\n/** What a {@link hostVersion} string names, or undefined when it names nothing. */\nexport function parseHostVersion(text: string): HostVersion | undefined {\n const fields = text.split(\"@\");\n if (fields.length !== 3) return undefined;\n const [name, version, posipaki] = fields as [string, string, string];\n if (!FACT.test(name) || !FACT.test(version)) return undefined;\n if (!posipaki.startsWith(POSIPAKI_FIELD)) return undefined;\n const release = posipaki.slice(POSIPAKI_FIELD.length);\n if (!FACT.test(release)) return undefined;\n return { name, version, posipaki: release };\n}\n\n/** Exit code a payload refuses a start with: it is not the build that was asked for. */\nexport const PAYLOAD_REFUSED = 78;\n\nexport type HostVersionVerdict = { ok: true } | { ok: false; reason: string };\n\n/**\n * Whether a payload may serve what it was asked for. `own` is what its bytes carry, `asked`\n * is what the caller said — two independent facts, and one of the four combinations is the\n * dangerous one: an artifact that knows what it is, started by a caller that never said which\n * host version it is for, is exactly the stale caller we refuse.\n *\n * No baked version, nothing to compare: an artifact that does not know what it is cannot be\n * wrong about it, and a caller that states a version for such a payload is only being helpful.\n */\nexport function hostVersionAccepts(\n own: string | undefined,\n asked: string | undefined,\n): HostVersionVerdict {\n if (own === undefined) return { ok: true };\n if (asked === undefined) {\n return { ok: false, reason: `this artifact is ${own}, and no host version was asked for` };\n }\n if (!sameHostVersion(own, asked)) {\n return { ok: false, reason: `asked to serve ${asked}, but this artifact is ${own}` };\n }\n return { ok: true };\n}\n\n/**\n * Whether two host versions are the same build. Equality for now: {@link hostVersionAccepts}\n * is the rule, and this is the one function that decides what \"the same\" means — so a range, a\n * compatibility table or a protocol-only comparison is a change here and nowhere else.\n */\nexport function sameHostVersion(own: string, asked: string): boolean {\n return own === asked;\n}\n\n/** Wrap base64 at a comfortable width; `base64 -d` ignores the newlines. */\nfunction wrapped(base64: string, width = 76): string {\n const lines: string[] = [];\n for (let at = 0; at < base64.length; at += width) lines.push(base64.slice(at, at + width));\n return lines.join(\"\\n\");\n}\n\n/**\n * Describe a kit around its files. Its name is the host version it serves, and `version.json`\n * — which records that name and every file's hash — is written as one of them: it describes\n * the payload and the gateway, never itself.\n */\nexport function makeKit(\n files: { name: string; content: string | Uint8Array }[],\n options: MakeKitOptions,\n): Kit {\n const raw = files.map((file) => ({\n name: file.name,\n bytes: Buffer.byteLength(file.content),\n sha256: sha256Hex(file.content),\n base64: Buffer.from(file.content).toString(\"base64\"),\n }));\n const kit: Kit = {\n name: hostVersion(options.app),\n files: raw,\n runtimes: [...(options.runtimes ?? DEFAULT_RUNTIMES)],\n parent: options.parent ?? DEFAULT_KIT_PARENT,\n app: options.app,\n };\n const version = Buffer.from(`${JSON.stringify(kitVersion(kit), null, 2)}\\n`);\n return {\n ...kit,\n files: [\n ...kit.files,\n {\n name: \"version.json\",\n bytes: version.byteLength,\n sha256: sha256Hex(version),\n base64: version.toString(\"base64\"),\n },\n ],\n };\n}\n\n/** What `version.json` says: the identity, the facts it was rendered from, and the hashes. */\nexport function kitVersion(kit: Kit): Record<string, unknown> {\n return {\n hostVersion: kit.name,\n app: kit.app,\n posipaki: LIB_VERSION,\n layout: KIT_LAYOUT,\n files: kit.files.map((file) => ({ name: file.name, sha256: file.sha256, bytes: file.bytes })),\n };\n}\n\n/** The `[ -f … ] && [ \"$(wc -c < …)\" -eq … ]` test for one file. */\nfunction presentTest(file: KitFile): string {\n return `[ -f \"$kit_dir/${file.name}\" ] && [ \"$(wc -c < \"$kit_dir/${file.name}\")\" -eq ${file.bytes} ]`;\n}\n\n/** One file: write it to a temp name, then move it into place. */\nfunction writeFile(file: KitFile): string {\n return [\n ` base64 -d > \"$tmp\" <<'KIT_FILE'`,\n wrapped(file.base64),\n \"KIT_FILE\",\n ` mv \"$tmp\" \"$kit_dir/${file.name}\" || { echo \"error cannot install ${file.name}\"; exit 73; }`,\n ].join(\"\\n\");\n}\n\n/**\n * The bootstrap, as one POSIX sh script.\n *\n * Staging is the whole job. The script always arrives on stdin, never in argv,\n * and that is also why it cannot carry the wire: the caller runs the payload on a\n * second channel into the same environment once this one has reported.\n */\nexport function bootstrapScript(kit: Kit): string {\n const checks = kit.files.map(presentTest).join(\" && \\\\\\n \");\n const lines = [\n \"# ── kit bootstrap ─────────────────────────────────────────────────────────\",\n \"# probe, stage what is missing, report every step as one machine-readable line.\",\n \"set -u\",\n \"\",\n 'if [ -z \"${HOME:-}\" ]; then echo \"error HOME is not set\"; exit 74; fi',\n `kit_dir=\"$HOME/${kit.parent}/${kit.name}\"`,\n \"\",\n 'runtime=\"\"',\n `for candidate in ${kit.runtimes.join(\" \")}; do`,\n ' if command -v \"$candidate\" >/dev/null 2>&1; then runtime=\"$(command -v \"$candidate\")\"; break; fi',\n \"done\",\n `if [ -z \"$runtime\" ]; then echo \"error no runtime among: ${kit.runtimes.join(\" \")}\"; exit 75; fi`,\n 'if ! command -v base64 >/dev/null 2>&1; then echo \"error base64 is missing\"; exit 76; fi',\n \"\",\n `if ${checks}; then`,\n ' echo \"present\"',\n \"else\",\n ` mkdir -p \"$kit_dir\" || { echo \"error cannot create $kit_dir\"; exit 73; }`,\n ' tmp=\"$kit_dir/.staging.$$\"',\n ...kit.files.map(writeFile),\n ' echo \"staged\"',\n \"fi\",\n 'echo \"kit $kit_dir\"',\n 'echo \"runtime $runtime\"',\n ];\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/** Read the script's report back. Missing or unknown lines are an error. */\nexport function parseBootstrapReport(stdout: string): BootstrapReport {\n let kitDir = \"\";\n let runtime = \"\";\n let staged: boolean | null = null;\n for (const raw of stdout.split(\"\\n\")) {\n const line = raw.trim();\n if (line === \"\") continue;\n if (line.startsWith(\"error \")) return { kind: \"error\", reason: line.slice(\"error \".length) };\n if (line === \"staged\") {\n staged = true;\n continue;\n }\n if (line === \"present\") {\n staged = false;\n continue;\n }\n if (line.startsWith(\"kit \")) {\n kitDir = line.slice(\"kit \".length);\n continue;\n }\n if (line.startsWith(\"runtime \")) {\n runtime = line.slice(\"runtime \".length);\n continue;\n }\n }\n if (kitDir === \"\" || runtime === \"\" || staged === null) {\n const seen = stdout.trim();\n return {\n kind: \"error\",\n reason: seen === \"\" ? \"no report at all\" : `incomplete report: ${seen}`,\n };\n }\n return { kind: \"ready\", kitDir, runtime, staged };\n}\n","// ── FIFO server spawner (argv) ─────────────────────────────────────────────\n//\n// Environment-specific: reads what the caller put in argv — the two named fifos, and the host\n// version this artifact is being started for — checks that it *is* that build, connects, and\n// returns a frame Channel with the $proto handshake already sent.\n//\n// The check comes first, before anything is opened: a payload that is not the build that was\n// asked for has no business on a wire, and saying so before it connects is the only moment\n// where the reason is still readable to the caller.\n\nimport { FifoUtf8NlineTransport } from \"../transports/fifo.js\";\nimport { json1Channel, VERSION } from \"../protocols/json1.js\";\nimport type { Channel } from \"../channel.js\";\nimport { PAYLOAD_REFUSED, hostVersionAccepts } from \"../kit.js\";\n\n/** A `--flag=<value>` argument, or undefined. */\nfunction flag(name: string): string | undefined {\n const prefix = `--${name}=`;\n return process.argv.find((a) => a.startsWith(prefix))?.slice(prefix.length);\n}\n\n/**\n * Serve one payload over the fifo pair in argv.\n *\n * `own` is the host version this artifact's bytes carry, when its build stamped one: given it,\n * the payload refuses to serve a host version that differs, and refuses a start that names\n * none. Nothing stamped means nothing to check — and `--host-version`, if the caller sent one\n * anyway, is simply not ours to compare with.\n */\nexport async function fifoArgvSpawner(own?: string): Promise<Channel> {\n const verdict = hostVersionAccepts(own, flag(\"host-version\"));\n if (!verdict.ok) {\n console.error(`payload: ${verdict.reason}`);\n process.exit(PAYLOAD_REFUSED);\n }\n\n const fifoIn = flag(\"fifo-in\");\n const fifoOut = flag(\"fifo-out\");\n if (!fifoIn || !fifoOut) {\n console.error(\"server: --fifo-in=<path> and --fifo-out=<path> required\");\n process.exit(1);\n }\n\n const transport = await FifoUtf8NlineTransport.connect(fifoOut, fifoIn);\n const channel = json1Channel(transport);\n await channel.send({ $proto: VERSION });\n return channel;\n}\n","// ── defineSubprocessActor ───────────────────────────────────────────────────\n//\n// Environment-specific glue: wraps an actor definition so that spawning it runs\n// it in a subprocess over two named fifos. It does the guesswork — argv marker\n// detection and runner auto-detection — and prepares the client spawner. This\n// is the only place in the subprocess path that imports transports/protocols.\n\nimport { fileURLToPath } from \"node:url\";\nimport { createHash } from \"node:crypto\";\nimport type { Message } from \"../types.js\";\nimport type { ActorDefinition, HandlerOptions, MethodOptions, ReflectionOptions } from \"../actor-types.js\";\nimport type { KitApp } from \"./kit.js\";\nimport { hostVersion } from \"./kit.js\";\nimport { remoteClient } from \"./client.js\";\nimport { serveRemoteActor } from \"./server.js\";\nimport { commandSpawner } from \"./spawners/fifo-command.js\";\nimport { fifoArgvSpawner } from \"./spawners/fifo-argv.js\";\n\nexport interface SubprocessActorOptions {\n manual?: boolean;\n /**\n * Who this payload is: the app name and the build its bytes came from, as the consumer states\n * them. This is one switch for both halves of the seam — stated, every start passes\n * `--host-version` and the payload refuses to serve anything else; unstated, neither happens,\n * because there is no version to pass and none to check.\n */\n hostVersion?: KitApp;\n}\n\nexport interface SubprocessActorBundle<\n Args,\n State,\n InMsg extends Message,\n OutMsg extends Message,\n R extends ReflectionOptions,\n> {\n actor: ActorDefinition<Args, State, InMsg, OutMsg, R>;\n runRemoteRoot(): Promise<void>;\n isRemoteRoot: boolean;\n}\n\nfunction pathHash(path: string): string {\n return createHash(\"sha256\").update(path).digest(\"hex\").slice(0, 12);\n}\n\nconst MARKER_PREFIX = \"--remote=\";\n\nexport function defineSubprocessActor<\n Args,\n State,\n InMsg extends Message,\n OutMsg extends Message,\n Methods extends MethodOptions,\n Handlers extends HandlerOptions<InMsg>,\n R extends ReflectionOptions,\n>(\n actor: ActorDefinition<Args, State, InMsg, OutMsg, R>,\n url: string,\n opts: SubprocessActorOptions = {},\n): SubprocessActorBundle<Args, State, InMsg, OutMsg, R> {\n const scriptPath = fileURLToPath(url);\n const marker = `${MARKER_PREFIX}${pathHash(scriptPath)}`;\n const isRemoteRoot = !opts.manual && process.argv.includes(marker);\n const own =\n opts.hostVersion === undefined\n ? undefined\n : hostVersion(opts.hostVersion);\n\n const serve = () => serveRemoteActor(actor, () => fifoArgvSpawner(own));\n\n if (isRemoteRoot) {\n void serve();\n }\n\n const isBun = typeof (globalThis as { Bun?: unknown }).Bun !== \"undefined\";\n const runner = isBun ? \"bun\" : \"node\";\n const spawner = commandSpawner([\n runner,\n scriptPath,\n marker,\n ...(own === undefined ? [] : [`--host-version=${own}`]),\n ]);\n\n const proxyDef = remoteClient<Args, State, InMsg, OutMsg, Methods, Handlers, R>(\n actor.name ?? \"actor\",\n spawner,\n );\n\n return {\n actor: proxyDef,\n runRemoteRoot: serve,\n isRemoteRoot,\n };\n}\n","// ── Running one command on the host ────────────────────────────────────────\n//\n// Everything a way in does, it does by running a command: staging is a command in an\n// environment, and the actor is another one whose stdin/stdout are the wire. Both go\n// through the two functions here, and both are handed in — so a test can stage and run\n// without a sandbox, a host or a container, and a consumer can run host commands its\n// own way.\n\nimport { spawn } from \"node:child_process\";\nimport type { ChildProcess, StdioOptions } from \"node:child_process\";\n\nexport interface HostResult {\n code: number | null;\n stdout: string;\n stderr: string;\n}\n\n/** Run a command, feed it its stdin, and wait for what it said. */\nexport type HostRun = (command: string[], stdin: string) => Promise<HostResult>;\n\n/** The real thing: spawn it, feed it, collect it. */\nexport const runHost: HostRun = (command, stdin) =>\n new Promise((settle, fail) => {\n const child = spawn(command[0]!, command.slice(1), { stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n const out: string[] = [];\n const err: string[] = [];\n child.stdout.setEncoding(\"utf-8\");\n child.stderr.setEncoding(\"utf-8\");\n child.stdout.on(\"data\", (chunk: string) => out.push(chunk));\n child.stderr.on(\"data\", (chunk: string) => err.push(chunk));\n child.once(\"error\", (error) => fail(error));\n // A host that hangs up while we are still writing has failed; the exit code and\n // stderr carry that, so the write error is not the story here.\n child.stdin.on(\"error\", () => {});\n child.once(\"close\", (code) => settle({ code, stdout: out.join(\"\"), stderr: err.join(\"\") }));\n child.stdin.end(stdin);\n });\n\n/** How the wire's process is started, with the stdio it needs. */\nexport type SpawnChild = (command: string[], stdio: StdioOptions) => ChildProcess;\n\n/** The real thing: node's own spawn, in our working directory. */\nexport const spawnChild: SpawnChild = (command, stdio) =>\n spawn(command[0]!, command.slice(1), { cwd: process.cwd(), stdio });\n","// ── The client side of the gateway ─────────────────────────────────────────\n//\n// A consumer has two things: a payload, and the build its bytes came from. A way in has\n// one: how to run a command somewhere else. Everything else is the same whatever the way\n// in is, and this module is that everything else:\n//\n// 1. the gateway script is posipaki's own entry (`posipaki/remote/gateway-cli.js`, a\n// published specifier, so it is resolved where the kit is staged from) — or the\n// consumer's, for a program that carries its own;\n// 2. both bundles go into one kit, whose directory name *is* the host version it serves —\n// the app, the build those bytes came from, the posipaki release that speaks to them —\n// so a second run of the same build finds the kit already there and copies nothing;\n// 3. whatever has to be *copied* there is staged through the way in's own channel — the\n// core's bootstrap script, one report line per step — which is also where the runtime\n// that will run it is found, so a way in never has to know what a `node` is. A\n// program the caller says is already installed is not staged at all, and then no\n// command of ours runs in the environment beyond the two that matter;\n// 4. the gateway runs there — `<runtime> <kit>/gateway.js` when it was staged, the caller's\n// own command when it was not — its stdin/stdout are the wire, and it makes the fifos\n// inside the environment and starts the payload against them. The payload is run this\n// way in *every* way in, which is why the payload is only ever a fifo worker. The\n// payload's own command and the host version are arguments the gateway carries without\n// knowing which is which: staged artifacts are ours, and the payload checks itself\n// against what it was asked for.\n//\n// What a way in supplies is `entry`: the argv that runs a command there. It is the whole\n// difference between bwrap, ssh, a container and this machine, and it is one function.\n\nimport type { ChildProcess, StdioOptions } from \"node:child_process\";\nimport { readFile } from \"node:fs/promises\";\nimport type { Readable, Writable } from \"node:stream\";\nimport { fileURLToPath } from \"node:url\";\nimport type { Channel } from \"./channel.js\";\nimport type { ClientSpawner } from \"./client.js\";\nimport { gatewayArgs } from \"./gateway.js\";\nimport { runHost, spawnChild } from \"./host.js\";\nimport type { HostResult, HostRun, SpawnChild } from \"./host.js\";\nimport {\n DEFAULT_KIT_PARENT,\n DEFAULT_RUNTIMES,\n GATEWAY_ARTIFACT,\n PAYLOAD_ARTIFACT,\n bootstrapScript,\n hostVersion,\n makeKit,\n parseBootstrapReport,\n} from \"./kit.js\";\nimport type { Kit, KitApp } from \"./kit.js\";\nimport { clientChannel, stderrSink } from \"./stdio.js\";\nimport type { LineStreams, OutputFd, OutputSink } from \"./stdio.js\";\n\n/** posipaki's own gateway program. A consumer with one of its own stages that instead. */\nexport const GATEWAY_ENTRY = \"posipaki/remote/gateway-cli.js\";\n\n/** How much of what a failed staging command said is quoted back as part of the reason. */\nconst SAID_TAIL = 400;\n\n/** How much of the far end's stderr is kept, to be the reason a start failed. */\nconst REASON_TAIL = 400;\n\n/** Staging failed, or the far end never spoke: the caller's spec, or the environment's answer. */\nexport class RemoteSpecError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"RemoteSpecError\";\n }\n}\n\n/** What a way in is handed, and how it runs what it is handed. */\nexport interface RemoteWayIn {\n /** What the far end's output and errors are tagged with. */\n name: string;\n /** The argv that runs `command` there: bwrap's policy, ssh's host, podman's container. */\n entry: (command: string[]) => string[];\n /** How a plain command runs there and is waited for. Injectable, so a test needs no way in. */\n run?: HostRun;\n /** How the wire's process is started there. Injectable likewise. */\n spawn?: SpawnChild;\n}\n\n/**\n * A program the way in starts there: a bundle of ours to copy into the environment, or a\n * command that already runs there — an absolute path, a name on `$PATH`, a runtime and a\n * script of its own. Which of the two it is, is the caller's decision, and it is the only\n * thing posipaki needs to know about where the far end comes from.\n */\nexport type HostProgram = { stage: string } | { run: string[] };\n\n/** Everything a client needs: where the two programs are, whose payload it is, and the way in. */\nexport interface RemoteSpec<Args> extends RemoteWayIn {\n /** The payload: ours to stage there, or the one already installed there. */\n payload: HostProgram;\n /**\n * Whose payload this is: the app name and the build those bytes came from. Required\n * whenever anything is staged — a kit's directory *is* the host version — and stated to the\n * payload whenever it is given at all, which is the one fact a caller can hand an installed\n * payload about the build it is expected to be. Omitted, nothing is stated and nothing is\n * checked, which is only right for a payload whose bytes carry no version of their own.\n */\n hostVersion?: KitApp;\n /** The gateway: posipaki's own bundle, staged, unless the caller says otherwise. */\n gateway?: HostProgram;\n /** Runtime candidates on the far side, best first. Defaults to `DEFAULT_RUNTIMES`. */\n runtime?: string[];\n /** Where the kit lands there, relative to its `$HOME`. Defaults to `DEFAULT_KIT_PARENT`. */\n parent?: string;\n /**\n * The payload's own arguments, built from the args the actor was spawned with. The gateway\n * carries them to the payload untouched — it does not know what they mean, and does not need\n * to: what a consumer tells its own far end is the consumer's business.\n */\n payloadArgs?: (args: Args) => string[];\n /** Where the far end's own output goes. Defaults to our stderr, tagged with `name`. */\n onOutput?: OutputSink;\n /** How long to wait for the far end's first protocol frame. */\n handshakeTimeoutMs?: number;\n /** Called when the far end's process is gone, whichever way it went. */\n onGone?: () => void;\n}\n\n/**\n * The part of a spec the machinery here reads, with the actor's own arguments left out:\n * staging, the channel and the argv are the same whatever an actor is told.\n */\ntype Spec = Omit<RemoteSpec<unknown>, \"payloadArgs\">;\n\n/** The argv that starts each of the two programs there. */\ninterface Programs {\n gateway: string[];\n payload: string[];\n}\n\nfunction isStage(program: HostProgram): program is { stage: string } {\n return \"stage\" in program;\n}\n\nfunction errorText(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * posipaki's own gateway program, resolved where the consumer installed it — the default for\n * a caller that has none of its own to deploy.\n */\nfunction gatewayEntry(): string {\n try {\n return fileURLToPath(import.meta.resolve(GATEWAY_ENTRY));\n } catch (err) {\n throw new RemoteSpecError(\n `cannot find posipaki's gateway program (${GATEWAY_ENTRY}): ${errorText(err)}. ` +\n \"Stage one with `gateway:` — a consumer whose program carries its own.\",\n );\n }\n}\n\n/** The kit's files: what has to be copied there, under the names the script writes them as. */\nasync function kitFiles(\n gateway: HostProgram,\n payload: HostProgram,\n): Promise<{ name: string; content: Buffer }[]> {\n const files: { name: string; content: Buffer }[] = [];\n if (isStage(payload)) files.push({ name: PAYLOAD_ARTIFACT, content: await readFile(payload.stage) });\n if (isStage(gateway)) files.push({ name: GATEWAY_ARTIFACT, content: await readFile(gateway.stage) });\n return files;\n}\n\n/** A command with nothing to run is a spec that says nothing; refuse it before spawning it. */\nfunction programCommand(program: { run: string[] }, what: string, name: string): string[] {\n if (program.run.length === 0 || program.run[0] === undefined || program.run[0] === \"\") {\n throw new RemoteSpecError(`${name}: ${what} is a command with nothing to run`);\n }\n return program.run;\n}\n\n/**\n * The two commands, staging whatever has to be copied there first.\n *\n * Nothing staged means no command of ours runs in the environment at all: the caller's own\n * argv is handed through as it stands, which is the case for a program that is already\n * installed. Something staged needs a host version to name the kit with — and a kit's\n * directory *is* the host version, so there is nothing to guess at.\n */\nasync function programsFor(spec: Spec, run: HostRun): Promise<Programs> {\n const gateway: HostProgram = spec.gateway ?? { stage: gatewayEntry() };\n const payload = spec.payload;\n if (!isStage(gateway) && !isStage(payload)) {\n return {\n gateway: programCommand(gateway, \"the gateway\", spec.name),\n payload: programCommand(payload, \"the payload\", spec.name),\n };\n }\n if (spec.hostVersion === undefined) {\n throw new RemoteSpecError(\n `nothing names the kit to stage into ${spec.name}: state hostVersion — a kit's directory ` +\n \"is the host version, and a payload refuses to serve one its own bytes do not carry\",\n );\n }\n const kit = makeKit(await kitFiles(gateway, payload), {\n app: spec.hostVersion,\n runtimes: spec.runtime ?? DEFAULT_RUNTIMES,\n parent: spec.parent ?? DEFAULT_KIT_PARENT,\n });\n const staged = await stageKit(spec, kit, run);\n const at = (artifact: string) => [staged.runtime, `${staged.kitDir}/${artifact}`];\n return {\n gateway: isStage(gateway) ? at(GATEWAY_ARTIFACT) : gateway.run,\n payload: isStage(payload) ? at(PAYLOAD_ARTIFACT) : payload.run,\n };\n}\n\n/** The tail of what a command said, for a failure that has no better story. */\nfunction said(result: HostResult): string {\n const text = `${result.stderr}${result.stdout}`.trim();\n const tail = text.length > SAID_TAIL ? text.slice(-SAID_TAIL) : text;\n return tail === \"\" ? `exit code ${result.code}` : tail;\n}\n\n/**\n * Put the kit where the environment can see it and say what will run it. Idempotent:\n * the script probes, writes only what is missing and reports — so a spawn of a build\n * that is already staged costs one process and no writes.\n */\nasync function stageKit(spec: Spec, kit: Kit, run: HostRun): Promise<{ kitDir: string; runtime: string }> {\n const result = await run(spec.entry([\"sh\", \"-s\"]), bootstrapScript(kit));\n const report = parseBootstrapReport(result.stdout);\n if (report.kind === \"error\") {\n // A report that never arrived is not a reason; what the environment said is.\n const incomplete =\n report.reason.startsWith(\"incomplete\") || report.reason.startsWith(\"no report\");\n throw new RemoteSpecError(\n `staging into ${spec.name} failed: ${report.reason}${incomplete ? ` (${said(result)})` : \"\"}`,\n );\n }\n return { kitDir: report.kitDir, runtime: report.runtime };\n}\n\n/**\n * Start the run command and hand its own stdin/stdout to the wire. An environment that\n * dies before it speaks — bwrap not installed, a host that refuses, a payload that\n * cannot be run — fails the spawn with what it said, rather than a channel that never\n * answers.\n */\nasync function openChannel(\n spec: Spec,\n command: string[],\n spawn: SpawnChild,\n): Promise<Channel> {\n const sink = spec.onOutput ?? stderrSink(spec.name);\n let tail = \"\";\n const output: OutputSink = (fd: OutputFd, data: string) => {\n if (fd === 2) tail = (tail + data).slice(-REASON_TAIL);\n sink(fd, data);\n };\n\n // The run's own stdin and stdout *are* the wire, so both have to be pipes.\n const stdio: StdioOptions = [\"pipe\", \"pipe\", \"pipe\"];\n const child: ChildProcess = spawn(command, stdio);\n const streams: LineStreams = { read: child.stdout as Readable, write: child.stdin as Writable };\n\n // The run's stdin and stdout are the wire; its stderr is not. That is where the way\n // in itself speaks — \"bwrap: Can't find source path\", \"Permission denied\" — so it goes\n // to the sink, and the tail of it becomes the reason a start that died is reported with.\n child.stderr?.setEncoding(\"utf-8\");\n child.stderr?.on(\"data\", (chunk: string) => output(2, chunk));\n\n const exited = new Promise<never>((_, reject) => {\n // `close`, not `exit`: the process being gone is not the same as everything it said\n // having arrived, and the tail of its stderr is the whole reason here.\n child.once(\"close\", (code, signal) => {\n const how = signal === null ? `code ${code}` : `signal ${signal}`;\n const why = tail.trim();\n reject(new Error(`exited with ${how}${why ? `: ${why}` : \"\"}`));\n });\n child.once(\"error\", (err) => reject(new Error(`cannot start: ${err.message}`)));\n });\n // A spawn that resolves leaves this rejection unobserved — fine, as long as it is not\n // reported as an unhandled rejection.\n exited.catch(() => {});\n\n let channel: Channel;\n try {\n channel = await Promise.race([\n clientChannel(streams, {\n onOutput: output,\n ...(spec.handshakeTimeoutMs === undefined ? {} : { timeoutMs: spec.handshakeTimeoutMs }),\n }),\n exited,\n ]);\n } catch (err) {\n child.kill();\n throw new RemoteSpecError(`${spec.name} is not available: ${errorText(err)}`);\n }\n\n // The far end's exit is the channel's close: whichever side goes first, the other stops\n // waiting — and the caller is told, because its slot must stop pointing at a process\n // that is gone.\n child.once(\"exit\", () => {\n spec.onGone?.();\n void channel.close().catch(() => {});\n });\n return channel;\n}\n\n/**\n * A spawner for an actor that lives behind a gateway in some environment: put the payload and\n * posipaki's gateway wherever they belong, run the gateway, speak the client side.\n */\nexport function gatewayClient<Args>(spec: RemoteSpec<Args>): ClientSpawner<Args> {\n const run: HostRun = spec.run ?? runHost;\n const spawn: SpawnChild = spec.spawn ?? spawnChild;\n return async (args: Args): Promise<Channel> => {\n // The spec is read before a command runs on its behalf: a payload that is not there\n // should fail here, not as an environment that cannot be reached.\n const programs = await programsFor(spec, run);\n const command = [\n ...programs.gateway,\n ...gatewayArgs({\n // What the payload is started as, then posipaki's own facts among the caller's\n // arguments: the host version, and whatever the consumer adds for its own far end.\n // Which of these is whose is not the gateway's business — it starts what it is handed\n // and carries the rest.\n passthrough: [\n ...programs.payload.slice(1),\n ...(spec.hostVersion === undefined\n ? []\n : [`--host-version=${hostVersion(spec.hostVersion)}`]),\n ...(spec.payloadArgs?.(args) ?? []),\n ],\n worker: programs.payload[0]!,\n }),\n ];\n return openChannel(spec, spec.entry(command), spawn);\n };\n}\n\n/**\n * The way in where the environment is this machine: nothing wraps the command, and the\n * kit is staged under `$HOME` like any other. Everything below the entry is the same as\n * for a sandbox, a host or a container — which is the point of it being here.\n */\nexport function hostRemote<Args>(\n spec: Omit<RemoteSpec<Args>, \"name\" | \"entry\"> & { name?: string },\n): ClientSpawner<Args> {\n return gatewayClient({ name: \"this host\", entry: (command) => command, ...spec });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"node.js","names":["errorText","errorText","Lines"],"sources":["../../src/remote/transports/fifo.ts","../../src/remote/spawners/fifo-command.ts","../../src/remote/kit.ts","../../src/remote/spawners/fifo-argv.ts","../../src/remote/define-subprocess.ts","../../src/remote/stdio.ts","../../src/remote/gateway.ts","../../src/remote/host.ts","../../src/remote/gateway-client.ts"],"sourcesContent":["// ── FIFO transport ─────────────────────────────────────────────────────────\n//\n// Newline-delimited UTF-8 over named fifos. Strict handler lifecycle:\n// onMessage() throws if a handler is already set; call removeHandler() first.\n\nimport { open, type FileHandle } from \"node:fs/promises\";\nimport type { ReadStream, WriteStream } from \"node:fs\";\nimport * as readline from \"node:readline\";\nimport type { StringTransport } from \"../channel.js\";\n\nexport class FifoUtf8NlineTransport implements StringTransport {\n private readFd: FileHandle | null;\n private writeFd: FileHandle | null;\n private rl: readline.Interface | null;\n private rs: ReadStream | null;\n private ws: WriteStream | null;\n private pvtOnMessage: ((line: string) => void) | null = null;\n private pvtOnClose: (() => void) | null = null;\n private closed = false;\n private closingPromise: Promise<void> | null = null;\n private pvtError: Error | null = null;\n\n private constructor(opts: { readFd?: FileHandle; writeFd?: FileHandle }) {\n this.readFd = opts.readFd ?? null;\n this.writeFd = opts.writeFd ?? null;\n\n if (this.readFd) {\n this.rs = this.readFd.createReadStream({ encoding: \"utf-8\" });\n this.rl = readline.createInterface({ input: this.rs });\n\n this.rl.on(\"line\", (line) => {\n if (this.pvtOnMessage && !this.closed) this.pvtOnMessage(line);\n });\n\n this.rl.on(\"close\", () => {\n this.pvtOnClose?.();\n this.close();\n });\n\n this.rs.on(\"error\", (err: Error) => {\n this.pvtError = err;\n this.close();\n });\n } else {\n this.rs = null;\n this.rl = null;\n }\n\n if (this.writeFd) {\n this.ws = this.writeFd.createWriteStream({ encoding: \"utf-8\" });\n this.ws.on(\"error\", (err: Error) => {\n this.pvtError = err;\n this.close();\n });\n } else {\n this.ws = null;\n }\n }\n\n // ── factories ──────────────────────────────────────────────────────────\n\n static openReaderFd(readFd: FileHandle): FifoUtf8NlineTransport {\n return new FifoUtf8NlineTransport({ readFd });\n }\n\n static openWriterFd(writeFd: FileHandle): FifoUtf8NlineTransport {\n return new FifoUtf8NlineTransport({ writeFd });\n }\n\n static fromFds(readFd: FileHandle, writeFd: FileHandle): FifoUtf8NlineTransport {\n return new FifoUtf8NlineTransport({ readFd, writeFd });\n }\n\n // ── bidirectional connection ───────────────────────────────────────────\n\n /**\n * Start opening a bidirectional connection. Opens readPath for reading\n * in the background, returns a promise. The caller should do whatever\n * setup is needed to unblock the read (e.g. spawn a process that opens\n * readPath for writing), then await the returned transport.\n *\n * The transport itself is built only once both directions are open. A read\n * stream created while no writer exists on its fifo is handed an immediate\n * end-of-stream by some runtimes (bun), which closes a channel that has not\n * carried a byte yet — so the writes side is opened before the reader exists.\n */\n static beginConnect(\n readPath: string,\n writePath: string,\n ): { transport: Promise<FifoUtf8NlineTransport> } {\n const readFdPromise = open(readPath, \"r\");\n\n const transport = readFdPromise.then(async (readFd) => {\n const writeFd = await open(writePath, \"w\");\n return FifoUtf8NlineTransport.fromFds(readFd, writeFd);\n });\n\n return { transport };\n }\n\n /**\n * Open a bidirectional connection. Opens writePath for writing first\n * (unblocking the peer's read), starts reading readPath in the\n * background, then awaits both.\n *\n * Use this when you are the side that responds to the peer's beginConnect\n * (i.e. you don't need to interleave any setup between the read and write\n * opens). Built like `beginConnect`: the transport exists only after both\n * directions do.\n */\n static async connect(readPath: string, writePath: string): Promise<FifoUtf8NlineTransport> {\n const readFdPromise = open(readPath, \"r\");\n const writeFd = await open(writePath, \"w\");\n const readFd = await readFdPromise;\n return FifoUtf8NlineTransport.fromFds(readFd, writeFd);\n }\n\n // ── public API ─────────────────────────────────────────────────────────\n\n get canSend(): boolean {\n return this.writeFd !== null && !this.closed;\n }\n\n onMessage(handler: (line: string) => void): void {\n if (this.closed) throw new Error(\"FifoUtf8NlineTransport: closed\");\n if (this.readFd === null) throw new Error(\"FifoUtf8NlineTransport: not a reader\");\n if (this.pvtOnMessage !== null) {\n throw new Error(\"FifoUtf8NlineTransport: handler already set — call removeHandler() first\");\n }\n this.pvtOnMessage = handler;\n }\n\n removeHandler(): ((line: string) => void) | null {\n const prev = this.pvtOnMessage;\n this.pvtOnMessage = null;\n return prev;\n }\n\n onClose(handler: () => void): void {\n this.pvtOnClose = handler;\n }\n\n get hasHandler(): boolean {\n return this.pvtOnMessage !== null;\n }\n\n get lastError(): Error | null {\n return this.pvtError;\n }\n\n async send(line: string): Promise<void> {\n if (this.closed) throw new Error(\"FifoUtf8NlineTransport: closed\");\n if (this.writeFd === null) throw new Error(\"FifoUtf8NlineTransport: not a writer\");\n if (!line.endsWith(\"\\n\")) line += \"\\n\";\n await this.ws?.write(line);\n }\n\n async close(): Promise<void> {\n this.closingPromise = this.closingPromise || this.pvtClose();\n await this.closingPromise;\n this.closed = true;\n }\n\n private async pvtClose(): Promise<void> {\n // tranfser control back to caller\n // so they can capture the promise reference\n // and prevent reentry\n await Promise.resolve();\n\n if (this.rl) {\n this.rl.close();\n this.rl = null;\n }\n if (this.rs) {\n this.rs.destroy();\n this.rs = null;\n }\n if (this.ws) {\n await new Promise<void>((resolve) => {\n try {\n this.ws?.end(\"\", resolve);\n } catch {\n resolve();\n }\n });\n this.ws = null;\n }\n\n if (this.readFd) {\n try {\n await this.readFd.close();\n } catch {}\n this.readFd = null;\n }\n if (this.writeFd) {\n try {\n await this.writeFd.close();\n } catch {}\n this.writeFd = null;\n }\n }\n}\n","// ── FIFO client spawner (command) ──────────────────────────────────────────\n//\n// Environment-specific: spawns a command, creates the two named fifos, and\n// returns a frame Channel with the $proto handshake already validated.\n\nimport { spawn, execSync } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { unlink } from \"node:fs/promises\";\nimport { FifoUtf8NlineTransport } from \"../transports/fifo.js\";\nimport { json1Channel, VERSION } from \"../protocols/json1.js\";\nimport { isProto } from \"../channel.js\";\nimport type { Channel } from \"../channel.js\";\n\nexport function commandSpawner(command: string[]): () => Promise<Channel> {\n return async () => {\n const basePath = join(tmpdir(), `posipaki-${randomUUID()}`);\n\n execSync(`mkfifo \"${basePath}.in\"`);\n execSync(`mkfifo \"${basePath}.out\"`);\n\n const setup = FifoUtf8NlineTransport.beginConnect(basePath + \".in\", basePath + \".out\");\n\n const childCmd = [\n ...command,\n `--fifo-in=${basePath + \".in\"}`,\n `--fifo-out=${basePath + \".out\"}`,\n ];\n const childProc = spawn(childCmd[0], childCmd.slice(1), {\n cwd: process.cwd(),\n stdio: [\"inherit\", \"inherit\", \"inherit\"],\n });\n\n const transport = await setup.transport;\n const channel = json1Channel(transport);\n\n const protoFrame = await new Promise<Record<string, unknown>>((resolve) => {\n channel.onMessage((frame) => resolve(frame));\n });\n channel.removeHandler();\n if (!isProto(protoFrame) || protoFrame.$proto !== VERSION) {\n throw new Error(`unsupported protocol: ${JSON.stringify(protoFrame).slice(0, 50)}`);\n }\n\n let cleaned = false;\n const cleanup = () => {\n if (cleaned) return;\n cleaned = true;\n transport.close();\n unlink(basePath + \".in\").catch(() => {});\n unlink(basePath + \".out\").catch(() => {});\n };\n childProc.on(\"exit\", cleanup);\n\n return channel;\n };\n}\n","// ── Kit ────────────────────────────────────────────────────────────────────\n//\n// Getting a payload onto a host we know nothing about. A way in gives us a\n// shell; from there one script runs everywhere: probe what is really there, write\n// the kit only if it is not, and report every step as one machine-readable line.\n// The script always arrives on stdin, never in argv — and since stdin *is* the\n// script, the payload gets its own channel into the same environment (a second\n// `ssh`, an `exec` on a container that is already running).\n//\n// The script assumes POSIX sh and nothing else: `command -v`, `mkdir -p`,\n// `wc -c`, `base64 -d`. A broken environment therefore reaches the client as a\n// readable reason instead of a channel that just closes.\n//\n// A kit is named after the *host version* it serves — the app, the build its bytes came from\n// and the posipaki release that speaks to it — so two consumers, two builds or two releases\n// cannot land on each other's files, and the directory a kit sits in cannot disagree with the\n// identity the client then states on the wire. A payload that is started for a host version\n// other than its own refuses to serve (see `hostVersionAccepts`), so a name alone is never\n// mistaken for the build behind it.\n\nimport { createHash } from \"node:crypto\";\nimport { LIB_VERSION } from \"../version.js\";\n\n/** Runtimes a kit is happy to be run with, best first. A caller may override. */\nexport const DEFAULT_RUNTIMES = [\"node\", \"nodejs\", \"bun\"];\n\n/** Layout of a staged kit — the contract's own version, recorded in `version.json`. */\nexport const KIT_LAYOUT = 1;\n\n/** Where kits live on a host, relative to `$HOME`, unless the caller says otherwise. */\nexport const DEFAULT_KIT_PARENT = \"bin/posipaki\";\n\n/** The names a kit's two bundles get: the payload, and the gateway that relays to it. */\nexport const PAYLOAD_ARTIFACT = \"payload.js\";\nexport const GATEWAY_ARTIFACT = \"gateway.js\";\n\nexport interface KitFile {\n /** Name inside the kit directory, e.g. `payload.js`. */\n name: string;\n /** The file's bytes, base64 — what the script writes. */\n base64: string;\n /** Byte count, for the cheap presence check. */\n bytes: number;\n /** sha256 of the file, recorded in `version.json`. */\n sha256: string;\n}\n\n/** Who is staging this kit: the consumer's own name and build version. */\nexport interface KitApp {\n name: string;\n version: string;\n}\n\nexport interface Kit {\n /** Directory name: the host version this kit serves, as {@link hostVersion} renders it. */\n name: string;\n /** Everything the kit installs, `version.json` last. */\n files: KitFile[];\n /** Runtime candidates, in order; the first one found wins. */\n runtimes: string[];\n /** Directory the kit lands in, relative to the host's `$HOME`. */\n parent: string;\n /** The consumer this kit belongs to. */\n app: KitApp;\n}\n\nexport interface MakeKitOptions {\n /** Required: a kit with no owner cannot be told apart from another's. */\n app: KitApp;\n /** Defaults to {@link DEFAULT_RUNTIMES}. */\n runtimes?: string[];\n /** Defaults to {@link DEFAULT_KIT_PARENT}. */\n parent?: string;\n}\n\nexport type BootstrapReport =\n | { kind: \"ready\"; kitDir: string; runtime: string; staged: boolean }\n | { kind: \"error\"; reason: string };\n\n/** Hex sha256 of some content. */\nexport function sha256Hex(content: string | Uint8Array): string {\n return createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\n/** What may stand as one fact in a host version: no `@`, no whitespace, no `/`. */\nconst FACT = /^[A-Za-z0-9][A-Za-z0-9.+_-]*$/;\n\n/** The label posipaki's own field carries, so nobody has to guess which field it is. */\nconst POSIPAKI_FIELD = \"posipaki-\";\n\n/** A host version read back: what a consumer stated, and the posipaki that rendered it. */\nexport interface HostVersion extends KitApp {\n posipaki: string;\n}\n\n/** A name or version that could not stand as one fact in a host version. */\nexport class HostVersionError extends Error {}\n\n/**\n * The one value that says what a host is expected to run: whose payload it is, which build\n * those bytes came from, and the posipaki release that speaks to it —\n * `email-agent@1.0.0+d52ee13@posipaki-0.35.0`.\n *\n * One string rather than three flags because it is one fact: it names the kit directory, it\n * travels with every way in, and the payload compares it with its own before it serves. A\n * consumer states the two facts only it knows; posipaki renders its own release, so that\n * field cannot be got wrong. `@` separates, and no name or version may contain one.\n */\nexport function hostVersion(app: KitApp): string {\n const facts: [string, string][] = [\n [\"name\", app.name],\n [\"version\", app.version],\n ];\n for (const [what, value] of facts) {\n if (!FACT.test(value)) {\n throw new HostVersionError(\n `an app ${what} must match ${FACT.source}: ${JSON.stringify(value)}`,\n );\n }\n }\n return `${app.name}@${app.version}@${POSIPAKI_FIELD}${LIB_VERSION}`;\n}\n\n/** What a {@link hostVersion} string names, or undefined when it names nothing. */\nexport function parseHostVersion(text: string): HostVersion | undefined {\n const fields = text.split(\"@\");\n if (fields.length !== 3) return undefined;\n const [name, version, posipaki] = fields as [string, string, string];\n if (!FACT.test(name) || !FACT.test(version)) return undefined;\n if (!posipaki.startsWith(POSIPAKI_FIELD)) return undefined;\n const release = posipaki.slice(POSIPAKI_FIELD.length);\n if (!FACT.test(release)) return undefined;\n return { name, version, posipaki: release };\n}\n\n/** Exit code a payload refuses a start with: it is not the build that was asked for. */\nexport const PAYLOAD_REFUSED = 78;\n\nexport type HostVersionVerdict = { ok: true } | { ok: false; reason: string };\n\n/**\n * Whether a payload may serve what it was asked for. `own` is what its bytes carry, `asked`\n * is what the caller said — two independent facts, and one of the four combinations is the\n * dangerous one: an artifact that knows what it is, started by a caller that never said which\n * host version it is for, is exactly the stale caller we refuse.\n *\n * No baked version, nothing to compare: an artifact that does not know what it is cannot be\n * wrong about it, and a caller that states a version for such a payload is only being helpful.\n */\nexport function hostVersionAccepts(\n own: string | undefined,\n asked: string | undefined,\n): HostVersionVerdict {\n if (own === undefined) return { ok: true };\n if (asked === undefined) {\n return { ok: false, reason: `this artifact is ${own}, and no host version was asked for` };\n }\n if (!sameHostVersion(own, asked)) {\n return { ok: false, reason: `asked to serve ${asked}, but this artifact is ${own}` };\n }\n return { ok: true };\n}\n\n/**\n * Whether two host versions are the same build. Equality for now: {@link hostVersionAccepts}\n * is the rule, and this is the one function that decides what \"the same\" means — so a range, a\n * compatibility table or a protocol-only comparison is a change here and nowhere else.\n */\nexport function sameHostVersion(own: string, asked: string): boolean {\n return own === asked;\n}\n\n/** Wrap base64 at a comfortable width; `base64 -d` ignores the newlines. */\nfunction wrapped(base64: string, width = 76): string {\n const lines: string[] = [];\n for (let at = 0; at < base64.length; at += width) lines.push(base64.slice(at, at + width));\n return lines.join(\"\\n\");\n}\n\n/**\n * Describe a kit around its files. Its name is the host version it serves, and `version.json`\n * — which records that name and every file's hash — is written as one of them: it describes\n * the payload and the gateway, never itself.\n */\nexport function makeKit(\n files: { name: string; content: string | Uint8Array }[],\n options: MakeKitOptions,\n): Kit {\n const raw = files.map((file) => ({\n name: file.name,\n bytes: Buffer.byteLength(file.content),\n sha256: sha256Hex(file.content),\n base64: Buffer.from(file.content).toString(\"base64\"),\n }));\n const kit: Kit = {\n name: hostVersion(options.app),\n files: raw,\n runtimes: [...(options.runtimes ?? DEFAULT_RUNTIMES)],\n parent: options.parent ?? DEFAULT_KIT_PARENT,\n app: options.app,\n };\n const version = Buffer.from(`${JSON.stringify(kitVersion(kit), null, 2)}\\n`);\n return {\n ...kit,\n files: [\n ...kit.files,\n {\n name: \"version.json\",\n bytes: version.byteLength,\n sha256: sha256Hex(version),\n base64: version.toString(\"base64\"),\n },\n ],\n };\n}\n\n/** What `version.json` says: the identity, the facts it was rendered from, and the hashes. */\nexport function kitVersion(kit: Kit): Record<string, unknown> {\n return {\n hostVersion: kit.name,\n app: kit.app,\n posipaki: LIB_VERSION,\n layout: KIT_LAYOUT,\n files: kit.files.map((file) => ({ name: file.name, sha256: file.sha256, bytes: file.bytes })),\n };\n}\n\n/** The `[ -f … ] && [ \"$(wc -c < …)\" -eq … ]` test for one file. */\nfunction presentTest(file: KitFile): string {\n return `[ -f \"$kit_dir/${file.name}\" ] && [ \"$(wc -c < \"$kit_dir/${file.name}\")\" -eq ${file.bytes} ]`;\n}\n\n/** One file: write it to a temp name, then move it into place. */\nfunction writeFile(file: KitFile): string {\n return [\n ` base64 -d > \"$tmp\" <<'KIT_FILE'`,\n wrapped(file.base64),\n \"KIT_FILE\",\n ` mv \"$tmp\" \"$kit_dir/${file.name}\" || { echo \"error cannot install ${file.name}\"; exit 73; }`,\n ].join(\"\\n\");\n}\n\n/**\n * The bootstrap, as one POSIX sh script.\n *\n * Staging is the whole job. The script always arrives on stdin, never in argv,\n * and that is also why it cannot carry the wire: the caller runs the payload on a\n * second channel into the same environment once this one has reported.\n */\nexport function bootstrapScript(kit: Kit): string {\n const checks = kit.files.map(presentTest).join(\" && \\\\\\n \");\n const lines = [\n \"# ── kit bootstrap ─────────────────────────────────────────────────────────\",\n \"# probe, stage what is missing, report every step as one machine-readable line.\",\n \"set -u\",\n \"\",\n 'if [ -z \"${HOME:-}\" ]; then echo \"error HOME is not set\"; exit 74; fi',\n `kit_dir=\"$HOME/${kit.parent}/${kit.name}\"`,\n \"\",\n 'runtime=\"\"',\n `for candidate in ${kit.runtimes.join(\" \")}; do`,\n ' if command -v \"$candidate\" >/dev/null 2>&1; then runtime=\"$(command -v \"$candidate\")\"; break; fi',\n \"done\",\n `if [ -z \"$runtime\" ]; then echo \"error no runtime among: ${kit.runtimes.join(\" \")}\"; exit 75; fi`,\n 'if ! command -v base64 >/dev/null 2>&1; then echo \"error base64 is missing\"; exit 76; fi',\n \"\",\n `if ${checks}; then`,\n ' echo \"present\"',\n \"else\",\n ` mkdir -p \"$kit_dir\" || { echo \"error cannot create $kit_dir\"; exit 73; }`,\n ' tmp=\"$kit_dir/.staging.$$\"',\n ...kit.files.map(writeFile),\n ' echo \"staged\"',\n \"fi\",\n 'echo \"kit $kit_dir\"',\n 'echo \"runtime $runtime\"',\n ];\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/** Read the script's report back. Missing or unknown lines are an error. */\nexport function parseBootstrapReport(stdout: string): BootstrapReport {\n let kitDir = \"\";\n let runtime = \"\";\n let staged: boolean | null = null;\n for (const raw of stdout.split(\"\\n\")) {\n const line = raw.trim();\n if (line === \"\") continue;\n if (line.startsWith(\"error \")) return { kind: \"error\", reason: line.slice(\"error \".length) };\n if (line === \"staged\") {\n staged = true;\n continue;\n }\n if (line === \"present\") {\n staged = false;\n continue;\n }\n if (line.startsWith(\"kit \")) {\n kitDir = line.slice(\"kit \".length);\n continue;\n }\n if (line.startsWith(\"runtime \")) {\n runtime = line.slice(\"runtime \".length);\n continue;\n }\n }\n if (kitDir === \"\" || runtime === \"\" || staged === null) {\n const seen = stdout.trim();\n return {\n kind: \"error\",\n reason: seen === \"\" ? \"no report at all\" : `incomplete report: ${seen}`,\n };\n }\n return { kind: \"ready\", kitDir, runtime, staged };\n}\n","// ── FIFO server spawner (argv) ─────────────────────────────────────────────\n//\n// Environment-specific: reads what the caller put in argv — the two named fifos, and the host\n// version this artifact is being started for — checks that it *is* that build, connects, and\n// returns a frame Channel with the $proto handshake already sent.\n//\n// The check comes first, before anything is opened: a payload that is not the build that was\n// asked for has no business on a wire, and saying so before it connects is the only moment\n// where the reason is still readable to the caller.\n\nimport { FifoUtf8NlineTransport } from \"../transports/fifo.js\";\nimport { json1Channel, VERSION } from \"../protocols/json1.js\";\nimport type { Channel } from \"../channel.js\";\nimport { PAYLOAD_REFUSED, hostVersionAccepts } from \"../kit.js\";\n\n/** A `--flag=<value>` argument, or undefined. */\nfunction flag(name: string): string | undefined {\n const prefix = `--${name}=`;\n return process.argv.find((a) => a.startsWith(prefix))?.slice(prefix.length);\n}\n\n/**\n * Serve one payload over the fifo pair in argv.\n *\n * `own` is the host version this artifact's bytes carry, when its build stamped one: given it,\n * the payload refuses to serve a host version that differs, and refuses a start that names\n * none. Nothing stamped means nothing to check — and `--host-version`, if the caller sent one\n * anyway, is simply not ours to compare with.\n */\nexport async function fifoArgvSpawner(own?: string): Promise<Channel> {\n const verdict = hostVersionAccepts(own, flag(\"host-version\"));\n if (!verdict.ok) {\n console.error(`payload: ${verdict.reason}`);\n process.exit(PAYLOAD_REFUSED);\n }\n\n const fifoIn = flag(\"fifo-in\");\n const fifoOut = flag(\"fifo-out\");\n if (!fifoIn || !fifoOut) {\n console.error(\"server: --fifo-in=<path> and --fifo-out=<path> required\");\n process.exit(1);\n }\n\n const transport = await FifoUtf8NlineTransport.connect(fifoOut, fifoIn);\n const channel = json1Channel(transport);\n await channel.send({ $proto: VERSION });\n return channel;\n}\n","// ── defineSubprocessActor ───────────────────────────────────────────────────\n//\n// Environment-specific glue: wraps an actor definition so that spawning it runs\n// it in a subprocess over two named fifos. It does the guesswork — argv marker\n// detection and runner auto-detection — and prepares the client spawner. This\n// is the only place in the subprocess path that imports transports/protocols.\n\nimport { fileURLToPath } from \"node:url\";\nimport { createHash } from \"node:crypto\";\nimport type { Message } from \"../types.js\";\nimport type { ActorDefinition, HandlerOptions, MethodOptions, ReflectionOptions } from \"../actor-types.js\";\nimport type { KitApp } from \"./kit.js\";\nimport { hostVersion } from \"./kit.js\";\nimport { remoteClient } from \"./client.js\";\nimport { serveRemoteActor } from \"./server.js\";\nimport { commandSpawner } from \"./spawners/fifo-command.js\";\nimport { fifoArgvSpawner } from \"./spawners/fifo-argv.js\";\n\nexport interface SubprocessActorOptions {\n manual?: boolean;\n /**\n * Who this payload is: the app name and the build its bytes came from, as the consumer states\n * them. This is one switch for both halves of the seam — stated, every start passes\n * `--host-version` and the payload refuses to serve anything else; unstated, neither happens,\n * because there is no version to pass and none to check.\n */\n hostVersion?: KitApp;\n}\n\nexport interface SubprocessActorBundle<\n Args,\n State,\n InMsg extends Message,\n OutMsg extends Message,\n R extends ReflectionOptions,\n> {\n actor: ActorDefinition<Args, State, InMsg, OutMsg, R>;\n runRemoteRoot(): Promise<void>;\n isRemoteRoot: boolean;\n}\n\nfunction pathHash(path: string): string {\n return createHash(\"sha256\").update(path).digest(\"hex\").slice(0, 12);\n}\n\nconst MARKER_PREFIX = \"--remote=\";\n\nexport function defineSubprocessActor<\n Args,\n State,\n InMsg extends Message,\n OutMsg extends Message,\n Methods extends MethodOptions,\n Handlers extends HandlerOptions<InMsg>,\n R extends ReflectionOptions,\n>(\n actor: ActorDefinition<Args, State, InMsg, OutMsg, R>,\n url: string,\n opts: SubprocessActorOptions = {},\n): SubprocessActorBundle<Args, State, InMsg, OutMsg, R> {\n const scriptPath = fileURLToPath(url);\n const marker = `${MARKER_PREFIX}${pathHash(scriptPath)}`;\n const isRemoteRoot = !opts.manual && process.argv.includes(marker);\n const own =\n opts.hostVersion === undefined\n ? undefined\n : hostVersion(opts.hostVersion);\n\n const serve = () => serveRemoteActor(actor, () => fifoArgvSpawner(own));\n\n if (isRemoteRoot) {\n void serve();\n }\n\n const isBun = typeof (globalThis as { Bun?: unknown }).Bun !== \"undefined\";\n const runner = isBun ? \"bun\" : \"node\";\n const spawner = commandSpawner([\n runner,\n scriptPath,\n marker,\n ...(own === undefined ? [] : [`--host-version=${own}`]),\n ]);\n\n const proxyDef = remoteClient<Args, State, InMsg, OutMsg, Methods, Handlers, R>(\n actor.name ?? \"actor\",\n spawner,\n );\n\n return {\n actor: proxyDef,\n runRemoteRoot: serve,\n isRemoteRoot,\n };\n}\n","// ── stdio wire ─────────────────────────────────────────────────────────────\n//\n// A newline-delimited JSON channel over a pair of streams: the one wire every\n// way into a foreign execution context can carry. Two callers rely on it, and\n// they are two halves of the same thing:\n//\n// * the client of a spawn we make ourselves, on fds handed to the child (see\n// `fdStreams`) — private fds, so the child's own stdout stays free;\n// * a first-stage command on its own stdin/stdout, over ssh, under `sudo`, in\n// a container — where stdio is the only channel there is.\n//\n// Frames are one JSON object per line. Whatever the far end prints is carried\n// as an *output frame* (`$fd`), so it can never be mistaken for a frame of the\n// protocol itself; a failed start is carried as an `$error` frame.\n//\n// Why the client filters output frames before handing the transport to the\n// seam: `json1Channel` decodes every line it sees, and the first frame of a\n// session is the peer's `$proto`. Stripping output frames in the transport\n// keeps the protocol view clean without the seam knowing anything about them.\n\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport * as readline from \"node:readline\";\nimport type { Readable, Writable } from \"node:stream\";\nimport { isProto, type Channel, type StringTransport } from \"./channel.js\";\nimport { VERSION, json1Channel } from \"./protocols/json1.js\";\n\n/** The stream pair a line transport talks over. */\nexport interface LineStreams {\n read: Readable;\n write: Writable;\n}\n\n/** A stream of the far end's process: its stdout or its stderr. */\nexport type OutputFd = 1 | 2;\n\n/** Where the far end's own output goes. */\nexport type OutputSink = (fd: OutputFd, data: string) => void;\n\n/** How long to wait for the far end's first protocol frame. */\nexport const HANDSHAKE_TIMEOUT_MS = 15_000;\n\nconst OUTPUT_KEY = \"$fd\";\n\nconst ERROR_KEY = \"$error\";\n\n/** Raised when the wire itself fails: no frame, the wrong protocol, a peer that went away. */\nexport class StdioWireError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"StdioWireError\";\n }\n}\n\n// ── output frames ──────────────────────────────────────────────────────────\n\n/** One frame of the far end's own output. */\nexport interface OutputFrame {\n fd: OutputFd;\n data: string;\n}\n\nexport function outputFrame(fd: OutputFd, data: string): string {\n return JSON.stringify({ [OUTPUT_KEY]: fd, data });\n}\n\n/** The output frame a line carries, or null when it carries something else. */\nexport function parseOutputFrame(line: string): OutputFrame | null {\n // Cheap reject first: almost every line of a session is protocol traffic.\n if (!line.startsWith('{\"$fd\"')) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n return null;\n }\n if (typeof parsed !== \"object\" || parsed === null) return null;\n const frame = parsed as Record<string, unknown>;\n const fd = frame[OUTPUT_KEY];\n const data = frame.data;\n if (fd !== 1 && fd !== 2) return null;\n return { fd, data: typeof data === \"string\" ? data : JSON.stringify(data) };\n}\n\nexport function errorFrame(reason: string): string {\n return JSON.stringify({ [ERROR_KEY]: reason });\n}\n\n/** The reason an `$error` frame carries, or null when it is not one. */\nexport function errorReason(frame: Record<string, unknown>): string | null {\n const reason = frame[ERROR_KEY];\n return typeof reason === \"string\" ? reason : null;\n}\n\n// ── transports ─────────────────────────────────────────────────────────────\n\n/** Is this write error just a peer that already left? */\nfunction peerGone(code: unknown): boolean {\n return (\n code === \"EPIPE\" ||\n code === \"ERR_STREAM_DESTROYED\" ||\n code === \"ERR_STREAM_WRITE_AFTER_END\" ||\n code === \"ERR_STREAM_ALREADY_FINISHED\"\n );\n}\n\nfunction errorText(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * A `StringTransport` over a read/write stream pair, one JSON frame per line.\n *\n * `close()` is idempotent and never throws: teardown races are normal here —\n * the peer exits, the read side ends, and a write arrives a moment later.\n */\nexport class LineTransport implements StringTransport {\n private readonly streams: LineStreams;\n private pvtOnMessage: ((line: string) => void) | null = null;\n private pvtOnClose: (() => void) | null = null;\n private pvtClosed = false;\n private pvtClosing: Promise<void> | null = null;\n\n constructor(streams: LineStreams) {\n this.streams = streams;\n const lines = readline.createInterface({ input: streams.read });\n lines.on(\"line\", (line) => {\n if (this.pvtOnMessage && !this.pvtClosed) this.pvtOnMessage(line);\n });\n lines.on(\"close\", () => {\n this.pvtOnClose?.();\n void this.close();\n });\n // A stream error is a peer that went away, not a reason to take the process\n // down with an unhandled 'error' event.\n streams.read.on(\"error\", () => {\n void this.close();\n });\n streams.write.on(\"error\", () => {\n void this.close();\n });\n }\n\n get closed(): boolean {\n return this.pvtClosed;\n }\n\n get hasHandler(): boolean {\n return this.pvtOnMessage !== null;\n }\n\n onMessage(handler: (line: string) => void): void {\n if (this.pvtClosed) throw new StdioWireError(\"stdio transport: closed\");\n if (this.pvtOnMessage !== null) {\n throw new StdioWireError(\"stdio transport: handler already set — call removeHandler()\");\n }\n this.pvtOnMessage = handler;\n }\n\n removeHandler(): ((line: string) => void) | null {\n const previous = this.pvtOnMessage;\n this.pvtOnMessage = null;\n return previous;\n }\n\n onClose(handler: () => void): void {\n this.pvtOnClose = handler;\n }\n\n send(frame: string): Promise<void> {\n if (this.pvtClosed) {\n return Promise.resolve();\n }\n const line = frame.endsWith(\"\\n\") ? frame : `${frame}\\n`;\n return new Promise<void>((resolve, reject) => {\n try {\n this.streams.write.write(line, (err?: Error | null) => {\n if (!err) return resolve();\n const code = (err as { code?: unknown }).code;\n // A peer that has hung up cannot be told anything; the write that\n // races its exit is not an error in this transport.\n if (peerGone(code)) {\n return resolve();\n }\n reject(new StdioWireError(`stdio transport: ${err.message}`));\n });\n } catch (err) {\n reject(new StdioWireError(`stdio transport: ${errorText(err)}`));\n }\n });\n }\n\n async close(): Promise<void> {\n this.pvtClosing ??= this.pvtClose();\n await this.pvtClosing;\n }\n\n private async pvtClose(): Promise<void> {\n await Promise.resolve();\n\n this.pvtClosed = true;\n // Best effort: a half-open stream (the peer is already gone) must not turn\n // teardown into an exception.\n try {\n this.streams.read.destroy?.();\n } catch {\n /* already gone */\n }\n try {\n if (!this.streams.write.writableEnded) this.streams.write.end();\n } catch {\n /* already gone */\n }\n }\n}\n\n/** Streams over two fds of *our own* process — the child side of an fd wire. */\nexport function fdStreams(readFd: number, writeFd: number): LineStreams {\n const read = createReadStream(\"\", { fd: readFd, encoding: \"utf-8\", autoClose: false });\n const write = createWriteStream(\"\", { fd: writeFd, encoding: \"utf-8\", autoClose: false });\n return { read, write };\n}\n\n/**\n * A transport that lifts output frames out of the stream: everything else is\n * passed through untouched, so the protocol sees frames and only frames.\n */\nexport class OutputFilter implements StringTransport {\n private readonly inner: StringTransport;\n private readonly sink: OutputSink;\n\n constructor(inner: StringTransport, sink: OutputSink) {\n this.inner = inner;\n this.sink = sink;\n }\n\n send(frame: string): void | Promise<void> {\n return this.inner.send(frame);\n }\n\n onMessage(handler: (frame: string) => void): void {\n this.inner.onMessage((line) => {\n const output = parseOutputFrame(line);\n if (output) this.sink(output.fd, output.data);\n else handler(line);\n });\n }\n\n removeHandler(): void {\n this.inner.removeHandler();\n }\n\n onClose(handler: () => void): void {\n this.inner.onClose(handler);\n }\n\n close(): Promise<void> {\n return this.inner.close();\n }\n}\n\n// ── sinks ──────────────────────────────────────────────────────────────────\n\n/** Write the far end's output to our stderr, tagged with its name. */\nexport function stderrSink(name = \"\"): OutputSink {\n const tag = name ? `[${name}] ` : \"\";\n return (fd, data) => {\n const lead = fd === 1 ? tag : `${tag}err: `;\n for (const line of data.split(\"\\n\")) {\n if (line.length > 0) process.stderr.write(`${lead}${line}\\n`);\n }\n };\n}\n\n// ── handshakes ─────────────────────────────────────────────────────────────\n\n/** The next frame that arrives, or a timeout. */\nasync function nextFrame(channel: Channel, timeoutMs: number): Promise<Record<string, unknown>> {\n return await new Promise((resolve, reject) => {\n const timer = setTimeout(\n () => reject(new StdioWireError(`no protocol frame within ${timeoutMs}ms`)),\n timeoutMs,\n );\n timer.unref?.();\n channel.onMessage((frame) => {\n clearTimeout(timer);\n resolve(frame);\n });\n });\n}\n\nexport interface ClientChannelOptions {\n /** Where the far end's own output goes. Defaults to our stderr. */\n onOutput?: OutputSink;\n /** How long to wait for the peer's protocol frame. Defaults to {@link HANDSHAKE_TIMEOUT_MS}. */\n timeoutMs?: number;\n}\n\n/**\n * The client end of the wire: hand the streams to the seam (minus output\n * frames) and wait for the peer's `$proto`. A first stage reports a failed\n * start as an `$error` frame instead.\n */\nexport async function clientChannel(\n streams: LineStreams,\n opts: ClientChannelOptions = {},\n): Promise<Channel> {\n const transport = new LineTransport(streams);\n const channel = json1Channel(new OutputFilter(transport, opts.onOutput ?? stderrSink()));\n const frame = await nextFrame(channel, opts.timeoutMs ?? HANDSHAKE_TIMEOUT_MS);\n channel.removeHandler();\n\n const reason = errorReason(frame);\n if (reason) throw new StdioWireError(reason);\n if (!isProto(frame)) {\n throw new StdioWireError(`unexpected first frame: ${JSON.stringify(frame).slice(0, 120)}`);\n }\n if (frame.$proto !== VERSION) {\n throw new StdioWireError(`unsupported protocol ${String(frame.$proto)}, expected ${VERSION}`);\n }\n return channel;\n}\n\n/** The server end of the wire — a payload speaking stdio itself. */\nexport async function serverChannel(streams: LineStreams): Promise<Channel> {\n const channel = json1Channel(new LineTransport(streams));\n await channel.send({ $proto: VERSION });\n return channel;\n}\n","// ── Gateway ────────────────────────────────────────────────────────────────\n//\n// The first-stage command. Run this *inside* a foreign environment — directly,\n// under `sudo`, over ssh, in a container — and it:\n//\n// 1. creates the channel to the payload (fifo paths it makes itself, in a\n// private temp dir, so they belong to the uid that will open them),\n// 2. starts the payload — the first argument, required — hands it everything else it was\n// given (the host version among it) and relays frames between stdin/stdout,\n// 3. carries whatever the payload prints to the client as output frames\n// (`$fd`), so the payload's own stdout can never be mistaken for protocol.\n//\n// Why a gateway at all: a fifo path created on the client side is useless when the\n// payload runs as another uid (it cannot open it) or on another host (the path is\n// not there). stdio is the one channel every way into an environment gives us, so\n// the wire is stdio and the fifo stays inside the environment.\n//\n// The gateway reads nothing out of what it carries and owns no flag: the payload's path and\n// every argument after it go through verbatim, in order. The payload is the one who refuses a\n// host version it is not, and a relay that judged would be a second place for that decision.\n//\n// Directories and processes are cleaned up on every exit path, which is also why\n// the client never sees a transport being closed twice.\n//\n// It imports nothing but node builtins and this package, so the payload behind it\n// can be anything the runtime there can start.\n\nimport { execFile, spawn } from \"node:child_process\";\nimport type { ChildProcess } from \"node:child_process\";\nimport { mkdtemp, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { errorFrame, outputFrame } from \"./stdio.js\";\nimport type { LineTransport, OutputFd } from \"./stdio.js\";\nimport { LineTransport as Lines } from \"./stdio.js\";\nimport { FifoUtf8NlineTransport } from \"./transports/fifo.js\";\n\nconst run = promisify(execFile);\n\n/** Exit code for \"this environment could not be set up\". */\nexport const GATEWAY_FAILED = 1;\n\nexport interface GatewayBoot {\n /**\n * The program to start for the payload: the first word of the command the client composed —\n * a runtime, or a program that runs itself. Required, and first, and the gateway has no\n * opinion of its own about which it is.\n */\n worker: string;\n /**\n * Everything after the payload, carried to it **as is** and in order: the consumer's own\n * arguments, and posipaki's own facts among them (the host version travels this way). The\n * gateway reads nothing by name and owns no flag, so nothing of the caller's is interpreted,\n * eaten, re-ordered or re-spelled — and the next fact added to that line needs no new\n * vocabulary here.\n */\n passthrough: string[];\n}\n\nfunction errorText(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * A label for the gateway's own temp directory: the program this environment was made for.\n * It is the gateway's business alone — the payload is not told which label it is.\n */\nfunction label(worker: string): string {\n const name = basename(worker).replace(/[^A-Za-z0-9.+@_-]/g, \"-\").slice(0, 60);\n return name === \"\" ? \"payload\" : name;\n}\n\n/**\n * The argv that boots this gateway — built by the client, read by\n * {@link gatewayBoot}, so the two halves cannot drift apart.\n *\n * The payload comes first and its arguments after it, so a positional argument of the\n * payload's own can never be mistaken for the payload.\n */\nexport function gatewayArgs(boot: GatewayBoot): string[] {\n return [boot.worker, ...boot.passthrough];\n}\n\n/** The boot a gateway was started with, or the reason it cannot start. */\nexport function gatewayBoot(argv: string[]): GatewayBoot {\n const [worker, ...passthrough] = argv;\n if (worker === undefined || worker.startsWith(\"--\")) {\n throw new Error(\"no <payload>: the gateway relays to a payload, named as its first argument\");\n }\n return { passthrough, worker };\n}\n\n/**\n * End a start that failed. The reason has already gone out on the wire, and\n * returning is not enough: the `open()` on a fifo no payload will ever write sits\n * in the threadpool forever, so the loop never drains and the gateway would linger\n * with a dead payload on its books.\n */\nfunction abandon(): never {\n process.exit(GATEWAY_FAILED);\n}\n\n/** Carry a failed start to the client, then let the caller exit non-zero. */\nasync function fail(wire: LineTransport, reason: string): Promise<void> {\n process.stderr.write(`gateway: ${reason}\\n`);\n try {\n await wire.send(errorFrame(reason));\n } catch {\n /* the client is gone too — the exit code still says what happened */\n }\n}\n\n/** Copy everything the payload prints onto the wire as output frames. */\nfunction forwardOutput(stream: NodeJS.ReadableStream, wire: LineTransport, fd: OutputFd): void {\n stream.setEncoding(\"utf-8\");\n stream.on(\"data\", (chunk: string) => {\n void wire.send(outputFrame(fd, chunk)).catch(() => {\n /* the client hung up: the payload's exit is what matters now */\n });\n });\n}\n\n/**\n * Serve one environment over stdin/stdout. Returns the exit code; the caller\n * turns it into `process.exitCode`.\n */\nexport async function runGateway(argv: string[] = process.argv.slice(2)): Promise<number> {\n const wire = new Lines({ read: process.stdin, write: process.stdout });\n\n let booted: GatewayBoot;\n try {\n booted = gatewayBoot(argv);\n } catch (err) {\n // Nothing has been started yet, so there is nothing to tear down — but the\n // client still deserves the reason rather than a channel that closes on it,\n // and the loop can outlive us: an open stdin nobody will close keeps the\n // process alive, so this is an exit, not a return.\n await fail(wire, errorText(err));\n abandon();\n }\n const { passthrough, worker } = booted;\n\n const dir = await mkdtemp(join(tmpdir(), `posipaki-${label(worker)}-`));\n const fifoIn = join(dir, \"in\"); // the payload writes, we read\n const fifoOut = join(dir, \"out\"); // we write, the payload reads\n\n let child: ChildProcess | null = null;\n let fifo: FifoUtf8NlineTransport | null = null;\n\n /**\n * Everything this gateway owns, undone. All of it is idempotent, because the\n * exit path and the signal path can both arrive.\n */\n const teardown = async (): Promise<void> => {\n child?.kill();\n await fifo?.close().catch(() => {});\n await rm(dir, { recursive: true, force: true }).catch(() => {});\n };\n\n /**\n * A client that gives up stops us with a signal, and a gateway that died on the\n * spot would leave its payload behind in the environment — a process holding a\n * fifo nobody will ever write, which is a leak the environment cannot clean up\n * for us. So the signals do what the exit path does.\n */\n const onTerminate = () => {\n void teardown()\n .catch(() => {})\n .then(() => process.exit(GATEWAY_FAILED));\n };\n for (const signal of [\"SIGTERM\", \"SIGINT\", \"SIGHUP\"] as const) process.on(signal, onTerminate);\n\n try {\n await run(\"mkfifo\", [\"-m\", \"600\", fifoIn, fifoOut]);\n } catch (err) {\n await fail(wire, `cannot create the environment's fifos: ${errorText(err)}`);\n await teardown();\n abandon();\n }\n\n // Open the read side in the background, then start the payload that unblocks\n // it — the order the fifo handshake requires.\n const connection = FifoUtf8NlineTransport.beginConnect(fifoIn, fifoOut);\n const workerArgs = [worker, ...passthrough, `--fifo-in=${fifoIn}`, `--fifo-out=${fifoOut}`];\n // Whatever the client said starts the payload, run as it said: a runtime with a script of\n // ours, or a program of the caller's own. The gateway does not know which it is, and does\n // not choose a runtime on the far end's behalf.\n const workerProc = spawn(workerArgs[0]!, workerArgs.slice(1), {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n child = workerProc;\n forwardOutput(workerProc.stdout, wire, 1);\n forwardOutput(workerProc.stderr, wire, 2);\n\n const exited = new Promise<number>((settle) => {\n // A payload killed by a signal (the client hung up, and we killed it) has no\n // code — for us that is a clean end, not a failure.\n workerProc.once(\"exit\", (code) => settle(code ?? 0));\n });\n\n try {\n fifo = await Promise.race([\n connection.transport,\n exited.then((code) => {\n throw new Error(`the payload exited with code ${code} before it opened its channel`);\n }),\n ]);\n } catch (err) {\n await fail(wire, errorText(err));\n await teardown();\n abandon();\n }\n\n // Relay: the protocol is the client's and the payload's business, not ours.\n const relay = fifo;\n relay.onMessage((line) => {\n void wire.send(line).catch(() => {\n /* client gone */\n });\n });\n wire.onMessage((line) => {\n void relay.send(line).catch(() => {\n /* the payload is gone; its exit path cleans up */\n });\n });\n wire.onClose(() => {\n child?.kill();\n });\n\n const code = await exited;\n await teardown();\n await wire.close();\n return code;\n}\n","// ── Running one command on the host ────────────────────────────────────────\n//\n// Everything a way in does, it does by running a command: staging is a command in an\n// environment, and the actor is another one whose stdin/stdout are the wire. Both go\n// through the two functions here, and both are handed in — so a test can stage and run\n// without a sandbox, a host or a container, and a consumer can run host commands its\n// own way.\n\nimport { spawn } from \"node:child_process\";\nimport type { ChildProcess, StdioOptions } from \"node:child_process\";\n\nexport interface HostResult {\n code: number | null;\n stdout: string;\n stderr: string;\n}\n\n/** Run a command, feed it its stdin, and wait for what it said. */\nexport type HostRun = (command: string[], stdin: string) => Promise<HostResult>;\n\n/** The real thing: spawn it, feed it, collect it. */\nexport const runHost: HostRun = (command, stdin) =>\n new Promise((settle, fail) => {\n const child = spawn(command[0]!, command.slice(1), { stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n const out: string[] = [];\n const err: string[] = [];\n child.stdout.setEncoding(\"utf-8\");\n child.stderr.setEncoding(\"utf-8\");\n child.stdout.on(\"data\", (chunk: string) => out.push(chunk));\n child.stderr.on(\"data\", (chunk: string) => err.push(chunk));\n child.once(\"error\", (error) => fail(error));\n // A host that hangs up while we are still writing has failed; the exit code and\n // stderr carry that, so the write error is not the story here.\n child.stdin.on(\"error\", () => {});\n child.once(\"close\", (code) => settle({ code, stdout: out.join(\"\"), stderr: err.join(\"\") }));\n child.stdin.end(stdin);\n });\n\n/** How the wire's process is started, with the stdio it needs. */\nexport type SpawnChild = (command: string[], stdio: StdioOptions) => ChildProcess;\n\n/** The real thing: node's own spawn, in our working directory. */\nexport const spawnChild: SpawnChild = (command, stdio) =>\n spawn(command[0]!, command.slice(1), { cwd: process.cwd(), stdio });\n","// ── The client side of the gateway ─────────────────────────────────────────\n//\n// A consumer has two things: a payload, and the build its bytes came from. A way in has\n// one: how to run a command somewhere else. Everything else is the same whatever the way\n// in is, and this module is that everything else:\n//\n// 1. the gateway script is posipaki's own entry (`posipaki/remote/gateway-cli.js`, a\n// published specifier, so it is resolved where the kit is staged from) — or the\n// consumer's, for a program that carries its own;\n// 2. both bundles go into one kit, whose directory name *is* the host version it serves —\n// the app, the build those bytes came from, the posipaki release that speaks to them —\n// so a second run of the same build finds the kit already there and copies nothing;\n// 3. whatever has to be *copied* there is staged through the way in's own channel — the\n// core's bootstrap script, one report line per step — which is also where the runtime\n// that will run it is found, so a way in never has to know what a `node` is. A\n// program the caller says is already installed is not staged at all, and then no\n// command of ours runs in the environment beyond the two that matter;\n// 4. the gateway runs there — `<runtime> <kit>/gateway.js` when it was staged, the caller's\n// own command when it was not — its stdin/stdout are the wire, and it makes the fifos\n// inside the environment and starts the payload against them. The payload is run this\n// way in *every* way in, which is why the payload is only ever a fifo worker. The\n// payload's own command and the host version are arguments the gateway carries without\n// knowing which is which: staged artifacts are ours, and the payload checks itself\n// against what it was asked for.\n//\n// What a way in supplies is `entry`: the argv that runs a command there. It is the whole\n// difference between bwrap, ssh, a container and this machine, and it is one function.\n\nimport type { ChildProcess, StdioOptions } from \"node:child_process\";\nimport { readFile } from \"node:fs/promises\";\nimport type { Readable, Writable } from \"node:stream\";\nimport { fileURLToPath } from \"node:url\";\nimport type { Channel } from \"./channel.js\";\nimport type { ClientSpawner } from \"./client.js\";\nimport { gatewayArgs } from \"./gateway.js\";\nimport { runHost, spawnChild } from \"./host.js\";\nimport type { HostResult, HostRun, SpawnChild } from \"./host.js\";\nimport {\n DEFAULT_KIT_PARENT,\n DEFAULT_RUNTIMES,\n GATEWAY_ARTIFACT,\n PAYLOAD_ARTIFACT,\n bootstrapScript,\n hostVersion,\n makeKit,\n parseBootstrapReport,\n} from \"./kit.js\";\nimport type { Kit, KitApp } from \"./kit.js\";\nimport { clientChannel, stderrSink } from \"./stdio.js\";\nimport type { LineStreams, OutputFd, OutputSink } from \"./stdio.js\";\n\n/** posipaki's own gateway program. A consumer with one of its own stages that instead. */\nexport const GATEWAY_ENTRY = \"posipaki/remote/gateway-cli.js\";\n\n/** How much of what a failed staging command said is quoted back as part of the reason. */\nconst SAID_TAIL = 400;\n\n/** How much of the far end's stderr is kept, to be the reason a start failed. */\nconst REASON_TAIL = 400;\n\n/** Staging failed, or the far end never spoke: the caller's spec, or the environment's answer. */\nexport class RemoteSpecError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"RemoteSpecError\";\n }\n}\n\n/** What a way in is handed, and how it runs what it is handed. */\nexport interface RemoteWayIn {\n /** What the far end's output and errors are tagged with. */\n name: string;\n /** The argv that runs `command` there: bwrap's policy, ssh's host, podman's container. */\n entry: (command: string[]) => string[];\n /** How a plain command runs there and is waited for. Injectable, so a test needs no way in. */\n run?: HostRun;\n /** How the wire's process is started there. Injectable likewise. */\n spawn?: SpawnChild;\n}\n\n/**\n * A program the way in starts there: a bundle of ours to copy into the environment, or a\n * command that already runs there — an absolute path, a name on `$PATH`, a runtime and a\n * script of its own. Which of the two it is, is the caller's decision, and it is the only\n * thing posipaki needs to know about where the far end comes from.\n */\nexport type HostProgram = { stage: string } | { run: string[] };\n\n/** Everything a client needs: where the two programs are, whose payload it is, and the way in. */\nexport interface RemoteSpec<Args> extends RemoteWayIn {\n /** The payload: ours to stage there, or the one already installed there. */\n payload: HostProgram;\n /**\n * Whose payload this is: the app name and the build those bytes came from. Required\n * whenever anything is staged — a kit's directory *is* the host version — and stated to the\n * payload whenever it is given at all, which is the one fact a caller can hand an installed\n * payload about the build it is expected to be. Omitted, nothing is stated and nothing is\n * checked, which is only right for a payload whose bytes carry no version of their own.\n */\n hostVersion?: KitApp;\n /** The gateway: posipaki's own bundle, staged, unless the caller says otherwise. */\n gateway?: HostProgram;\n /** Runtime candidates on the far side, best first. Defaults to `DEFAULT_RUNTIMES`. */\n runtime?: string[];\n /** Where the kit lands there, relative to its `$HOME`. Defaults to `DEFAULT_KIT_PARENT`. */\n parent?: string;\n /**\n * The payload's own arguments, built from the args the actor was spawned with. The gateway\n * carries them to the payload untouched — it does not know what they mean, and does not need\n * to: what a consumer tells its own far end is the consumer's business.\n */\n payloadArgs?: (args: Args) => string[];\n /** Where the far end's own output goes. Defaults to our stderr, tagged with `name`. */\n onOutput?: OutputSink;\n /** How long to wait for the far end's first protocol frame. */\n handshakeTimeoutMs?: number;\n /** Called when the far end's process is gone, whichever way it went. */\n onGone?: () => void;\n}\n\n/**\n * The part of a spec the machinery here reads, with the actor's own arguments left out:\n * staging, the channel and the argv are the same whatever an actor is told.\n */\ntype Spec = Omit<RemoteSpec<unknown>, \"payloadArgs\">;\n\n/** The argv that starts each of the two programs there. */\ninterface Programs {\n gateway: string[];\n payload: string[];\n}\n\nfunction isStage(program: HostProgram): program is { stage: string } {\n return \"stage\" in program;\n}\n\nfunction errorText(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * posipaki's own gateway program, resolved where the consumer installed it — the default for\n * a caller that has none of its own to deploy.\n */\nfunction gatewayEntry(): string {\n try {\n return fileURLToPath(import.meta.resolve(GATEWAY_ENTRY));\n } catch (err) {\n throw new RemoteSpecError(\n `cannot find posipaki's gateway program (${GATEWAY_ENTRY}): ${errorText(err)}. ` +\n \"Stage one with `gateway:` — a consumer whose program carries its own.\",\n );\n }\n}\n\n/** The kit's files: what has to be copied there, under the names the script writes them as. */\nasync function kitFiles(\n gateway: HostProgram,\n payload: HostProgram,\n): Promise<{ name: string; content: Buffer }[]> {\n const files: { name: string; content: Buffer }[] = [];\n if (isStage(payload)) files.push({ name: PAYLOAD_ARTIFACT, content: await readFile(payload.stage) });\n if (isStage(gateway)) files.push({ name: GATEWAY_ARTIFACT, content: await readFile(gateway.stage) });\n return files;\n}\n\n/** A command with nothing to run is a spec that says nothing; refuse it before spawning it. */\nfunction programCommand(program: { run: string[] }, what: string, name: string): string[] {\n if (program.run.length === 0 || program.run[0] === undefined || program.run[0] === \"\") {\n throw new RemoteSpecError(`${name}: ${what} is a command with nothing to run`);\n }\n return program.run;\n}\n\n/**\n * The two commands, staging whatever has to be copied there first.\n *\n * Nothing staged means no command of ours runs in the environment at all: the caller's own\n * argv is handed through as it stands, which is the case for a program that is already\n * installed. Something staged needs a host version to name the kit with — and a kit's\n * directory *is* the host version, so there is nothing to guess at.\n */\nasync function programsFor(spec: Spec, run: HostRun): Promise<Programs> {\n const gateway: HostProgram = spec.gateway ?? { stage: gatewayEntry() };\n const payload = spec.payload;\n if (!isStage(gateway) && !isStage(payload)) {\n return {\n gateway: programCommand(gateway, \"the gateway\", spec.name),\n payload: programCommand(payload, \"the payload\", spec.name),\n };\n }\n if (spec.hostVersion === undefined) {\n throw new RemoteSpecError(\n `nothing names the kit to stage into ${spec.name}: state hostVersion — a kit's directory ` +\n \"is the host version, and a payload refuses to serve one its own bytes do not carry\",\n );\n }\n const kit = makeKit(await kitFiles(gateway, payload), {\n app: spec.hostVersion,\n runtimes: spec.runtime ?? DEFAULT_RUNTIMES,\n parent: spec.parent ?? DEFAULT_KIT_PARENT,\n });\n const staged = await stageKit(spec, kit, run);\n const at = (artifact: string) => [staged.runtime, `${staged.kitDir}/${artifact}`];\n return {\n gateway: isStage(gateway) ? at(GATEWAY_ARTIFACT) : gateway.run,\n payload: isStage(payload) ? at(PAYLOAD_ARTIFACT) : payload.run,\n };\n}\n\n/** The tail of what a command said, for a failure that has no better story. */\nfunction said(result: HostResult): string {\n const text = `${result.stderr}${result.stdout}`.trim();\n const tail = text.length > SAID_TAIL ? text.slice(-SAID_TAIL) : text;\n return tail === \"\" ? `exit code ${result.code}` : tail;\n}\n\n/**\n * Put the kit where the environment can see it and say what will run it. Idempotent:\n * the script probes, writes only what is missing and reports — so a spawn of a build\n * that is already staged costs one process and no writes.\n */\nasync function stageKit(spec: Spec, kit: Kit, run: HostRun): Promise<{ kitDir: string; runtime: string }> {\n const result = await run(spec.entry([\"sh\", \"-s\"]), bootstrapScript(kit));\n const report = parseBootstrapReport(result.stdout);\n if (report.kind === \"error\") {\n // A report that never arrived is not a reason; what the environment said is.\n const incomplete =\n report.reason.startsWith(\"incomplete\") || report.reason.startsWith(\"no report\");\n throw new RemoteSpecError(\n `staging into ${spec.name} failed: ${report.reason}${incomplete ? ` (${said(result)})` : \"\"}`,\n );\n }\n return { kitDir: report.kitDir, runtime: report.runtime };\n}\n\n/**\n * Start the run command and hand its own stdin/stdout to the wire. An environment that\n * dies before it speaks — bwrap not installed, a host that refuses, a payload that\n * cannot be run — fails the spawn with what it said, rather than a channel that never\n * answers.\n */\nasync function openChannel(\n spec: Spec,\n command: string[],\n spawn: SpawnChild,\n): Promise<Channel> {\n const sink = spec.onOutput ?? stderrSink(spec.name);\n let tail = \"\";\n const output: OutputSink = (fd: OutputFd, data: string) => {\n if (fd === 2) tail = (tail + data).slice(-REASON_TAIL);\n sink(fd, data);\n };\n\n // The run's own stdin and stdout *are* the wire, so both have to be pipes.\n const stdio: StdioOptions = [\"pipe\", \"pipe\", \"pipe\"];\n const child: ChildProcess = spawn(command, stdio);\n const streams: LineStreams = { read: child.stdout as Readable, write: child.stdin as Writable };\n\n // The run's stdin and stdout are the wire; its stderr is not. That is where the way\n // in itself speaks — \"bwrap: Can't find source path\", \"Permission denied\" — so it goes\n // to the sink, and the tail of it becomes the reason a start that died is reported with.\n child.stderr?.setEncoding(\"utf-8\");\n child.stderr?.on(\"data\", (chunk: string) => output(2, chunk));\n\n const exited = new Promise<never>((_, reject) => {\n // `close`, not `exit`: the process being gone is not the same as everything it said\n // having arrived, and the tail of its stderr is the whole reason here.\n child.once(\"close\", (code, signal) => {\n const how = signal === null ? `code ${code}` : `signal ${signal}`;\n const why = tail.trim();\n reject(new Error(`exited with ${how}${why ? `: ${why}` : \"\"}`));\n });\n child.once(\"error\", (err) => reject(new Error(`cannot start: ${err.message}`)));\n });\n // A spawn that resolves leaves this rejection unobserved — fine, as long as it is not\n // reported as an unhandled rejection.\n exited.catch(() => {});\n\n let channel: Channel;\n try {\n channel = await Promise.race([\n clientChannel(streams, {\n onOutput: output,\n ...(spec.handshakeTimeoutMs === undefined ? {} : { timeoutMs: spec.handshakeTimeoutMs }),\n }),\n exited,\n ]);\n } catch (err) {\n child.kill();\n throw new RemoteSpecError(`${spec.name} is not available: ${errorText(err)}`);\n }\n\n // The far end's exit is the channel's close: whichever side goes first, the other stops\n // waiting — and the caller is told, because its slot must stop pointing at a process\n // that is gone.\n child.once(\"exit\", () => {\n spec.onGone?.();\n void channel.close().catch(() => {});\n });\n return channel;\n}\n\n/**\n * A spawner for an actor that lives behind a gateway in some environment: put the payload and\n * posipaki's gateway wherever they belong, run the gateway, speak the client side.\n */\nexport function gatewayClient<Args>(spec: RemoteSpec<Args>): ClientSpawner<Args> {\n const run: HostRun = spec.run ?? runHost;\n const spawn: SpawnChild = spec.spawn ?? spawnChild;\n return async (args: Args): Promise<Channel> => {\n // The spec is read before a command runs on its behalf: a payload that is not there\n // should fail here, not as an environment that cannot be reached.\n const programs = await programsFor(spec, run);\n const command = [\n ...programs.gateway,\n ...gatewayArgs({\n // What the payload is started as, then posipaki's own facts among the caller's\n // arguments: the host version, and whatever the consumer adds for its own far end.\n // Which of these is whose is not the gateway's business — it starts what it is handed\n // and carries the rest.\n passthrough: [\n ...programs.payload.slice(1),\n ...(spec.hostVersion === undefined\n ? []\n : [`--host-version=${hostVersion(spec.hostVersion)}`]),\n ...(spec.payloadArgs?.(args) ?? []),\n ],\n worker: programs.payload[0]!,\n }),\n ];\n return openChannel(spec, spec.entry(command), spawn);\n };\n}\n\n/**\n * The way in where the environment is this machine: nothing wraps the command, and the\n * kit is staged under `$HOME` like any other. Everything below the entry is the same as\n * for a sandbox, a host or a container — which is the point of it being here.\n */\nexport function hostRemote<Args>(\n spec: Omit<RemoteSpec<Args>, \"name\" | \"entry\"> & { name?: string },\n): ClientSpawner<Args> {\n return gatewayClient({ name: \"this host\", entry: (command) => command, ...spec });\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { f as AsyncProcess, r as Message } from "../types-CsE8xK3a.js";
2
- import { d as ActorPlugin } from "../hooks-CfQ8EhKW.js";
2
+ import { d as ActorPlugin } from "../hooks-Dj59y_jN.js";
3
3
  //#region src/testing/msg-matcher.d.ts
4
4
  type Matcher<M extends Message> = (msg: M, history: M[]) => boolean;
5
5
  type MatchSpec<M extends Message> = Matcher<M> | Partial<M> | Array<Partial<M>>;
@@ -1,7 +1,7 @@
1
1
  //#region src/version.ts
2
2
  /** posipaki's release version. Must equal package.json's `version`. */
3
- const LIB_VERSION = "0.35.0";
3
+ const LIB_VERSION = "0.35.2";
4
4
  //#endregion
5
5
  export { LIB_VERSION as t };
6
6
 
7
- //# sourceMappingURL=version-DDAdqTTr.js.map
7
+ //# sourceMappingURL=version-0mlM8dbC.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"version-DDAdqTTr.js","names":[],"sources":["../src/version.ts"],"sourcesContent":["// ── Library identity ───────────────────────────────────────────────────────\n//\n// posipaki's own release version, as a plain inlined string. A consumer that\n// stages artifacts onto a host it knows nothing about has to name those\n// artifacts after which posipaki they speak, and the bundle that runs there has\n// no node_modules and no posipaki on disk — it can never read a file to find out.\n//\n// It is not the protocol version: that is `VERSION` in ./remote (`json.v1`), and\n// it does not move between releases. The two answer different questions.\n//\n// version.test.ts pins this to package.json, so a release that forgets to bump it\n// fails the suite rather than shipping a lie.\n\n/** posipaki's release version. Must equal package.json's `version`. */\nexport const LIB_VERSION = \"0.35.0\";\n"],"mappings":";;AAcA,MAAa,cAAc"}
1
+ {"version":3,"file":"version-0mlM8dbC.js","names":[],"sources":["../src/version.ts"],"sourcesContent":["// ── Library identity ───────────────────────────────────────────────────────\n//\n// posipaki's own release version, as a plain inlined string. A consumer that\n// stages artifacts onto a host it knows nothing about has to name those\n// artifacts after which posipaki they speak, and the bundle that runs there has\n// no node_modules and no posipaki on disk — it can never read a file to find out.\n//\n// It is not the protocol version: that is `VERSION` in ./remote (`json.v1`), and\n// it does not move between releases. The two answer different questions.\n//\n// version.test.ts pins this to package.json, so a release that forgets to bump it\n// fails the suite rather than shipping a lie.\n\n/** posipaki's release version. Must equal package.json's `version`. */\nexport const LIB_VERSION = \"0.35.2\";\n"],"mappings":";;AAcA,MAAa,cAAc"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posipaki",
3
- "version": "0.35.0",
3
+ "version": "0.35.2",
4
4
  "homepage": "https://posipaki-docs.muromec.nl/",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1 +0,0 @@
1
- {"version":3,"file":"client-D23TlXPf.js","names":[],"sources":["../src/remote/server.ts","../src/remote/client.ts"],"sourcesContent":["// ── Server side of the seam ────────────────────────────────────────────────\n//\n// serveRemoteActor serves a posipaki actor over a frame Channel produced by a\n// spawner. It knows only the frame vocabulary (channel.ts) — no protocol, no\n// transport, no spawner. The spawner has already done the $proto handshake.\n\nimport type { ActorDefinition, ReflectionOptions } from \"../actor-types.js\";\nimport type { Message } from \"../types.js\";\nimport type { Channel } from \"./channel.js\";\nimport { isInit, isMsg } from \"./channel.js\";\n\nexport type Spawner = () => Promise<Channel>;\n\nexport function makeSender(\n fromName: string,\n parentName: string | null,\n parentId: symbol | null,\n): { fromName: string; fromId: symbol } {\n if (parentId && fromName === parentName) {\n return { fromName, fromId: parentId };\n }\n return { fromName, fromId: Symbol() };\n}\n\nexport async function serveRemoteActor<\n Args,\n State,\n InMsg extends Message,\n OutMsg extends Message,\n R extends ReflectionOptions,\n>(actor: ActorDefinition<Args, State, InMsg, OutMsg, R>, spawner: Spawner): Promise<void> {\n const channel = await spawner();\n\n // await $init\n const initFrame = await new Promise<Record<string, unknown>>((resolve) => {\n channel.onMessage((frame) => resolve(frame));\n });\n channel.removeHandler();\n if (!isInit(initFrame)) {\n throw new Error(\"serveRemoteActor: expected $init\");\n }\n\n const init = initFrame.$init;\n const parentName = (init.parentName as string) ?? null;\n const parentIdName = (init.parentIdName as string) ?? null;\n const parentId = parentIdName ? Symbol.for(parentIdName) : null;\n const { parentName: _pn, parentIdName: _pid, ...initArgs } = init;\n\n const proc = await actor.spawn(initArgs as unknown as Args, {\n name: \"remote\",\n parentName,\n parentId,\n });\n\n // bridge actor output → channel\n proc.subscribe(\"message\", async (msg, sender) => {\n try {\n await channel.send({ $msg: { fromName: sender.fromName, body: msg } });\n } catch {\n console.error(\"Error sending out the message\");\n }\n });\n proc.subscribe(\"state\", async () => {\n try {\n await channel.send({ $state: proc.state as Record<string, unknown> });\n } catch (e) {\n console.error(\"Error sending out the message\", e);\n }\n });\n\n await proc.ready();\n await channel.send({ $state: proc.state as Record<string, unknown> });\n\n // bridge channel input → actor\n channel.onMessage((frame) => {\n if (isMsg(frame)) {\n const { fromName, body } = frame.$msg;\n proc.send(body as InMsg, makeSender(fromName, parentName, parentId));\n }\n });\n\n // await actor exit, announce it, close\n let code = 0;\n try {\n await proc.wait();\n } catch (err) {\n console.error(\"server actor error:\", err);\n code = 1;\n }\n await channel.send({ $exit: { code, state: proc.state } });\n await channel.close();\n}\n","// ── Client side of the seam ────────────────────────────────────────────────\n//\n// remoteClient builds a posipaki actor that talks to a remote server through a\n// frame Channel produced by a spawner. It knows only the frame vocabulary\n// (channel.ts) — no protocol, no transport, no spawner. The spawner has\n// already done the $proto handshake.\n\nimport { defineActor } from \"../define-actor.js\";\nimport { stopPropagation } from \"../hooks.js\";\nimport type { HookResult } from \"../hooks.js\";\nimport type { ActorDefinition, HandlerOptions, MethodOptions, ReflectionOptions } from \"../actor-types.js\";\nimport type { Message } from \"../types.js\";\nimport type { Channel } from \"./channel.js\";\nimport { isState, isMsg, isExit } from \"./channel.js\";\n\nexport type ClientSpawner<Args> = (args: Args) => Promise<Channel>;\n\nexport function remoteClient<\n Args,\n State,\n InMsg extends Message,\n OutMsg extends Message,\n Methods extends MethodOptions = MethodOptions,\n Handlers extends HandlerOptions<InMsg> = HandlerOptions<InMsg>,\n R extends ReflectionOptions = ReflectionOptions,\n>(\n name: string,\n spawner: ClientSpawner<Args>,\n): ActorDefinition<Args, State, InMsg, OutMsg, R> {\n const proxyDef = defineActor<\n Args,\n { public: State; private: { channel: Channel; exitPromise: Promise<{ code: number | null; state: State }>; fromName: string } },\n InMsg,\n OutMsg,\n Methods,\n Handlers,\n R\n >({\n name,\n handlers: {} as unknown as Handlers,\n async setup(args: Args) {\n const channel = await spawner(args);\n const fromName = name;\n await channel.send({\n $init: { ...(args as unknown as Record<string, unknown>), parentName: fromName, parentIdName: fromName },\n });\n\n let currentState: Record<string, unknown> = {};\n await new Promise<void>((resolve) => {\n channel.onMessage((frame) => {\n if (isState(frame)) {\n Object.assign(currentState, frame.$state);\n resolve();\n }\n });\n });\n channel.removeHandler();\n\n let exitResolver: ((v: { code: number | null; state: State }) => void) | null = null;\n const exitPromise = new Promise<{ code: number | null; state: State }>((resolve) => {\n exitResolver = resolve;\n });\n\n channel.onMessage((frame) => {\n if (isState(frame)) {\n Object.assign(currentState, frame.$state);\n // $state frames arrive outside this actor's dispatch loop; notify\n // state subscribers so observers (Vue, nextState, …) see the change.\n this.ctx.notify();\n } else if (isMsg(frame)) {\n this.emit(frame.$msg.body as OutMsg);\n } else if (isExit(frame)) {\n if (exitResolver) {\n exitResolver({ code: frame.$exit.code, state: frame.$exit.state as State });\n exitResolver = null;\n }\n }\n });\n channel.onClose(() => {\n if (exitResolver) {\n exitResolver({ code: null, state: currentState as State });\n exitResolver = null;\n }\n });\n\n return {\n public: currentState as State,\n private: { channel, exitPromise, fromName },\n };\n },\n async onMessage(msg: InMsg): Promise<HookResult> {\n this.state.private.channel?.send({ $msg: { fromName: this.state.private.fromName, body: msg } });\n return stopPropagation();\n },\n async onStopRequested() {\n if (this.state.private.channel) {\n this.state.private.channel.send({ $msg: { fromName: this.state.private.fromName, body: { type: \"STOP\" } } });\n await this.state.private.exitPromise;\n }\n this.agreeToStop();\n },\n async afterEnd() {\n if (this.state.private.channel) {\n await this.state.private.channel.close();\n }\n },\n });\n\n return proxyDef as unknown as ActorDefinition<Args, State, InMsg, OutMsg, R>;\n}\n"],"mappings":"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"}