@ai-ecoverse/slicc-agent 6.19.12 → 6.19.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/guests/tags.js +46 -0
- package/dist/guests/tags.js.map +7 -0
- package/dist/kernel/env.js +15 -10
- package/dist/kernel/env.js.map +2 -2
- package/dist/kernel/exec.js +15 -3
- package/dist/kernel/exec.js.map +2 -2
- package/dist/kernel/groups.js +2 -2
- package/dist/kernel/groups.js.map +2 -2
- package/dist/kernel/lineage.js +12 -4
- package/dist/kernel/lineage.js.map +2 -2
- package/dist/kernel/sightings.js +83 -0
- package/dist/kernel/sightings.js.map +7 -0
- package/dist/main.js +8 -7
- package/dist/main.js.map +2 -2
- package/dist/scoops/cli.js +18 -6
- package/dist/scoops/cli.js.map +2 -2
- package/dist/types/guests/tags.d.ts +9 -0
- package/dist/types/kernel/client.d.ts +3 -0
- package/dist/types/kernel/env.d.ts +2 -0
- package/dist/types/kernel/exec.d.ts +2 -1
- package/dist/types/kernel/groups.d.ts +2 -2
- package/dist/types/kernel/lineage.d.ts +4 -2
- package/dist/types/kernel/sightings.d.ts +23 -0
- package/dist/types/scoops/cli.d.ts +2 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -359,6 +359,7 @@ A **workflow** is a Claude Code dynamic workflow script, a `.workflow.js`, run a
|
|
|
359
359
|
- **State.** The session document `slicc.workflows` holds every run: its name, source, status (`running`, `completed`, `failed`, `stopped`), phase, agent counts, output tokens, the last 200 log lines and the result or error. The newest 50 finished runs are kept.
|
|
360
360
|
- **Steps are scoops.** Each agent is a synchronous scoop with the scoop guard, removed when it answers. Scoops don't get the `workflow` tool, and a scoop can't start, stop or save workflows.
|
|
361
361
|
- **Guests.** A run started from a run a guest started (through the `workflow` tool or `agent workflow run` in its bash) records the guest's seats and tiers. Each step's scoop gets them in its `slicc.guest-taint` document, and the guest tool gate reviews every tool call in that scoop as it would the guest's own; a seat that is gone denies them. A scoop that `agent` or `agent --async` starts, or `agent send` feeds, from such a run gets them the same way; a caller that names no agent carries the seats of every running agent and every live scoop. Licks carry them too: `agent send parent`, a scoop's report (with the scoop's seats), a `wait --notify` lick (with the waited scoops' seats and the waiting caller's, taken when it asked), `--lick=cron` and `--lick=webhook` record the caller's seats in `slicc.lick-taint` under the lick's id, and only the run that takes that lick in is reviewed, not the cone's later runs.
|
|
362
|
+
- **What a guest's bash leaves behind** ([#615](https://github.com/ai-ecoverse/slicc-agent/issues/615)). With kernel users on (`report` or `enforce`; kernel 1.144.0 or later), every process a tainted run starts (bash, codemode's and a workflow step's bash included) carries a per-spawn kernel tag `slicc-taint:<id>`, which its background jobs, `setsid` and double-forked daemons, `sudo` and the cron jobs it installs with `crontab` inherit, and which nothing in the kernel can change. The id is opaque: the session document `slicc.taint-tags` maps it to the run's seats and tiers (`{ tags: { <id>: [{ seat, tier }] } }`), and the same set reuses its id. An `agent` request or a `--lick=cron`/`--lick=webhook` lick adds the seats of the requesting process's tags, read from `ps`, so a background `agent` call or a guest-written cron line stays under the guest gate after the run ended; a tag the document doesn't know denies like an unknown seat. A request file names its process, and any process that may write the spool could name another one, such as a clean `agent` call running at that moment. So a request also takes the seats of every tag that may have been alive since the agent first saw the named process in `ps`, under whatever uid: a tagged process can `sudo` to another user, a short child is easily missed between reads, and a `NOPASSWD` rule isn't asked again, so the agent doesn't narrow tags by uid. The agent learns a tag at each tagged spawn, at every `ps` read (each request, and every 2 s while a tag it knows is alive), and from the `crontab` record (`/var/spool/cron/tags`, with or without its `"v"` key, slicc-kernel#1406; every `slicc-taint:` id in it counts, whatever the version). A tag counts as gone only after a `ps` read that started after its last sighting finds no process with it, and a read of the record finds no line holding it; a record that can't be read (other than missing) holds every tag the agent knows. So a forger that writes a request and exits before it is read is still caught. This is conservative: while a guest's leftover job or `crontab` line lives, every user's `agent` calls and licks are reviewed too, and so is a call that starts in the 2 s after the last tagged process ends. With users `off`, nothing is tagged and this doesn't apply, as before: the kernel then doesn't fence `/etc/crontab` or the spool, so tags would prove nothing. A kernel older than 1.144.0 answers a tagged spawn with `ENOSYS`, and the agent then spawns it untagged, as before.
|
|
362
363
|
- **The `workflow` tool.** A cone's model calls it with `action: run` (`file`, a path or a saved name, or `script`, the source, with `args`, `budget`, `concurrency` and `wait`), `list`, `status`, `stop` or `save` (`run`, `name`, `force`). Guest runs go through the guest tool gate as any tool call does.
|
|
363
364
|
- **The CLI.** `agent workflow run <file.js | name> [--args <json>] [--budget <tokens>] [--concurrency <n>] [--wait]`, or `--script '<js>'`, prints the run id, or with `--wait` the result as JSON (exit 1 if it failed or was stopped). `list`, `status <run>`, `wait <run>`, `stop <run>` and `save <run> <name> [--force]` manage runs. `save` writes the run's source to `~/.slicc/workflows/<name>.workflow.js`, and `agent workflow run <name>` runs it; v6's bare `<name>` command is gone.
|
|
364
365
|
- **Differences from awsl-workflow@1.** No nested `workflow()`, `isolation`, timers or model tiers; a direct `agent()` failure always rejects, with no Claude-compatible `null`; a resumed run continues the same attempt instead of starting a new one with a fresh budget.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineDoc } from "@earendil-works/pi-durable";
|
|
2
|
+
import { TAINT_TAG } from "../kernel/sightings.js";
|
|
3
|
+
import { canonical, distinctTaints, runTaints } from "./guard.js";
|
|
4
|
+
const TaintTagDoc = defineDoc({
|
|
5
|
+
kind: "slicc.taint-tags",
|
|
6
|
+
version: 1,
|
|
7
|
+
scope: "session",
|
|
8
|
+
initial: () => ({ tags: {} })
|
|
9
|
+
});
|
|
10
|
+
const keyOf = (taints) => canonical(
|
|
11
|
+
distinctTaints(taints).map((taint) => [taint.seat, taint.tier]).sort()
|
|
12
|
+
);
|
|
13
|
+
function taintTag(harness, taints, context) {
|
|
14
|
+
const key = keyOf(taints);
|
|
15
|
+
return harness.commit(async (tx) => {
|
|
16
|
+
const doc = await tx.doc(TaintTagDoc);
|
|
17
|
+
const found = Object.entries(doc.tags).find(([, kept]) => keyOf(kept) === key)?.[0];
|
|
18
|
+
if (found) return `${TAINT_TAG}${found}`;
|
|
19
|
+
const id = crypto.randomUUID();
|
|
20
|
+
doc.tags[id] = distinctTaints(taints);
|
|
21
|
+
return `${TAINT_TAG}${id}`;
|
|
22
|
+
}, context);
|
|
23
|
+
}
|
|
24
|
+
async function taggedTaints(harness, tags, context) {
|
|
25
|
+
const ours = tags.filter((tag) => tag.startsWith(TAINT_TAG));
|
|
26
|
+
if (!ours.length) return [];
|
|
27
|
+
const kept = (await harness.snapshot(TaintTagDoc, context))?.tags ?? {};
|
|
28
|
+
return distinctTaints(
|
|
29
|
+
ours.flatMap((tag) => kept[tag.slice(TAINT_TAG.length)] ?? [{ seat: null, tier: "" }])
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
function runTag(harness) {
|
|
33
|
+
return async (conversationId, context) => {
|
|
34
|
+
const held = harness();
|
|
35
|
+
if (!held) return void 0;
|
|
36
|
+
const taints = await runTaints(held, conversationId, context);
|
|
37
|
+
return taints.length ? taintTag(held, taints, context) : void 0;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
TaintTagDoc,
|
|
42
|
+
runTag,
|
|
43
|
+
taggedTaints,
|
|
44
|
+
taintTag
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=tags.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/guests/tags.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { type ConversationId, defineDoc, type Harness } from '@earendil-works/pi-durable';\nimport { TAINT_TAG } from '../kernel/sightings.ts';\nimport { canonical, distinctTaints, type InheritedTaint, runTaints } from './guard.ts';\n\nexport const TaintTagDoc = defineDoc<{ tags: Record<string, InheritedTaint[]> }>({\n kind: 'slicc.taint-tags',\n version: 1,\n scope: 'session',\n initial: () => ({ tags: {} }),\n});\n\nconst keyOf = (taints: readonly InheritedTaint[]) =>\n canonical(\n distinctTaints(taints)\n .map((taint) => [taint.seat, taint.tier])\n .sort()\n );\n\nexport function taintTag(\n harness: Pick<Harness, 'commit'>,\n taints: readonly InheritedTaint[],\n context: Context\n): Promise<string> {\n const key = keyOf(taints);\n return harness.commit(async (tx) => {\n const doc = await tx.doc(TaintTagDoc);\n const found = Object.entries(doc.tags).find(([, kept]) => keyOf(kept) === key)?.[0];\n if (found) return `${TAINT_TAG}${found}`;\n const id = crypto.randomUUID();\n doc.tags[id] = distinctTaints(taints);\n return `${TAINT_TAG}${id}`;\n }, context);\n}\n\nexport async function taggedTaints(\n harness: Pick<Harness, 'snapshot'>,\n tags: readonly string[],\n context: Context\n): Promise<InheritedTaint[]> {\n const ours = tags.filter((tag) => tag.startsWith(TAINT_TAG));\n if (!ours.length) return [];\n const kept = (await harness.snapshot(TaintTagDoc, context))?.tags ?? {};\n return distinctTaints(\n ours.flatMap((tag) => kept[tag.slice(TAINT_TAG.length)] ?? [{ seat: null, tier: '' }])\n );\n}\n\nexport function runTag(\n harness: () => Pick<Harness, 'commit' | 'submission' | 'snapshot'> | undefined\n) {\n return async (conversationId: number, context: Context): Promise<string | undefined> => {\n const held = harness();\n if (!held) return undefined;\n const taints = await runTaints(held, conversationId as ConversationId, context);\n return taints.length ? taintTag(held, taints, context) : undefined;\n };\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAA8B,iBAA+B;AAC7D,SAAS,iBAAiB;AAC1B,SAAS,WAAW,gBAAqC,iBAAiB;AAEnE,MAAM,cAAc,UAAsD;AAAA,EAC/E,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,OAAO,EAAE,MAAM,CAAC,EAAE;AAC7B,CAAC;AAED,MAAM,QAAQ,CAAC,WACb;AAAA,EACE,eAAe,MAAM,EAClB,IAAI,CAAC,UAAU,CAAC,MAAM,MAAM,MAAM,IAAI,CAAC,EACvC,KAAK;AACV;AAEK,SAAS,SACd,SACA,QACA,SACiB;AACjB,QAAM,MAAM,MAAM,MAAM;AACxB,SAAO,QAAQ,OAAO,OAAO,OAAO;AAClC,UAAM,MAAM,MAAM,GAAG,IAAI,WAAW;AACpC,UAAM,QAAQ,OAAO,QAAQ,IAAI,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,IAAI,MAAM,MAAM,IAAI,MAAM,GAAG,IAAI,CAAC;AAClF,QAAI,MAAO,QAAO,GAAG,SAAS,GAAG,KAAK;AACtC,UAAM,KAAK,OAAO,WAAW;AAC7B,QAAI,KAAK,EAAE,IAAI,eAAe,MAAM;AACpC,WAAO,GAAG,SAAS,GAAG,EAAE;AAAA,EAC1B,GAAG,OAAO;AACZ;AAEA,eAAsB,aACpB,SACA,MACA,SAC2B;AAC3B,QAAM,OAAO,KAAK,OAAO,CAAC,QAAQ,IAAI,WAAW,SAAS,CAAC;AAC3D,MAAI,CAAC,KAAK,OAAQ,QAAO,CAAC;AAC1B,QAAM,QAAQ,MAAM,QAAQ,SAAS,aAAa,OAAO,IAAI,QAAQ,CAAC;AACtE,SAAO;AAAA,IACL,KAAK,QAAQ,CAAC,QAAQ,KAAK,IAAI,MAAM,UAAU,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC;AAAA,EACvF;AACF;AAEO,SAAS,OACd,SACA;AACA,SAAO,OAAO,gBAAwB,YAAkD;AACtF,UAAM,OAAO,QAAQ;AACrB,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,SAAS,MAAM,UAAU,MAAM,gBAAkC,OAAO;AAC9E,WAAO,OAAO,SAAS,SAAS,MAAM,QAAQ,OAAO,IAAI;AAAA,EAC3D;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/kernel/env.js
CHANGED
|
@@ -61,7 +61,8 @@ class SliccKernelEnv {
|
|
|
61
61
|
const scrub = options.scrub;
|
|
62
62
|
this.#hooks = {
|
|
63
63
|
...scrub ? { scrub: () => scrub.stream() } : {},
|
|
64
|
-
...options.spawn ? { spawn: options.spawn } : {}
|
|
64
|
+
...options.spawn ? { spawn: options.spawn } : {},
|
|
65
|
+
...options.tag ? { tag: options.tag } : {}
|
|
65
66
|
};
|
|
66
67
|
}
|
|
67
68
|
#path(path) {
|
|
@@ -275,8 +276,8 @@ class SliccKernelEnv {
|
|
|
275
276
|
for (let wait = groups.pending(owner); wait; wait = groups.pending(owner)) await wait;
|
|
276
277
|
const pgid = groups.shared(owner);
|
|
277
278
|
const release = pgid === void 0 && groups.joins() ? groups.reserve(owner) : () => void 0;
|
|
278
|
-
const spawned = (process) => {
|
|
279
|
-
groups.track(owner, process);
|
|
279
|
+
const spawned = (process, tag) => {
|
|
280
|
+
groups.track(owner, process, tag);
|
|
280
281
|
tracked?.(process);
|
|
281
282
|
release();
|
|
282
283
|
};
|
|
@@ -302,13 +303,17 @@ class SliccKernelEnv {
|
|
|
302
303
|
}
|
|
303
304
|
const HOME = "/home";
|
|
304
305
|
function kernelEnvironment(client, options = {}) {
|
|
305
|
-
const { identify, route, ...rest } = options;
|
|
306
|
-
const make = (target, exports, routed) =>
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
306
|
+
const { identify, route, tagOf, ...rest } = options;
|
|
307
|
+
const make = (target, exports, routed) => {
|
|
308
|
+
const owner = target.conversationId;
|
|
309
|
+
return new SliccKernelEnv(routed?.client ?? client, {
|
|
310
|
+
...rest,
|
|
311
|
+
cwd: routed?.cwd ?? target.cwd ?? routed?.home ?? HOME,
|
|
312
|
+
...owner === void 0 ? {} : { owner },
|
|
313
|
+
...owner !== void 0 && tagOf ? { tag: (context) => tagOf(owner, context) } : {},
|
|
314
|
+
...exports ? { exports } : {}
|
|
315
|
+
});
|
|
316
|
+
};
|
|
312
317
|
if (route)
|
|
313
318
|
return async (target, context) => {
|
|
314
319
|
const routed = await route(target);
|
package/dist/kernel/env.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/kernel/env.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type BinaryReader,\n type DirReader,\n type ExecutionEnv,\n type ExecutionError,\n err,\n FileError,\n type FileInfo,\n type FileWatcher,\n ok,\n type Result,\n type ShellExecOptions,\n type ShellExecResult,\n type TextLineReader,\n type WatchChange,\n type WatchTarget,\n} from '@earendil-works/pi-durable/env';\nimport { mimeOf } from '../images/index.ts';\nimport type { Scrub } from '../secrets/scrub.ts';\nimport type { Activity } from './activity.ts';\nimport type { KernelClient, KernelProcess } from './client.ts';\nimport { type ExecHooks, execute } from './exec.ts';\nimport { attempt, info, KernelDirReader, SnapshotLines, SnapshotReader } from './files.ts';\nimport type { ProcessGroups } from './groups.ts';\nimport type { Jobs } from './jobs.ts';\nimport { dirname, normalize, resolve } from './paths.ts';\nimport { nativeWatch, type PollOptions, pollWatch, RESCAN_MS } from './watch.ts';\n\nexport interface KernelEnvOptions {\n cwd: string;\n id?: string;\n watch?: PollOptions;\n activity?: Activity;\n groups?: ProcessGroups;\n jobs?: Jobs;\n owner?: number;\n exports?: Readonly<Record<string, string>>;\n scrub?: Scrub;\n spawn?: (argv: readonly string[]) => readonly string[];\n}\n\nexport const STREAMED_READ = 8 * 1024 * 1024;\nexport const READ_CHUNK = 1024 * 1024;\n\nexport async function scrubbedFile(scrub: Scrub, bytes: Uint8Array): Promise<Uint8Array> {\n if (mimeOf(bytes)) return bytes;\n if (bytes.length <= STREAMED_READ) return (await scrub.texts([bytes]))[0] as Uint8Array;\n const stream = scrub.stream();\n const parts: Uint8Array[] = [];\n for (let at = 0; at < bytes.length; at += READ_CHUNK)\n parts.push(await stream.push(bytes.subarray(at, at + READ_CHUNK)));\n parts.push(await stream.end());\n const all = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));\n let offset = 0;\n for (const part of parts) {\n all.set(part, offset);\n offset += part.length;\n }\n return all;\n}\n\nexport function quote(value: string): string {\n return `'${value.replace(/'/g, \"'\\\\''\")}'`;\n}\n\nexport function prefixed(\n command: string,\n exports: Readonly<Record<string, string>> | undefined\n): string {\n const names = Object.keys(exports ?? {}).filter((name) => /^[A-Z_][A-Z0-9_]*$/.test(name));\n if (!names.length) return command;\n return `export ${names.map((name) => `${name}=${quote((exports as Record<string, string>)[name] as string)}`).join(' ')}; ${command}`;\n}\n\ntype FileResult<T> = Promise<Result<T, FileError>>;\n\nexport class SliccKernelEnv implements ExecutionEnv {\n readonly id: string;\n cwd: string;\n readonly #client: KernelClient;\n readonly #watchers = new Set<FileWatcher>();\n readonly #poll: PollOptions;\n readonly #activity: Activity | undefined;\n readonly #groups: ProcessGroups | undefined;\n readonly #jobs: Jobs | undefined;\n readonly #owner: number | undefined;\n readonly #exports: Readonly<Record<string, string>> | undefined;\n readonly #hooks: ExecHooks;\n readonly #scrub: Scrub | undefined;\n\n constructor(client: KernelClient, options: KernelEnvOptions) {\n this.#client = client;\n this.cwd = options.cwd;\n this.id = options.id ?? 'slicc-kernel';\n this.#poll = options.watch ?? {};\n this.#activity = options.activity;\n this.#groups = options.groups;\n this.#jobs = options.jobs;\n this.#owner = options.owner;\n this.#exports = options.exports;\n this.#scrub = options.scrub;\n const scrub = options.scrub;\n this.#hooks = {\n ...(scrub ? { scrub: () => scrub.stream() } : {}),\n ...(options.spawn ? { spawn: options.spawn } : {}),\n };\n }\n\n #path(path: string): string {\n return resolve(this.cwd, path);\n }\n\n #written(path: string): string {\n const resolved = this.#path(path);\n this.#activity?.wrote(resolved);\n return resolved;\n }\n\n get #fs() {\n return this.#client.fs;\n }\n\n async absolutePath(path: string): FileResult<string> {\n return ok(this.#path(path));\n }\n\n async joinPath(parts: string[]): FileResult<string> {\n return ok(normalize(parts.join('/')));\n }\n\n readTextFile(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readText(resolved));\n }\n\n readBinaryFile(path: string, context: Context): FileResult<Uint8Array> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readFile(resolved));\n }\n\n async openTextLineReader(path: string, context: Context): FileResult<TextLineReader> {\n const text = await this.readTextFile(path, context);\n return text.ok ? ok(new SnapshotLines(text.value)) : text;\n }\n\n async readTextLines(\n path: string,\n options: { maxLines?: number } | undefined,\n context: Context\n ): FileResult<string[]> {\n if (options?.maxLines !== undefined && options.maxLines <= 0) return ok([]);\n const text = await this.readTextFile(path, context);\n if (!text.ok) return text;\n const reader = new SnapshotLines(text.value);\n const lines: string[] = [];\n for (\n let line = await reader.readLine();\n line.ok && line.value;\n line = await reader.readLine()\n ) {\n if (options?.maxLines !== undefined && lines.length >= options.maxLines) break;\n lines.push(line.value.text);\n }\n return ok(lines);\n }\n\n openBinaryReader(\n path: string,\n options: { noFollow?: boolean } | undefined,\n context: Context\n ): FileResult<BinaryReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (options?.noFollow && (await this.#fs.lstat(resolved)).isSymbolicLink) {\n throw new FileError('invalid', 'Refusing to follow a symbolic link', resolved);\n }\n const stat = await this.#fs.stat(resolved);\n if (stat.isDirectory) throw new FileError('is_directory', 'Is a directory', resolved);\n if (!stat.isFile) throw new FileError('invalid', 'Not a regular file', resolved);\n const bytes = await this.#fs.readFile(resolved);\n if (!this.#scrub) return new SnapshotReader(bytes, info(resolved, stat));\n const shown = await scrubbedFile(this.#scrub, bytes);\n return new SnapshotReader(shown, { ...info(resolved, stat), size: shown.length });\n });\n }\n\n writeFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, content);\n });\n }\n\n appendFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n const before = (await this.#fs.exists(resolved))\n ? await this.#fs.readFile(resolved)\n : new Uint8Array();\n const added = typeof content === 'string' ? new TextEncoder().encode(content) : content;\n const all = new Uint8Array(before.length + added.length);\n all.set(before);\n all.set(added, before.length);\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, all);\n });\n }\n\n truncateFile(path: string, size: number, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n if (!Number.isSafeInteger(size) || size < 0) {\n return Promise.resolve(\n err(new FileError('invalid', 'File size must be a non-negative integer', resolved))\n );\n }\n return attempt(context, resolved, async () => {\n const before = await this.#fs.readFile(resolved);\n const after = new Uint8Array(size);\n after.set(before.subarray(0, size));\n await this.#fs.writeFile(resolved, after);\n });\n }\n\n flushFile(path: string, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if ((await this.#fs.stat(resolved)).isDirectory)\n throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n });\n }\n\n renameFile(sourcePath: string, destinationPath: string, context: Context): FileResult<void> {\n const source = this.#written(sourcePath);\n return attempt(context, source, () => this.#fs.rename(source, this.#written(destinationPath)));\n }\n\n fileInfo(path: string, context: Context): FileResult<FileInfo> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => info(resolved, await this.#fs.lstat(resolved)));\n }\n\n async listDir(path: string, context: Context): FileResult<FileInfo[]> {\n const reader = await this.openDirReader(path, context);\n if (!reader.ok) return reader;\n return ok(await (reader.value as KernelDirReader).rest());\n }\n\n openDirReader(path: string, context: Context): FileResult<DirReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (!(await this.#fs.stat(resolved)).isDirectory) {\n throw Object.assign(new Error('Not a directory'), { code: 'ENOTDIR' });\n }\n return new KernelDirReader(this.#fs, resolved, await this.#fs.readdir(resolved));\n });\n }\n\n watch(\n targets: readonly WatchTarget[],\n onChange: (change: WatchChange) => void,\n context: Context\n ): FileResult<FileWatcher> {\n return attempt(context, this.cwd, async () => {\n let absorb: (paths: readonly string[]) => void = () => undefined;\n const reported = (change: WatchChange) => {\n if ('paths' in change) absorb(change.paths);\n onChange(change);\n };\n const native =\n this.#poll.mode === 'polling'\n ? undefined\n : await nativeWatch(this.#fs, this.cwd, targets, reported);\n const intervalMs = native ? (this.#poll.rescanMs ?? RESCAN_MS) : this.#poll.intervalMs;\n const polled = await pollWatch(this.#fs, this.cwd, targets, onChange, {\n ...(intervalMs === undefined ? {} : { intervalMs }),\n });\n absorb = (paths) => polled.absorb(paths);\n const watcher: FileWatcher = native\n ? {\n mode: 'native',\n async close(closing: Context) {\n await native.close(closing);\n await polled.close(closing);\n },\n }\n : polled;\n this.#watchers.add(watcher);\n return {\n mode: watcher.mode,\n close: async (closing: Context) => {\n this.#watchers.delete(watcher);\n await watcher.close(closing);\n },\n };\n });\n }\n\n canonicalPath(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.realpath(resolved));\n }\n\n async exists(path: string, context: Context): FileResult<boolean> {\n const found = await this.fileInfo(path, context);\n if (found.ok) return ok(true);\n return found.error.code === 'not_found' ? ok(false) : found;\n }\n\n createDir(\n path: string,\n _options: { recursive?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, () => this.#fs.mkdir(resolved));\n }\n\n remove(\n path: string,\n options: { recursive?: boolean; force?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n if (!options?.recursive) {\n const stat = await this.#fs.lstat(resolved).catch((error: unknown) => {\n if (options?.force) return undefined;\n throw error;\n });\n if (stat?.isDirectory) throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n }\n await this.#fs.rm(resolved, { force: options?.force ?? false });\n });\n }\n\n async createTempDir(prefix: string | undefined, context: Context): FileResult<string> {\n const path = `/tmp/${prefix ?? 'tmp-'}${crypto.randomUUID()}`;\n return attempt(context, path, async () => {\n await this.#fs.mkdir(path);\n return path;\n });\n }\n\n async createTempFile(\n options: { prefix?: string; suffix?: string } | undefined,\n context: Context\n ): FileResult<string> {\n const dir = await this.createTempDir('tmp-', context);\n if (!dir.ok) return dir;\n const path = `${dir.value}/${options?.prefix ?? ''}${crypto.randomUUID()}${options?.suffix ?? ''}`;\n return attempt(context, path, async () => {\n await this.#fs.writeFile(path, '');\n return path;\n });\n }\n\n exec(\n command: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context\n ): Promise<Result<ShellExecResult, ExecutionError>> {\n const ended = this.#activity?.began();\n const line = typeof command === 'string' ? prefixed(command, this.#exports) : command;\n const jobs = this.#jobs;\n const client = this.#client;\n let recorded: Promise<void> | undefined;\n const tracked =\n jobs && typeof command === 'string'\n ? (process: KernelProcess) => {\n recorded = jobs.started(client, command, process);\n }\n : undefined;\n const groups = this.#groups;\n const owner = this.#owner;\n const run =\n !groups || owner === undefined\n ? execute(client, this.cwd, line, options, context, tracked, undefined, this.#hooks)\n : this.#grouped(groups, owner, line, options, context, tracked);\n return run\n .then(async (result) => {\n await recorded;\n return result;\n })\n .finally(ended);\n }\n\n async #grouped(\n groups: ProcessGroups,\n owner: number,\n line: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context,\n tracked: ((process: KernelProcess) => void) | undefined\n ): Promise<Result<ShellExecResult, ExecutionError>> {\n for (let wait = groups.pending(owner); wait; wait = groups.pending(owner)) await wait;\n const pgid = groups.shared(owner);\n const release = pgid === undefined && groups.joins() ? groups.reserve(owner) : () => undefined;\n const spawned = (process: KernelProcess) => {\n groups.track(owner, process);\n tracked?.(process);\n release();\n };\n const join = groups.joins()\n ? {\n ...(pgid === undefined ? {} : { pgid }),\n refused: (code: string) => groups.refused(owner, code),\n }\n : undefined;\n return execute(\n this.#client,\n this.cwd,\n line,\n options,\n context,\n spawned,\n join,\n this.#hooks\n ).finally(release);\n }\n\n async cleanup(context: Context): Promise<void> {\n for (const watcher of [...this.#watchers]) await watcher.close(context);\n this.#watchers.clear();\n }\n}\n\nexport const HOME = '/home';\n\nexport type EnvTargetLike = { cwd?: string; conversationId?: number; read?: unknown };\n\nexport type EnvRoute = { client: KernelClient; home: string; cwd?: string };\n\nexport function kernelEnvironment(\n client: KernelClient,\n options: Omit<KernelEnvOptions, 'cwd'> & {\n identify?: (target: EnvTargetLike, context: Context) => Promise<Record<string, string>>;\n route?: (target: EnvTargetLike) => Promise<EnvRoute | undefined>;\n } = {}\n): (target: EnvTargetLike, context?: Context) => SliccKernelEnv | Promise<SliccKernelEnv> {\n const { identify, route, ...rest } = options;\n const make = (target: EnvTargetLike, exports?: Record<string, string>, routed?: EnvRoute) =>\n new SliccKernelEnv(routed?.client ?? client, {\n ...rest,\n cwd: routed?.cwd ?? target.cwd ?? routed?.home ?? HOME,\n ...(target.conversationId === undefined ? {} : { owner: target.conversationId }),\n ...(exports ? { exports } : {}),\n });\n if (route)\n return async (target, context) => {\n const routed = await route(target);\n const exports = identify && context ? await identify(target, context) : undefined;\n return make(target, exports, routed);\n };\n return (target, context) =>\n identify && context\n ? identify(target, context).then((exports) => make(target, exports))\n : make(target);\n}\n"],
|
|
5
|
-
"mappings": "AACA;AAAA,EAKE;AAAA,EACA;AAAA,EAGA;AAAA,OAOK;AACP,SAAS,cAAc;AAIvB,SAAyB,eAAe;AACxC,SAAS,SAAS,MAAM,iBAAiB,eAAe,sBAAsB;AAG9E,SAAS,SAAS,WAAW,eAAe;AAC5C,SAAS,aAA+B,WAAW,iBAAiB;
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type BinaryReader,\n type DirReader,\n type ExecutionEnv,\n type ExecutionError,\n err,\n FileError,\n type FileInfo,\n type FileWatcher,\n ok,\n type Result,\n type ShellExecOptions,\n type ShellExecResult,\n type TextLineReader,\n type WatchChange,\n type WatchTarget,\n} from '@earendil-works/pi-durable/env';\nimport { mimeOf } from '../images/index.ts';\nimport type { Scrub } from '../secrets/scrub.ts';\nimport type { Activity } from './activity.ts';\nimport type { KernelClient, KernelProcess } from './client.ts';\nimport { type ExecHooks, execute } from './exec.ts';\nimport { attempt, info, KernelDirReader, SnapshotLines, SnapshotReader } from './files.ts';\nimport type { ProcessGroups } from './groups.ts';\nimport type { Jobs } from './jobs.ts';\nimport { dirname, normalize, resolve } from './paths.ts';\nimport { nativeWatch, type PollOptions, pollWatch, RESCAN_MS } from './watch.ts';\n\nexport interface KernelEnvOptions {\n cwd: string;\n id?: string;\n watch?: PollOptions;\n activity?: Activity;\n groups?: ProcessGroups;\n jobs?: Jobs;\n owner?: number;\n exports?: Readonly<Record<string, string>>;\n scrub?: Scrub;\n spawn?: (argv: readonly string[]) => readonly string[];\n tag?: (context: Context) => Promise<string | undefined>;\n}\n\nexport const STREAMED_READ = 8 * 1024 * 1024;\nexport const READ_CHUNK = 1024 * 1024;\n\nexport async function scrubbedFile(scrub: Scrub, bytes: Uint8Array): Promise<Uint8Array> {\n if (mimeOf(bytes)) return bytes;\n if (bytes.length <= STREAMED_READ) return (await scrub.texts([bytes]))[0] as Uint8Array;\n const stream = scrub.stream();\n const parts: Uint8Array[] = [];\n for (let at = 0; at < bytes.length; at += READ_CHUNK)\n parts.push(await stream.push(bytes.subarray(at, at + READ_CHUNK)));\n parts.push(await stream.end());\n const all = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));\n let offset = 0;\n for (const part of parts) {\n all.set(part, offset);\n offset += part.length;\n }\n return all;\n}\n\nexport function quote(value: string): string {\n return `'${value.replace(/'/g, \"'\\\\''\")}'`;\n}\n\nexport function prefixed(\n command: string,\n exports: Readonly<Record<string, string>> | undefined\n): string {\n const names = Object.keys(exports ?? {}).filter((name) => /^[A-Z_][A-Z0-9_]*$/.test(name));\n if (!names.length) return command;\n return `export ${names.map((name) => `${name}=${quote((exports as Record<string, string>)[name] as string)}`).join(' ')}; ${command}`;\n}\n\ntype FileResult<T> = Promise<Result<T, FileError>>;\n\nexport class SliccKernelEnv implements ExecutionEnv {\n readonly id: string;\n cwd: string;\n readonly #client: KernelClient;\n readonly #watchers = new Set<FileWatcher>();\n readonly #poll: PollOptions;\n readonly #activity: Activity | undefined;\n readonly #groups: ProcessGroups | undefined;\n readonly #jobs: Jobs | undefined;\n readonly #owner: number | undefined;\n readonly #exports: Readonly<Record<string, string>> | undefined;\n readonly #hooks: ExecHooks;\n readonly #scrub: Scrub | undefined;\n\n constructor(client: KernelClient, options: KernelEnvOptions) {\n this.#client = client;\n this.cwd = options.cwd;\n this.id = options.id ?? 'slicc-kernel';\n this.#poll = options.watch ?? {};\n this.#activity = options.activity;\n this.#groups = options.groups;\n this.#jobs = options.jobs;\n this.#owner = options.owner;\n this.#exports = options.exports;\n this.#scrub = options.scrub;\n const scrub = options.scrub;\n this.#hooks = {\n ...(scrub ? { scrub: () => scrub.stream() } : {}),\n ...(options.spawn ? { spawn: options.spawn } : {}),\n ...(options.tag ? { tag: options.tag } : {}),\n };\n }\n\n #path(path: string): string {\n return resolve(this.cwd, path);\n }\n\n #written(path: string): string {\n const resolved = this.#path(path);\n this.#activity?.wrote(resolved);\n return resolved;\n }\n\n get #fs() {\n return this.#client.fs;\n }\n\n async absolutePath(path: string): FileResult<string> {\n return ok(this.#path(path));\n }\n\n async joinPath(parts: string[]): FileResult<string> {\n return ok(normalize(parts.join('/')));\n }\n\n readTextFile(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readText(resolved));\n }\n\n readBinaryFile(path: string, context: Context): FileResult<Uint8Array> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readFile(resolved));\n }\n\n async openTextLineReader(path: string, context: Context): FileResult<TextLineReader> {\n const text = await this.readTextFile(path, context);\n return text.ok ? ok(new SnapshotLines(text.value)) : text;\n }\n\n async readTextLines(\n path: string,\n options: { maxLines?: number } | undefined,\n context: Context\n ): FileResult<string[]> {\n if (options?.maxLines !== undefined && options.maxLines <= 0) return ok([]);\n const text = await this.readTextFile(path, context);\n if (!text.ok) return text;\n const reader = new SnapshotLines(text.value);\n const lines: string[] = [];\n for (\n let line = await reader.readLine();\n line.ok && line.value;\n line = await reader.readLine()\n ) {\n if (options?.maxLines !== undefined && lines.length >= options.maxLines) break;\n lines.push(line.value.text);\n }\n return ok(lines);\n }\n\n openBinaryReader(\n path: string,\n options: { noFollow?: boolean } | undefined,\n context: Context\n ): FileResult<BinaryReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (options?.noFollow && (await this.#fs.lstat(resolved)).isSymbolicLink) {\n throw new FileError('invalid', 'Refusing to follow a symbolic link', resolved);\n }\n const stat = await this.#fs.stat(resolved);\n if (stat.isDirectory) throw new FileError('is_directory', 'Is a directory', resolved);\n if (!stat.isFile) throw new FileError('invalid', 'Not a regular file', resolved);\n const bytes = await this.#fs.readFile(resolved);\n if (!this.#scrub) return new SnapshotReader(bytes, info(resolved, stat));\n const shown = await scrubbedFile(this.#scrub, bytes);\n return new SnapshotReader(shown, { ...info(resolved, stat), size: shown.length });\n });\n }\n\n writeFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, content);\n });\n }\n\n appendFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n const before = (await this.#fs.exists(resolved))\n ? await this.#fs.readFile(resolved)\n : new Uint8Array();\n const added = typeof content === 'string' ? new TextEncoder().encode(content) : content;\n const all = new Uint8Array(before.length + added.length);\n all.set(before);\n all.set(added, before.length);\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, all);\n });\n }\n\n truncateFile(path: string, size: number, context: Context): FileResult<void> {\n const resolved = this.#written(path);\n if (!Number.isSafeInteger(size) || size < 0) {\n return Promise.resolve(\n err(new FileError('invalid', 'File size must be a non-negative integer', resolved))\n );\n }\n return attempt(context, resolved, async () => {\n const before = await this.#fs.readFile(resolved);\n const after = new Uint8Array(size);\n after.set(before.subarray(0, size));\n await this.#fs.writeFile(resolved, after);\n });\n }\n\n flushFile(path: string, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if ((await this.#fs.stat(resolved)).isDirectory)\n throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n });\n }\n\n renameFile(sourcePath: string, destinationPath: string, context: Context): FileResult<void> {\n const source = this.#written(sourcePath);\n return attempt(context, source, () => this.#fs.rename(source, this.#written(destinationPath)));\n }\n\n fileInfo(path: string, context: Context): FileResult<FileInfo> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => info(resolved, await this.#fs.lstat(resolved)));\n }\n\n async listDir(path: string, context: Context): FileResult<FileInfo[]> {\n const reader = await this.openDirReader(path, context);\n if (!reader.ok) return reader;\n return ok(await (reader.value as KernelDirReader).rest());\n }\n\n openDirReader(path: string, context: Context): FileResult<DirReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (!(await this.#fs.stat(resolved)).isDirectory) {\n throw Object.assign(new Error('Not a directory'), { code: 'ENOTDIR' });\n }\n return new KernelDirReader(this.#fs, resolved, await this.#fs.readdir(resolved));\n });\n }\n\n watch(\n targets: readonly WatchTarget[],\n onChange: (change: WatchChange) => void,\n context: Context\n ): FileResult<FileWatcher> {\n return attempt(context, this.cwd, async () => {\n let absorb: (paths: readonly string[]) => void = () => undefined;\n const reported = (change: WatchChange) => {\n if ('paths' in change) absorb(change.paths);\n onChange(change);\n };\n const native =\n this.#poll.mode === 'polling'\n ? undefined\n : await nativeWatch(this.#fs, this.cwd, targets, reported);\n const intervalMs = native ? (this.#poll.rescanMs ?? RESCAN_MS) : this.#poll.intervalMs;\n const polled = await pollWatch(this.#fs, this.cwd, targets, onChange, {\n ...(intervalMs === undefined ? {} : { intervalMs }),\n });\n absorb = (paths) => polled.absorb(paths);\n const watcher: FileWatcher = native\n ? {\n mode: 'native',\n async close(closing: Context) {\n await native.close(closing);\n await polled.close(closing);\n },\n }\n : polled;\n this.#watchers.add(watcher);\n return {\n mode: watcher.mode,\n close: async (closing: Context) => {\n this.#watchers.delete(watcher);\n await watcher.close(closing);\n },\n };\n });\n }\n\n canonicalPath(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.realpath(resolved));\n }\n\n async exists(path: string, context: Context): FileResult<boolean> {\n const found = await this.fileInfo(path, context);\n if (found.ok) return ok(true);\n return found.error.code === 'not_found' ? ok(false) : found;\n }\n\n createDir(\n path: string,\n _options: { recursive?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, () => this.#fs.mkdir(resolved));\n }\n\n remove(\n path: string,\n options: { recursive?: boolean; force?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#written(path);\n return attempt(context, resolved, async () => {\n if (!options?.recursive) {\n const stat = await this.#fs.lstat(resolved).catch((error: unknown) => {\n if (options?.force) return undefined;\n throw error;\n });\n if (stat?.isDirectory) throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n }\n await this.#fs.rm(resolved, { force: options?.force ?? false });\n });\n }\n\n async createTempDir(prefix: string | undefined, context: Context): FileResult<string> {\n const path = `/tmp/${prefix ?? 'tmp-'}${crypto.randomUUID()}`;\n return attempt(context, path, async () => {\n await this.#fs.mkdir(path);\n return path;\n });\n }\n\n async createTempFile(\n options: { prefix?: string; suffix?: string } | undefined,\n context: Context\n ): FileResult<string> {\n const dir = await this.createTempDir('tmp-', context);\n if (!dir.ok) return dir;\n const path = `${dir.value}/${options?.prefix ?? ''}${crypto.randomUUID()}${options?.suffix ?? ''}`;\n return attempt(context, path, async () => {\n await this.#fs.writeFile(path, '');\n return path;\n });\n }\n\n exec(\n command: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context\n ): Promise<Result<ShellExecResult, ExecutionError>> {\n const ended = this.#activity?.began();\n const line = typeof command === 'string' ? prefixed(command, this.#exports) : command;\n const jobs = this.#jobs;\n const client = this.#client;\n let recorded: Promise<void> | undefined;\n const tracked =\n jobs && typeof command === 'string'\n ? (process: KernelProcess) => {\n recorded = jobs.started(client, command, process);\n }\n : undefined;\n const groups = this.#groups;\n const owner = this.#owner;\n const run =\n !groups || owner === undefined\n ? execute(client, this.cwd, line, options, context, tracked, undefined, this.#hooks)\n : this.#grouped(groups, owner, line, options, context, tracked);\n return run\n .then(async (result) => {\n await recorded;\n return result;\n })\n .finally(ended);\n }\n\n async #grouped(\n groups: ProcessGroups,\n owner: number,\n line: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context,\n tracked: ((process: KernelProcess) => void) | undefined\n ): Promise<Result<ShellExecResult, ExecutionError>> {\n for (let wait = groups.pending(owner); wait; wait = groups.pending(owner)) await wait;\n const pgid = groups.shared(owner);\n const release = pgid === undefined && groups.joins() ? groups.reserve(owner) : () => undefined;\n const spawned = (process: KernelProcess, tag?: string) => {\n groups.track(owner, process, tag);\n tracked?.(process);\n release();\n };\n const join = groups.joins()\n ? {\n ...(pgid === undefined ? {} : { pgid }),\n refused: (code: string) => groups.refused(owner, code),\n }\n : undefined;\n return execute(\n this.#client,\n this.cwd,\n line,\n options,\n context,\n spawned,\n join,\n this.#hooks\n ).finally(release);\n }\n\n async cleanup(context: Context): Promise<void> {\n for (const watcher of [...this.#watchers]) await watcher.close(context);\n this.#watchers.clear();\n }\n}\n\nexport const HOME = '/home';\n\nexport type EnvTargetLike = { cwd?: string; conversationId?: number; read?: unknown };\n\nexport type EnvRoute = { client: KernelClient; home: string; cwd?: string };\n\nexport function kernelEnvironment(\n client: KernelClient,\n options: Omit<KernelEnvOptions, 'cwd'> & {\n identify?: (target: EnvTargetLike, context: Context) => Promise<Record<string, string>>;\n route?: (target: EnvTargetLike) => Promise<EnvRoute | undefined>;\n tagOf?: (conversationId: number, context: Context) => Promise<string | undefined>;\n } = {}\n): (target: EnvTargetLike, context?: Context) => SliccKernelEnv | Promise<SliccKernelEnv> {\n const { identify, route, tagOf, ...rest } = options;\n const make = (target: EnvTargetLike, exports?: Record<string, string>, routed?: EnvRoute) => {\n const owner = target.conversationId;\n return new SliccKernelEnv(routed?.client ?? client, {\n ...rest,\n cwd: routed?.cwd ?? target.cwd ?? routed?.home ?? HOME,\n ...(owner === undefined ? {} : { owner }),\n ...(owner !== undefined && tagOf ? { tag: (context: Context) => tagOf(owner, context) } : {}),\n ...(exports ? { exports } : {}),\n });\n };\n if (route)\n return async (target, context) => {\n const routed = await route(target);\n const exports = identify && context ? await identify(target, context) : undefined;\n return make(target, exports, routed);\n };\n return (target, context) =>\n identify && context\n ? identify(target, context).then((exports) => make(target, exports))\n : make(target);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/kernel/exec.js
CHANGED
|
@@ -126,17 +126,27 @@ async function drained(sinks, context) {
|
|
|
126
126
|
off();
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
+
async function tagged(client, argv, options) {
|
|
130
|
+
const { tag, ...untagged } = options;
|
|
131
|
+
if (tag === void 0) return { process: await client.spawn(argv, options) };
|
|
132
|
+
try {
|
|
133
|
+
return { process: await client.spawn(argv, options), tag };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error.code !== "ENOSYS") throw error;
|
|
136
|
+
return { process: await client.spawn(argv, untagged) };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
129
139
|
async function start(client, argv, options, join) {
|
|
130
140
|
if (join?.pgid !== void 0) {
|
|
131
141
|
try {
|
|
132
|
-
return {
|
|
142
|
+
return { ...await tagged(client, argv, { ...options, pgid: join.pgid }), joined: true };
|
|
133
143
|
} catch (error) {
|
|
134
144
|
const code = String(error.code ?? "");
|
|
135
145
|
if (!REFUSED.has(code)) throw error;
|
|
136
146
|
join.refused(code);
|
|
137
147
|
}
|
|
138
148
|
}
|
|
139
|
-
return {
|
|
149
|
+
return { ...await tagged(client, argv, options), joined: false };
|
|
140
150
|
}
|
|
141
151
|
async function execute(client, cwd, command, options, context, spawned, join, hooks = {}) {
|
|
142
152
|
const given = typeof command === "string" ? [...SHELL, command] : [...command];
|
|
@@ -161,12 +171,14 @@ async function execute(client, cwd, command, options, context, spawned, join, ho
|
|
|
161
171
|
const errors = sink("stderr", spill, options, context, scrubbed());
|
|
162
172
|
let started;
|
|
163
173
|
try {
|
|
174
|
+
const tag = await hooks.tag?.(context);
|
|
164
175
|
started = await start(
|
|
165
176
|
client,
|
|
166
177
|
argv,
|
|
167
178
|
{
|
|
168
179
|
cwd: resolve(cwd, options?.cwd ?? "."),
|
|
169
180
|
...options?.env ? { env: options.env } : {},
|
|
181
|
+
...tag === void 0 ? {} : { tag },
|
|
170
182
|
onStdout: (bytes) => out.push(bytes),
|
|
171
183
|
onStderr: (bytes) => errors.push(bytes)
|
|
172
184
|
},
|
|
@@ -177,7 +189,7 @@ async function execute(client, cwd, command, options, context, spawned, join, ho
|
|
|
177
189
|
return err(new ExecutionError("spawn_error", message));
|
|
178
190
|
}
|
|
179
191
|
const { process } = started;
|
|
180
|
-
spawned?.(process);
|
|
192
|
+
spawned?.(process, started.tag);
|
|
181
193
|
const watch = guard(
|
|
182
194
|
process,
|
|
183
195
|
options,
|
package/dist/kernel/exec.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/kernel/exec.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n ExecutionError,\n err,\n ok,\n type Result,\n type ShellExecOptions,\n type ShellExecResult,\n StreamDecoder,\n} from '@earendil-works/pi-durable/env';\nimport type { ScrubStream } from '../secrets/scrub.ts';\nimport type { KernelClient, KernelProcess, SpawnOptions } from './client.ts';\nimport { descendants, REFUSED } from './groups.ts';\nimport { resolve } from './paths.ts';\n\nexport const SHELL = ['bash', '-c'] as const;\nexport const HELD_LIMIT = 32 * 1024 * 1024;\nexport const HELD = 'output held: waiting for the vault';\n\nexport interface ExecHooks {\n scrub?: () => ScrubStream;\n spawn?: (argv: readonly string[]) => readonly string[];\n}\n\nclass Spill {\n readonly #chunks: Uint8Array[] = [];\n readonly #limit: { afterBytes: number; afterLines: number } | undefined;\n #bytes = 0;\n #lines = 0;\n\n constructor(limit: ShellExecOptions['spill']) {\n this.#limit = limit;\n }\n\n add(chunk: Uint8Array): void {\n if (!this.#limit) return;\n this.#chunks.push(chunk);\n this.#bytes += chunk.length;\n for (const byte of chunk) if (byte === 10) this.#lines++;\n }\n\n get over(): boolean {\n const limit = this.#limit;\n return (\n limit !== undefined && (this.#bytes > limit.afterBytes || this.#lines > limit.afterLines)\n );\n }\n\n async write(client: KernelClient): Promise<string | undefined> {\n if (!this.over) return undefined;\n const path = `/tmp/slicc-agent-output-${crypto.randomUUID()}.log`;\n const all = new Uint8Array(this.#bytes);\n let offset = 0;\n for (const chunk of this.#chunks) {\n all.set(chunk, offset);\n offset += chunk.length;\n }\n await client.fs.mkdir('/tmp');\n await client.fs.writeFile(path, all);\n return path;\n }\n}\n\ninterface Sink {\n push(bytes: Uint8Array): void;\n end(): Promise<void>;\n drop(): void;\n}\n\nfunction sink(\n stream: 'stdout' | 'stderr',\n spill: Spill,\n options: ShellExecOptions | undefined,\n context: Context,\n scrub?: { stream: ScrubStream; held(bytes: number): void }\n): Sink {\n const decoder = new StreamDecoder();\n const emit = (bytes: Uint8Array) => {\n spill.add(bytes);\n const text = decoder.decode(bytes);\n if (text) options?.onOutput?.(text, context, { stream });\n };\n const flush = () => {\n const text = decoder.decode();\n if (text) options?.onOutput?.(text, context, { stream });\n };\n if (!scrub)\n return {\n push: emit,\n end: async () => flush(),\n drop: flush,\n };\n let chain: Promise<void> = Promise.resolve();\n let dropped = false;\n const live = (bytes: Uint8Array) => {\n if (!dropped) emit(bytes);\n };\n return {\n push(bytes) {\n scrub.held(bytes.length);\n chain = chain\n .then(() => scrub.stream.push(bytes))\n .then((out) => {\n scrub.held(-bytes.length);\n live(out);\n });\n },\n async end() {\n await chain;\n live(await scrub.stream.end());\n if (!dropped) flush();\n },\n drop() {\n dropped = true;\n chain.catch(() => undefined);\n scrub.stream.drop();\n },\n };\n}\n\ntype Ending = 'timeout' | 'aborted' | 'held' | undefined;\n\nexport async function sweep(\n client: Pick<KernelClient, 'kill' | 'ps'>,\n process: KernelProcess\n): Promise<void> {\n const table = (await client.ps?.().catch(() => undefined)) ?? [];\n const pids = [process.pid, ...descendants(table, process.pid)];\n await Promise.all(pids.map((pid) => client.kill?.(pid, 'SIGKILL').catch(() => undefined)));\n}\n\nfunction guard(\n process: KernelProcess,\n options: ShellExecOptions | undefined,\n context: Context,\n kill: () => void\n) {\n let ending: Ending;\n const stop = (why: Ending) => {\n ending ??= why;\n kill();\n };\n const timer =\n options?.timeout === undefined\n ? undefined\n : setTimeout(() => stop('timeout'), options.timeout * 1000);\n const onAbort = () => stop('aborted');\n context.abortSignal?.addEventListener('abort', onAbort, { once: true });\n if (context.abortSignal?.aborted) onAbort();\n return {\n get ending() {\n return ending;\n },\n stop,\n release() {\n clearTimeout(timer);\n context.abortSignal?.removeEventListener('abort', onAbort);\n },\n };\n}\n\nasync function drained(sinks: readonly Sink[], context: Context): Promise<Ending> {\n const signal = context.abortSignal;\n if (signal?.aborted) return 'aborted';\n let off = () => undefined as void;\n const aborted = new Promise<Ending>((resolve) => {\n const onAbort = () => resolve('aborted');\n signal?.addEventListener('abort', onAbort, { once: true });\n off = () => signal?.removeEventListener('abort', onAbort);\n });\n const ends = Promise.all(sinks.map((each) => each.end())).then(() => undefined);\n ends.catch(() => undefined);\n try {\n return await Promise.race([ends, aborted]);\n } finally {\n off();\n }\n}\n\nexport interface Join {\n pgid?: number;\n refused(code: string): void;\n}\n\nasync function start(\n client: KernelClient,\n argv: readonly string[],\n options: SpawnOptions,\n join: Join | undefined\n): Promise<
|
|
5
|
-
"mappings": "AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EAIA;AAAA,OACK;AAGP,SAAS,aAAa,eAAe;AACrC,SAAS,eAAe;AAEjB,MAAM,QAAQ,CAAC,QAAQ,IAAI;AAC3B,MAAM,aAAa,KAAK,OAAO;AAC/B,MAAM,OAAO;
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n ExecutionError,\n err,\n ok,\n type Result,\n type ShellExecOptions,\n type ShellExecResult,\n StreamDecoder,\n} from '@earendil-works/pi-durable/env';\nimport type { ScrubStream } from '../secrets/scrub.ts';\nimport type { KernelClient, KernelProcess, SpawnOptions } from './client.ts';\nimport { descendants, REFUSED } from './groups.ts';\nimport { resolve } from './paths.ts';\n\nexport const SHELL = ['bash', '-c'] as const;\nexport const HELD_LIMIT = 32 * 1024 * 1024;\nexport const HELD = 'output held: waiting for the vault';\n\nexport interface ExecHooks {\n scrub?: () => ScrubStream;\n spawn?: (argv: readonly string[]) => readonly string[];\n tag?: (context: Context) => Promise<string | undefined>;\n}\n\nclass Spill {\n readonly #chunks: Uint8Array[] = [];\n readonly #limit: { afterBytes: number; afterLines: number } | undefined;\n #bytes = 0;\n #lines = 0;\n\n constructor(limit: ShellExecOptions['spill']) {\n this.#limit = limit;\n }\n\n add(chunk: Uint8Array): void {\n if (!this.#limit) return;\n this.#chunks.push(chunk);\n this.#bytes += chunk.length;\n for (const byte of chunk) if (byte === 10) this.#lines++;\n }\n\n get over(): boolean {\n const limit = this.#limit;\n return (\n limit !== undefined && (this.#bytes > limit.afterBytes || this.#lines > limit.afterLines)\n );\n }\n\n async write(client: KernelClient): Promise<string | undefined> {\n if (!this.over) return undefined;\n const path = `/tmp/slicc-agent-output-${crypto.randomUUID()}.log`;\n const all = new Uint8Array(this.#bytes);\n let offset = 0;\n for (const chunk of this.#chunks) {\n all.set(chunk, offset);\n offset += chunk.length;\n }\n await client.fs.mkdir('/tmp');\n await client.fs.writeFile(path, all);\n return path;\n }\n}\n\ninterface Sink {\n push(bytes: Uint8Array): void;\n end(): Promise<void>;\n drop(): void;\n}\n\nfunction sink(\n stream: 'stdout' | 'stderr',\n spill: Spill,\n options: ShellExecOptions | undefined,\n context: Context,\n scrub?: { stream: ScrubStream; held(bytes: number): void }\n): Sink {\n const decoder = new StreamDecoder();\n const emit = (bytes: Uint8Array) => {\n spill.add(bytes);\n const text = decoder.decode(bytes);\n if (text) options?.onOutput?.(text, context, { stream });\n };\n const flush = () => {\n const text = decoder.decode();\n if (text) options?.onOutput?.(text, context, { stream });\n };\n if (!scrub)\n return {\n push: emit,\n end: async () => flush(),\n drop: flush,\n };\n let chain: Promise<void> = Promise.resolve();\n let dropped = false;\n const live = (bytes: Uint8Array) => {\n if (!dropped) emit(bytes);\n };\n return {\n push(bytes) {\n scrub.held(bytes.length);\n chain = chain\n .then(() => scrub.stream.push(bytes))\n .then((out) => {\n scrub.held(-bytes.length);\n live(out);\n });\n },\n async end() {\n await chain;\n live(await scrub.stream.end());\n if (!dropped) flush();\n },\n drop() {\n dropped = true;\n chain.catch(() => undefined);\n scrub.stream.drop();\n },\n };\n}\n\ntype Ending = 'timeout' | 'aborted' | 'held' | undefined;\n\nexport async function sweep(\n client: Pick<KernelClient, 'kill' | 'ps'>,\n process: KernelProcess\n): Promise<void> {\n const table = (await client.ps?.().catch(() => undefined)) ?? [];\n const pids = [process.pid, ...descendants(table, process.pid)];\n await Promise.all(pids.map((pid) => client.kill?.(pid, 'SIGKILL').catch(() => undefined)));\n}\n\nfunction guard(\n process: KernelProcess,\n options: ShellExecOptions | undefined,\n context: Context,\n kill: () => void\n) {\n let ending: Ending;\n const stop = (why: Ending) => {\n ending ??= why;\n kill();\n };\n const timer =\n options?.timeout === undefined\n ? undefined\n : setTimeout(() => stop('timeout'), options.timeout * 1000);\n const onAbort = () => stop('aborted');\n context.abortSignal?.addEventListener('abort', onAbort, { once: true });\n if (context.abortSignal?.aborted) onAbort();\n return {\n get ending() {\n return ending;\n },\n stop,\n release() {\n clearTimeout(timer);\n context.abortSignal?.removeEventListener('abort', onAbort);\n },\n };\n}\n\nasync function drained(sinks: readonly Sink[], context: Context): Promise<Ending> {\n const signal = context.abortSignal;\n if (signal?.aborted) return 'aborted';\n let off = () => undefined as void;\n const aborted = new Promise<Ending>((resolve) => {\n const onAbort = () => resolve('aborted');\n signal?.addEventListener('abort', onAbort, { once: true });\n off = () => signal?.removeEventListener('abort', onAbort);\n });\n const ends = Promise.all(sinks.map((each) => each.end())).then(() => undefined);\n ends.catch(() => undefined);\n try {\n return await Promise.race([ends, aborted]);\n } finally {\n off();\n }\n}\n\nexport interface Join {\n pgid?: number;\n refused(code: string): void;\n}\n\ntype Started = { process: KernelProcess; tag?: string };\n\nasync function tagged(\n client: KernelClient,\n argv: readonly string[],\n options: SpawnOptions\n): Promise<Started> {\n const { tag, ...untagged } = options;\n if (tag === undefined) return { process: await client.spawn(argv, options) };\n try {\n return { process: await client.spawn(argv, options), tag };\n } catch (error) {\n if ((error as { code?: unknown }).code !== 'ENOSYS') throw error;\n return { process: await client.spawn(argv, untagged) };\n }\n}\n\nasync function start(\n client: KernelClient,\n argv: readonly string[],\n options: SpawnOptions,\n join: Join | undefined\n): Promise<Started & { joined: boolean }> {\n if (join?.pgid !== undefined) {\n try {\n return { ...(await tagged(client, argv, { ...options, pgid: join.pgid })), joined: true };\n } catch (error) {\n const code = String((error as { code?: unknown }).code ?? '');\n if (!REFUSED.has(code)) throw error;\n join.refused(code);\n }\n }\n return { ...(await tagged(client, argv, options)), joined: false };\n}\n\nexport async function execute(\n client: KernelClient,\n cwd: string,\n command: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context,\n spawned?: (process: KernelProcess, tag?: string) => void,\n join?: Join,\n hooks: ExecHooks = {}\n): Promise<Result<ShellExecResult, ExecutionError>> {\n const given = typeof command === 'string' ? [...SHELL, command] : [...command];\n if (given.length === 0) return err(new ExecutionError('spawn_error', 'No program to run'));\n const argv = hooks.spawn ? [...hooks.spawn(given)] : given;\n if (context.abortSignal?.aborted)\n return err(new ExecutionError('aborted', 'The command was aborted'));\n const spill = new Spill(options?.spill);\n let held = 0;\n let overflowed = false;\n let over = () => {\n overflowed = true;\n };\n const scrubbed = () =>\n hooks.scrub && {\n stream: hooks.scrub(),\n held(bytes: number) {\n held += bytes;\n if (held > HELD_LIMIT) over();\n },\n };\n const out = sink('stdout', spill, options, context, scrubbed());\n const errors = sink('stderr', spill, options, context, scrubbed());\n let started: Started & { joined: boolean };\n try {\n const tag = await hooks.tag?.(context);\n started = await start(\n client,\n argv,\n {\n cwd: resolve(cwd, options?.cwd ?? '.'),\n ...(options?.env ? { env: options.env } : {}),\n ...(tag === undefined ? {} : { tag }),\n onStdout: (bytes) => out.push(bytes),\n onStderr: (bytes) => errors.push(bytes),\n },\n join\n );\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return err(new ExecutionError('spawn_error', message));\n }\n const { process } = started;\n spawned?.(process, started.tag);\n const watch = guard(process, options, context, () =>\n join ? void sweep(client, process) : process.signal('SIGKILL')\n );\n over = () => watch.stop('held');\n if (overflowed) over();\n const exitCode = await process.exited.finally(() => watch.release());\n const ended = watch.ending ?? (await drained([out, errors], context));\n if (ended) {\n out.drop();\n errors.drop();\n }\n const spillPath = await spill.write(client);\n const ending = watch.ending ?? ended;\n if (ending) {\n const error =\n ending === 'held'\n ? new ExecutionError('unknown', HELD)\n : new ExecutionError(\n ending,\n `The command was ${ending === 'timeout' ? 'timed out' : 'aborted'}`\n );\n if (spillPath) error.spillPath = spillPath;\n return err(error);\n }\n return ok(spillPath ? { exitCode, spillPath } : { exitCode });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["resolve"]
|
|
7
7
|
}
|
package/dist/kernel/groups.js
CHANGED
|
@@ -7,8 +7,8 @@ function processGroups(client) {
|
|
|
7
7
|
const leading = /* @__PURE__ */ new Map();
|
|
8
8
|
let joining = true;
|
|
9
9
|
return {
|
|
10
|
-
track(owner, process) {
|
|
11
|
-
spawned.started(process, owner);
|
|
10
|
+
track(owner, process, tag) {
|
|
11
|
+
spawned.started(process, owner, tag);
|
|
12
12
|
const set = groups.get(owner) ?? /* @__PURE__ */ new Set();
|
|
13
13
|
groups.set(owner, set);
|
|
14
14
|
set.add(process.pgid);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/kernel/groups.ts"],
|
|
4
|
-
"sourcesContent": ["import type { KernelClient, KernelProcess } from './client.ts';\nimport { type Lineage, spawns } from './lineage.ts';\n\nexport const REFUSED = new Set(['ESRCH', 'EPERM', 'ENOSYS']);\n\nexport interface ProcessGroups {\n track(owner: number, process: KernelProcess): void;\n of(owner: number): number[];\n owned(owner: number): number[];\n shared(owner: number): number | undefined;\n joins(): boolean;\n pending(owner: number): Promise<void> | undefined;\n reserve(owner: number): () => void;\n refused(owner: number, code: string): void;\n signal(owner: number, signal?: string): Promise<number[]>;\n lineage(pid: number | null): Promise<Lineage | undefined>;\n}\n\nexport function processGroups(client: Pick<KernelClient, 'kill' | 'ps'>): ProcessGroups {\n const spawned = spawns(client);\n const groups = new Map<number, Set<number>>();\n const shared = new Map<number, number>();\n const leading = new Map<number, Promise<void>>();\n let joining = true;\n return {\n track(owner, process) {\n spawned.started(process, owner);\n const set = groups.get(owner) ?? new Set<number>();\n groups.set(owner, set);\n set.add(process.pgid);\n if (!shared.has(owner)) shared.set(owner, process.pgid);\n process.exited.then(\n () => set.delete(process.pgid),\n () => set.delete(process.pgid)\n );\n },\n of: (owner) => [...(groups.get(owner) ?? [])],\n owned: (owner) =>\n [\n ...new Set([\n ...(groups.get(owner) ?? []),\n ...(shared.has(owner) ? [shared.get(owner)] : []),\n ]),\n ] as number[],\n shared: (owner) => (joining ? shared.get(owner) : undefined),\n joins: () => joining,\n pending: (owner) => leading.get(owner),\n reserve(owner) {\n let done: () => void = () => undefined;\n leading.set(\n owner,\n new Promise<void>((resolve) => {\n done = resolve;\n })\n );\n return () => {\n leading.delete(owner);\n done();\n };\n },\n refused(owner, code) {\n if (code === 'ENOSYS') joining = false;\n shared.delete(owner);\n },\n async signal(owner, signal = 'SIGTERM') {\n const signalled: number[] = [];\n const all = new Set([\n ...(groups.get(owner) ?? []),\n ...(shared.has(owner) ? [shared.get(owner) as number] : []),\n ]);\n for (const pgid of all) {\n try {\n await client.kill?.(-pgid, signal);\n signalled.push(pgid);\n } catch {}\n }\n groups.delete(owner);\n shared.delete(owner);\n return signalled;\n },\n lineage: spawned.lineage,\n };\n}\n\nexport function descendants(\n table: readonly { pid: number; ppid?: number }[],\n root: number\n): number[] {\n const out: number[] = [];\n const queue = [root];\n while (queue.length) {\n const parent = queue.shift() as number;\n for (const entry of table)\n if (entry.ppid === parent && !out.includes(entry.pid)) {\n out.push(entry.pid);\n queue.push(entry.pid);\n }\n }\n return out;\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAuB,cAAc;AAE9B,MAAM,UAAU,oBAAI,IAAI,CAAC,SAAS,SAAS,QAAQ,CAAC;AAepD,SAAS,cAAc,
|
|
4
|
+
"sourcesContent": ["import type { KernelClient, KernelProcess } from './client.ts';\nimport { type Lineage, spawns } from './lineage.ts';\n\nexport const REFUSED = new Set(['ESRCH', 'EPERM', 'ENOSYS']);\n\nexport interface ProcessGroups {\n track(owner: number, process: KernelProcess, tag?: string): void;\n of(owner: number): number[];\n owned(owner: number): number[];\n shared(owner: number): number | undefined;\n joins(): boolean;\n pending(owner: number): Promise<void> | undefined;\n reserve(owner: number): () => void;\n refused(owner: number, code: string): void;\n signal(owner: number, signal?: string): Promise<number[]>;\n lineage(pid: number | null): Promise<Lineage | undefined>;\n}\n\nexport function processGroups(client: Pick<KernelClient, 'kill' | 'ps' | 'fs'>): ProcessGroups {\n const spawned = spawns(client);\n const groups = new Map<number, Set<number>>();\n const shared = new Map<number, number>();\n const leading = new Map<number, Promise<void>>();\n let joining = true;\n return {\n track(owner, process, tag) {\n spawned.started(process, owner, tag);\n const set = groups.get(owner) ?? new Set<number>();\n groups.set(owner, set);\n set.add(process.pgid);\n if (!shared.has(owner)) shared.set(owner, process.pgid);\n process.exited.then(\n () => set.delete(process.pgid),\n () => set.delete(process.pgid)\n );\n },\n of: (owner) => [...(groups.get(owner) ?? [])],\n owned: (owner) =>\n [\n ...new Set([\n ...(groups.get(owner) ?? []),\n ...(shared.has(owner) ? [shared.get(owner)] : []),\n ]),\n ] as number[],\n shared: (owner) => (joining ? shared.get(owner) : undefined),\n joins: () => joining,\n pending: (owner) => leading.get(owner),\n reserve(owner) {\n let done: () => void = () => undefined;\n leading.set(\n owner,\n new Promise<void>((resolve) => {\n done = resolve;\n })\n );\n return () => {\n leading.delete(owner);\n done();\n };\n },\n refused(owner, code) {\n if (code === 'ENOSYS') joining = false;\n shared.delete(owner);\n },\n async signal(owner, signal = 'SIGTERM') {\n const signalled: number[] = [];\n const all = new Set([\n ...(groups.get(owner) ?? []),\n ...(shared.has(owner) ? [shared.get(owner) as number] : []),\n ]);\n for (const pgid of all) {\n try {\n await client.kill?.(-pgid, signal);\n signalled.push(pgid);\n } catch {}\n }\n groups.delete(owner);\n shared.delete(owner);\n return signalled;\n },\n lineage: spawned.lineage,\n };\n}\n\nexport function descendants(\n table: readonly { pid: number; ppid?: number }[],\n root: number\n): number[] {\n const out: number[] = [];\n const queue = [root];\n while (queue.length) {\n const parent = queue.shift() as number;\n for (const entry of table)\n if (entry.ppid === parent && !out.includes(entry.pid)) {\n out.push(entry.pid);\n queue.push(entry.pid);\n }\n }\n return out;\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAuB,cAAc;AAE9B,MAAM,UAAU,oBAAI,IAAI,CAAC,SAAS,SAAS,QAAQ,CAAC;AAepD,SAAS,cAAc,QAAiE;AAC7F,QAAM,UAAU,OAAO,MAAM;AAC7B,QAAM,SAAS,oBAAI,IAAyB;AAC5C,QAAM,SAAS,oBAAI,IAAoB;AACvC,QAAM,UAAU,oBAAI,IAA2B;AAC/C,MAAI,UAAU;AACd,SAAO;AAAA,IACL,MAAM,OAAO,SAAS,KAAK;AACzB,cAAQ,QAAQ,SAAS,OAAO,GAAG;AACnC,YAAM,MAAM,OAAO,IAAI,KAAK,KAAK,oBAAI,IAAY;AACjD,aAAO,IAAI,OAAO,GAAG;AACrB,UAAI,IAAI,QAAQ,IAAI;AACpB,UAAI,CAAC,OAAO,IAAI,KAAK,EAAG,QAAO,IAAI,OAAO,QAAQ,IAAI;AACtD,cAAQ,OAAO;AAAA,QACb,MAAM,IAAI,OAAO,QAAQ,IAAI;AAAA,QAC7B,MAAM,IAAI,OAAO,QAAQ,IAAI;AAAA,MAC/B;AAAA,IACF;AAAA,IACA,IAAI,CAAC,UAAU,CAAC,GAAI,OAAO,IAAI,KAAK,KAAK,CAAC,CAAE;AAAA,IAC5C,OAAO,CAAC,UACN;AAAA,MACE,GAAG,oBAAI,IAAI;AAAA,QACT,GAAI,OAAO,IAAI,KAAK,KAAK,CAAC;AAAA,QAC1B,GAAI,OAAO,IAAI,KAAK,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,IAAI,CAAC;AAAA,MACjD,CAAC;AAAA,IACH;AAAA,IACF,QAAQ,CAAC,UAAW,UAAU,OAAO,IAAI,KAAK,IAAI;AAAA,IAClD,OAAO,MAAM;AAAA,IACb,SAAS,CAAC,UAAU,QAAQ,IAAI,KAAK;AAAA,IACrC,QAAQ,OAAO;AACb,UAAI,OAAmB,MAAM;AAC7B,cAAQ;AAAA,QACN;AAAA,QACA,IAAI,QAAc,CAAC,YAAY;AAC7B,iBAAO;AAAA,QACT,CAAC;AAAA,MACH;AACA,aAAO,MAAM;AACX,gBAAQ,OAAO,KAAK;AACpB,aAAK;AAAA,MACP;AAAA,IACF;AAAA,IACA,QAAQ,OAAO,MAAM;AACnB,UAAI,SAAS,SAAU,WAAU;AACjC,aAAO,OAAO,KAAK;AAAA,IACrB;AAAA,IACA,MAAM,OAAO,OAAO,SAAS,WAAW;AACtC,YAAM,YAAsB,CAAC;AAC7B,YAAM,MAAM,oBAAI,IAAI;AAAA,QAClB,GAAI,OAAO,IAAI,KAAK,KAAK,CAAC;AAAA,QAC1B,GAAI,OAAO,IAAI,KAAK,IAAI,CAAC,OAAO,IAAI,KAAK,CAAW,IAAI,CAAC;AAAA,MAC3D,CAAC;AACD,iBAAW,QAAQ,KAAK;AACtB,YAAI;AACF,gBAAM,OAAO,OAAO,CAAC,MAAM,MAAM;AACjC,oBAAU,KAAK,IAAI;AAAA,QACrB,QAAQ;AAAA,QAAC;AAAA,MACX;AACA,aAAO,OAAO,KAAK;AACnB,aAAO,OAAO,KAAK;AACnB,aAAO;AAAA,IACT;AAAA,IACA,SAAS,QAAQ;AAAA,EACnB;AACF;AAEO,SAAS,YACd,OACA,MACU;AACV,QAAM,MAAgB,CAAC;AACvB,QAAM,QAAQ,CAAC,IAAI;AACnB,SAAO,MAAM,QAAQ;AACnB,UAAM,SAAS,MAAM,MAAM;AAC3B,eAAW,SAAS;AAClB,UAAI,MAAM,SAAS,UAAU,CAAC,IAAI,SAAS,MAAM,GAAG,GAAG;AACrD,YAAI,KAAK,MAAM,GAAG;AAClB,cAAM,KAAK,MAAM,GAAG;AAAA,MACtB;AAAA,EACJ;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/kernel/lineage.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { boundedPs, PS_MS } from "./jobs.js";
|
|
2
|
+
import { sightings, tagsOf } from "./sightings.js";
|
|
2
3
|
function indexed(rows) {
|
|
3
4
|
const byPid = /* @__PURE__ */ new Map();
|
|
4
5
|
for (const row of rows) if (!byPid.has(row.pid)) byPid.set(row.pid, row);
|
|
@@ -9,6 +10,7 @@ function parentOf(byPid, row) {
|
|
|
9
10
|
}
|
|
10
11
|
function spawns(client, warn = (message) => console.warn(message), bound = PS_MS) {
|
|
11
12
|
const live = /* @__PURE__ */ new Map();
|
|
13
|
+
const tagged = sightings(client, bound);
|
|
12
14
|
const groups = /* @__PURE__ */ new Set();
|
|
13
15
|
const seen = (found, row) => {
|
|
14
16
|
const owner = live.get(row.pid);
|
|
@@ -16,7 +18,7 @@ function spawns(client, warn = (message) => console.warn(message), bound = PS_MS
|
|
|
16
18
|
found.spawned = true;
|
|
17
19
|
if (owner !== void 0 && found.agent === null) found.agent = owner;
|
|
18
20
|
};
|
|
19
|
-
const walk = (rows, pid) => {
|
|
21
|
+
const walk = (rows, pid, at) => {
|
|
20
22
|
for (const pgid of [...groups]) if (!rows.some((row2) => row2.pgid === pgid)) groups.delete(pgid);
|
|
21
23
|
const byPid = indexed(rows);
|
|
22
24
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -26,7 +28,9 @@ function spawns(client, warn = (message) => console.warn(message), bound = PS_MS
|
|
|
26
28
|
agent: null,
|
|
27
29
|
spawned: false,
|
|
28
30
|
argv: row.argv ?? [],
|
|
29
|
-
...row.uid === void 0 ? {} : { uid: row.uid }
|
|
31
|
+
...row.uid === void 0 ? {} : { uid: row.uid },
|
|
32
|
+
tags: tagsOf(row),
|
|
33
|
+
recent: tagged.recent(pid, rows, at)
|
|
30
34
|
};
|
|
31
35
|
for (; row && !visited.has(row.pid); row = parentOf(byPid, row)) {
|
|
32
36
|
visited.add(row.pid);
|
|
@@ -35,7 +39,8 @@ function spawns(client, warn = (message) => console.warn(message), bound = PS_MS
|
|
|
35
39
|
return found;
|
|
36
40
|
};
|
|
37
41
|
return {
|
|
38
|
-
started(process, owner) {
|
|
42
|
+
started(process, owner, tag) {
|
|
43
|
+
if (tag !== void 0) tagged.spawned(process, tag);
|
|
39
44
|
live.set(process.pid, owner);
|
|
40
45
|
groups.add(process.pgid);
|
|
41
46
|
const ended = () => live.delete(process.pid);
|
|
@@ -43,8 +48,11 @@ function spawns(client, warn = (message) => console.warn(message), bound = PS_MS
|
|
|
43
48
|
},
|
|
44
49
|
async lineage(pid) {
|
|
45
50
|
if (pid === null) return void 0;
|
|
51
|
+
const at = tagged.mark();
|
|
46
52
|
const rows = await boundedPs(client, warn, bound);
|
|
47
|
-
|
|
53
|
+
if (!rows) return void 0;
|
|
54
|
+
await tagged.observe(rows, at);
|
|
55
|
+
return walk(rows, pid, at);
|
|
48
56
|
}
|
|
49
57
|
};
|
|
50
58
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/kernel/lineage.ts"],
|
|
4
|
-
"sourcesContent": ["import type { KernelClient, KernelProcess } from './client.ts';\nimport { boundedPs, PS_MS } from './jobs.ts';\n\nexport interface Lineage {\n agent: number | null;\n spawned: boolean;\n argv: readonly string[];\n uid?: number;\n}\n\nexport interface Spawns {\n started(process: KernelProcess, owner: number): void;\n lineage(pid: number | null): Promise<Lineage | undefined>;\n}\n\ntype Row = {\n pid: number;\n ppid?: number;\n pgid?: number;\n argv?: readonly string[];\n uid?: number;\n};\n\nfunction indexed(rows: readonly Row[]): Map<number, Row> {\n const byPid = new Map<number, Row>();\n for (const row of rows) if (!byPid.has(row.pid)) byPid.set(row.pid, row);\n return byPid;\n}\n\nfunction parentOf(byPid: ReadonlyMap<number, Row>, row: Row): Row | undefined {\n return row.ppid === undefined || row.ppid <= 1 ? undefined : byPid.get(row.ppid);\n}\n\nexport function spawns(\n client: Pick<KernelClient, 'ps'>,\n warn: (message: string) => void = (message) => console.warn(message),\n bound = PS_MS\n): Spawns {\n const live = new Map<number, number>();\n const groups = new Set<number>();\n const seen = (found: Lineage, row: Row) => {\n const owner = live.get(row.pid);\n if (owner !== undefined || (row.pgid !== undefined && groups.has(row.pgid)))\n found.spawned = true;\n if (owner !== undefined && found.agent === null) found.agent = owner;\n };\n const walk = (rows: readonly Row[], pid: number): Lineage | undefined => {\n for (const pgid of [...groups]) if (!rows.some((row) => row.pgid === pgid)) groups.delete(pgid);\n const byPid = indexed(rows);\n const visited = new Set<number>();\n let row = byPid.get(pid);\n if (!row) return undefined;\n const found: Lineage = {\n agent: null,\n spawned: false,\n argv: row.argv ?? [],\n ...(row.uid === undefined ? {} : { uid: row.uid }),\n };\n for (; row && !visited.has(row.pid); row = parentOf(byPid, row)) {\n visited.add(row.pid);\n seen(found, row);\n }\n return found;\n };\n return {\n started(process, owner) {\n live.set(process.pid, owner);\n groups.add(process.pgid);\n const ended = () => live.delete(process.pid);\n process.exited.then(ended, ended);\n },\n async lineage(pid) {\n if (pid === null) return undefined;\n const rows = await boundedPs(client, warn, bound);\n return rows
|
|
5
|
-
"mappings": "AACA,SAAS,WAAW,aAAa;
|
|
4
|
+
"sourcesContent": ["import type { KernelClient, KernelProcess } from './client.ts';\nimport { boundedPs, PS_MS } from './jobs.ts';\nimport { sightings, tagsOf } from './sightings.ts';\n\nexport interface Lineage {\n agent: number | null;\n spawned: boolean;\n argv: readonly string[];\n uid?: number;\n tags: string[];\n recent: string[];\n}\n\nexport interface Spawns {\n started(process: KernelProcess, owner: number, tag?: string): void;\n lineage(pid: number | null): Promise<Lineage | undefined>;\n}\n\ntype Row = {\n pid: number;\n ppid?: number;\n pgid?: number;\n argv?: readonly string[];\n uid?: number;\n started?: number;\n tag?: string;\n tags?: readonly string[];\n};\n\nfunction indexed(rows: readonly Row[]): Map<number, Row> {\n const byPid = new Map<number, Row>();\n for (const row of rows) if (!byPid.has(row.pid)) byPid.set(row.pid, row);\n return byPid;\n}\n\nfunction parentOf(byPid: ReadonlyMap<number, Row>, row: Row): Row | undefined {\n return row.ppid === undefined || row.ppid <= 1 ? undefined : byPid.get(row.ppid);\n}\n\nexport function spawns(\n client: Pick<KernelClient, 'ps' | 'fs'>,\n warn: (message: string) => void = (message) => console.warn(message),\n bound = PS_MS\n): Spawns {\n const live = new Map<number, number>();\n const tagged = sightings(client, bound);\n const groups = new Set<number>();\n const seen = (found: Lineage, row: Row) => {\n const owner = live.get(row.pid);\n if (owner !== undefined || (row.pgid !== undefined && groups.has(row.pgid)))\n found.spawned = true;\n if (owner !== undefined && found.agent === null) found.agent = owner;\n };\n const walk = (rows: readonly Row[], pid: number, at: number): Lineage | undefined => {\n for (const pgid of [...groups]) if (!rows.some((row) => row.pgid === pgid)) groups.delete(pgid);\n const byPid = indexed(rows);\n const visited = new Set<number>();\n let row = byPid.get(pid);\n if (!row) return undefined;\n const found: Lineage = {\n agent: null,\n spawned: false,\n argv: row.argv ?? [],\n ...(row.uid === undefined ? {} : { uid: row.uid }),\n tags: tagsOf(row),\n recent: tagged.recent(pid, rows, at),\n };\n for (; row && !visited.has(row.pid); row = parentOf(byPid, row)) {\n visited.add(row.pid);\n seen(found, row);\n }\n return found;\n };\n return {\n started(process, owner, tag) {\n if (tag !== undefined) tagged.spawned(process, tag);\n live.set(process.pid, owner);\n groups.add(process.pgid);\n const ended = () => live.delete(process.pid);\n process.exited.then(ended, ended);\n },\n async lineage(pid) {\n if (pid === null) return undefined;\n const at = tagged.mark();\n const rows = await boundedPs(client, warn, bound);\n if (!rows) return undefined;\n await tagged.observe(rows, at);\n return walk(rows, pid, at);\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,WAAW,aAAa;AACjC,SAAS,WAAW,cAAc;AA2BlC,SAAS,QAAQ,MAAwC;AACvD,QAAM,QAAQ,oBAAI,IAAiB;AACnC,aAAW,OAAO,KAAM,KAAI,CAAC,MAAM,IAAI,IAAI,GAAG,EAAG,OAAM,IAAI,IAAI,KAAK,GAAG;AACvE,SAAO;AACT;AAEA,SAAS,SAAS,OAAiC,KAA2B;AAC5E,SAAO,IAAI,SAAS,UAAa,IAAI,QAAQ,IAAI,SAAY,MAAM,IAAI,IAAI,IAAI;AACjF;AAEO,SAAS,OACd,QACA,OAAkC,CAAC,YAAY,QAAQ,KAAK,OAAO,GACnE,QAAQ,OACA;AACR,QAAM,OAAO,oBAAI,IAAoB;AACrC,QAAM,SAAS,UAAU,QAAQ,KAAK;AACtC,QAAM,SAAS,oBAAI,IAAY;AAC/B,QAAM,OAAO,CAAC,OAAgB,QAAa;AACzC,UAAM,QAAQ,KAAK,IAAI,IAAI,GAAG;AAC9B,QAAI,UAAU,UAAc,IAAI,SAAS,UAAa,OAAO,IAAI,IAAI,IAAI;AACvE,YAAM,UAAU;AAClB,QAAI,UAAU,UAAa,MAAM,UAAU,KAAM,OAAM,QAAQ;AAAA,EACjE;AACA,QAAM,OAAO,CAAC,MAAsB,KAAa,OAAoC;AACnF,eAAW,QAAQ,CAAC,GAAG,MAAM,EAAG,KAAI,CAAC,KAAK,KAAK,CAACA,SAAQA,KAAI,SAAS,IAAI,EAAG,QAAO,OAAO,IAAI;AAC9F,UAAM,QAAQ,QAAQ,IAAI;AAC1B,UAAM,UAAU,oBAAI,IAAY;AAChC,QAAI,MAAM,MAAM,IAAI,GAAG;AACvB,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,QAAiB;AAAA,MACrB,OAAO;AAAA,MACP,SAAS;AAAA,MACT,MAAM,IAAI,QAAQ,CAAC;AAAA,MACnB,GAAI,IAAI,QAAQ,SAAY,CAAC,IAAI,EAAE,KAAK,IAAI,IAAI;AAAA,MAChD,MAAM,OAAO,GAAG;AAAA,MAChB,QAAQ,OAAO,OAAO,KAAK,MAAM,EAAE;AAAA,IACrC;AACA,WAAO,OAAO,CAAC,QAAQ,IAAI,IAAI,GAAG,GAAG,MAAM,SAAS,OAAO,GAAG,GAAG;AAC/D,cAAQ,IAAI,IAAI,GAAG;AACnB,WAAK,OAAO,GAAG;AAAA,IACjB;AACA,WAAO;AAAA,EACT;AACA,SAAO;AAAA,IACL,QAAQ,SAAS,OAAO,KAAK;AAC3B,UAAI,QAAQ,OAAW,QAAO,QAAQ,SAAS,GAAG;AAClD,WAAK,IAAI,QAAQ,KAAK,KAAK;AAC3B,aAAO,IAAI,QAAQ,IAAI;AACvB,YAAM,QAAQ,MAAM,KAAK,OAAO,QAAQ,GAAG;AAC3C,cAAQ,OAAO,KAAK,OAAO,KAAK;AAAA,IAClC;AAAA,IACA,MAAM,QAAQ,KAAK;AACjB,UAAI,QAAQ,KAAM,QAAO;AACzB,YAAM,KAAK,OAAO,KAAK;AACvB,YAAM,OAAO,MAAM,UAAU,QAAQ,MAAM,KAAK;AAChD,UAAI,CAAC,KAAM,QAAO;AAClB,YAAM,OAAO,QAAQ,MAAM,EAAE;AAC7B,aAAO,KAAK,MAAM,KAAK,EAAE;AAAA,IAC3B;AAAA,EACF;AACF;",
|
|
6
6
|
"names": ["row"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { boundedPs, PS_MS } from "./jobs.js";
|
|
2
|
+
const TAINT_TAG = "slicc-taint:";
|
|
3
|
+
const CRON_TAGS = "/var/spool/cron/tags";
|
|
4
|
+
const TAGGED = /slicc-taint:[0-9a-f-]+/g;
|
|
5
|
+
function tagsOf(row) {
|
|
6
|
+
if (row.tags?.length) return [...row.tags];
|
|
7
|
+
return row.tag === void 0 ? [] : [row.tag];
|
|
8
|
+
}
|
|
9
|
+
const keyOf = (row) => `${row.pid}:${row.started ?? ""}`;
|
|
10
|
+
async function cronHeld(client) {
|
|
11
|
+
try {
|
|
12
|
+
return new Set((await client.fs.readText(CRON_TAGS)).match(TAGGED) ?? []);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return error.code === "ENOENT" ? /* @__PURE__ */ new Set() : null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function sightings(client, poll = PS_MS, warn = () => void 0) {
|
|
18
|
+
const entries = /* @__PURE__ */ new Map();
|
|
19
|
+
const first = /* @__PURE__ */ new Map();
|
|
20
|
+
let clock = 0;
|
|
21
|
+
let newest = -1;
|
|
22
|
+
let timer;
|
|
23
|
+
const mark = () => ++clock;
|
|
24
|
+
const alive = (tag, at) => {
|
|
25
|
+
const found = entries.get(tag) ?? { seen: at, gone: void 0 };
|
|
26
|
+
found.seen = Math.max(found.seen, at);
|
|
27
|
+
found.gone = void 0;
|
|
28
|
+
entries.set(tag, found);
|
|
29
|
+
};
|
|
30
|
+
const stamp = (rows, at) => {
|
|
31
|
+
const keys = new Set(rows.map(keyOf));
|
|
32
|
+
for (const key of keys) first.set(key, Math.min(first.get(key) ?? at, at));
|
|
33
|
+
if (at < newest) return;
|
|
34
|
+
newest = at;
|
|
35
|
+
for (const key of [...first.keys()]) if (!keys.has(key)) first.delete(key);
|
|
36
|
+
};
|
|
37
|
+
const observe = async (rows, at) => {
|
|
38
|
+
const held = await cronHeld(client);
|
|
39
|
+
const carried = new Set(
|
|
40
|
+
rows.flatMap((row) => tagsOf(row).filter((tag) => tag.startsWith(TAINT_TAG)))
|
|
41
|
+
);
|
|
42
|
+
for (const tag of [...carried, ...held ?? entries.keys()]) alive(tag, at);
|
|
43
|
+
for (const [tag, found] of entries)
|
|
44
|
+
if (!carried.has(tag) && held && !held.has(tag) && found.gone === void 0 && found.seen < at)
|
|
45
|
+
found.gone = at;
|
|
46
|
+
stamp(rows, at);
|
|
47
|
+
const waiting = [...entries].some(
|
|
48
|
+
([tag, found]) => found.gone === void 0 && !held?.has(tag)
|
|
49
|
+
);
|
|
50
|
+
if (waiting && held && timer === void 0) timer = setTimeout(tick, poll);
|
|
51
|
+
};
|
|
52
|
+
const fresh = async () => {
|
|
53
|
+
const at = mark();
|
|
54
|
+
const rows = await boundedPs(client, warn, poll);
|
|
55
|
+
if (rows) await observe(rows, at).catch(() => void 0);
|
|
56
|
+
};
|
|
57
|
+
function tick() {
|
|
58
|
+
timer = void 0;
|
|
59
|
+
void fresh();
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
mark,
|
|
63
|
+
spawned(process, tag) {
|
|
64
|
+
alive(tag, mark());
|
|
65
|
+
const ended = () => void fresh();
|
|
66
|
+
process.exited.then(ended, ended);
|
|
67
|
+
},
|
|
68
|
+
observe,
|
|
69
|
+
recent(pid, rows, at) {
|
|
70
|
+
const row = rows.find((candidate) => candidate.pid === pid);
|
|
71
|
+
const since = (row && first.get(keyOf(row))) ?? at;
|
|
72
|
+
return [...entries].filter(([, found]) => found.gone === void 0 || found.gone >= since).map(([tag]) => tag);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
CRON_TAGS,
|
|
78
|
+
TAINT_TAG,
|
|
79
|
+
cronHeld,
|
|
80
|
+
sightings,
|
|
81
|
+
tagsOf
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=sightings.js.map
|