@ai-ecoverse/slicc-agent 6.19.5 → 6.19.7
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 +3 -3
- package/dist/guests/guard.js +27 -2
- package/dist/guests/guard.js.map +2 -2
- package/dist/kernel/groups.js +5 -1
- package/dist/kernel/groups.js.map +2 -2
- package/dist/kernel/lineage.js +54 -0
- package/dist/kernel/lineage.js.map +7 -0
- package/dist/licks/licks.js +19 -3
- package/dist/licks/licks.js.map +2 -2
- package/dist/licks/sources.js +2 -1
- package/dist/licks/sources.js.map +2 -2
- package/dist/scoops/cli.js +66 -19
- package/dist/scoops/cli.js.map +2 -2
- package/dist/scoops/extension.js +44 -16
- package/dist/scoops/extension.js.map +2 -2
- package/dist/scoops/service.js +8 -6
- package/dist/scoops/service.js.map +2 -2
- package/dist/scoops/taints.js +2 -6
- package/dist/scoops/taints.js.map +2 -2
- package/dist/types/guests/guard.d.ts +5 -0
- package/dist/types/kernel/client.d.ts +2 -0
- package/dist/types/kernel/groups.d.ts +3 -1
- package/dist/types/kernel/lineage.d.ts +12 -0
- package/dist/types/licks/licks.d.ts +3 -0
- package/dist/types/licks/sources.d.ts +2 -0
- package/dist/types/scoops/cli.d.ts +2 -1
- package/dist/types/scoops/extension.d.ts +5 -1
- package/dist/types/scoops/service.d.ts +3 -2
- package/dist/types/scoops/taints.d.ts +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -231,7 +231,7 @@ gone /home/notes/old.md
|
|
|
231
231
|
# name: backup
|
|
232
232
|
@daily agent send scoop:ops --lick=cron
|
|
233
233
|
```
|
|
234
|
-
- **`agent send <target> --lick=cron`** delivers `<lick channel="cron">` instead of a message. It works only inside a kernel cron job: the kernel sets `SLICC_CRON_JOB`, `SLICC_CRON_DUE` and `SLICC_CRON_MISSED` (and `SLICC_CRON=1`) for every job, and without `SLICC_CRON_JOB` the flag is an error. The variables carry the run's metadata but don't choose the mode: a plain `agent send` in a job sends a message. The lick: source and title are the job's `# name:` (the kernel's hash of the line without one), the text says when it was due (`standup fired at 2026-10-17 11:00`), the message is the body, and `count` is 1 plus the missed runs (`standup was due at 2026-10-17 14:00; 2 more fires were missed while seven was closed`). Its id is `<job>@<due>`, so a run is delivered once. The `agent` script passes the three `SLICC_CRON_*` values in its request's `cron` field whenever `SLICC_CRON_JOB` is set (protocol `slicc-agent/2`; `slicc-agent/1` requests still parse). `--lick` and `--source` are argv.
|
|
234
|
+
- **`agent send <target> --lick=cron`** delivers `<lick channel="cron">` instead of a message. It works only inside a kernel cron job: the kernel sets `SLICC_CRON_JOB`, `SLICC_CRON_DUE` and `SLICC_CRON_MISSED` (and `SLICC_CRON=1`) for every job, and without `SLICC_CRON_JOB` the flag is an error. A request from a process that an agent's bash started, or that is in one of its process groups, is refused as well, whatever its `SLICC_CRON_*` values say. The variables carry the run's metadata but don't choose the mode: a plain `agent send` in a job sends a message. The lick: source and title are the job's `# name:` (the kernel's hash of the line without one), the text says when it was due (`standup fired at 2026-10-17 11:00`), the message is the body, and `count` is 1 plus the missed runs (`standup was due at 2026-10-17 14:00; 2 more fires were missed while seven was closed`). Its id is `<job>@<due>`, so a run is delivered once. The `agent` script passes the three `SLICC_CRON_*` values in its request's `cron` field whenever `SLICC_CRON_JOB` is set (protocol `slicc-agent/2`; `slicc-agent/1` requests still parse). `--lick` and `--source` are argv.
|
|
235
235
|
- **Which agent:** a job's environment is a login's, so `SLICC_AGENT` and the page's variables don't reach it. The `agent` script needs only `$USER`: it writes to `/var/lib/slicc/agent/requests/$USER/`, the folder of that kernel user, so the job's user decides who it is. In a cone's crontab, `agent send cone` wakes that cone and `agent send <handle>` one of its scoops (a scoop the cone doesn't have falls back to the cone); in root's, `cone` is the active cone; a scoop's own crontab can only wake the scoop itself. A frozen cone's job is skipped with the usual `warn` lick.
|
|
236
236
|
- **Missed runs, overlap, several tabs and logs** are the kernel's: missed runs fold into one, at most once per due time, one tab per storage root fires, and each run's output goes to `/var/log/cron/<user>/<name>.log`.
|
|
237
237
|
- **Moving `~/.slicc/crontab`:** the agent no longer schedules anything. At start and whenever the file changes, each `~/.slicc/crontab` (`<schedule> <name> [<target>] [<message>]` per line) is moved into the kernel crontab: its jobs become `# name: <name>` and `<schedule> agent send <target> --lick=cron '<message>'`, replacing jobs of the same names and keeping the rest of the table. A line aimed at a live scoop goes into that scoop's own user's crontab; the others go into the crontab of the file's owner (the cone's user, or root's for `/root/.slicc` and without kernel users). Then the file is kept as `~/.slicc/crontab.migrated` and removed. An empty or blank file is left where it is, so a file caught while it is being written is not moved before its lines arrive. A line that doesn't parse, or a table `crontab` refuses (it exits 1 and keeps the old table), becomes a `cron` lick with `severity="error"` naming the line and the problem, and the file stays until it's fixed. Durable `slicc.cron` tasks from earlier versions are aborted at start. Jobs an earlier version migrated without `--lick=cron` get it once per worker start: for each `~/.slicc/crontab.migrated`, a job under one of its `# name:` lines whose command is still exactly the `agent send` the migration wrote is rewritten in place, whatever its schedule; edited jobs and the rest of the table are left alone. A crontab that can't be read or written becomes a `cron` lick with `severity="error"` naming `~/.slicc/crontab.migrated`.
|
|
@@ -253,7 +253,7 @@ A **cone** is a top-level agent the user talks to; a **scoop** is a helper agent
|
|
|
253
253
|
|
|
254
254
|
Each scoop is owned by a background durable task, its **anchor**, so Stop in its cone doesn't end it; `agent stop` and Drop do. They keep its transcript in durable storage and its reports in `/scoops/<handle>/reports/`, which report licks point at, and remove its working folder `/scoops/<handle>/workspace`. Each request handed to a scoop gets a **reporter** task that waits for the run, writes the answer to `/scoops/<handle>/reports/<task id>.md` and reports it to whoever asked. Requests from a cone's or a scoop's bash report into that agent's `pi.inbox` as a lick, through the licks outbox, so they steer and coalesce like any lick; requests from a terminal report nowhere, and a user typing in a scoop's chat gets the answer there. In a scoop's chat, a request someone else sent shows who sent it (`UserMessage.from`): the asking cone's or scoop's name, with `origin: 'agent'` so spectrum (≥ 1.26.0) shows it as that agent's request with an "Agent" badge, or `terminal` without an origin. Once the request is placed, the reporter appends a `slicc.from` entry (`{ from, entry }`) without model messages, and the adapter sets `from` on that entry's message. A request withdrawn before it was placed leaves no entry. The report lick's text is `[scoop <name> (<role>) <status>]`, and its body has the report's path and line count and a 1,000-character preview. A scoop's prompt has a `scoop` section with its name, its cone, its folders and its limits. The `agent` command is taught by the built-in `agent` skill (below), whose description says that roles exist and that `agent list --agents` lists them.
|
|
255
255
|
|
|
256
|
-
**The `agent` command.** One bash script is installed under seven names, `agent`, `subagent`, `memory`, `gelatiere`, `freezer`, `models` and `panel`, in `/usr/local/bin`, mode `0755`, which is on every user's `PATH`, so the user, a cone or a scoop runs it in any bash. A file at one of those paths that isn't slicc-agent's (it lacks the `slicc-agent/1` request marker), such as a root global package's `memory`, is left alone, with one warning per boot in the worker's console. Root's `$PNPM_HOME/bin` (`/usr/local/share/pnpm/bin`) is on root's `PATH` only: other users find root's installed packages in `/usr/bin`, but not loose scripts. Copies that earlier versions put in the kernel's `$PNPM_HOME/bin` are removed, so they can't shadow the new ones for root. It writes a request (NUL-separated fields: `slicc-agent/1`, the name it was called by, `$SLICC_AGENT`, or `tab:$SLICC_TAB` from a terminal that has only that, the working directory, its pid, argc, the arguments and stdin) to `/var/lib/slicc/agent/requests/in/` and polls `out/` for the answer and exit code (with an optional `<id>.err`, the answer's stdout is `<id>.out` and its stderr `<id>.err`); the worker writes `<id>.ack` when it picks a request up, and the script gives up after 20 s without one ("the SLICC agent isn't running", exit 1). The kernel environment exports `SLICC_AGENT`, `PI_PROVIDER`, `PI_MODEL` and `PI_REASONING_LEVEL` into every agent command, so the worker knows the caller, and `PLAYWRIGHT_CLI_SESSION` with the same agent id, so each agent's `playwright-cli` keeps its own current tab (slicc-cdp #129). The user's terminals get none of these. `/var/lib/slicc/agent/scoops/<handle>/metadata.json` mirrors each scoop's state for scripts.
|
|
256
|
+
**The `agent` command.** One bash script is installed under seven names, `agent`, `subagent`, `memory`, `gelatiere`, `freezer`, `models` and `panel`, in `/usr/local/bin`, mode `0755`, which is on every user's `PATH`, so the user, a cone or a scoop runs it in any bash. A file at one of those paths that isn't slicc-agent's (it lacks the `slicc-agent/1` request marker), such as a root global package's `memory`, is left alone, with one warning per boot in the worker's console. Root's `$PNPM_HOME/bin` (`/usr/local/share/pnpm/bin`) is on root's `PATH` only: other users find root's installed packages in `/usr/bin`, but not loose scripts. Copies that earlier versions put in the kernel's `$PNPM_HOME/bin` are removed, so they can't shadow the new ones for root. It writes a request (NUL-separated fields: `slicc-agent/1`, the name it was called by, `$SLICC_AGENT`, or `tab:$SLICC_TAB` from a terminal that has only that, the working directory, its pid, argc, the arguments and stdin) to `/var/lib/slicc/agent/requests/in/` and polls `out/` for the answer and exit code (with an optional `<id>.err`, the answer's stdout is `<id>.out` and its stderr `<id>.err`); the worker writes `<id>.ack` when it picks a request up, and the script gives up after 20 s without one ("the SLICC agent isn't running", exit 1). The kernel environment exports `SLICC_AGENT`, `PI_PROVIDER`, `PI_MODEL` and `PI_REASONING_LEVEL` into every agent command, so the worker knows the caller, and `PLAYWRIGHT_CLI_SESSION` with the same agent id, so each agent's `playwright-cli` keeps its own current tab (slicc-cdp #129). The user's terminals get none of these. `SLICC_AGENT` is checked against the process tree: the worker reads `ps`, walks up from the request's pid to the nearest bash an agent's kernel environment started, and refuses a request whose `SLICC_AGENT` names another agent, or names one when no agent's bash is above it; with kernel users, a user whose processes belong to another agent is refused too. The pid in the request is the script's own `$$`, so it is checked first: its `ps` row has to be the command it was called as (`/…/agent`, `subagent`, …) followed by exactly the request's arguments, and with kernel users it has to run as the spool's user (by `/etc/passwd`). A request whose pid isn't in the table or fails these checks, or a `ps` read that fails or stalls, is refused. A caller with no agent above it and no `SLICC_AGENT` is the user. `/var/lib/slicc/agent/scoops/<handle>/metadata.json` mirrors each scoop's state for scripts.
|
|
257
257
|
- `agent [options] --prompt <text>` (or `--file <path>`, or `-` for stdin, or v6's `agent <cwd> <allowed-commands> <prompt>`) is **synchronous**: an ephemeral scoop runs, and the command prints its final answer and exits 0, or prints the reason on stderr and exits 1. Afterwards the scoop is stopped, its folder removed, its conversation marked dropped and its transcript archived as Markdown in `/tmp/agent-sessions/<handle>.md` (`/home/sessions/` with `--persist-session`, nowhere with `--no-persist-session`). The script traps TERM, INT and HUP and cancels the scoop, and the worker checks the script's pid in the kernel's process table every second for a SIGKILL. If the caller's run is still going when the bash call dies (a bash timeout), the scoop continues and its answer arrives as a `bash` lick; `--background-after <seconds>` does the same on purpose. Sync calls nest up to three deep. Allowed commands in v6's form are told to the scoop, not enforced.
|
|
258
258
|
- `agent --async …` (= `subagent spawn …`) starts a persistent scoop and prints its handle; only cones start them. `list [--agents]`, `status`, `rename`, `send [--follow-up]`, `wait [--timeout <s>] [--notify]` (30 s by default; `--notify` returns at once and brings the answers together in one `scoop-wait` lick) and `stop` manage them. From a scoop, `agent send parent "<note>"` posts a progress note to its cone as a lick, coalesced per scoop.
|
|
259
259
|
- Options: `--name`, `--agent <role>`, `--model <provider/model>`, `--thinking <level>` or `--effort low|medium|high|max`, `--tools <a,b>` (or `auto`/`full`), `--read-only <paths>`, `--system-prompt[-file]`. `--schema-b64`, `--tools output`, `--session`, `--resume`, `--image`, `--no-escalate`, `--minimal` and `--usage` answer "not supported in SLICC yet".
|
|
@@ -358,7 +358,7 @@ A **workflow** is a Claude Code dynamic workflow script, a `.workflow.js`, run a
|
|
|
358
358
|
- **Stop.** `agent workflow stop` aborts the task, which stops every scoop the run has running (each is listed with its creation) and removes their folders.
|
|
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
|
-
- **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.
|
|
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
362
|
- **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
363
|
- **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
364
|
- **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.
|
package/dist/guests/guard.js
CHANGED
|
@@ -12,6 +12,7 @@ import { gateKind, gateRefusal } from "./policy.js";
|
|
|
12
12
|
import { GuestsDoc, isGate, liveSeat } from "./seats.js";
|
|
13
13
|
const GUEST_GUARD = "slicc-guest-guard";
|
|
14
14
|
const GUEST_REQUEST = "guest:";
|
|
15
|
+
const LICK_REQUEST = "lick:";
|
|
15
16
|
const GUEST_TOOL_MEMO = "slicc.guest-tool";
|
|
16
17
|
const SUMMARY_LENGTH = 2e3;
|
|
17
18
|
const GATE_REASONS = {
|
|
@@ -109,15 +110,34 @@ const GuestTaintDoc = defineDoc({
|
|
|
109
110
|
fork: "asOf",
|
|
110
111
|
initial: () => ({ taints: [] })
|
|
111
112
|
});
|
|
113
|
+
const LickTaintDoc = defineDoc({
|
|
114
|
+
kind: "slicc.lick-taint",
|
|
115
|
+
version: 1,
|
|
116
|
+
scope: "session",
|
|
117
|
+
initial: () => ({ licks: {} })
|
|
118
|
+
});
|
|
119
|
+
async function lickTaints(inputs, licks, harness, context) {
|
|
120
|
+
if (!Object.keys(licks).length) return [];
|
|
121
|
+
const found = [];
|
|
122
|
+
for (const id of inputs) {
|
|
123
|
+
const record = await (await harness.submission(id, context))?.status(context);
|
|
124
|
+
const request = record?.requestId ?? "";
|
|
125
|
+
if (request.startsWith(LICK_REQUEST))
|
|
126
|
+
found.push(...licks[request.slice(LICK_REQUEST.length)] ?? []);
|
|
127
|
+
}
|
|
128
|
+
return distinctTaints(found);
|
|
129
|
+
}
|
|
112
130
|
async function runTaints(harness, conversationId, context) {
|
|
113
131
|
const live = await harness.snapshot(LiveDoc, conversationId, context);
|
|
114
132
|
const inputs = live?.run?.inputs ?? [];
|
|
115
133
|
const inherited = (await harness.snapshot(GuestTaintDoc, conversationId, context))?.taints ?? [];
|
|
116
134
|
const seats = (await harness.snapshot(GuestsDoc, context))?.seats ?? {};
|
|
117
135
|
const found = inputs.length ? await taintsOf(inputs, seats, harness, context) : [];
|
|
136
|
+
const licks = (await harness.snapshot(LickTaintDoc, context))?.licks ?? {};
|
|
118
137
|
return distinctTaints([
|
|
119
138
|
...found.map((taint) => ({ seat: taint.seat?.id ?? null, tier: taint.tier })),
|
|
120
|
-
...inherited
|
|
139
|
+
...inherited,
|
|
140
|
+
...await lickTaints(inputs, licks, harness, context)
|
|
121
141
|
]);
|
|
122
142
|
}
|
|
123
143
|
function distinctTaints(taints) {
|
|
@@ -249,7 +269,9 @@ function guestToolGate(harness) {
|
|
|
249
269
|
const seats = (await api.snapshot(GuestsDoc, context))?.seats ?? {};
|
|
250
270
|
const held = harness();
|
|
251
271
|
const found = inputs.length ? await taintsOf(inputs, seats, held, context) : [];
|
|
252
|
-
|
|
272
|
+
const licks = (await api.snapshot(LickTaintDoc, context))?.licks ?? {};
|
|
273
|
+
const licked = await lickTaints(inputs, licks, held, context);
|
|
274
|
+
for (const taint of [...found, ...inheritedTaints([...inherited, ...licked], seats)]) {
|
|
253
275
|
const block = await review(taint, call, api, context);
|
|
254
276
|
if (block) return { block };
|
|
255
277
|
}
|
|
@@ -272,11 +294,14 @@ export {
|
|
|
272
294
|
GUEST_REQUEST,
|
|
273
295
|
GUEST_TOOL_MEMO,
|
|
274
296
|
GuestTaintDoc,
|
|
297
|
+
LICK_REQUEST,
|
|
298
|
+
LickTaintDoc,
|
|
275
299
|
SUMMARY_LENGTH,
|
|
276
300
|
argumentsHash,
|
|
277
301
|
canonical,
|
|
278
302
|
distinctTaints,
|
|
279
303
|
guestToolGate,
|
|
304
|
+
lickTaints,
|
|
280
305
|
reasonOf,
|
|
281
306
|
runTaints,
|
|
282
307
|
scrubbedArguments,
|
package/dist/guests/guard.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/guests/guard.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { awaitWithContext, BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport {\n type ConversationId,\n defineDoc,\n defineExtension,\n type Extension,\n type Harness,\n type HookApi,\n hook,\n LiveDoc,\n type SubmissionId,\n type TaskId,\n type ToolHookCall,\n ToolTask,\n} from '@earendil-works/pi-durable';\nimport type { Approval, Approvals } from '../approvals/index.ts';\nimport { REASONS, scoopTier } from '../approvals/record.ts';\nimport { scrubText, scrubTexts } from '../secrets/scrub.ts';\nimport { type GuestPolicy, gateKind, gateRefusal } from './policy.ts';\nimport { type AlwaysTool, GuestsDoc, isGate, liveSeat, type SeatRecord } from './seats.ts';\n\nexport const GUEST_GUARD = 'slicc-guest-guard';\nexport const GUEST_REQUEST = 'guest:';\nexport const GUEST_TOOL_MEMO = 'slicc.guest-tool';\nexport const SUMMARY_LENGTH = 2000;\n\nexport const GATE_REASONS = {\n unknown:\n 'A guest started this run, and its seat names no review for tool calls that this agent knows.',\n ended: \"A guest started this run, and that guest's seat has ended.\",\n rejected: 'The owner declined this tool call in a run a guest started.',\n judged: 'The automatic check declined this tool call in a run a guest started',\n unanswered: 'Nobody approved this tool call in a run a guest started in time.',\n unroutable: \"The cone can't review a tool call it is itself waiting on.\",\n withdrawn: 'The review of this tool call was withdrawn.',\n} as const;\n\nexport interface GateHost {\n approvals: Pick<Approvals, 'state' | 'request' | 'settled' | 'withdraw'>;\n policy(): GuestPolicy;\n now?: () => number;\n}\n\nexport interface GuestToolGate {\n readonly extension: Extension;\n bind(host: GateHost): void;\n}\n\ntype Held = Pick<Harness, 'submission' | 'commit' | 'waitForTask'>;\n\ntype Taint = { seat: SeatRecord | null; tier: string };\n\nexport function canonical(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;\n if (value && typeof value === 'object')\n return `{${Object.keys(value)\n .sort()\n .map((key) => `${JSON.stringify(key)}:${canonical((value as Record<string, unknown>)[key])}`)\n .join(',')}}`;\n return JSON.stringify(value ?? null);\n}\n\nfunction leaves(value: unknown, found: string[]): void {\n if (typeof value === 'string') found.push(value);\n else if (value && typeof value === 'object')\n for (const item of Object.values(value)) leaves(item, found);\n}\n\nfunction replaced(value: unknown, texts: string[]): unknown {\n if (typeof value === 'string') return texts.shift();\n if (Array.isArray(value)) return value.map((item) => replaced(item, texts));\n if (value && typeof value === 'object')\n return Object.fromEntries(\n Object.entries(value).map(([key, item]) => [key, replaced(item, texts)])\n );\n return value;\n}\n\nexport async function scrubbedArguments(\n args: Record<string, unknown>\n): Promise<Record<string, unknown>> {\n const found: string[] = [];\n leaves(args, found);\n return replaced(args, await scrubTexts(found)) as Record<string, unknown>;\n}\n\nexport async function argumentsHash(args: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(args));\n return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\nasync function shownArguments(text: string): Promise<string> {\n try {\n return canonical(await scrubbedArguments(JSON.parse(text)));\n } catch {\n return scrubText(text);\n }\n}\n\nexport async function sealedRules(rules: readonly AlwaysTool[]): Promise<AlwaysTool[] | null> {\n if (rules.every((rule) => rule.argumentsHash)) return null;\n return Promise.all(\n rules.map(async (rule) =>\n rule.argumentsHash\n ? rule\n : {\n ...rule,\n argumentsHash: await argumentsHash(rule.arguments),\n arguments: await shownArguments(rule.arguments),\n }\n )\n );\n}\n\nexport function summaryOf(args: Record<string, unknown>): string {\n const values = Object.values(args);\n const text = values.length === 1 && typeof values[0] === 'string' ? values[0] : canonical(args);\n return text.length > SUMMARY_LENGTH ? `${text.slice(0, SUMMARY_LENGTH - 1)}\u2026` : text;\n}\n\nconst SCOOP_SAID: Record<string, (name: string) => string> = {\n [REASONS.noScoop]: (name) => `there is no scoop ${name} in this cone to review it.`,\n [REASONS.scoopStopped]: (name) => `the scoop ${name} is stopped, so it can't review it.`,\n [REASONS.scoopAsked]: (name) =>\n `the scoop ${name} can't review a call that asks something of it.`,\n [REASONS.scoopUndecided]: (name) => `the scoop ${name} ended its review without deciding.`,\n};\n\nfunction scoopReason(item: Approval): string | null {\n const name = item.tier.slice('scoop:'.length);\n if (item.decidedBy?.startsWith('scoop:'))\n return `The scoop ${name} declined this tool call in a run a guest started${item.reason ? `: ${item.reason}` : '.'}`;\n const said = SCOOP_SAID[item.reason ?? ''];\n return said ? `A guest started this run, and ${said(name)}` : null;\n}\n\nexport function reasonOf(item: Approval): string {\n const scooped = item.state === 'rejected' && scoopTier(item.tier) ? scoopReason(item) : null;\n if (scooped) return scooped;\n if (item.state === 'rejected' && item.decidedBy === 'judge')\n return item.verdict\n ? `${GATE_REASONS.judged}: ${item.verdict.reason}`\n : `${GATE_REASONS.judged}.`;\n if (item.state === 'rejected')\n return item.reason && item.reason !== REASONS.unknownAnswer\n ? `${GATE_REASONS.rejected} ${item.reason}`\n : GATE_REASONS.rejected;\n if (item.state === 'withdrawn') return GATE_REASONS.withdrawn;\n if (item.reason === REASONS.unknownTier) return GATE_REASONS.unknown;\n if (item.reason === REASONS.unroutable) return GATE_REASONS.unroutable;\n return GATE_REASONS.unanswered;\n}\n\nexport type InheritedTaint = { seat: string | null; tier: string };\n\nexport const GuestTaintDoc = defineDoc<{ taints: InheritedTaint[] }>({\n kind: 'slicc.guest-taint',\n version: 1,\n scope: 'conversation',\n history: 'rewindable',\n fork: 'asOf',\n initial: () => ({ taints: [] }),\n});\n\ntype TaintReader = Pick<Harness, 'submission' | 'snapshot'>;\n\nexport async function runTaints(\n harness: TaintReader,\n conversationId: ConversationId,\n context: Context\n): Promise<InheritedTaint[]> {\n const live = await harness.snapshot(LiveDoc, conversationId, context);\n const inputs = live?.run?.inputs ?? [];\n const inherited = (await harness.snapshot(GuestTaintDoc, conversationId, context))?.taints ?? [];\n const seats = (await harness.snapshot(GuestsDoc, context))?.seats ?? {};\n const found = inputs.length ? await taintsOf(inputs, seats, harness, context) : [];\n return distinctTaints([\n ...found.map((taint) => ({ seat: taint.seat?.id ?? null, tier: taint.tier })),\n ...inherited,\n ]);\n}\n\nexport function distinctTaints(taints: readonly InheritedTaint[]): InheritedTaint[] {\n const kept = new Map<string, InheritedTaint>();\n for (const taint of taints) kept.set(`${taint.seat}\\n${taint.tier}`, taint);\n return [...kept.values()];\n}\n\nfunction inheritedTaints(\n inherited: readonly InheritedTaint[],\n seats: Readonly<Record<string, SeatRecord>>\n): Taint[] {\n return inherited.map((taint) => ({\n seat: taint.seat === null ? null : (seats[taint.seat] ?? null),\n tier: taint.tier,\n }));\n}\n\nexport async function taintsOf(\n inputs: readonly SubmissionId[],\n seats: Readonly<Record<string, SeatRecord>>,\n harness: Pick<Harness, 'submission'>,\n context: Context\n): Promise<Taint[]> {\n const found = new Map<string, Taint>();\n for (const id of inputs) {\n const seat = Object.values(seats).find((record) =>\n record.submissions?.some((entry) => entry.id === String(id))\n );\n const tier = seat?.submissions?.find((entry) => entry.id === String(id))?.tools;\n if (seat) {\n found.set(`${seat.id}\\n${tier}`, { seat, tier: String(tier) });\n continue;\n }\n const submission = await harness.submission(id, context);\n const record = submission ? await submission.status(context) : undefined;\n if (record?.requestId?.startsWith(GUEST_REQUEST)) return [{ seat: null, tier: '' }];\n }\n return [...found.values()];\n}\n\nfunction settledEarly(\n seat: SeatRecord,\n tier: string,\n policy: GuestPolicy,\n at: number,\n tool: string,\n hash: string\n): string | null {\n if (!policy.allowedToolGates.includes(gateKind(tier)))\n return gateRefusal(\"a guest's tool calls\", policy.allowedToolGates);\n if (!liveSeat(seat, at)) return GATE_REASONS.ended;\n if (tier === 'off') return '';\n const kept = seat.alwaysTools?.some((rule) => rule.tool === tool && rule.argumentsHash === hash);\n return policy.always.tools && kept ? '' : null;\n}\n\nconst ended = (status: string) => status === 'terminal' || status === 'completing';\n\nexport function guestToolGate(harness: () => Held | undefined): GuestToolGate {\n let resolveHost: (host: GateHost) => void = () => {};\n const bound = new Promise<GateHost>((resolve) => {\n resolveHost = resolve;\n });\n const watched = new Set<string>();\n const watch = (host: GateHost, id: string, task: number) => {\n const held = harness();\n if (!held || watched.has(id)) return;\n watched.add(id);\n void held\n .waitForTask(task as unknown as TaskId, BACKGROUND_CONTEXT)\n .then((record) =>\n ended(record.state.status)\n ? host.approvals.withdraw(id, REASONS.aborted, BACKGROUND_CONTEXT)\n : undefined\n )\n .catch(() => undefined)\n .finally(() => watched.delete(id));\n };\n const remember = (seat: string, rule: AlwaysTool) =>\n (harness() as Held).commit(async (tx) => {\n const record = (await tx.doc(GuestsDoc)).seats[seat];\n if (!record || record.revoked !== null) return;\n const kept = (record.alwaysTools ?? []).filter(\n (entry) => entry.tool !== rule.tool || entry.argumentsHash !== rule.argumentsHash\n );\n record.alwaysTools = [...kept, rule];\n }, BACKGROUND_CONTEXT);\n const review = async (\n taint: Taint,\n call: ToolHookCall,\n api: HookApi,\n context: Context\n ): Promise<string | undefined> => {\n const { seat } = taint;\n if (seat === null || !isGate(taint.tier)) return GATE_REASONS.unknown;\n const host = await awaitWithContext(bound, context);\n const policy = host.policy();\n const now = host.now ?? Date.now;\n const hash = await argumentsHash(canonical(call.arguments));\n const always = policy.always.tools;\n const early = settledEarly(seat, taint.tier, policy, now(), call.name, hash);\n if (early !== null) return early || undefined;\n const id = await api.memo<string>(\n `${GUEST_TOOL_MEMO}:${seat.id}:${taint.tier}`,\n `apr-${crypto.randomUUID()}`,\n context\n );\n const task = Number(api.taskId);\n const shown = await scrubbedArguments(call.arguments);\n const asked = await host.approvals.request(\n {\n id,\n kind: 'guest-tool',\n seat: seat.id,\n agentId: seat.agentId,\n requester: { kind: 'guest', name: seat.label, seat: seat.id },\n tier: taint.tier,\n detail: {\n tool: call.name,\n summary: summaryOf(shown),\n arguments: canonical(shown),\n task,\n },\n },\n BACKGROUND_CONTEXT\n );\n watch(host, id, task);\n const live = async () =>\n liveSeat((await api.snapshot(GuestsDoc, context))?.seats[seat.id], now());\n if (!(await live())) {\n await host.approvals.withdraw(id, REASONS.revoked, BACKGROUND_CONTEXT);\n return GATE_REASONS.ended;\n }\n const item = asked.state === 'pending' ? await host.approvals.settled(id, context) : asked;\n if (item.state !== 'approved') return reasonOf(item);\n if (!(await live())) return GATE_REASONS.ended;\n if (item.answer === 'always' && always)\n await remember(seat.id, {\n approval: item.id,\n tool: call.name,\n argumentsHash: hash,\n arguments: canonical(shown),\n by: item.decidedBy,\n at: item.decidedAt ?? now(),\n });\n return undefined;\n };\n return {\n extension: defineExtension({\n name: GUEST_GUARD,\n hooks: [\n hook(ToolTask, {\n async beforeTool(call, api, context) {\n const live = await api.snapshot(LiveDoc, api.conversationId, context);\n const inputs = live?.run?.inputs ?? [];\n const inherited =\n (await api.snapshot(GuestTaintDoc, api.conversationId, context))?.taints ?? [];\n if (!inputs.length && !inherited.length) return undefined;\n const seats = (await api.snapshot(GuestsDoc, context))?.seats ?? {};\n const held = harness() as Held;\n const found = inputs.length ? await taintsOf(inputs, seats, held, context) : [];\n for (const taint of [...found, ...inheritedTaints(inherited, seats)]) {\n const block = await review(taint, call, api, context);\n if (block) return { block };\n }\n return undefined;\n },\n }),\n ],\n }),\n bind(host) {\n resolveHost(host);\n for (const item of host.approvals.state.value ?? [])\n if (item.kind === 'guest-tool' && item.state === 'pending' && item.detail.task)\n watch(host, item.id, item.detail.task);\n },\n };\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { awaitWithContext, BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport {\n type ConversationId,\n defineDoc,\n defineExtension,\n type Extension,\n type Harness,\n type HookApi,\n hook,\n LiveDoc,\n type SubmissionId,\n type TaskId,\n type ToolHookCall,\n ToolTask,\n} from '@earendil-works/pi-durable';\nimport type { Approval, Approvals } from '../approvals/index.ts';\nimport { REASONS, scoopTier } from '../approvals/record.ts';\nimport { scrubText, scrubTexts } from '../secrets/scrub.ts';\nimport { type GuestPolicy, gateKind, gateRefusal } from './policy.ts';\nimport { type AlwaysTool, GuestsDoc, isGate, liveSeat, type SeatRecord } from './seats.ts';\n\nexport const GUEST_GUARD = 'slicc-guest-guard';\nexport const GUEST_REQUEST = 'guest:';\nexport const LICK_REQUEST = 'lick:';\nexport const GUEST_TOOL_MEMO = 'slicc.guest-tool';\nexport const SUMMARY_LENGTH = 2000;\n\nexport const GATE_REASONS = {\n unknown:\n 'A guest started this run, and its seat names no review for tool calls that this agent knows.',\n ended: \"A guest started this run, and that guest's seat has ended.\",\n rejected: 'The owner declined this tool call in a run a guest started.',\n judged: 'The automatic check declined this tool call in a run a guest started',\n unanswered: 'Nobody approved this tool call in a run a guest started in time.',\n unroutable: \"The cone can't review a tool call it is itself waiting on.\",\n withdrawn: 'The review of this tool call was withdrawn.',\n} as const;\n\nexport interface GateHost {\n approvals: Pick<Approvals, 'state' | 'request' | 'settled' | 'withdraw'>;\n policy(): GuestPolicy;\n now?: () => number;\n}\n\nexport interface GuestToolGate {\n readonly extension: Extension;\n bind(host: GateHost): void;\n}\n\ntype Held = Pick<Harness, 'submission' | 'commit' | 'waitForTask'>;\n\ntype Taint = { seat: SeatRecord | null; tier: string };\n\nexport function canonical(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;\n if (value && typeof value === 'object')\n return `{${Object.keys(value)\n .sort()\n .map((key) => `${JSON.stringify(key)}:${canonical((value as Record<string, unknown>)[key])}`)\n .join(',')}}`;\n return JSON.stringify(value ?? null);\n}\n\nfunction leaves(value: unknown, found: string[]): void {\n if (typeof value === 'string') found.push(value);\n else if (value && typeof value === 'object')\n for (const item of Object.values(value)) leaves(item, found);\n}\n\nfunction replaced(value: unknown, texts: string[]): unknown {\n if (typeof value === 'string') return texts.shift();\n if (Array.isArray(value)) return value.map((item) => replaced(item, texts));\n if (value && typeof value === 'object')\n return Object.fromEntries(\n Object.entries(value).map(([key, item]) => [key, replaced(item, texts)])\n );\n return value;\n}\n\nexport async function scrubbedArguments(\n args: Record<string, unknown>\n): Promise<Record<string, unknown>> {\n const found: string[] = [];\n leaves(args, found);\n return replaced(args, await scrubTexts(found)) as Record<string, unknown>;\n}\n\nexport async function argumentsHash(args: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(args));\n return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\nasync function shownArguments(text: string): Promise<string> {\n try {\n return canonical(await scrubbedArguments(JSON.parse(text)));\n } catch {\n return scrubText(text);\n }\n}\n\nexport async function sealedRules(rules: readonly AlwaysTool[]): Promise<AlwaysTool[] | null> {\n if (rules.every((rule) => rule.argumentsHash)) return null;\n return Promise.all(\n rules.map(async (rule) =>\n rule.argumentsHash\n ? rule\n : {\n ...rule,\n argumentsHash: await argumentsHash(rule.arguments),\n arguments: await shownArguments(rule.arguments),\n }\n )\n );\n}\n\nexport function summaryOf(args: Record<string, unknown>): string {\n const values = Object.values(args);\n const text = values.length === 1 && typeof values[0] === 'string' ? values[0] : canonical(args);\n return text.length > SUMMARY_LENGTH ? `${text.slice(0, SUMMARY_LENGTH - 1)}\u2026` : text;\n}\n\nconst SCOOP_SAID: Record<string, (name: string) => string> = {\n [REASONS.noScoop]: (name) => `there is no scoop ${name} in this cone to review it.`,\n [REASONS.scoopStopped]: (name) => `the scoop ${name} is stopped, so it can't review it.`,\n [REASONS.scoopAsked]: (name) =>\n `the scoop ${name} can't review a call that asks something of it.`,\n [REASONS.scoopUndecided]: (name) => `the scoop ${name} ended its review without deciding.`,\n};\n\nfunction scoopReason(item: Approval): string | null {\n const name = item.tier.slice('scoop:'.length);\n if (item.decidedBy?.startsWith('scoop:'))\n return `The scoop ${name} declined this tool call in a run a guest started${item.reason ? `: ${item.reason}` : '.'}`;\n const said = SCOOP_SAID[item.reason ?? ''];\n return said ? `A guest started this run, and ${said(name)}` : null;\n}\n\nexport function reasonOf(item: Approval): string {\n const scooped = item.state === 'rejected' && scoopTier(item.tier) ? scoopReason(item) : null;\n if (scooped) return scooped;\n if (item.state === 'rejected' && item.decidedBy === 'judge')\n return item.verdict\n ? `${GATE_REASONS.judged}: ${item.verdict.reason}`\n : `${GATE_REASONS.judged}.`;\n if (item.state === 'rejected')\n return item.reason && item.reason !== REASONS.unknownAnswer\n ? `${GATE_REASONS.rejected} ${item.reason}`\n : GATE_REASONS.rejected;\n if (item.state === 'withdrawn') return GATE_REASONS.withdrawn;\n if (item.reason === REASONS.unknownTier) return GATE_REASONS.unknown;\n if (item.reason === REASONS.unroutable) return GATE_REASONS.unroutable;\n return GATE_REASONS.unanswered;\n}\n\nexport type InheritedTaint = { seat: string | null; tier: string };\n\nexport const GuestTaintDoc = defineDoc<{ taints: InheritedTaint[] }>({\n kind: 'slicc.guest-taint',\n version: 1,\n scope: 'conversation',\n history: 'rewindable',\n fork: 'asOf',\n initial: () => ({ taints: [] }),\n});\n\nexport const LickTaintDoc = defineDoc<{ licks: Record<string, InheritedTaint[]> }>({\n kind: 'slicc.lick-taint',\n version: 1,\n scope: 'session',\n initial: () => ({ licks: {} }),\n});\n\ntype TaintReader = Pick<Harness, 'submission' | 'snapshot'>;\n\nexport async function lickTaints(\n inputs: readonly SubmissionId[],\n licks: Readonly<Record<string, InheritedTaint[]>>,\n harness: Pick<Harness, 'submission'>,\n context: Context\n): Promise<InheritedTaint[]> {\n if (!Object.keys(licks).length) return [];\n const found: InheritedTaint[] = [];\n for (const id of inputs) {\n const record = await (await harness.submission(id, context))?.status(context);\n const request = record?.requestId ?? '';\n if (request.startsWith(LICK_REQUEST))\n found.push(...(licks[request.slice(LICK_REQUEST.length)] ?? []));\n }\n return distinctTaints(found);\n}\n\nexport async function runTaints(\n harness: TaintReader,\n conversationId: ConversationId,\n context: Context\n): Promise<InheritedTaint[]> {\n const live = await harness.snapshot(LiveDoc, conversationId, context);\n const inputs = live?.run?.inputs ?? [];\n const inherited = (await harness.snapshot(GuestTaintDoc, conversationId, context))?.taints ?? [];\n const seats = (await harness.snapshot(GuestsDoc, context))?.seats ?? {};\n const found = inputs.length ? await taintsOf(inputs, seats, harness, context) : [];\n const licks = (await harness.snapshot(LickTaintDoc, context))?.licks ?? {};\n return distinctTaints([\n ...found.map((taint) => ({ seat: taint.seat?.id ?? null, tier: taint.tier })),\n ...inherited,\n ...(await lickTaints(inputs, licks, harness, context)),\n ]);\n}\n\nexport function distinctTaints(taints: readonly InheritedTaint[]): InheritedTaint[] {\n const kept = new Map<string, InheritedTaint>();\n for (const taint of taints) kept.set(`${taint.seat}\\n${taint.tier}`, taint);\n return [...kept.values()];\n}\n\nfunction inheritedTaints(\n inherited: readonly InheritedTaint[],\n seats: Readonly<Record<string, SeatRecord>>\n): Taint[] {\n return inherited.map((taint) => ({\n seat: taint.seat === null ? null : (seats[taint.seat] ?? null),\n tier: taint.tier,\n }));\n}\n\nexport async function taintsOf(\n inputs: readonly SubmissionId[],\n seats: Readonly<Record<string, SeatRecord>>,\n harness: Pick<Harness, 'submission'>,\n context: Context\n): Promise<Taint[]> {\n const found = new Map<string, Taint>();\n for (const id of inputs) {\n const seat = Object.values(seats).find((record) =>\n record.submissions?.some((entry) => entry.id === String(id))\n );\n const tier = seat?.submissions?.find((entry) => entry.id === String(id))?.tools;\n if (seat) {\n found.set(`${seat.id}\\n${tier}`, { seat, tier: String(tier) });\n continue;\n }\n const submission = await harness.submission(id, context);\n const record = submission ? await submission.status(context) : undefined;\n if (record?.requestId?.startsWith(GUEST_REQUEST)) return [{ seat: null, tier: '' }];\n }\n return [...found.values()];\n}\n\nfunction settledEarly(\n seat: SeatRecord,\n tier: string,\n policy: GuestPolicy,\n at: number,\n tool: string,\n hash: string\n): string | null {\n if (!policy.allowedToolGates.includes(gateKind(tier)))\n return gateRefusal(\"a guest's tool calls\", policy.allowedToolGates);\n if (!liveSeat(seat, at)) return GATE_REASONS.ended;\n if (tier === 'off') return '';\n const kept = seat.alwaysTools?.some((rule) => rule.tool === tool && rule.argumentsHash === hash);\n return policy.always.tools && kept ? '' : null;\n}\n\nconst ended = (status: string) => status === 'terminal' || status === 'completing';\n\nexport function guestToolGate(harness: () => Held | undefined): GuestToolGate {\n let resolveHost: (host: GateHost) => void = () => {};\n const bound = new Promise<GateHost>((resolve) => {\n resolveHost = resolve;\n });\n const watched = new Set<string>();\n const watch = (host: GateHost, id: string, task: number) => {\n const held = harness();\n if (!held || watched.has(id)) return;\n watched.add(id);\n void held\n .waitForTask(task as unknown as TaskId, BACKGROUND_CONTEXT)\n .then((record) =>\n ended(record.state.status)\n ? host.approvals.withdraw(id, REASONS.aborted, BACKGROUND_CONTEXT)\n : undefined\n )\n .catch(() => undefined)\n .finally(() => watched.delete(id));\n };\n const remember = (seat: string, rule: AlwaysTool) =>\n (harness() as Held).commit(async (tx) => {\n const record = (await tx.doc(GuestsDoc)).seats[seat];\n if (!record || record.revoked !== null) return;\n const kept = (record.alwaysTools ?? []).filter(\n (entry) => entry.tool !== rule.tool || entry.argumentsHash !== rule.argumentsHash\n );\n record.alwaysTools = [...kept, rule];\n }, BACKGROUND_CONTEXT);\n const review = async (\n taint: Taint,\n call: ToolHookCall,\n api: HookApi,\n context: Context\n ): Promise<string | undefined> => {\n const { seat } = taint;\n if (seat === null || !isGate(taint.tier)) return GATE_REASONS.unknown;\n const host = await awaitWithContext(bound, context);\n const policy = host.policy();\n const now = host.now ?? Date.now;\n const hash = await argumentsHash(canonical(call.arguments));\n const always = policy.always.tools;\n const early = settledEarly(seat, taint.tier, policy, now(), call.name, hash);\n if (early !== null) return early || undefined;\n const id = await api.memo<string>(\n `${GUEST_TOOL_MEMO}:${seat.id}:${taint.tier}`,\n `apr-${crypto.randomUUID()}`,\n context\n );\n const task = Number(api.taskId);\n const shown = await scrubbedArguments(call.arguments);\n const asked = await host.approvals.request(\n {\n id,\n kind: 'guest-tool',\n seat: seat.id,\n agentId: seat.agentId,\n requester: { kind: 'guest', name: seat.label, seat: seat.id },\n tier: taint.tier,\n detail: {\n tool: call.name,\n summary: summaryOf(shown),\n arguments: canonical(shown),\n task,\n },\n },\n BACKGROUND_CONTEXT\n );\n watch(host, id, task);\n const live = async () =>\n liveSeat((await api.snapshot(GuestsDoc, context))?.seats[seat.id], now());\n if (!(await live())) {\n await host.approvals.withdraw(id, REASONS.revoked, BACKGROUND_CONTEXT);\n return GATE_REASONS.ended;\n }\n const item = asked.state === 'pending' ? await host.approvals.settled(id, context) : asked;\n if (item.state !== 'approved') return reasonOf(item);\n if (!(await live())) return GATE_REASONS.ended;\n if (item.answer === 'always' && always)\n await remember(seat.id, {\n approval: item.id,\n tool: call.name,\n argumentsHash: hash,\n arguments: canonical(shown),\n by: item.decidedBy,\n at: item.decidedAt ?? now(),\n });\n return undefined;\n };\n return {\n extension: defineExtension({\n name: GUEST_GUARD,\n hooks: [\n hook(ToolTask, {\n async beforeTool(call, api, context) {\n const live = await api.snapshot(LiveDoc, api.conversationId, context);\n const inputs = live?.run?.inputs ?? [];\n const inherited =\n (await api.snapshot(GuestTaintDoc, api.conversationId, context))?.taints ?? [];\n if (!inputs.length && !inherited.length) return undefined;\n const seats = (await api.snapshot(GuestsDoc, context))?.seats ?? {};\n const held = harness() as Held;\n const found = inputs.length ? await taintsOf(inputs, seats, held, context) : [];\n const licks = (await api.snapshot(LickTaintDoc, context))?.licks ?? {};\n const licked = await lickTaints(inputs, licks, held, context);\n for (const taint of [...found, ...inheritedTaints([...inherited, ...licked], seats)]) {\n const block = await review(taint, call, api, context);\n if (block) return { block };\n }\n return undefined;\n },\n }),\n ],\n }),\n bind(host) {\n resolveHost(host);\n for (const item of host.approvals.state.value ?? [])\n if (item.kind === 'guest-tool' && item.state === 'pending' && item.detail.task)\n watch(host, item.id, item.detail.task);\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["record"]
|
|
7
7
|
}
|
package/dist/kernel/groups.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { spawns } from "./lineage.js";
|
|
1
2
|
const REFUSED = /* @__PURE__ */ new Set(["ESRCH", "EPERM", "ENOSYS"]);
|
|
2
3
|
function processGroups(client) {
|
|
4
|
+
const spawned = spawns(client);
|
|
3
5
|
const groups = /* @__PURE__ */ new Map();
|
|
4
6
|
const shared = /* @__PURE__ */ new Map();
|
|
5
7
|
const leading = /* @__PURE__ */ new Map();
|
|
6
8
|
let joining = true;
|
|
7
9
|
return {
|
|
8
10
|
track(owner, process) {
|
|
11
|
+
spawned.started(process, owner);
|
|
9
12
|
const set = groups.get(owner) ?? /* @__PURE__ */ new Set();
|
|
10
13
|
groups.set(owner, set);
|
|
11
14
|
set.add(process.pgid);
|
|
@@ -58,7 +61,8 @@ function processGroups(client) {
|
|
|
58
61
|
groups.delete(owner);
|
|
59
62
|
shared.delete(owner);
|
|
60
63
|
return signalled;
|
|
61
|
-
}
|
|
64
|
+
},
|
|
65
|
+
lineage: spawned.lineage
|
|
62
66
|
};
|
|
63
67
|
}
|
|
64
68
|
function descendants(table, root) {
|
|
@@ -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';\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}\n\nexport function processGroups(client: Pick<KernelClient, 'kill'>): ProcessGroups {\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 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 };\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": "
|
|
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,QAA0D;AACtF,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;AACpB,cAAQ,QAAQ,SAAS,KAAK;AAC9B,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
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { boundedPs, PS_MS } from "./jobs.js";
|
|
2
|
+
function indexed(rows) {
|
|
3
|
+
const byPid = /* @__PURE__ */ new Map();
|
|
4
|
+
for (const row of rows) if (!byPid.has(row.pid)) byPid.set(row.pid, row);
|
|
5
|
+
return byPid;
|
|
6
|
+
}
|
|
7
|
+
function parentOf(byPid, row) {
|
|
8
|
+
return row.ppid === void 0 || row.ppid <= 1 ? void 0 : byPid.get(row.ppid);
|
|
9
|
+
}
|
|
10
|
+
function spawns(client, warn = (message) => console.warn(message), bound = PS_MS) {
|
|
11
|
+
const live = /* @__PURE__ */ new Map();
|
|
12
|
+
const groups = /* @__PURE__ */ new Set();
|
|
13
|
+
const seen = (found, row) => {
|
|
14
|
+
const owner = live.get(row.pid);
|
|
15
|
+
if (owner !== void 0 || row.pgid !== void 0 && groups.has(row.pgid))
|
|
16
|
+
found.spawned = true;
|
|
17
|
+
if (owner !== void 0 && found.agent === null) found.agent = owner;
|
|
18
|
+
};
|
|
19
|
+
const walk = (rows, pid) => {
|
|
20
|
+
for (const pgid of [...groups]) if (!rows.some((row2) => row2.pgid === pgid)) groups.delete(pgid);
|
|
21
|
+
const byPid = indexed(rows);
|
|
22
|
+
const visited = /* @__PURE__ */ new Set();
|
|
23
|
+
let row = byPid.get(pid);
|
|
24
|
+
if (!row) return void 0;
|
|
25
|
+
const found = {
|
|
26
|
+
agent: null,
|
|
27
|
+
spawned: false,
|
|
28
|
+
argv: row.argv ?? [],
|
|
29
|
+
...row.uid === void 0 ? {} : { uid: row.uid }
|
|
30
|
+
};
|
|
31
|
+
for (; row && !visited.has(row.pid); row = parentOf(byPid, row)) {
|
|
32
|
+
visited.add(row.pid);
|
|
33
|
+
seen(found, row);
|
|
34
|
+
}
|
|
35
|
+
return found;
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
started(process, owner) {
|
|
39
|
+
live.set(process.pid, owner);
|
|
40
|
+
groups.add(process.pgid);
|
|
41
|
+
const ended = () => live.delete(process.pid);
|
|
42
|
+
process.exited.then(ended, ended);
|
|
43
|
+
},
|
|
44
|
+
async lineage(pid) {
|
|
45
|
+
if (pid === null) return void 0;
|
|
46
|
+
const rows = await boundedPs(client, warn, bound);
|
|
47
|
+
return rows ? walk(rows, pid) : void 0;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
spawns
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=lineage.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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 ? walk(rows, pid) : undefined;\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,WAAW,aAAa;AAsBjC,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,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,QAAqC;AACvE,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,IAClD;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;AACtB,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,OAAO,MAAM,UAAU,QAAQ,MAAM,KAAK;AAChD,aAAO,OAAO,KAAK,MAAM,GAAG,IAAI;AAAA,IAClC;AAAA,EACF;AACF;",
|
|
6
|
+
"names": ["row"]
|
|
7
|
+
}
|
package/dist/licks/licks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defineDoc
|
|
3
3
|
} from "@earendil-works/pi-durable";
|
|
4
|
+
import { distinctTaints, LickTaintDoc } from "../guests/guard.js";
|
|
4
5
|
import { scrubTexts } from "../secrets/scrub.js";
|
|
5
6
|
import {
|
|
6
7
|
clip,
|
|
@@ -15,6 +16,9 @@ function card(event) {
|
|
|
15
16
|
...event.report ? { report: event.report } : {}
|
|
16
17
|
};
|
|
17
18
|
}
|
|
19
|
+
function tainted(taints) {
|
|
20
|
+
return taints.length ? { taints: distinctTaints(taints) } : {};
|
|
21
|
+
}
|
|
18
22
|
const LicksOutbox = defineDoc({
|
|
19
23
|
kind: "slicc.licks",
|
|
20
24
|
version: 1,
|
|
@@ -30,7 +34,8 @@ function frozenNotice(event, blocked) {
|
|
|
30
34
|
body: `${event.target} belongs to ${blocked.name}, which is frozen (${blocked.id}). Thaw it in the Freezer to get its licks again.`,
|
|
31
35
|
target: "cone",
|
|
32
36
|
severity: "warn",
|
|
33
|
-
eventId: `${blocked.id}|${event.target}|${event.channel}|${event.source}|${blocked.at}
|
|
37
|
+
eventId: `${blocked.id}|${event.target}|${event.channel}|${event.source}|${blocked.at}`,
|
|
38
|
+
...event.taints ? { taints: event.taints } : {}
|
|
34
39
|
};
|
|
35
40
|
}
|
|
36
41
|
const MAX_ITEMS = 50;
|
|
@@ -57,6 +62,7 @@ function merge(pending, event, now) {
|
|
|
57
62
|
for (const item of event.items ?? []) if (!items.includes(item)) items.push(item);
|
|
58
63
|
const { severity: _, summary: __, report: ___, ...lick } = pending.lick;
|
|
59
64
|
const severity = strongest(pending.lick.severity, event.severity);
|
|
65
|
+
const taints = tainted([...pending.taints ?? [], ...event.taints ?? []]);
|
|
60
66
|
return {
|
|
61
67
|
lick: {
|
|
62
68
|
...lick,
|
|
@@ -68,7 +74,8 @@ function merge(pending, event, now) {
|
|
|
68
74
|
...card(event)
|
|
69
75
|
},
|
|
70
76
|
items,
|
|
71
|
-
body: event.body ?? pending.body
|
|
77
|
+
body: event.body ?? pending.body,
|
|
78
|
+
...taints
|
|
72
79
|
};
|
|
73
80
|
}
|
|
74
81
|
function fresh(event, now, listed) {
|
|
@@ -86,7 +93,8 @@ function fresh(event, now, listed) {
|
|
|
86
93
|
...card(event)
|
|
87
94
|
},
|
|
88
95
|
items: [...new Set(event.items ?? [])],
|
|
89
|
-
body: event.body ?? null
|
|
96
|
+
body: event.body ?? null,
|
|
97
|
+
...tainted(event.taints ?? [])
|
|
90
98
|
};
|
|
91
99
|
}
|
|
92
100
|
function keyOf(event) {
|
|
@@ -107,6 +115,13 @@ async function scrubbedLick(event) {
|
|
|
107
115
|
if (out.summary !== void 0) out.summary = clip(out.summary);
|
|
108
116
|
return out;
|
|
109
117
|
}
|
|
118
|
+
async function marked(harness, pending, id, context) {
|
|
119
|
+
const taints = pending.taints;
|
|
120
|
+
if (!taints) return;
|
|
121
|
+
await harness.commit(async (tx) => {
|
|
122
|
+
(await tx.doc(LickTaintDoc)).licks[id] = taints;
|
|
123
|
+
}, context);
|
|
124
|
+
}
|
|
110
125
|
function createLicks(host) {
|
|
111
126
|
const handlers = /* @__PURE__ */ new Map();
|
|
112
127
|
let lock = Promise.resolve();
|
|
@@ -150,6 +165,7 @@ function createLicks(host) {
|
|
|
150
165
|
const target = key.split("|")[0];
|
|
151
166
|
const conversation = await resolve(target, queue.pending.lick.channel, context);
|
|
152
167
|
if (runnable && !await runnable(conversation, context)) return;
|
|
168
|
+
await marked(harness, queue.pending, lick.id, context);
|
|
153
169
|
const submission = await conversation.submit(
|
|
154
170
|
{
|
|
155
171
|
type: "input",
|
package/dist/licks/licks.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/licks/licks.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n defineDoc,\n type Harness,\n type SubmissionId,\n} from '@earendil-works/pi-durable';\nimport { scrubTexts } from '../secrets/scrub.ts';\nimport {\n clip,\n formatLick,\n type Lick,\n type LickAction,\n type LickChannel,\n type LickSeverity,\n type LickTarget,\n lickId,\n} from './lick.ts';\nimport { entryLick, LICK_STATE_KIND, type LickDecision, lickDecisions } from './state.ts';\nimport { skipEntry, skips, type Triage, type Verdict } from './triage.ts';\n\nexport interface LickEvent {\n channel: LickChannel;\n source: string;\n title: string;\n text: string;\n body?: string;\n items?: string[];\n count?: number;\n target: LickTarget;\n coalesce?: boolean;\n severity?: LickSeverity;\n eventId?: string;\n at?: number;\n triage?: string;\n summary?: string;\n report?: string;\n}\n\nfunction card(event: LickEvent): Pick<Lick, 'summary' | 'report'> {\n return {\n ...(event.summary ? { summary: event.summary } : {}),\n ...(event.report ? { report: event.report } : {}),\n };\n}\n\ntype Pending = {\n lick: Lick;\n items: string[];\n body: string | null;\n};\n\ntype Queue = {\n submission: number | null;\n pending: Pending | null;\n};\n\ntype OutboxState = {\n queues: Record<string, Queue>;\n events: Record<string, string[]>;\n};\n\nexport const LicksOutbox = defineDoc<OutboxState>({\n kind: 'slicc.licks',\n version: 1,\n scope: 'session',\n initial: () => ({ queues: {}, events: {} }),\n});\n\nexport interface LickHandler {\n confirm?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n dismiss?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n}\n\nexport interface LickFound {\n lick: Lick;\n decision: LickDecision | undefined;\n}\n\nexport interface Licks {\n deliver(event: LickEvent, context: Context): Promise<void>;\n flush(context: Context): Promise<void>;\n find(conversation: Conversation, id: string, context: Context): Promise<LickFound | undefined>;\n decide(\n conversation: Conversation,\n id: string,\n action: LickAction,\n reason: string | undefined,\n context: Context\n ): Promise<string>;\n handle(channel: LickChannel, handler: LickHandler): void;\n pending(context: Context): Promise<number>;\n forget(target: LickTarget, channel: LickChannel, source: string, context: Context): Promise<void>;\n guard(check: (target: LickTarget) => Blocked | undefined): void;\n triage(judge: Triage): void;\n}\n\nexport type Blocked = { id: string; name: string; at: number };\n\nexport function frozenNotice(event: LickEvent, blocked: Blocked): LickEvent {\n return {\n channel: event.channel,\n source: `frozen:${event.source}`,\n title: `${blocked.name} is frozen`,\n text: `${event.title} was skipped`,\n body: `${event.target} belongs to ${blocked.name}, which is frozen (${blocked.id}). Thaw it in the Freezer to get its licks again.`,\n target: 'cone',\n severity: 'warn',\n eventId: `${blocked.id}|${event.target}|${event.channel}|${event.source}|${blocked.at}`,\n };\n}\n\nexport interface LicksHost {\n harness: Harness;\n resolve(target: LickTarget, channel: LickChannel, context: Context): Promise<Conversation>;\n runnable?(conversation: Conversation, context: Context): Promise<boolean>;\n}\n\nexport const MAX_ITEMS = 50;\nexport const MAX_EVENTS = 64;\n\nexport function render(pending: Pending): Lick {\n const shown = pending.items.slice(0, MAX_ITEMS);\n const more = pending.items.length - shown.length;\n const lines = [\n ...(pending.body ? [pending.body] : []),\n ...shown,\n ...(more > 0 ? [`\u2026 and ${more} more`] : []),\n ];\n const { body: _, ...lick } = pending.lick;\n return lines.length ? { ...lick, body: lines.join('\\n') } : lick;\n}\n\nconst RANK: Record<LickSeverity, number> = { warn: 1, error: 2 };\n\nexport function strongest(\n a: LickSeverity | undefined,\n b: LickSeverity | undefined\n): LickSeverity | undefined {\n if (!a) return b;\n if (!b) return a;\n return RANK[b] > RANK[a] ? b : a;\n}\n\nfunction merge(pending: Pending, event: LickEvent, now: number): Pending {\n const items = [...pending.items];\n for (const item of event.items ?? []) if (!items.includes(item)) items.push(item);\n const { severity: _, summary: __, report: ___, ...lick } = pending.lick;\n const severity = strongest(pending.lick.severity, event.severity);\n return {\n lick: {\n ...lick,\n title: event.title,\n text: event.text,\n count: pending.lick.count + (event.count ?? 1),\n at: event.at ?? now,\n ...(severity ? { severity } : {}),\n ...card(event),\n },\n items,\n body: event.body ?? pending.body,\n };\n}\n\nfunction fresh(event: LickEvent, now: number, listed: LickAction[] | undefined): Pending {\n return {\n lick: {\n id: lickId(),\n channel: event.channel,\n source: event.source,\n title: event.title,\n text: event.text,\n count: event.count ?? 1,\n at: now,\n ...(listed ? { actions: listed } : {}),\n ...(event.severity ? { severity: event.severity } : {}),\n ...card(event),\n },\n items: [...new Set(event.items ?? [])],\n body: event.body ?? null,\n };\n}\n\nfunction keyOf(event: LickEvent): string {\n const base = `${event.target}|${event.channel}|${event.source}`;\n return event.coalesce === false ? `${base}|${event.eventId ?? lickId()}` : base;\n}\n\nexport async function scrubbedLick(event: LickEvent): Promise<LickEvent> {\n const fields = (['title', 'text', 'body', 'triage', 'summary', 'report'] as const).filter(\n (key) => event[key] !== undefined\n );\n const items = event.items ?? [];\n const scrubbed = await scrubTexts([...fields.map((key) => event[key] as string), ...items]);\n const out: LickEvent = { ...event };\n fields.forEach((key, at) => {\n out[key] = scrubbed[at] as string;\n });\n if (event.items) out.items = scrubbed.slice(fields.length);\n if (out.summary !== undefined) out.summary = clip(out.summary);\n return out;\n}\n\nexport function createLicks(host: Promise<LicksHost>): Licks {\n const handlers = new Map<LickChannel, LickHandler>();\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(operation: () => Promise<T>): Promise<T> => {\n const run = lock.then(operation, operation);\n lock = run.catch(() => undefined);\n return run;\n };\n\n const actions = (channel: LickChannel): LickAction[] | undefined => {\n const handler = handlers.get(channel);\n const list: LickAction[] = [];\n if (handler?.confirm) list.push('confirm');\n if (handler?.dismiss) list.push('dismiss');\n return list.length ? list : undefined;\n };\n\n async function previous(\n harness: Harness,\n id: number | null,\n context: Context\n ): Promise<'queued' | 'withdrawn' | 'gone'> {\n if (id === null) return 'gone';\n const submission = await harness.submission(id as SubmissionId, context);\n const record = await submission?.status(context);\n if (record?.status === 'queued') return 'queued';\n return record?.status === 'unanswered' && record.reason === 'aborted' ? 'withdrawn' : 'gone';\n }\n\n async function settled(key: string, context: Context): Promise<Queue | undefined> {\n const { harness } = await host;\n const queue = (await harness.snapshot(LicksOutbox, context))?.queues[key];\n if (!queue) return undefined;\n const before = await previous(harness, queue.submission, context);\n if (before === 'queued') return undefined;\n if (queue.pending && before === 'gone') return queue;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n if (doc.queues[key]?.submission === queue.submission) delete doc.queues[key];\n }, context);\n return undefined;\n }\n\n async function flushKey(key: string, context: Context): Promise<void> {\n const { harness, resolve, runnable } = await host;\n const queue = await settled(key, context);\n if (!queue?.pending) return;\n const lick = render(queue.pending);\n const target = key.split('|')[0] as LickTarget;\n const conversation = await resolve(target, queue.pending.lick.channel, context);\n if (runnable && !(await runnable(conversation, context))) return;\n const submission = await conversation.submit(\n {\n type: 'input',\n content: formatLick(lick),\n whenBusy: 'steer',\n requestId: `lick:${lick.id}`,\n },\n context\n );\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n doc.queues[key] = { submission: submission.id, pending: null };\n }, context);\n }\n\n async function flushAll(context: Context): Promise<void> {\n const { harness } = await host;\n const keys = Object.keys((await harness.snapshot(LicksOutbox, context))?.queues ?? {});\n for (const key of keys) await flushKey(key, context);\n }\n\n async function find(\n conversation: Conversation,\n id: string,\n context: Context\n ): Promise<LickFound | undefined> {\n const { entries } = await conversation.context(context);\n const lick = entries.map(entryLick).find((candidate) => candidate?.id === id);\n if (!lick) return undefined;\n return { lick, decision: lickDecisions(entries).get(id) };\n }\n\n async function judged(event: LickEvent, context: Context): Promise<Verdict | undefined> {\n if (!judge || !event.triage) return undefined;\n return judge(event, context).catch(() => undefined);\n }\n\n async function seenBefore(event: LickEvent, context: Context): Promise<boolean> {\n if (event.eventId === undefined) return false;\n const { harness } = await host;\n const events = (await harness.snapshot(LicksOutbox, context))?.events ?? {};\n return (events[`${event.channel}|${event.source}`] ?? []).includes(event.eventId);\n }\n\n async function record(event: LickEvent, verdict: Verdict, now: number, context: Context) {\n const conversation = await (await host).resolve(event.target, event.channel, context);\n await conversation.submit(skipEntry(event, verdict, now), context);\n }\n\n let check: ((target: LickTarget) => Blocked | undefined) | undefined;\n let judge: Triage | undefined;\n return {\n guard(next) {\n check = next;\n },\n triage(next) {\n judge = next;\n },\n async deliver(raw, context) {\n const incoming = await scrubbedLick(raw);\n const blocked = check?.(incoming.target);\n const event = blocked ? frozenNotice(incoming, blocked) : incoming;\n if (judge && event.triage && (await seenBefore(event, context))) return;\n const verdict = await judged(event, context);\n const skipped = skips(verdict);\n return serial(async () => {\n const { harness } = await host;\n const key = keyOf(event);\n await settled(key, context);\n const stream = `${event.channel}|${event.source}`;\n const now = event.at ?? Date.now();\n if (skipped) {\n if (await seenBefore(event, context)) return;\n await record(event, verdict, now, context);\n }\n const accepted = await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n const seen = doc.events[stream] ?? [];\n if (event.eventId !== undefined) {\n if (seen.includes(event.eventId)) return false;\n doc.events[stream] = [...seen, event.eventId].slice(-MAX_EVENTS);\n }\n if (skipped) return false;\n const queue = doc.queues[key] ?? { submission: null, pending: null };\n queue.pending = queue.pending\n ? merge(queue.pending, event, now)\n : fresh(event, now, actions(event.channel));\n doc.queues[key] = queue;\n return true;\n }, context);\n if (accepted) await flushKey(key, context);\n });\n },\n flush: (context) => serial(() => flushAll(context)),\n find,\n async decide(conversation, id, action, reason, context) {\n const found = await find(conversation, id, context);\n if (!found) throw new Error(`There is no lick ${id} in this conversation.`);\n if (found.decision) throw new Error(`Lick ${id} is already ${found.decision}.`);\n if (!found.lick.actions?.includes(action))\n throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n const run = handlers.get(found.lick.channel)?.[action];\n if (!run) throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n return (await run(found.lick, reason, context)) ?? `Lick ${id} ${action}ed.`;\n },\n handle(channel, handler) {\n handlers.set(channel, handler);\n },\n forget(target, channel, source, context) {\n return serial(async () => {\n const { harness } = await host;\n const key = `${target}|${channel}|${source}`;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n const queue = doc.queues[key];\n if (queue?.pending) queue.pending = null;\n }, context);\n });\n },\n async pending(context) {\n const { harness } = await host;\n const queues = (await harness.snapshot(LicksOutbox, context))?.queues ?? {};\n return Object.values(queues).filter((queue) => queue.submission !== null || queue.pending)\n .length;\n },\n };\n}\n\nexport { LICK_STATE_KIND };\n"],
|
|
5
|
-
"mappings": "AACA;AAAA,EAEE;AAAA,OAGK;AACP,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EAMA;AAAA,OACK;AACP,SAAS,WAAW,iBAAoC,qBAAqB;AAC7E,SAAS,WAAW,aAAwC;
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n defineDoc,\n type Harness,\n type SubmissionId,\n} from '@earendil-works/pi-durable';\nimport { distinctTaints, type InheritedTaint, LickTaintDoc } from '../guests/guard.ts';\nimport { scrubTexts } from '../secrets/scrub.ts';\nimport {\n clip,\n formatLick,\n type Lick,\n type LickAction,\n type LickChannel,\n type LickSeverity,\n type LickTarget,\n lickId,\n} from './lick.ts';\nimport { entryLick, LICK_STATE_KIND, type LickDecision, lickDecisions } from './state.ts';\nimport { skipEntry, skips, type Triage, type Verdict } from './triage.ts';\n\nexport interface LickEvent {\n channel: LickChannel;\n source: string;\n title: string;\n text: string;\n body?: string;\n items?: string[];\n count?: number;\n target: LickTarget;\n coalesce?: boolean;\n severity?: LickSeverity;\n eventId?: string;\n at?: number;\n triage?: string;\n summary?: string;\n report?: string;\n taints?: readonly InheritedTaint[];\n}\n\nfunction card(event: LickEvent): Pick<Lick, 'summary' | 'report'> {\n return {\n ...(event.summary ? { summary: event.summary } : {}),\n ...(event.report ? { report: event.report } : {}),\n };\n}\n\ntype Pending = {\n lick: Lick;\n items: string[];\n body: string | null;\n taints?: InheritedTaint[];\n};\n\nfunction tainted(taints: readonly InheritedTaint[]): Pick<Pending, 'taints'> {\n return taints.length ? { taints: distinctTaints(taints) } : {};\n}\n\ntype Queue = {\n submission: number | null;\n pending: Pending | null;\n};\n\ntype OutboxState = {\n queues: Record<string, Queue>;\n events: Record<string, string[]>;\n};\n\nexport const LicksOutbox = defineDoc<OutboxState>({\n kind: 'slicc.licks',\n version: 1,\n scope: 'session',\n initial: () => ({ queues: {}, events: {} }),\n});\n\nexport interface LickHandler {\n confirm?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n dismiss?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n}\n\nexport interface LickFound {\n lick: Lick;\n decision: LickDecision | undefined;\n}\n\nexport interface Licks {\n deliver(event: LickEvent, context: Context): Promise<void>;\n flush(context: Context): Promise<void>;\n find(conversation: Conversation, id: string, context: Context): Promise<LickFound | undefined>;\n decide(\n conversation: Conversation,\n id: string,\n action: LickAction,\n reason: string | undefined,\n context: Context\n ): Promise<string>;\n handle(channel: LickChannel, handler: LickHandler): void;\n pending(context: Context): Promise<number>;\n forget(target: LickTarget, channel: LickChannel, source: string, context: Context): Promise<void>;\n guard(check: (target: LickTarget) => Blocked | undefined): void;\n triage(judge: Triage): void;\n}\n\nexport type Blocked = { id: string; name: string; at: number };\n\nexport function frozenNotice(event: LickEvent, blocked: Blocked): LickEvent {\n return {\n channel: event.channel,\n source: `frozen:${event.source}`,\n title: `${blocked.name} is frozen`,\n text: `${event.title} was skipped`,\n body: `${event.target} belongs to ${blocked.name}, which is frozen (${blocked.id}). Thaw it in the Freezer to get its licks again.`,\n target: 'cone',\n severity: 'warn',\n eventId: `${blocked.id}|${event.target}|${event.channel}|${event.source}|${blocked.at}`,\n ...(event.taints ? { taints: event.taints } : {}),\n };\n}\n\nexport interface LicksHost {\n harness: Harness;\n resolve(target: LickTarget, channel: LickChannel, context: Context): Promise<Conversation>;\n runnable?(conversation: Conversation, context: Context): Promise<boolean>;\n}\n\nexport const MAX_ITEMS = 50;\nexport const MAX_EVENTS = 64;\n\nexport function render(pending: Pending): Lick {\n const shown = pending.items.slice(0, MAX_ITEMS);\n const more = pending.items.length - shown.length;\n const lines = [\n ...(pending.body ? [pending.body] : []),\n ...shown,\n ...(more > 0 ? [`\u2026 and ${more} more`] : []),\n ];\n const { body: _, ...lick } = pending.lick;\n return lines.length ? { ...lick, body: lines.join('\\n') } : lick;\n}\n\nconst RANK: Record<LickSeverity, number> = { warn: 1, error: 2 };\n\nexport function strongest(\n a: LickSeverity | undefined,\n b: LickSeverity | undefined\n): LickSeverity | undefined {\n if (!a) return b;\n if (!b) return a;\n return RANK[b] > RANK[a] ? b : a;\n}\n\nfunction merge(pending: Pending, event: LickEvent, now: number): Pending {\n const items = [...pending.items];\n for (const item of event.items ?? []) if (!items.includes(item)) items.push(item);\n const { severity: _, summary: __, report: ___, ...lick } = pending.lick;\n const severity = strongest(pending.lick.severity, event.severity);\n const taints = tainted([...(pending.taints ?? []), ...(event.taints ?? [])]);\n return {\n lick: {\n ...lick,\n title: event.title,\n text: event.text,\n count: pending.lick.count + (event.count ?? 1),\n at: event.at ?? now,\n ...(severity ? { severity } : {}),\n ...card(event),\n },\n items,\n body: event.body ?? pending.body,\n ...taints,\n };\n}\n\nfunction fresh(event: LickEvent, now: number, listed: LickAction[] | undefined): Pending {\n return {\n lick: {\n id: lickId(),\n channel: event.channel,\n source: event.source,\n title: event.title,\n text: event.text,\n count: event.count ?? 1,\n at: now,\n ...(listed ? { actions: listed } : {}),\n ...(event.severity ? { severity: event.severity } : {}),\n ...card(event),\n },\n items: [...new Set(event.items ?? [])],\n body: event.body ?? null,\n ...tainted(event.taints ?? []),\n };\n}\n\nfunction keyOf(event: LickEvent): string {\n const base = `${event.target}|${event.channel}|${event.source}`;\n return event.coalesce === false ? `${base}|${event.eventId ?? lickId()}` : base;\n}\n\nexport async function scrubbedLick(event: LickEvent): Promise<LickEvent> {\n const fields = (['title', 'text', 'body', 'triage', 'summary', 'report'] as const).filter(\n (key) => event[key] !== undefined\n );\n const items = event.items ?? [];\n const scrubbed = await scrubTexts([...fields.map((key) => event[key] as string), ...items]);\n const out: LickEvent = { ...event };\n fields.forEach((key, at) => {\n out[key] = scrubbed[at] as string;\n });\n if (event.items) out.items = scrubbed.slice(fields.length);\n if (out.summary !== undefined) out.summary = clip(out.summary);\n return out;\n}\n\nasync function marked(harness: Harness, pending: Pending, id: string, context: Context) {\n const taints = pending.taints;\n if (!taints) return;\n await harness.commit(async (tx) => {\n (await tx.doc(LickTaintDoc)).licks[id] = taints;\n }, context);\n}\n\nexport function createLicks(host: Promise<LicksHost>): Licks {\n const handlers = new Map<LickChannel, LickHandler>();\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(operation: () => Promise<T>): Promise<T> => {\n const run = lock.then(operation, operation);\n lock = run.catch(() => undefined);\n return run;\n };\n\n const actions = (channel: LickChannel): LickAction[] | undefined => {\n const handler = handlers.get(channel);\n const list: LickAction[] = [];\n if (handler?.confirm) list.push('confirm');\n if (handler?.dismiss) list.push('dismiss');\n return list.length ? list : undefined;\n };\n\n async function previous(\n harness: Harness,\n id: number | null,\n context: Context\n ): Promise<'queued' | 'withdrawn' | 'gone'> {\n if (id === null) return 'gone';\n const submission = await harness.submission(id as SubmissionId, context);\n const record = await submission?.status(context);\n if (record?.status === 'queued') return 'queued';\n return record?.status === 'unanswered' && record.reason === 'aborted' ? 'withdrawn' : 'gone';\n }\n\n async function settled(key: string, context: Context): Promise<Queue | undefined> {\n const { harness } = await host;\n const queue = (await harness.snapshot(LicksOutbox, context))?.queues[key];\n if (!queue) return undefined;\n const before = await previous(harness, queue.submission, context);\n if (before === 'queued') return undefined;\n if (queue.pending && before === 'gone') return queue;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n if (doc.queues[key]?.submission === queue.submission) delete doc.queues[key];\n }, context);\n return undefined;\n }\n\n async function flushKey(key: string, context: Context): Promise<void> {\n const { harness, resolve, runnable } = await host;\n const queue = await settled(key, context);\n if (!queue?.pending) return;\n const lick = render(queue.pending);\n const target = key.split('|')[0] as LickTarget;\n const conversation = await resolve(target, queue.pending.lick.channel, context);\n if (runnable && !(await runnable(conversation, context))) return;\n await marked(harness, queue.pending, lick.id, context);\n const submission = await conversation.submit(\n {\n type: 'input',\n content: formatLick(lick),\n whenBusy: 'steer',\n requestId: `lick:${lick.id}`,\n },\n context\n );\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n doc.queues[key] = { submission: submission.id, pending: null };\n }, context);\n }\n\n async function flushAll(context: Context): Promise<void> {\n const { harness } = await host;\n const keys = Object.keys((await harness.snapshot(LicksOutbox, context))?.queues ?? {});\n for (const key of keys) await flushKey(key, context);\n }\n\n async function find(\n conversation: Conversation,\n id: string,\n context: Context\n ): Promise<LickFound | undefined> {\n const { entries } = await conversation.context(context);\n const lick = entries.map(entryLick).find((candidate) => candidate?.id === id);\n if (!lick) return undefined;\n return { lick, decision: lickDecisions(entries).get(id) };\n }\n\n async function judged(event: LickEvent, context: Context): Promise<Verdict | undefined> {\n if (!judge || !event.triage) return undefined;\n return judge(event, context).catch(() => undefined);\n }\n\n async function seenBefore(event: LickEvent, context: Context): Promise<boolean> {\n if (event.eventId === undefined) return false;\n const { harness } = await host;\n const events = (await harness.snapshot(LicksOutbox, context))?.events ?? {};\n return (events[`${event.channel}|${event.source}`] ?? []).includes(event.eventId);\n }\n\n async function record(event: LickEvent, verdict: Verdict, now: number, context: Context) {\n const conversation = await (await host).resolve(event.target, event.channel, context);\n await conversation.submit(skipEntry(event, verdict, now), context);\n }\n\n let check: ((target: LickTarget) => Blocked | undefined) | undefined;\n let judge: Triage | undefined;\n return {\n guard(next) {\n check = next;\n },\n triage(next) {\n judge = next;\n },\n async deliver(raw, context) {\n const incoming = await scrubbedLick(raw);\n const blocked = check?.(incoming.target);\n const event = blocked ? frozenNotice(incoming, blocked) : incoming;\n if (judge && event.triage && (await seenBefore(event, context))) return;\n const verdict = await judged(event, context);\n const skipped = skips(verdict);\n return serial(async () => {\n const { harness } = await host;\n const key = keyOf(event);\n await settled(key, context);\n const stream = `${event.channel}|${event.source}`;\n const now = event.at ?? Date.now();\n if (skipped) {\n if (await seenBefore(event, context)) return;\n await record(event, verdict, now, context);\n }\n const accepted = await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n const seen = doc.events[stream] ?? [];\n if (event.eventId !== undefined) {\n if (seen.includes(event.eventId)) return false;\n doc.events[stream] = [...seen, event.eventId].slice(-MAX_EVENTS);\n }\n if (skipped) return false;\n const queue = doc.queues[key] ?? { submission: null, pending: null };\n queue.pending = queue.pending\n ? merge(queue.pending, event, now)\n : fresh(event, now, actions(event.channel));\n doc.queues[key] = queue;\n return true;\n }, context);\n if (accepted) await flushKey(key, context);\n });\n },\n flush: (context) => serial(() => flushAll(context)),\n find,\n async decide(conversation, id, action, reason, context) {\n const found = await find(conversation, id, context);\n if (!found) throw new Error(`There is no lick ${id} in this conversation.`);\n if (found.decision) throw new Error(`Lick ${id} is already ${found.decision}.`);\n if (!found.lick.actions?.includes(action))\n throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n const run = handlers.get(found.lick.channel)?.[action];\n if (!run) throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n return (await run(found.lick, reason, context)) ?? `Lick ${id} ${action}ed.`;\n },\n handle(channel, handler) {\n handlers.set(channel, handler);\n },\n forget(target, channel, source, context) {\n return serial(async () => {\n const { harness } = await host;\n const key = `${target}|${channel}|${source}`;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n const queue = doc.queues[key];\n if (queue?.pending) queue.pending = null;\n }, context);\n });\n },\n async pending(context) {\n const { harness } = await host;\n const queues = (await harness.snapshot(LicksOutbox, context))?.queues ?? {};\n return Object.values(queues).filter((queue) => queue.submission !== null || queue.pending)\n .length;\n },\n };\n}\n\nexport { LICK_STATE_KIND };\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["record"]
|
|
7
7
|
}
|
package/dist/licks/sources.js
CHANGED
|
@@ -62,7 +62,8 @@ function webhookLick(name, entry, delivery) {
|
|
|
62
62
|
target: entry.target,
|
|
63
63
|
coalesce: false,
|
|
64
64
|
...delivery.id ? { eventId: delivery.id } : {},
|
|
65
|
-
...entry.triage ? { triage: entry.triage } : {}
|
|
65
|
+
...entry.triage ? { triage: entry.triage } : {},
|
|
66
|
+
...delivery.taints?.length ? { taints: delivery.taints } : {}
|
|
66
67
|
};
|
|
67
68
|
}
|
|
68
69
|
async function webhooksIn(files, dir, context) {
|