diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..022eb638 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +# Build context hygiene. Without this the whole tree ships to the daemon, including every +# node_modules and every local artifact, which is slow and puts a developer's .env in an image. +node_modules +**/node_modules +.git +.env +.env.* +!.env.example +dist +**/dist +app/dist +.nx +*.log +.DS_Store +assets/*.svg diff --git a/.env.example b/.env.example index 5ad0165f..cdaf0f11 100644 --- a/.env.example +++ b/.env.example @@ -202,3 +202,4 @@ COMPUTER_RUNTIME= # that granted the tool, which is where the grant, the policy and the audit row are. Absent, no Bot # may call tools back and it is told so rather than being quietly allowed. AGENT_TOOL_TOKEN= + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 389183f8..cb7df08d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -33,6 +33,11 @@ conditional update, `LISTEN`/`NOTIFY` for fan-out. - [ ] New refusals and new failures each write a row. - [ ] Nothing new is trusted from the client that the server can resolve itself. +## Changelog + +- [ ] A line in `CHANGELOG.md` under `Unreleased`, or a sentence on why a deployment behaves no + differently afterwards. + ## Proof diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c19cfef8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,48 @@ +# Changelog + +What changed, for somebody deciding whether to upgrade. Written for the person running OpenBot, not +for the person who wrote the commit: a line belongs here when a deployment behaves differently +afterwards, and does not when only the code moved. + +Newest first. `Unreleased` is what is on `main` and not yet tagged. + +## Unreleased + +### Added + +- **One container that runs the whole thing.** The root `Dockerfile` builds an image carrying the + app, the API, a Bot computer, and optionally PostgreSQL, supervised together. Point `DATABASE_URL` + at a database you already run and the built-in one never starts; leave it unset and the container + is self-contained. See [docs/deployment.md](docs/deployment.md) for the measured minimum sizes and + the platforms it has been run on. +- **Bots can run commands.** `computer_run_command` runs a command in the Bot's `/workspace`, so a + Bot can install a tool, unpack what it downloaded, or run what it was asked to run instead of only + driving a browser. Governed like every other action: the policy decides, the audit row is written + first, and a rule can refuse a shell outright with `intent == "run_command"` or refuse particular + commands. The command is recorded; its output is not. +- **The audit trail shows the command.** A command row names what ran, the way a file row names the + path, rather than reporting an element it was never about. +- **`COMPUTER_SANDBOX=on`** turns on Chromium's own sandbox where the host permits user namespaces. + Which way it went is printed at start-up either way. + +### Fixed + +- **A deployment served over plain HTTP could not start a conversation.** The chat surface minted + identifiers with `crypto.randomUUID`, which browsers withhold outside a secure context. On a + laptop `http://localhost` counts as one, so this never showed up in development; on a real + address it does not, and the surface did nothing at all when you pressed send. No message, no + error. Ids now come from an API with no such restriction. + +### Changed + +- **Where a Bot's computer runs is now a plug.** One `ComputerProvider` interface sits under the + gateway, with the Docker supervisor as one implementation and a shared computer as another. A + computer somewhere else is an adapter rather than a change to the governed path. Thanks to + [@mu-hashmi](https://github.com/CopilotKit/OpenBot/pull/57) for the refactor. +- The address a provider hands back is checked before anything is sent to it, and the cloud metadata + addresses are refused whatever a provider says. +- The container image runs as an unprivileged user rather than root. + +## 0.0.1 + +First tag. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..0e29dc63 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,192 @@ +# OpenBot, whole, in one container. +# +# WHAT THIS IS FOR. Everything a laptop runs, minus the database, in one image on one port. Deploy it +# anywhere that runs a container and you get what `scripts/start.sh` gives you locally: the app, the +# API, and a browser the Bots can drive. +# +# WHAT IS NOT HERE, AND WHY. +# +# PostgreSQL. A container filesystem does not survive a redeploy and the audit trail is the +# product. `DATABASE_URL` points at a managed instance, which is one click on every platform this +# is meant to run on. +# +# The supervisor. It exists to give each Bot its own container, which needs a Docker socket, which +# no serverless container platform permits. Without it every Bot shares the browser below, exactly +# as they do on a laptop with no supervisor configured. Per-Bot isolation is A6. +# +# THE BASE IS PLAYWRIGHT'S, not Bun's, because Chromium and its system libraries have to stay +# matched and that image is the only place that is guaranteed. The tag must move with the +# `playwright` dependency in `agent-computer/package.json`. Bump both or neither. + +FROM mcr.microsoft.com/playwright:v1.62.1-noble AS base + +# unzip is not in the Playwright image and bun's installer needs it. +# Bun is pinned. The installer takes whatever is newest otherwise, so the runtime drifts from the +# one the lockfile was resolved against and an image built next month is not the image built today. +ARG BUN_VERSION=1.3.14 +# Into /usr/local rather than /root/.bun, because the runtime stage runs as `pwuser` and cannot read +# root's home. Set before the install, or the installer has already chosen the wrong directory. +ENV BUN_INSTALL=/usr/local +ENV PATH="/usr/local/bin:${PATH}" +RUN apt-get update && apt-get install -y --no-install-recommends unzip xz-utils \ + && rm -rf /var/lib/apt/lists/* \ + && curl -fsSL https://bun.sh/install | bash -s "bun-v${BUN_VERSION}" + + +FROM base AS deps + +WORKDIR /src + +# Manifests first, so editing a source file does not reinstall the world. +COPY package.json bun.lock ./ +COPY tsconfig.base.json bunfig.toml ./ +COPY app/package.json app/package.json +COPY server/package.json server/package.json +COPY worker/package.json worker/package.json +RUN bun install --frozen-lockfile + +COPY agent-computer/package.json agent-computer/package.json +RUN cd agent-computer && bun install + +# A second tree with the build-time dependencies left out, for the runtime stage to take. Vite, +# biome and the test tooling are a gigabyte that nothing in a running container imports. +RUN mkdir -p /prod && cp package.json bun.lock /prod/ \ + && cp -r app/package.json /prod/app-package.json \ + && cd /prod && mkdir -p app server worker \ + && cp /src/app/package.json app/package.json \ + && cp /src/server/package.json server/package.json \ + && cp /src/worker/package.json worker/package.json \ + && bun install --frozen-lockfile --production + + +FROM deps AS app-build + +COPY app app +COPY scripts scripts +COPY shared shared +# The server's source as well: the app's prebuild step reads the tenant package through +# `server/src/tenant-package`, so the app cannot be built without it. +COPY server server +COPY examples examples +RUN bun run --cwd app build + + +FROM base AS runtime + +# s6 rather than supervisord. The deciding difference is that s6 brings the container down when a +# supervised process exits, which is what makes the platform restart it. supervisord stays alive and +# the container keeps reporting healthy while the API inside it is dead. +ARG S6_OVERLAY_VERSION=3.2.1.0 +# `TARGETARCH` is filled in by the builder. s6 names its tarballs by uname, so amd64 and arm64 have +# to be translated. Hardcoding one of them builds fine on the other and then fails at start with an +# exec format error, which reads as a broken image rather than a wrong download. +ARG TARGETARCH +ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp/ +RUN case "${TARGETARCH}" in \ + amd64) S6_ARCH=x86_64 ;; \ + arm64) S6_ARCH=aarch64 ;; \ + *) echo "unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \ + esac \ + && curl -fsSL -o /tmp/s6-overlay-arch.tar.xz \ + "https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" \ + && tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz \ + && tar -C / -Jxpf /tmp/s6-overlay-arch.tar.xz \ + && rm /tmp/s6-overlay-*.tar.xz + +WORKDIR /app + +COPY --from=deps /prod/node_modules node_modules +COPY --from=deps /src/package.json package.json +COPY --from=deps /src/bun.lock bun.lock +COPY --from=deps /src/server/node_modules server/node_modules +COPY --from=deps /src/agent-computer/node_modules agent-computer/node_modules + +COPY server server +COPY shared shared +COPY examples examples +COPY agent-computer/src agent-computer/src +COPY agent-computer/package.json agent-computer/package.json + +# The built app, served by the API on the same origin. There is no CORS in this server, so this is +# not a convenience: two origins would simply fail. +COPY --from=app-build /src/app/dist app/dist +ENV APP_DIST_DIR=/app/app/dist + +COPY docker/s6 /etc/s6-overlay + +# PostgreSQL, for the deployment that wants one thing to run rather than two. +# +# OFF UNLESS ASKED FOR. Set `EMBEDDED_POSTGRES=on` and the container runs its own; leave it and +# `DATABASE_URL` points wherever you like. The trade is the one you would expect: a database inside +# a container lives and dies with that container unless /var/lib/postgresql is a mounted volume, and +# the audit trail is the thing you would be losing. +RUN apt-get update && apt-get install -y --no-install-recommends \ + postgresql-16 postgresql-16-pgvector \ + && rm -rf /var/lib/apt/lists/* \ + && mkdir -p /var/lib/postgresql/data /var/run/postgresql \ + && chown -R postgres:postgres /var/lib/postgresql /var/run/postgresql + +# A Bot can install what a task needs. +# +# `sudo` for one user, no password, because a package manager that cannot install is not one, and +# "install a tool then use it" is the whole point of giving a Bot a shell. +# +# BE CLEAR WHAT THIS COSTS. It means a Bot can become root inside its container. That is acceptable +# when the container is the Bot's alone and is contained from below, which is why per-Bot computers +# and gVisor are not optional extras next to this feature; they are what makes it sane. In a +# container shared between Bots, or one holding a database, a Bot with sudo can reach all of it. +RUN apt-get update && apt-get install -y --no-install-recommends sudo \ + && rm -rf /var/lib/apt/lists/* \ + && echo 'pwuser ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/pwuser \ + && chmod 0440 /etc/sudoers.d/pwuser + +# THE PACKAGE MANAGER AND THE SHELL STAY. Both were removed here once as hardening, which was +# backwards: a Bot being able to open a shell and install what a task needs is a requested feature, +# not an oversight. Removing them hardens the image by deleting the product. +# +# What makes that safe is not their absence. It is that a Bot reaches them the same way it reaches +# anything else, through the gateway: resolve, decide against the policy, write the audit row, then +# act. A command is a decision like a click is. + +# Where a Bot's files live. Mount a volume here to keep them across a redeploy; without one they are +# as durable as the container, which for a trial is the honest default. +ENV WORKSPACE_DIR=/workspace +ENV PROFILES_DIR=/profiles + +# The browser is on loopback inside this container and reachable from nowhere else, which is why the +# private-host allowance is on: the server is browsing to its own sibling process, not the internet. +ENV AGENT_COMPUTER_URL=http://127.0.0.1:4100 +ENV AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS=true + +# NOTHING THAT MATTERS RUNS AS ROOT. +# +# s6 stays root because that is the only way it can drop each service to a different user, and they +# genuinely differ: the browser and API run as `pwuser`, the database as `postgres`. One shared +# account would put the process that renders the open internet in the same skin as the one holding +# the audit trail. +# +# This matters more than usual here. Chromium is launched with `--no-sandbox` unless the host can +# support its sandbox, and with that flag the process user IS the boundary, so root would mean a +# page exploit lands as root. +# +# The two directories the browser writes are its workspace and its profile, the second being what +# keeps a Bot signed in between turns. Owned here, because a non-root process cannot create them at +# the root of the filesystem and the failure surfaces as EACCES on the first navigation. +RUN mkdir -p /workspace /profiles \ + && chown -R pwuser:pwuser /workspace /profiles /app + +# Where the embedded database answers, when there is one. Overridden by whatever you set, so an +# external database needs no special case: set DATABASE_URL and EMBEDDED_POSTGRES stays off. +ENV EMBEDDED_POSTGRES=off +ENV DATABASE_URL=postgres://openbot@127.0.0.1:5432/openbot + +ENV NODE_ENV=production +ENV PORT=3001 +EXPOSE 3001 + +# One port out. The browser's 4100 is deliberately not exposed: it holds real logins and its only +# caller is the process next to it. +HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \ + CMD bun -e "const r = await fetch('http://127.0.0.1:3001/health'); process.exit(r.ok ? 0 : 1)" + +ENTRYPOINT ["/init"] diff --git a/README.md b/README.md index 56e20207..27d13b0c 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ A Bot is any endpoint speaking [AG-UI](https://github.com/ag-ui-protocol/ag-ui), ## Requirements -- Docker, for PostgreSQL, browser computers, the supervisor, and the shipped Bots. +- Docker, for PostgreSQL and the shipped Bots. - [Bun](https://bun.sh) 1.3+, for the app and API server. - A CopilotKit Intelligence project and license. A free plan is available, and Intelligence can be self-hosted. - A model key. The proof-of-concept Bot uses OpenAI; the LangGraph Bot can use OpenAI, Anthropic, or Google. @@ -96,6 +96,21 @@ A Bot is any endpoint speaking [AG-UI](https://github.com/ag-ui-protocol/ag-ui), `scripts/start.sh` starts Docker services, applies migrations, starts the API server on port 3001, starts the app on port 3010, and checks that the services answer their own health routes before printing next steps. +## Deploy it + +One image carries the app, the API, the browser the Bots drive, and optionally PostgreSQL. Same +`.env`, no Kubernetes. + +```sh +docker build -t openbot . +docker run -p 3001:3001 --env-file .env \ + -e EMBEDDED_POSTGRES=on -v openbot-data:/var/lib/postgresql/data openbot +``` + +Leave `EMBEDDED_POSTGRES` off and set `DATABASE_URL` to point at a database you already run. +[docs/deployment.md](docs/deployment.md) has the minimum sizes, the platform notes, and why this runs +as one replica for now. + ## Try it - Open `/bot` and ask: `Open news.ycombinator.com and tell me the top story.` @@ -125,6 +140,7 @@ A Bot is any endpoint speaking [AG-UI](https://github.com/ag-ui-protocol/ag-ui), ## Features - **A computer per Bot**: the supervisor gives each Bot its own container, its own `/workspace` volume and its own browser profile. Set `COMPUTER_RUNTIME=runsc` to run them under gVisor where the host supports it. +- **A shell, not just a browser**: a Bot can run a command in its workspace, install what it needs, and process a file it saved. Through the same gate as everything else, so a rule can refuse a shell outright or refuse particular commands, and the command is on the record either way. - **The gateway is the only way in**: it resolves the target from a server-held snapshot, evaluates the policy, writes the audit row, and only then calls the computer. There is no path that acts without the record existing first. - **CEL policy, fail closed**: rules can inspect `tool.name`, `intent`, `bot.id`, `actor.id`, `page.url`, `page.host`, `element.*`, `key`, `file.*` and `mcp.*`. Deny is evaluated before allow, a missing policy permits nothing, and a broken rule refuses rather than opens. - **Take the wheel**: a Bot that hits a login wall or a 2FA prompt asks for help. Control is handed over in the same panel and recorded as `computer.help_requested`, `computer.control_taken` and `computer.control_released`. While a person is driving, Bot actions are refused rather than queued. @@ -181,6 +197,8 @@ Settings worth knowing: | `SUPERVISOR_TOKEN` | Secret the supervisor requires. `start.sh` sets one. | | `COMPUTER_SUPERVISOR_URL` | Gives each Bot a computer of its own instead of one shared computer. | | `COMPUTER_RUNTIME` | Set to `runsc` to run computers under gVisor, where the host has it. | +| `COMPUTER_SANDBOX` | Set to `on` for Chromium's own sandbox, where the host permits it. | +| `EMBEDDED_POSTGRES` | Set to `on` for a database inside the deployment container. | | `AGENT_COMPUTER_POLICY` | JSON action policy. Malformed JSON stops server startup. | | `AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS` | Lets a Bot reach this machine's own services. | | `TENANT_PACKAGE_DIR` | Directory containing tenant YAML. Defaults to `../examples/fintech`. | diff --git a/agent-computer/src/index.ts b/agent-computer/src/index.ts index f8690607..350fe812 100644 --- a/agent-computer/src/index.ts +++ b/agent-computer/src/index.ts @@ -22,6 +22,7 @@ import { WorkspaceFileError, WorkspacePathError, } from "./workspace"; +import { createShell } from "./shell"; /** * The Bot's computer: one long-lived browser, reachable over HTTP. @@ -165,6 +166,9 @@ const workspace = createWorkspace(process.env.WORKSPACE_DIR ?? "/workspace"); * mounted volume so sign-in state survives the container. */ const profiles = createProfiles(process.env.PROFILES_DIR ?? "/profiles"); +// Rooted in the same workspace the file tools use, so a command and a written file see one +// directory rather than two. +const shell = createShell(process.env.WORKSPACE_DIR ?? "/workspace"); /** * The id normally arrives as a header on every request. This is the fallback for a caller that has no @@ -746,6 +750,39 @@ serve({ } } + /* + * A command on this computer. + * + * Nothing here decides whether it may run: the gateway already asked the deployment's policy and + * wrote the audit row before this was called. Refusing again here would be a second, quieter + * policy nobody configured. + */ + if (url.pathname === "/exec" && request.method === "POST") { + const body = (await request.json().catch(() => null)) as { + command?: unknown; + timeoutMs?: unknown; + } | null; + if (typeof body?.command !== "string" || !body.command.trim()) { + return json({ error: "A command is required." }, 400); + } + try { + return json( + await shell.run({ + command: body.command, + ...(typeof body.timeoutMs === "number" + ? { timeoutMs: body.timeoutMs } + : {}), + signal: request.signal, + }), + ); + } catch (error) { + return json( + { error: describe(error, "The command could not be run.") }, + 500, + ); + } + } + if (url.pathname === "/files/write" && request.method === "POST") { const body = (await request.json().catch(() => null)) as { path?: unknown; diff --git a/agent-computer/src/profiles.ts b/agent-computer/src/profiles.ts index de45097d..368a93bf 100644 --- a/agent-computer/src/profiles.ts +++ b/agent-computer/src/profiles.ts @@ -60,12 +60,48 @@ const SINGLETON_FILES = ["SingletonLock", "SingletonSocket", "SingletonCookie"]; * This is obfuscation at rest, not protection. Anything that can read the volume can read the * cookies. The volume's own permissions are the security boundary. */ +/** + * Whether Chromium gets to use its own sandbox. + * + * OFF BY DEFAULT, AND THAT IS NOT A PREFERENCE. Chromium's sandbox creates user namespaces, and + * Docker's default seccomp profile blocks the syscall it needs, so a container that does nothing + * special gets `No usable sandbox!` and the browser will not start at all. Verified both ways in + * this image: default profile fails, relaxed profile renders. + * + * TURN IT ON WHERE THE HOST ALLOWS IT. On a VM or self-hosted Docker, run with a Chromium seccomp + * profile and set `COMPUTER_SANDBOX=on`. That is strictly better than everything below, because it + * is the boundary Chromium itself maintains against the pages it renders. + * + * WHERE IT CANNOT BE ON. Serverless container platforms do not let you set a seccomp profile or add + * capabilities; Fargate restricts `CAP_SYS_ADMIN` explicitly. There the sandbox is unavailable, and + * the compensating controls are the ones this image already has, a non-root user, plus gVisor + * underneath, which Cloud Run applies to everything by default. + * + * Said out loud at start-up either way. An operator should not have to read this file to find out + * whether the browser rendering the open internet is sandboxed. + */ +const SANDBOX_ENABLED = process.env.COMPUTER_SANDBOX === "on"; + const LAUNCH_ARGS = [ - "--no-sandbox", + ...(SANDBOX_ENABLED ? [] : ["--no-sandbox"]), "--disable-dev-shm-usage", "--password-store=basic", ]; +console.info( + JSON.stringify({ + type: "computer-sandbox", + sandbox: SANDBOX_ENABLED ? "on" : "off", + ...(SANDBOX_ENABLED + ? { + note: "Chromium's own sandbox is in use. It will refuse to start if the host does not permit user namespaces.", + } + : { + note: "Chromium runs without its own sandbox, which is the only thing that works under a default container seccomp profile. Set COMPUTER_SANDBOX=on where the host allows it.", + }), + }), +); + /** * How long to let a closing browser finish writing before moving on. * @@ -160,6 +196,10 @@ export function createProfiles(root: string) { const proxy = egressFor(botId, process.env); const context = await chromium.launchPersistentContext(dir, { args: LAUNCH_ARGS, + // Playwright adds `--no-sandbox` on its own unless told otherwise, so leaving this out + // means the flag above decides nothing and a deployment that asked for the sandbox does + // not get one. Verified by reading the launched process arguments, not by trusting either. + chromiumSandbox: SANDBOX_ENABLED, viewport: VIEWPORT, // This process owns shutdown. Playwright's signal handlers kill Chromium immediately on // SIGTERM, before pending cookie writes have time to flush. diff --git a/agent-computer/src/shell.ts b/agent-computer/src/shell.ts new file mode 100644 index 00000000..40e487fa --- /dev/null +++ b/agent-computer/src/shell.ts @@ -0,0 +1,127 @@ +import { spawn } from "node:child_process"; + +/** + * Running a command on the Bot's computer. + * + * WHY THIS EXISTS. A browser answers questions about the web; a shell answers everything else. A Bot + * that can install a tool and run it is a Bot that can do the task rather than describe it. + * + * WHAT MAKES IT SAFE IS NOT THIS FILE. Nothing here decides whether a command may run. The gateway + * decides, against the deployment's policy, and writes the audit row before this is called at all, + * the same as it does for a click. This file is the hands, not the judgement. + * + * WHAT THIS DOES DEFEND is the shape of the call rather than its content: a command cannot run + * forever, cannot return unbounded output, and runs in the workspace rather than wherever the + * process happens to be. + * + * ISOLATION IS THE CONTAINER'S JOB. A shell can reach whatever the container can reach, so the + * deployment that gives a Bot one should give each Bot a computer of its own. In a container shared + * between Bots, a shell is shared too. + */ + +/** Long enough for an install, short enough that a hung command is not a hung Bot. */ +const DEFAULT_TIMEOUT_MS = 120_000; +const MAX_TIMEOUT_MS = 600_000; + +/** + * How much output comes back. + * + * A build log is megabytes and the model that reads this has a context window. Truncated at a size + * a person can still read, and the result says it was truncated rather than quietly ending. + */ +const MAX_OUTPUT_BYTES = 64 * 1024; + +export type ShellResult = { + command: string; + exitCode: number; + stdout: string; + stderr: string; + truncated: boolean; + timedOut: boolean; + elapsedMs: number; +}; + +function clamp(text: string): { text: string; truncated: boolean } { + if (Buffer.byteLength(text, "utf8") <= MAX_OUTPUT_BYTES) { + return { text, truncated: false }; + } + // Kept from the end. A command that fails says why in its last lines, and the first 64KB of a + // build log is the part nobody needs. + const kept = Buffer.from(text, "utf8") + .subarray(-MAX_OUTPUT_BYTES) + .toString("utf8"); + return { text: kept, truncated: true }; +} + +export function createShell(workspaceDir: string) { + return { + async run(input: { + command: string; + timeoutMs?: number; + signal?: AbortSignal; + }): Promise { + const started = Date.now(); + const timeoutMs = Math.min( + input.timeoutMs ?? DEFAULT_TIMEOUT_MS, + MAX_TIMEOUT_MS, + ); + + /* + * Through a shell on purpose. A Bot writes `apt-get install -y jq && jq --version`, and pipes, + * redirection and `&&` are most of why a shell is useful. Refusing them would leave something + * that runs one binary and calls itself a shell. + * + * This is the argument for the container boundary rather than for string parsing: there is no + * safe way to read a command's intent from its text, so nothing here tries. The policy decides + * whether this Bot may run commands at all and what they may say; the container decides what a + * command can reach. + */ + const child = spawn("/bin/bash", ["-lc", input.command], { + cwd: workspaceDir, + env: { ...process.env, HOME: workspaceDir }, + }); + + let stdout = ""; + let stderr = ""; + let timedOut = false; + + child.stdout.on("data", (chunk) => { + stdout += String(chunk); + }); + child.stderr.on("data", (chunk) => { + stderr += String(chunk); + }); + + const timer = setTimeout(() => { + timedOut = true; + child.kill("SIGKILL"); + }, timeoutMs); + + // The person's Stop reaches the command, not just the request that started it. + const onAbort = () => child.kill("SIGKILL"); + input.signal?.addEventListener("abort", onAbort, { once: true }); + + const exitCode = await new Promise((resolve) => { + child.on("close", (code) => resolve(code ?? -1)); + child.on("error", () => resolve(-1)); + }); + + clearTimeout(timer); + input.signal?.removeEventListener("abort", onAbort); + + const out = clamp(stdout); + const err = clamp(stderr); + return { + command: input.command, + exitCode, + stdout: out.text, + stderr: err.text, + truncated: out.truncated || err.truncated, + timedOut, + elapsedMs: Date.now() - started, + }; + }, + }; +} + +export type Shell = ReturnType; diff --git a/app/package.json b/app/package.json index 60e4f996..8fd0e461 100644 --- a/app/package.json +++ b/app/package.json @@ -15,7 +15,7 @@ "dependencies": { "@ag-ui/core": "0.0.57", "@base-ui/react": "^1.6.0", - "@copilotkit/react-core": "1.67.1", + "@copilotkit/react-core": "1.68.3", "@fontsource-variable/inter": "^5.3.0", "@shadcn/react": "^0.3.0", "@tabler/icons-react": "^3.36.1", diff --git a/app/src/components/channels/channel-chat.tsx b/app/src/components/channels/channel-chat.tsx index f27a7dfb..1a127211 100644 --- a/app/src/components/channels/channel-chat.tsx +++ b/app/src/components/channels/channel-chat.tsx @@ -22,6 +22,7 @@ import { ConversationProvider } from "@/lib/copilot/conversation"; import { repairUnansweredToolCalls } from "@/lib/copilot/repair-history"; import { stoppedReason } from "@/lib/copilot/stopped-turn"; import { useSkillCommands } from "@/lib/plugins/skill-commands"; +import { newId } from "../../lib/new-id"; /** * Backstop for the first message of a new channel; a stalled join must not lose the message. @@ -61,7 +62,7 @@ export function ChannelChat({ */ const [seed] = useState(() => { const pending = takeFirstMessage(channel.id); - return pending ? seedMessage(pending, crypto.randomUUID()) : null; + return pending ? seedMessage(pending, newId()) : null; }); /** Cleared by the send-on-mount effect without restarting it. */ @@ -211,14 +212,14 @@ export function ChannelChat({ for (const instruction of skillInstructions) { agent.addMessage({ content: instruction, - id: crypto.randomUUID(), + id: newId(), role: "system", }); } agent.addMessage({ content: trimmed, - id: crypto.randomUUID(), + id: newId(), role: "user", }); report(trimmed, null); diff --git a/app/src/components/channels/conversation-view.tsx b/app/src/components/channels/conversation-view.tsx index 13ac5135..ff7406dc 100644 --- a/app/src/components/channels/conversation-view.tsx +++ b/app/src/components/channels/conversation-view.tsx @@ -16,6 +16,7 @@ import { type QueuedMessage, reduceQueue, } from "@/components/channels/composer"; +import { newId } from "../../lib/new-id"; export function ConversationView({ messages, @@ -147,7 +148,7 @@ export function ConversationView({ const run = apply({ busy: whileBusy, draft, - id: crypto.randomUUID(), + id: newId(), type: "submit", }); return run ? startRef.current(run) : undefined; diff --git a/app/src/lib/computers/queries.ts b/app/src/lib/computers/queries.ts index 23873ae3..03aba9fb 100644 --- a/app/src/lib/computers/queries.ts +++ b/app/src/lib/computers/queries.ts @@ -6,7 +6,8 @@ export type ComputerProfile = { botId: string; running: boolean; startedAt: string | null; - egress: string | null; + /** Absent when the provider does not report egress at all, which is not the same as none. */ + egress?: string | null; }; /** Whether each Bot has a browser profile of its own, or they share one. */ diff --git a/app/src/lib/copilot/bot-thread.ts b/app/src/lib/copilot/bot-thread.ts index 82480558..d1a1366d 100644 --- a/app/src/lib/copilot/bot-thread.ts +++ b/app/src/lib/copilot/bot-thread.ts @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { tryClient } from "@/lib/client"; +import { newId } from "../new-id"; /** * The thread the direct Bot chat talks in. @@ -63,7 +64,7 @@ export function useBotThread(agentId: string): string | undefined { if (!current) return; // Falling back to one made here keeps the chat working when the deployment cannot be asked; // it is simply a thread nothing can later attribute. - const next = minted ?? crypto.randomUUID(); + const next = minted ?? newId(); if (minted) remember(agentId, minted); setThreadId(next); }); diff --git a/app/src/lib/copilot/computer-tools.tsx b/app/src/lib/copilot/computer-tools.tsx index a7be0e15..d72f35f9 100644 --- a/app/src/lib/copilot/computer-tools.tsx +++ b/app/src/lib/copilot/computer-tools.tsx @@ -612,6 +612,58 @@ export function ComputerTools() { }, }); + useFrontendTool({ + name: "computer_run_command", + description: + "Run a shell command on your own computer. Use this for anything the browser cannot do: " + + "installing a tool you need, processing a file you saved, running a script. The working " + + "directory is your workspace, so paths are relative to it and files you write here are the " + + "same ones the file tools see. Commands run in bash, so pipes and && work. Long output is " + + "truncated from the start, and a command that runs too long is stopped. " + + "You are not the root user, so anything that writes outside your workspace needs sudo, " + + "which asks for no password: installing a package is " + + "`sudo apt-get update && sudo apt-get install -y `. If sudo is refused, this " + + "computer does not grant it, so say so rather than retrying.", + parameters: z.object({ + command: z + .string() + .describe("The command to run, such as: sudo apt-get install -y jq"), + }), + handler: async ( + input: { command: string }, + { signal }: { signal?: AbortSignal } = {}, + ) => + callComputer( + bot.current, + "/exec", + { method: "POST", body: input }, + signal, + ), + render: ({ args, result, status }) => { + const outcome = outcomeOf(result); + /* + * The command, not its output. A person watching wants to know what their Bot just ran on a + * machine holding their logins, and that is the command; the output belongs in the answer the + * Bot gives, where the model has already decided which part of it mattered. + */ + return ( + + ); + }, + }); + useFrontendTool({ name: "computer_write_file", description: diff --git a/app/src/lib/copilot/repair-history.ts b/app/src/lib/copilot/repair-history.ts index 424d438b..04ac6156 100644 --- a/app/src/lib/copilot/repair-history.ts +++ b/app/src/lib/copilot/repair-history.ts @@ -1,4 +1,5 @@ import type { Message } from "@ag-ui/core"; +import { newId } from "../new-id"; /** * Insert explanatory tool results for unanswered tool calls before sending history to providers. @@ -21,7 +22,7 @@ function isToolResult(message: Message): message is Message & ToolResult { */ export function repairUnansweredToolCalls( messages: ReadonlyArray, - newId: () => string = () => crypto.randomUUID(), + newId: () => string = () => newId(), ): ReadonlyArray { const answered = new Set(); for (const message of messages) { diff --git a/app/src/lib/new-id.ts b/app/src/lib/new-id.ts new file mode 100644 index 00000000..d07fd661 --- /dev/null +++ b/app/src/lib/new-id.ts @@ -0,0 +1,36 @@ +/** + * A fresh identifier, on every origin this app can be served from. + * + * `crypto.randomUUID` exists only in a secure context. On a laptop that is invisible, because + * `http://localhost` counts as one; on a deployment reached at `http://
` it does not, and + * the function is simply not there. Calling it throws inside a submit handler, the throw is + * swallowed by React's event boundary, and the surface does nothing at all: no message, no error, + * no clue. Found by opening a real deployment over plain HTTP and watching a chat quietly refuse to + * start. This module exists so that failure cannot come back. + * + * `crypto.getRandomValues` has no such restriction and is what the fallback uses, so the ids are as + * random either way. They are not secrets, but they do have to be unique, and `Math.random` is the + * thing to avoid here rather than something to fall back to further. + * + * TLS is still what a deployment should have, for the cookies and the logins if nothing else. The + * point is that a deployment without it should misbehave in ways an operator can see. + */ +export function newId(): string { + if (typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + + const bytes = crypto.getRandomValues(new Uint8Array(16)); + // Version 4 and the RFC variant, so the result is a UUID rather than sixteen random bytes wearing + // the shape of one. + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")); + return [ + hex.slice(0, 4).join(""), + hex.slice(4, 6).join(""), + hex.slice(6, 8).join(""), + hex.slice(8, 10).join(""), + hex.slice(10, 16).join(""), + ].join("-"); +} diff --git a/app/src/routes/_authed/_app/channel/new.tsx b/app/src/routes/_authed/_app/channel/new.tsx index 0c15c6af..520b5f89 100644 --- a/app/src/routes/_authed/_app/channel/new.tsx +++ b/app/src/routes/_authed/_app/channel/new.tsx @@ -21,6 +21,7 @@ import { } from "@/lib/agents/queries"; import { useStartChannel } from "@/lib/channels/start"; import { useSkillCommands } from "@/lib/plugins/skill-commands"; +import { newId } from "../../../../lib/new-id"; /** * Creates the channel on first send. The selected coworker stays in the URL so profile links and @@ -122,7 +123,7 @@ function RouteComponent() { if (!recipient || !canSend(recipients, draft.text)) return; setError(null); - setSent(seedMessage(draft.text, crypto.randomUUID())); + setSent(seedMessage(draft.text, newId())); try { await start(recipient.id, draft.text); diff --git a/app/src/routes/_authed/admin/audit.tsx b/app/src/routes/_authed/admin/audit.tsx index 6a1fffe1..e18600d0 100644 --- a/app/src/routes/_authed/admin/audit.tsx +++ b/app/src/routes/_authed/admin/audit.tsx @@ -173,6 +173,9 @@ function Row({ ) : typeof payload.file === "string" ? ( {payload.file} + ) : typeof payload.command === "string" ? ( + // The command is the subject of its own row, the way a path is for a file action. + {payload.command} ) : typeof element === "object" && element?.name ? ( {element.name} @@ -187,6 +190,7 @@ function Row({ )} {/* Page host is meaningful only for browser actions, not workspace file actions. */} {typeof payload.file !== "string" && + typeof payload.command !== "string" && typeof payload.page === "string" && payload.page ? (
diff --git a/app/src/routes/_authed/admin/boundaries.tsx b/app/src/routes/_authed/admin/boundaries.tsx index 2f74ac12..66644bd0 100644 --- a/app/src/routes/_authed/admin/boundaries.tsx +++ b/app/src/routes/_authed/admin/boundaries.tsx @@ -139,11 +139,12 @@ function BoundariesPage() { about tool.name, intent,{" "} bot.id, actor.id, page.url{" "} and page.host, the element being acted on, the{" "} - key being pressed, the file being touched, and{" "} - mcp.server, mcp.tool and{" "} - mcp.effect for a call to somebody else’s tools. A - rule that cannot be evaluated counts as a match, so a mistyped deny - refuses rather than quietly permitting what it was meant to forbid. + key being pressed, the file being touched, the{" "} + command being run, and mcp.server,{" "} + mcp.tool and mcp.effect for a call to + somebody else’s tools. A rule that cannot be evaluated counts + as a match, so a mistyped deny refuses rather than quietly + permitting what it was meant to forbid. } title="It may never" diff --git a/app/src/routes/_authed/admin/computers.tsx b/app/src/routes/_authed/admin/computers.tsx index 2cb48df3..9d41bb71 100644 --- a/app/src/routes/_authed/admin/computers.tsx +++ b/app/src/routes/_authed/admin/computers.tsx @@ -113,9 +113,11 @@ function ComputersPage() { ? `Browser running since ${new Date(computer.startedAt ?? "").toLocaleTimeString()}` : "No browser running. It starts when the Bot next needs it."} {" ยท "} - {computer.egress - ? `Leaves through ${computer.egress}` - : "Leaves directly"} + {computer.egress === undefined + ? "Egress not reported" + : computer.egress === null + ? "Leaves directly" + : `Leaves through ${computer.egress}`} diff --git a/app/tests/new-id.test.ts b/app/tests/new-id.test.ts new file mode 100644 index 00000000..90f193a7 --- /dev/null +++ b/app/tests/new-id.test.ts @@ -0,0 +1,50 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { newId } from "../src/lib/new-id"; + +/** + * The identifiers a surface mints, on an origin that is not a secure context. + * + * A deployment reached at `http://
` has no `crypto.randomUUID`, and the app used to call it + * directly. The throw landed inside a submit handler, React swallowed it, and the chat did nothing + * at all. These assert the fallback rather than the happy path, because the happy path is the one + * that was never broken. + */ +const UUID_V4 = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +const original = crypto.randomUUID; + +/** An origin that is not a secure context, which is what the absent function means in practice. */ +function withoutRandomUUID(): void { + Object.defineProperty(crypto, "randomUUID", { + configurable: true, + value: undefined, + }); +} + +afterEach(() => { + Object.defineProperty(crypto, "randomUUID", { + configurable: true, + value: original, + }); +}); + +describe("newId", () => { + test("mints a UUID where randomUUID exists", () => { + expect(newId()).toMatch(UUID_V4); + }); + + test("mints one where it does not", () => { + withoutRandomUUID(); + + expect(newId()).toMatch(UUID_V4); + }); + + test("does not repeat itself without randomUUID", () => { + withoutRandomUUID(); + + const ids = new Set(Array.from({ length: 1000 }, () => newId())); + + expect(ids.size).toBe(1000); + }); +}); diff --git a/bun.lock b/bun.lock index b98eb296..892f9efc 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "openbot", "devDependencies": { "@biomejs/biome": "^2.3.8", - "@copilotkit/aimock": "^1.38.0", + "@copilotkit/aimock": "1.39.0", "@types/bun": "^1.3.3", "roughjs": "^4.6.6", "typescript": "^5.9.3", @@ -19,7 +19,7 @@ "dependencies": { "@ag-ui/core": "0.0.57", "@base-ui/react": "^1.6.0", - "@copilotkit/react-core": "1.67.1", + "@copilotkit/react-core": "1.68.3", "@fontsource-variable/inter": "^5.3.0", "@shadcn/react": "^0.3.0", "@tabler/icons-react": "^3.36.1", @@ -61,8 +61,7 @@ "dependencies": { "@ag-ui/client": "0.0.57", "@better-auth/drizzle-adapter": "^1.6.27", - "@copilotkit/runtime": "1.67.1", - "@copilotkit/shared": "1.67.1", + "@copilotkit/runtime": "1.68.3", "@modelcontextprotocol/sdk": "^1.30.0", "better-auth": "^1.6.27", "cel-js": "^0.8.2", @@ -73,7 +72,7 @@ "zod": "^4.4.3", }, "devDependencies": { - "@copilotkit/aimock": "^1.38.0", + "@copilotkit/aimock": "1.39.0", "drizzle-kit": "^0.31.10", "eventsource": "3.0.7", }, @@ -106,26 +105,56 @@ "@ag-ui/proto": ["@ag-ui/proto@0.0.57", "", { "dependencies": { "@ag-ui/core": "0.0.57", "@bufbuild/protobuf": "^2.2.5", "@protobuf-ts/protoc": "^2.11.1" } }, "sha512-pPENOZt0P6ibH8sCTgq05wLYXi5t3P9B5r/1bWYehXjUxtyOdnukSlWM++SsCIwUXsQdm/b3aBgGjEeTF7RenA=="], - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.110", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rNkamQCeAUOUGr5Npg5pXZyYFH4fS1U6Mbdy3dF/NNBEI3D2Chc/ruRrwNegP0gfpX3cllP3O4jSibGBbWPZ7A=="], + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.111", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-atgBW8jZPr/KuaKX5FvDIHuXBI8VCol6kVeoD4P0657+VXR73QsLogXQVN/Zt5FHtq9WzpdIZseCJiXPqkgwwA=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.172", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FSH+0q6wKNq6/WjWy10m3gbeW8MR6D7Qt380evNQinpfG88mOT2sP3x1U82tBsLlKe9O306LPfgQ3NDXy7HNbA=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.177", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-U579dA3K2UcezpzJHjyw6UhctFtzFRKOSIrAgxr77teluPGP7vj8aLJpZvEEPf+JbUqbvsHRKKYZzip/+NQ89A=="], - "@ai-sdk/google": ["@ai-sdk/google@3.0.108", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kwvYpRNghqt0VRKE7Hx1UWZQCUJJFqUITj24baxy+ApS0Hru0PkBJHD75a36Wc+e6e+wHcKR2MconTeJiBZigA=="], + "@ai-sdk/google": ["@ai-sdk/google@3.0.111", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XWWju5pVD7Tf30c2VziMfPph3XpN/jDTE5aJhqAPJ2zRmHSSveNYFoosFLOWWFIgQnwGe9AM3bAXR8kHyC2MdA=="], - "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.163", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.95", "@ai-sdk/google": "2.0.88", "@ai-sdk/openai-compatible": "1.0.48", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.32", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0JuayxQV4TgAkhoNjMX4jPDmGvq3hV24ijE8PO1VlnFoSwMOsm4NmVse0eemM9WJ98SmHMV65Syu72meo+2JJA=="], + "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.164", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.95", "@ai-sdk/google": "2.0.89", "@ai-sdk/openai-compatible": "1.0.48", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.32", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TJ54snZgD5kxWsHmMETkLhoN5333GKp6Me47HkGB01dnLMnrxSD3cSTiEompnZY4tTonxwm1Jl78dUiTKDaKXQ=="], - "@ai-sdk/mcp": ["@ai-sdk/mcp@1.0.70", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45", "pkce-challenge": "^5.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rgbR+UZQF2DTsYUxgqTFjpFMaqXvQoGo9hS9q27qNpnNu5Pfv0qzUCg41YyED19cv3/LmF4tcpBY0YALsfdMxQ=="], + "@ai-sdk/mcp": ["@ai-sdk/mcp@1.0.71", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46", "pkce-challenge": "^5.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-woKUwm/yiqxEC+E863tqAjEpiyWCERmy//k9pdNYP8Ta6+PF6Ge2X6p99e72BdBovB+/McUWgbJ4+Fiht+41WQ=="], - "@ai-sdk/openai": ["@ai-sdk/openai@3.0.96", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Pex8vOj1y05j7jtBS39cJJRDjJbMIyCY9+01cSIp1hwEJTKImrFejMgsAazMWXSi/HU+B9ZE6ElftCOwvg4mmQ=="], + "@ai-sdk/openai": ["@ai-sdk/openai@3.0.99", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-0Y6E5bvR+3eugt5rq2iy39EAAFi4nijXf1kdvcfoMm4erEGMe/6egIpvdvv5ePBU2YsnsxFuLRmMJ76Kv/babQ=="], "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.48", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.32" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Zl5+5VHId7g344LVSjZvPuFuRVej+u+MB+0ibFVGaXG1qUykyjKu2ptHinK5RjDdviCm+Bs+t3ZLegB39e8VyA=="], "@ai-sdk/provider": ["@ai-sdk/provider@3.0.15", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-XeZW1CcDF2GMbH4wejW6xBRI2QCOgnkVYUnxoeDadB1mf85riL2bMUeDoh+6gJ/r4mjNfzUPW8OjLjvwTP0u1Q=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.45", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7u5B/E2uZmU65SlJhhQGFHZwRCN0xOz4HHtFc4sEGV9PHbX3fGiEiZBpc/SABay1dGeJgK3VD60rvLGoWdWPXA=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.46", "", { "dependencies": { "@ai-sdk/provider": "3.0.15", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^6.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tEtld97plCFiYevsJuOkGkeuhQndeMWFBVrJS4AjnbD5AqrNSXRCe0p+BZ3Cju/sxDeeZ9ym3q9YUV8fASA7aQ=="], "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], + "@aws-sdk/core": ["@aws-sdk/core@3.977.8", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@aws-sdk/xml-builder": "^3.972.39", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.31.1", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-7+Kcrkvrk9lM/m7jRhHpT4jCdvzGHsuaSRbF8TdzzkY1mRzp/Ogwf9c7H29k4gGhey0BBWhCWr16+t0J61gwmg=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.69", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-AreCFzcB4kH2HF9031Ot0jSJr3KXvRg6e8uDeub20JEVdZU3Bv0sTq1plc7VsT3KiqutlzH7l0j50UcCWHUioA=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.71", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-A8ObcqVmDMnk4F9NozZ7JwmUu9Q4xyBJkmyq1C5U+wNM9ht9J7+EuuyabsLWXZnOoTqFaJuYBYTKf5CTipkEjA=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.14", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-login": "^3.972.76", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-7c+Wti2LsERNWMfm7ySz3/6RPopFW3Nmn7s63Xpcq6R/tRuY5hpvkHA2xVgi5ukJbvok9l0IDtVEvqTtg+X7dw=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-LVixwOnEJfrrfKHeZjBA8pIMTZjNDq8ak8VpcoWUuCJDrSnBNU8POJksULMgvN089P0MXtQYH2Zs627/MK1K0g=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.80", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.69", "@aws-sdk/credential-provider-http": "^3.972.71", "@aws-sdk/credential-provider-ini": "^3.973.14", "@aws-sdk/credential-provider-process": "^3.972.69", "@aws-sdk/credential-provider-sso": "^3.973.13", "@aws-sdk/credential-provider-web-identity": "^3.972.75", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bE2qh8ww4iClO1jHsBXdOE8FUgzDbdxbyorNjSCoPSkQd51k3jODItuPZfuwcLHZqDXsH+bI4AMHhqtuyR7mSg=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.69", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-9kpTNdZTrcqXTfhxM7fgl9Z68ek3Fu5oe3Yf+A/pJGibEqpgZxz2tSY7SinmyCIU2PJ+ygY4FPoBBnLpocMtrQ=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.13", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/token-providers": "3.1111.0", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-Oc81qauMPzUoTnAS2YKpNwY6sY/LUyQTEeaf6yP197WMxkEBQfcKLR1MFpD7+pNTubXnfkH6gwpji+Gc7iyD2Q=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.75", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-YPN6uoGDgjjjeVFZrcOeCJqmB6zpXoeeNgIjqe+DexJaWqdjVfCCe+VAZwli9Z2h8KhFW8oxkO39emQ1tyz/Mw=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.43", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/signature-v4-multi-region": "^3.996.45", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/fetch-http-handler": "^5.6.13", "@smithy/node-http-handler": "^4.9.13", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bit+VpqWNyi3wHxFoTsTliNXimCSL2r2OeDTm7ZrG+YsTZ2D7ofDJ6r/t9PVBn80i6/v0X2h9Tgw6QP2MAKfPw=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.45", "", { "dependencies": { "@aws-sdk/types": "^3.974.4", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-bBuyztukzXq6plzFGHAWiQt0QXo+HL8b8lX5cFTzkez/74PtS1c0qPFCIVuHkyoT+miH2qOjAcm1/yoro2ESPA=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1111.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.8", "@aws-sdk/nested-clients": "^3.997.43", "@aws-sdk/types": "^3.974.4", "@smithy/core": "^3.31.1", "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-JfljgoVtl+s3Qy21n9a7Z48uCQaOXcN74KJ3TEQfPoB293GrXFSt6HSQJF1sTZ8c/5QedEvd3NjJQMO4u9qa5A=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.974.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-dSFDNG00MEz0/xl5gxL62giLd1iYyJsTxZ1I1DOj6lC+bbgLB4TRsYClJg3b62dhXT1uATzsTNXPnC+33EJV3A=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.39", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-FTti8DS5MMWXNUWiRwXAJeYS+0GHHiMy0+7XOhcwk63ILHmfS2UFy2z/HNpZCSOJJ3P3dnWY6hfYNW3DF0nXUA=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@azure/abort-controller": ["@azure/abort-controller@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg=="], "@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="], @@ -202,41 +231,41 @@ "@base-ui/utils": ["@base-ui/utils@0.3.2", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.12", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-oWy1aq/I2GmYjpl4PhEAhzflF8VPGKgZeq0xAWTbfD5KBWyxcN0ZP2+WHSUm/5Z6lVMBDLReLcoXwSYoRc/zNQ=="], - "@better-auth/core": ["@better-auth/core@1.6.27", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.4.0", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-A6/mQW4AT2kSHCRZDh9+k8jPUqnCUUCymzBgIroK0l60wLioMtJdcmZiTwrAn6KVUw+4XWw64MgaRn7LOie3wg=="], + "@better-auth/core": ["@better-auth/core@1.7.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.41.1", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.4.0", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-eZ9lqcnVLMZ3QtUByRo4VZqkB1ESyRddd9NfWjBdDPgh+jcwLScoIUAqhtHLR8zaSUJZah8OLGlkzObyPdUH7A=="], - "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.27", "", { "peerDependencies": { "@better-auth/core": "^1.6.27", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-BDJ02ra/fji/ah3aIpxjjNOu/JQVex0UisxS4S0vGZZyiAs+DL24mn3/iovyD5dWB3u3NaGg1n8zpTOntiIXcg=="], + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.7.1", "", { "peerDependencies": { "@better-auth/core": "^1.7.1", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-qlqNyg5V9bXHSP68/vtlsiZayhR4hgvEGiS/E3SIj8bCpWWFGmyQkxJbQCqpBmC7vT30wE/kNtJMHIgnV3rkiw=="], - "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.27", "", { "peerDependencies": { "@better-auth/core": "^1.6.27", "@better-auth/utils": "0.4.2", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-aavv7W4+b3QVObYo166baplWvwocCk8ORDxRqQ9ytzVl/waiUpSXAOtDHdXZHFsoVHFVPtEzyEq0Tqr3Qvvfig=="], + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.7.1", "", { "peerDependencies": { "@better-auth/core": "^1.7.1", "@better-auth/utils": "0.4.2", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-yWCpE1cZpMUj37nD6JFDK+GDR8zS37L5WI73il3qbU9TXtWsxUQKc/5c3IHsHizWQsmcQI8uv2pAFKxsRDa+AQ=="], - "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.27", "", { "peerDependencies": { "@better-auth/core": "^1.6.27", "@better-auth/utils": "0.4.2" } }, "sha512-p4NB37MdaVFkxkpLEAKjORgTPhaOAPu1M2zgQXiTJJ3F6Uq3Y1YcCgoz+VxJu0TQfxibCsJD/dZlJMVgf+CF7A=="], + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.7.1", "", { "peerDependencies": { "@better-auth/core": "^1.7.1", "@better-auth/utils": "0.4.2" } }, "sha512-6NX1yv88DeqdoG7owYFqKwlrDGaIPhsC52JUGrUgeGVKyOq8a/6hHlHsqG1C2FwT23SHQiKVYCEAG9N6aH5OvQ=="], - "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.27", "", { "peerDependencies": { "@better-auth/core": "^1.6.27", "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-IOYbJMIjEC//f+JpFlmIQjh6x1R/rGAfdzlojFk6HeAJqbsELuMcI+27dIoesbfHLF/icTrSpZtK986XhJrCfA=="], + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.7.1", "", { "peerDependencies": { "@better-auth/core": "^1.7.1", "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-9ILTcNqhG37QK//qR4UhYLyKzNqq6w6zVTf5KX6xkiTjNcV7Oh1yS31lkIJEVTqRNcy9AoV6FZMW6Bbsm8IMDA=="], - "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.27", "", { "peerDependencies": { "@better-auth/core": "^1.6.27", "@better-auth/utils": "0.4.2", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-v7DXVyaFbkrfLoiFDtcVF7BkEZgFa/DgEGE7XjrAXmMACa5pjDvb7lm8W5X+/qgIbQP04eThhgFQ6EWOsjr8OQ=="], + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.7.1", "", { "peerDependencies": { "@better-auth/core": "^1.7.1", "@better-auth/utils": "0.4.2", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-ZiUcafQ85InAofcUjyGgCPjKLfQjXr9SvDmMjuFUW8oEbreA6C6GaFAEA77VuV2doZUQlzvQQ4gCoTmS28W92A=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.6.27", "", { "peerDependencies": { "@better-auth/core": "^1.6.27", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1" } }, "sha512-aYrSiVWQfua8w5YX8X1cM6ca48EdS0t5k+CvYXIsruaNIpR1DXMe1DOD0M5FWAVABnlCf9joyHXbFRSIZSNY/A=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.7.1", "", { "peerDependencies": { "@better-auth/core": "^1.7.1", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1" } }, "sha512-kLKjMfFlTbyt49DGeI9okHAsn0MtBZcMoQYKaEdgR0H3BHzqqyzePcQz/hxAmRgjB4p/6inise3zJwhX0sgXrQ=="], "@better-auth/utils": ["@better-auth/utils@0.4.2", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="], - "@biomejs/biome": ["@biomejs/biome@2.5.8", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.8", "@biomejs/cli-darwin-x64": "2.5.8", "@biomejs/cli-linux-arm64": "2.5.8", "@biomejs/cli-linux-arm64-musl": "2.5.8", "@biomejs/cli-linux-x64": "2.5.8", "@biomejs/cli-linux-x64-musl": "2.5.8", "@biomejs/cli-win32-arm64": "2.5.8", "@biomejs/cli-win32-x64": "2.5.8" }, "bin": { "biome": "bin/biome" } }, "sha512-aeAeeJB9fSDc7Gq+2GqpQxA0qBj6gj1k2R6L1cYqGePKP/baIq1WX8y6B+D+nRsO5ViQL22K/8IwbqERW0q1nw=="], + "@biomejs/biome": ["@biomejs/biome@2.5.9", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.9", "@biomejs/cli-darwin-x64": "2.5.9", "@biomejs/cli-linux-arm64": "2.5.9", "@biomejs/cli-linux-arm64-musl": "2.5.9", "@biomejs/cli-linux-x64": "2.5.9", "@biomejs/cli-linux-x64-musl": "2.5.9", "@biomejs/cli-win32-arm64": "2.5.9", "@biomejs/cli-win32-x64": "2.5.9" }, "bin": { "biome": "bin/biome" } }, "sha512-KkgCvdHB4IhtpHpF564plA9jo6fDOwWGQ/3jvreLzgOtRLEDoPqr7QO9qejNA8jKwDsSkAKr77hqBHnyUbIw4g=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mk1QON9PHllvvLN5gU3f4rMxeh4syK5p9OvKyWH6/W8ueh04uaC8TUXXByhGufWf/y5mQc03ZLM45zU+cmqMjA=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-am22pX2aBqznqq1eMyIj/bZ++riF3Lk6ct7cbv+gQK0csFhr+d8O0RkOi2FF2qSgFgANbqNkIZ0/PxlnW2pLFg=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-bsGwFMBNyHPyiLSsQcZJxdoRrg1V4JL+d7wEsvUBczlP9U9lwM+7mzQHxI4o1mhBsTmdOBbAb6fHU3Z3snN45w=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-l44KWDHLDvEnD0N/XcrVs7VXb3A18xL7QS3WB0eL93wbmk529ffIG55vleGCqaunpRUjLrdnjK05Qki1dsjylg=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-XmFiA0WPYFC+uiUDC8WRFzAIH9bo7vwQLav38Uoq4ETC+T/+uBi0TsYGJECkugY3r8USl3jc+Ae2/irAF6F2lQ=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-ICaK+IYaVZvKbBxX2rwrPT0DdUDMnE9Vm3nQGe+mltQPmUg19pONzkPWGdY4FCsoreDETWDynvdt4ysCbF5gNQ=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-VcJNbstduTHx83NGAdhp78/JOcP45BZHXL7yNsfI1uGzdUgegAz2s+mSoT7wK6PBNzLoqG0zDOXaz/RQYVtSiw=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-7ImVPwBLCtkmpR5esd8RHhTqW94f0JLJQum6AneYcy94jRm18TaPPm7slaigGzFhfgt3QiD1Vj52LKmBAnKizA=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.8", "", { "os": "linux", "cpu": "x64" }, "sha512-S5wcm9OBDvLHodD4PUaN488hCpco9QD/9ZxuYJiw4euWtr/oQvLR72z2ixItH8Wd5BCm6FZaeb+YNvOoM1xHtQ=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.9", "", { "os": "linux", "cpu": "x64" }, "sha512-z22Q/zFYSvbIJfW1CbfZPu4X8PddS6Qd2ORbc6h+aT6EcwAxUF3m6fA4HjNvA3TU4X0dTJRwNPB165ES3PJXzg=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.8", "", { "os": "linux", "cpu": "x64" }, "sha512-kKmiyokeISRGq2FLwvr+TzsgBusfxaZ0FZNLcOYOpCK/78tRrEjeEBLvq3xLZMpqbANgJdRPI7vZX8ZL37u9/w=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.9", "", { "os": "linux", "cpu": "x64" }, "sha512-RXGaD0o1/pTTguYw1aeDJh9ad6Lfrui0fI7mBderTyGr7WuUJkBIttgLkR3XJyoxOkkgfBDspaUT8wXArTqLZw=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-nILH0mzm3Hi3iEdd7o7GpB8kBR/mSQwfQG/tyBqyNrY2GFtcgwfV9nV8xLmbtUpMNY/Oi0Ml1XgfR4flOdq+AA=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-nHK+/HHC+D0ogAHUxomgoSTdjImb6fmNNVTKmf0tyu4eDL1DqPKIHc+i+UL8+b0RnAu8224qo8F2tCVnaT0A3w=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.8", "", { "os": "win32", "cpu": "x64" }, "sha512-I2czzXTY61f3nFJxXoMDq80t7MivxDEnCjE+8sDKoFfcKMaoQdkqhIFQ3KyY0XLzeSpUBYeNAXgD+iOV/BU0VA=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.9", "", { "os": "win32", "cpu": "x64" }, "sha512-Yiq0H56LjXSSw/hd9YkXgSLQfzyDJzbzU2TezozxyNw+uKWAqOtqGVvBfzKRRDiaFF5avGAhHdWKx7LtDOShUw=="], "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], @@ -254,35 +283,35 @@ "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], - "@copilotkit/a2ui-renderer": ["@copilotkit/a2ui-renderer@1.67.1", "", { "dependencies": { "@a2ui/web_core": "0.10.4", "clsx": "^2.1.1", "lit": "^3.3.2", "zod": "^3.25.75", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" }, "optionalPeers": ["react", "react-dom"] }, "sha512-/fELhn0gvC+LCurKwdxrF1BRiPbpYUU5UmZjwy1bu1q8brQbXs8UjCcz7oCiCRfhXirzAwEH2LwdPS9ElHJPNw=="], + "@copilotkit/a2ui-renderer": ["@copilotkit/a2ui-renderer@1.68.3", "", { "dependencies": { "@a2ui/web_core": "0.10.4", "clsx": "^2.1.1", "lit": "^3.3.2", "zod": "^3.25.75", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" }, "optionalPeers": ["react", "react-dom"] }, "sha512-rnD94CIJtLAGDmrJCjwJAFtAUwgNXRt+Ril+rf4hUdUxcYgCNvV43RYOenahbYAF+M9GBrBe+9zWlkLfjtUIig=="], - "@copilotkit/aimock": ["@copilotkit/aimock@1.38.0", "", { "peerDependencies": { "jest": ">=29", "vitest": ">=3" }, "optionalPeers": ["jest", "vitest"], "bin": { "aimock": "dist/aimock-cli.js", "llmock": "dist/cli.js" } }, "sha512-zrGXeSu0/LkHQgrM9Z68uU/k7qpYbnZ25i9djpNlYyDCDCUgKaQPbG1iIYOc+4vuigJmD91ujcK36mBKWz+JVw=="], + "@copilotkit/aimock": ["@copilotkit/aimock@1.39.0", "", { "peerDependencies": { "jest": ">=29", "vitest": ">=3" }, "optionalPeers": ["jest", "vitest"], "bin": { "aimock": "dist/aimock-cli.js", "llmock": "dist/cli.js" } }, "sha512-AWw4vmW2hBchHoggh0G4McWGmGZD6wtXAehL6K5ncWF5lVIjlv++bPmxmRwrpQCi/K4/xK10N9Zp9srJYipEJw=="], - "@copilotkit/channels-core": ["@copilotkit/channels-core@0.8.1", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@ag-ui/core": "0.0.57", "@copilotkit/channels-ui": "~0.8.1", "@copilotkit/core": "^1.67.0", "@copilotkit/shared": "^1.67.0", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "vitest": "^4.0.0" }, "optionalPeers": ["vitest"] }, "sha512-FOopmGLUncVCZ7g2s6opvJZoiAFaU49cgu2+T6TGtVfTVZLTaO5OzDNEOafIqh7xMm6TfMWCjr1o1oZ3njKKpw=="], + "@copilotkit/channels-core": ["@copilotkit/channels-core@0.9.0", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@ag-ui/core": "0.0.57", "@copilotkit/channels-ui": "~0.9.0", "@copilotkit/core": "^1.68.0", "@copilotkit/shared": "^1.68.0", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "vitest": "^4.0.0" }, "optionalPeers": ["vitest"] }, "sha512-bCWLb/jb9j8O+JvOvf/jkJ/Nb8B09U/tAdTCQ221mE2AEHS2dn5PMlQih9gqyF6kbJFO0Nmf+IDTritWswmfaA=="], - "@copilotkit/channels-intelligence": ["@copilotkit/channels-intelligence@0.8.1", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@copilotkit/channels-core": "^0.8.1", "@copilotkit/channels-slack": "^0.8.1", "@copilotkit/channels-teams": "^0.8.1", "@copilotkit/channels-ui": "^0.8.1", "phoenix": "^1.8.4" } }, "sha512-b9KtG81v52JhyguyVMyUm4VgARAV6WDfQBB2hLHMI89GwQlAA1LhWYkrPUoG6fk29P3SO8I/LYJjonRes3km5Q=="], + "@copilotkit/channels-intelligence": ["@copilotkit/channels-intelligence@0.9.0", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@copilotkit/channels-core": "^0.9.0", "@copilotkit/channels-slack": "^0.9.0", "@copilotkit/channels-teams": "^0.9.0", "@copilotkit/channels-ui": "^0.9.0", "phoenix": "^1.8.4" } }, "sha512-w+ARYm+i30buMGJB+E3QFBze41s464MKkXGikgLYg3k9WEFHE3BgTRKz6MbHxv0yN9L6mOAjwMhR8LkTSufMcw=="], - "@copilotkit/channels-slack": ["@copilotkit/channels-slack@0.8.1", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@ag-ui/core": "0.0.57", "@copilotkit/channels-core": "^0.8.1", "@copilotkit/channels-ui": "^0.8.1", "@copilotkit/core": "^1.67.0", "@copilotkit/shared": "^1.67.0", "@slack/bolt": "^4.2.0", "@slack/types": "^2.21.1", "@slack/web-api": "^7.16.0", "rxjs": "^7.8.1", "zod": "^3.25.76", "zod-to-json-schema": "^3.25.1" } }, "sha512-rrXmX7x/FnJWyhwOlvesz3lMcFXqXMqnF6yeHHMUuCyuYsH54dxZLj0nb3MYMFX40LLuNlUbQtEIHlX2HkKh7A=="], + "@copilotkit/channels-slack": ["@copilotkit/channels-slack@0.9.0", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@ag-ui/core": "0.0.57", "@copilotkit/channels-core": "^0.9.0", "@copilotkit/channels-ui": "^0.9.0", "@copilotkit/core": "^1.68.0", "@copilotkit/shared": "^1.68.0", "@slack/bolt": "^4.2.0", "@slack/types": "^2.21.1", "@slack/web-api": "^7.16.0", "rxjs": "^7.8.1", "zod": "^3.25.76", "zod-to-json-schema": "^3.25.1" } }, "sha512-3QkCInbQmruSP875x02YG1Ez59jPzQMDUlWRlNKPvzcAvnPfOmHxaXVVmBnS+fwcWccd4AAaeRvslTWBwU9s6g=="], - "@copilotkit/channels-teams": ["@copilotkit/channels-teams@0.8.1", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@ag-ui/core": "0.0.57", "@copilotkit/channels-core": "^0.8.1", "@copilotkit/channels-ui": "^0.8.1", "@copilotkit/core": "^1.67.0", "@copilotkit/shared": "^1.67.0", "@microsoft/agents-activity": "^1.5.3", "@microsoft/agents-hosting": "^1.5.3", "express": "^4.21.2", "rxjs": "^7.8.1", "zod": "^3.25.76", "zod-to-json-schema": "^3.25.1" } }, "sha512-tvFkNBwPSGTYR//uJ5WIrLLFtp047XO2I8OJkllhTcASRvCLcQkQokhOfukxiS8iEwGiuOxwbCuVlOi6ibhkLw=="], + "@copilotkit/channels-teams": ["@copilotkit/channels-teams@0.9.0", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@ag-ui/core": "0.0.57", "@copilotkit/channels-core": "^0.9.0", "@copilotkit/channels-ui": "^0.9.0", "@copilotkit/core": "^1.68.0", "@copilotkit/shared": "^1.68.0", "@microsoft/agents-activity": "^1.5.3", "@microsoft/agents-hosting": "^1.5.3", "express": "^4.21.2", "rxjs": "^7.8.1", "zod": "^3.25.76", "zod-to-json-schema": "^3.25.1" } }, "sha512-OI1xaIg9Ho7vMUgPpxM2h4LZQRvQKRt+1yy2Er8nMBy1IJXQaNP4of4XI5pQA/WnskL/07xcGPrFiXrTRs4Hyg=="], - "@copilotkit/channels-ui": ["@copilotkit/channels-ui@0.8.1", "", { "dependencies": { "@copilotkit/shared": "^1.67.0" } }, "sha512-ZHhfpPP5UYs8Fjl9bDBVFfuJUcV/Kq8vxkksTJuFsfEZTpExALTOq9EPEHV+Pr/flVzIdrlJilnsL7fC4tOsJQ=="], + "@copilotkit/channels-ui": ["@copilotkit/channels-ui@0.9.0", "", { "dependencies": { "@copilotkit/shared": "^1.68.0" } }, "sha512-6nhmIuexyW+fOBp3BE3ZWk4Mco5NOG4sr//BZ46RynSYYD36klTQQbTKA0ntSR6nIGJ/luAGc8WbVdPH8srYOA=="], - "@copilotkit/core": ["@copilotkit/core@1.67.1", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@copilotkit/shared": "1.67.1", "@tanstack/pacer": "^0.20.1", "phoenix": "^1.8.4", "rxjs": "7.8.1", "zod-to-json-schema": "^3.24.6" } }, "sha512-QRMsznHFzia+ZF5G8/cn9+91IOZzztUTZi0nXYw/3nENP6Vb8SLCnoH8hUiwGTo8TeoJMhfasGVG8Kt3MpNWNw=="], + "@copilotkit/core": ["@copilotkit/core@1.68.3", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@copilotkit/shared": "1.68.3", "@tanstack/pacer": "^0.20.1", "phoenix": "^1.8.4", "rxjs": "7.8.1", "zod-to-json-schema": "^3.24.6" } }, "sha512-HcMX5/QwYGsEBLqZUxCeTqhCPaihtCq1Fjm6xUygX6WVrdbqtalKWuRQNFCwvzB0YMYvnNNZLRYcp37PprJq9A=="], "@copilotkit/license-verifier": ["@copilotkit/license-verifier@0.5.0", "", {}, "sha512-vrwKtIpYwF0FT9ZoYASH8owa2cGV0dhDvJGaCRaRMStwDxpc6DRdydKkhx8cWZXyBRxEYcq/Vygv4JvevhQQdQ=="], - "@copilotkit/react-core": ["@copilotkit/react-core@1.67.1", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@ag-ui/core": "0.0.57", "@copilotkit/a2ui-renderer": "1.67.1", "@copilotkit/core": "1.67.1", "@copilotkit/runtime-client-gql": "1.67.1", "@copilotkit/shared": "1.67.1", "@copilotkit/web-components": "1.67.1", "@copilotkit/web-inspector": "1.67.1", "@jetbrains/websandbox": "^1.1.3", "@lit-labs/react": "^2.0.2", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.7", "@scarf/scarf": "^1.3.0", "@tanstack/react-virtual": "^3.13.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "katex": "^0.16.22", "lit": "^3.3.2", "lucide-react": "^0.525.0", "react-markdown": "^8.0.7", "rxjs": "7.8.1", "streamdown": "^1.3.0", "tailwind-merge": "^3.3.1", "tw-animate-css": "^1.3.5", "untruncate-json": "^0.0.1", "use-stick-to-bottom": "^1.1.1", "zod-to-json-schema": "^3.24.5" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc", "zod": ">=3.0.0" } }, "sha512-daW5zMR8ujruq42NGJoqCBgHgrwAejRn6iK7+FJEkCmLoV8OonS0/fDsuLdWsuWfhHxmRP+CGjRwfwqfaE5kGg=="], + "@copilotkit/react-core": ["@copilotkit/react-core@1.68.3", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@ag-ui/core": "0.0.57", "@copilotkit/a2ui-renderer": "1.68.3", "@copilotkit/core": "1.68.3", "@copilotkit/runtime-client-gql": "1.68.3", "@copilotkit/shared": "1.68.3", "@copilotkit/web-components": "1.68.3", "@copilotkit/web-inspector": "1.68.3", "@jetbrains/websandbox": "^1.1.3", "@lit-labs/react": "^2.0.2", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.7", "@scarf/scarf": "^1.3.0", "@tanstack/react-virtual": "^3.13.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "katex": "^0.16.22", "lit": "^3.3.2", "lucide-react": "^0.525.0", "react-markdown": "^8.0.7", "rxjs": "7.8.1", "streamdown": "^1.3.0", "tailwind-merge": "^3.3.1", "tw-animate-css": "^1.3.5", "untruncate-json": "^0.0.1", "use-stick-to-bottom": "^1.1.1", "zod-to-json-schema": "^3.24.5" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc", "zod": ">=3.0.0" } }, "sha512-CRlwG7VeDmUofAvlOzw+RI/3+vCPR6NJD66MxUtBTMmHhPaEEahXSBSBqwLJ9MwLSCO+83J1dTLeTuGmWivDjg=="], - "@copilotkit/runtime": ["@copilotkit/runtime@1.67.1", "", { "dependencies": { "@ag-ui/a2ui-middleware": "0.0.10", "@ag-ui/client": "0.0.57", "@ag-ui/core": "0.0.57", "@ag-ui/encoder": "0.0.57", "@ag-ui/langgraph": "0.0.42", "@ag-ui/mcp-apps-middleware": "0.0.3", "@ag-ui/mcp-middleware": "0.0.1", "@ai-sdk/anthropic": "^3.0.49", "@ai-sdk/google": "^3.0.33", "@ai-sdk/google-vertex": "^3.0.97", "@ai-sdk/mcp": "^1.0.21", "@ai-sdk/openai": "^3.0.36", "@copilotkit/channels-core": "^0.8.1", "@copilotkit/channels-intelligence": "0.8.1", "@copilotkit/license-verifier": "~0.5.0", "@copilotkit/shared": "1.67.1", "@graphql-yoga/plugin-defer-stream": "^3.3.1", "@hono/node-server": "^1.13.5", "@modelcontextprotocol/sdk": "^1.18.2", "@remix-run/node-fetch-server": "^0.13.0", "@scarf/scarf": "^1.3.0", "@segment/analytics-node": "^2.1.2", "ai": "^6.0.104", "clarinet": "^0.12.4", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cors": "^2.8.5", "express": "^4.21.2", "graphql": "^16.8.1", "graphql-scalars": "^1.23.0", "graphql-yoga": "^5.3.1", "hono": "^4.11.4", "openai": "^4.85.1 || >=5.0.0", "partial-json": "^0.1.7", "phoenix": "^1.8.4", "pino": "^9.2.0", "pino-pretty": "^11.2.1", "reflect-metadata": "^0.2.2", "rxjs": "7.8.1", "type-graphql": "2.0.0-rc.1", "uuid": "^10.0.0", "ws": "^8.18.0", "zod": "^3.23.3" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.57.0", "@langchain/aws": ">=0.1.9", "@langchain/community": ">=0.3.58", "@langchain/core": ">=0.3.66", "@langchain/google-gauth": ">=0.1.0", "@langchain/langgraph-sdk": ">=0.1.2", "@langchain/openai": ">=0.4.2", "groq-sdk": ">=0.3.0 <1.0.0", "langchain": ">=0.3.3" }, "optionalPeers": ["@anthropic-ai/sdk", "@langchain/aws", "@langchain/community", "@langchain/google-gauth", "@langchain/langgraph-sdk", "@langchain/openai", "groq-sdk", "langchain"] }, "sha512-q3WT/YhzXhRc/K82rGfSyb5K8jSuYV8On51WTQ8dRlx8pm7H4+z/uFuIPA8uO5RKy8Blm1GPatyPozqFuNbh0w=="], + "@copilotkit/runtime": ["@copilotkit/runtime@1.68.3", "", { "dependencies": { "@ag-ui/a2ui-middleware": "0.0.10", "@ag-ui/client": "0.0.57", "@ag-ui/core": "0.0.57", "@ag-ui/encoder": "0.0.57", "@ag-ui/langgraph": "0.0.42", "@ag-ui/mcp-apps-middleware": "0.0.3", "@ag-ui/mcp-middleware": "0.0.1", "@ai-sdk/anthropic": "^3.0.49", "@ai-sdk/google": "^3.0.33", "@ai-sdk/google-vertex": "^3.0.97", "@ai-sdk/mcp": "^1.0.21", "@ai-sdk/openai": "^3.0.36", "@copilotkit/channels-core": "^0.9.0", "@copilotkit/channels-intelligence": "0.9.0", "@copilotkit/license-verifier": "~0.5.0", "@copilotkit/shared": "1.68.3", "@graphql-yoga/plugin-defer-stream": "^3.3.1", "@hono/node-server": "^1.13.5", "@modelcontextprotocol/sdk": "^1.18.2", "@remix-run/node-fetch-server": "^0.13.0", "@scarf/scarf": "^1.3.0", "@segment/analytics-node": "^2.1.2", "ai": "^6.0.104", "clarinet": "^0.12.4", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cors": "^2.8.5", "express": "^4.21.2", "graphql": "^16.8.1", "graphql-scalars": "^1.23.0", "graphql-yoga": "^5.3.1", "hono": "^4.11.4", "openai": "^4.85.1 || >=5.0.0", "partial-json": "^0.1.7", "phoenix": "^1.8.4", "pino": "^9.2.0", "pino-pretty": "^11.2.1", "reflect-metadata": "^0.2.2", "rxjs": "7.8.1", "type-graphql": "2.0.0-rc.1", "uuid": "^11.1.0", "ws": "^8.18.0", "zod": "^3.23.3" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.57.0", "@langchain/aws": ">=0.1.9", "@langchain/community": ">=0.3.58", "@langchain/core": ">=0.3.66", "@langchain/google-gauth": ">=0.1.0", "@langchain/langgraph-sdk": ">=0.1.2", "@langchain/openai": ">=0.4.2", "groq-sdk": ">=0.3.0 <1.0.0", "langchain": ">=0.3.3" }, "optionalPeers": ["@anthropic-ai/sdk", "@langchain/aws", "@langchain/community", "@langchain/google-gauth", "@langchain/langgraph-sdk", "@langchain/openai", "groq-sdk", "langchain"] }, "sha512-BOgYbRIXOXLcgiW+OISuMrzxXyXN0Ghk1xuHbd8XPTtgTl82NGBnv8XHHfxsMolta3SVAUaIkcphaUlw2439MQ=="], - "@copilotkit/runtime-client-gql": ["@copilotkit/runtime-client-gql@1.67.1", "", { "dependencies": { "@copilotkit/shared": "1.67.1", "@urql/core": "^5.0.3", "untruncate-json": "^0.0.1", "urql": "^4.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-ir2OUeA0iNhLzTVC9peee7qRiLYhje9tLKWdFSVpcFFGpRUAQcz23larOy6r+4hEOp56hV+DQS5wBrS//AqvyA=="], + "@copilotkit/runtime-client-gql": ["@copilotkit/runtime-client-gql@1.68.3", "", { "dependencies": { "@copilotkit/shared": "1.68.3", "@urql/core": "^5.0.3", "untruncate-json": "^0.0.1", "urql": "^4.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-GJRdcVU7Z3RDd8N6RDgMFi9M0QidU8UxWnns4d40UhriZxXN51jeGnw8E21P5DLtF41wfyn39gfK2SRvnm/jCQ=="], - "@copilotkit/shared": ["@copilotkit/shared@1.67.1", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@copilotkit/license-verifier": "~0.5.0", "@segment/analytics-node": "^2.1.2", "@standard-schema/spec": "^1.0.0", "chalk": "4.1.2", "graphql": "^16.8.1", "partial-json": "^0.1.7", "uuid": "^11.1.0", "zod": "^3.23.3", "zod-to-json-schema": "^3.23.5" }, "peerDependencies": { "@ag-ui/core": ">=0.0.48" } }, "sha512-hGruBM3EDNg+wM76ECfUEj5xpnj7rOkzv/n/k5brEpdiIIs13v4/wQZf8nw2Rv6uyym7LO3yNRfbgKtcHsQvyQ=="], + "@copilotkit/shared": ["@copilotkit/shared@1.68.3", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@copilotkit/license-verifier": "~0.5.0", "@segment/analytics-node": "^2.1.2", "@standard-schema/spec": "^1.0.0", "chalk": "4.1.2", "graphql": "^16.8.1", "partial-json": "^0.1.7", "uuid": "^11.1.0", "zod": "^3.23.3", "zod-to-json-schema": "^3.23.5" }, "peerDependencies": { "@ag-ui/core": ">=0.0.48" } }, "sha512-y8d7zrz4T81cpZwYHjVa4SOMbEhPzdlxJN5I9Q1JiasIuwcMEbuxXlk5H0Jc5xhZlb0vmaOsJ1ednatTe4gPdg=="], - "@copilotkit/web-components": ["@copilotkit/web-components@1.67.1", "", { "peerDependencies": { "lit": "^3.3.2" } }, "sha512-ypuXZqA/Vlk7xhvpYPI+u4DnIDV0ZhzHzjo7V0/By6EL7Wy4XAjpLI+1d9FxEWC8JEL0QJVkn17UJMpfnmeEoQ=="], + "@copilotkit/web-components": ["@copilotkit/web-components@1.68.3", "", { "peerDependencies": { "lit": "^3.3.2" } }, "sha512-7pj1HXhk2DG/1jdvpJmJIQxmxWsl3RRdme9ganf7HWKn5COLlTp7k+mkgk+1s3iKaAno2snGuWdyGa7xNQDR1g=="], - "@copilotkit/web-inspector": ["@copilotkit/web-inspector@1.67.1", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@copilotkit/core": "1.67.1", "@copilotkit/shared": "1.67.1", "lit": "^3.2.0", "lucide": "^0.525.0", "marked": "^12.0.2" } }, "sha512-DskVsDrUtZPt9Ymh6UxGElUIETBq8Lxa/vtl9eUdvJ+Ply1xUaGKwfNUpIaGiMWzTML4X7diSVdVrmemlnlYjQ=="], + "@copilotkit/web-inspector": ["@copilotkit/web-inspector@1.68.3", "", { "dependencies": { "@ag-ui/client": "0.0.57", "@copilotkit/core": "1.68.3", "@copilotkit/shared": "1.68.3", "lit": "^3.2.0", "lucide": "^0.525.0", "marked": "^12.0.2" } }, "sha512-VJtKeNEA86FV1jrwVeXvoV3F8Iors9BclV5t7rfGduCw67rxVgl4wWydeOLqiJ/Jlm9F+nDywN3zPSJjjspjaQ=="], "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.75.1", "", { "dependencies": { "@dotenvx/primitives": "^0.8.0", "commander": "^11.1.0", "conf": "^10.2.0", "dotenv": "^17.2.1", "enquirer": "^2.4.1", "env-paths": "^2.2.1", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "open": "^8.4.2", "picomatch": "^4.0.4", "systeminformation": "^5.22.11", "undici": "^7.11.0", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ=="], @@ -290,7 +319,7 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], - "@envelop/core": ["@envelop/core@5.5.1", "", { "dependencies": { "@envelop/instrumentation": "^1.0.0", "@envelop/types": "^5.2.1", "@whatwg-node/promise-helpers": "^1.2.4", "tslib": "^2.5.0" } }, "sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw=="], + "@envelop/core": ["@envelop/core@5.6.0", "", { "dependencies": { "@envelop/instrumentation": "^1.0.0", "@envelop/types": "^5.2.1", "@whatwg-node/promise-helpers": "^1.2.4", "tslib": "^2.5.0" } }, "sha512-cD7HNfAzJVw/0Pxneu51UAKzUGLvkctk9rr9DVJ9b7FDe4nSa9kAGMRxx145H6ooELIUMjTd2buk3PuvjJmp/A=="], "@envelop/instrumentation": ["@envelop/instrumentation@1.0.0", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.2.1", "tslib": "^2.5.0" } }, "sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw=="], @@ -364,25 +393,25 @@ "@fontsource-variable/inter": ["@fontsource-variable/inter@5.3.0", "", {}, "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA=="], - "@graphql-tools/executor": ["@graphql-tools/executor@1.5.7", "", { "dependencies": { "@graphql-tools/utils": "^11.2.2", "@graphql-typed-document-node/core": "^3.2.0", "@repeaterjs/repeater": "^3.1.0", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-UcXVClkBml+qyGEsQxfEaAkboqySVGFUd9ivn7pDc9jZSckgF6zL21cNxuRH5ZA2exneV3PTtcc0I0rDOdE0Tg=="], + "@graphql-tools/executor": ["@graphql-tools/executor@2.0.0", "", { "dependencies": { "@graphql-tools/utils": "^12.0.0", "@graphql-typed-document-node/core": "^3.2.0", "@repeaterjs/repeater": "^3.1.0", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-BjoqO5UfcV3BUqamGtHNm4ITBk040VOmWql/MrUefPo30x6t4v1+engfjgAjWikMsRPLU4ZC6e2QjICLucnOTA=="], "@graphql-tools/merge": ["@graphql-tools/merge@9.2.3", "", { "dependencies": { "@graphql-tools/utils": "^12.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-cKRoXqJGy2zSRBLvotQpkACbXlHAb0yuHLN0l0ypKGCuL3NnF0zofYalkBJqiBxxxpK0lr3s9uowKFHCKi1/ZQ=="], "@graphql-tools/schema": ["@graphql-tools/schema@10.1.0", "", { "dependencies": { "@graphql-tools/merge": "^9.2.3", "@graphql-tools/utils": "^12.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-wao48XQnfY631s3jXoNrhEHvCI8mlKXmIuWrR7F6zAdv92VuSOfHoq9P9KL2EnUMgBUnaStnByOx9Mn6RieWDg=="], - "@graphql-tools/utils": ["@graphql-tools/utils@10.11.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q=="], + "@graphql-tools/utils": ["@graphql-tools/utils@11.2.2", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-Do2A/t6ayqOma8m7PvpYMsCBswL+NB35BQSng4GWSBqafL2/BnfAZy/NSENPwV721Rm2fG0BHETFC0+FJJZmLw=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], "@graphql-yoga/logger": ["@graphql-yoga/logger@2.0.1", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA=="], - "@graphql-yoga/plugin-defer-stream": ["@graphql-yoga/plugin-defer-stream@3.21.3", "", { "dependencies": { "@graphql-tools/utils": "^10.11.0" }, "peerDependencies": { "graphql": "^15.2.0 || ^16.0.0", "graphql-yoga": "^5.21.3" } }, "sha512-DnwO7lDWGZMvdv3ps5T15km9I9+pftyfS3XXA9oYJmYjexPJQJQThh7+cEgMeCxOvAZjqJfFWhLyrEdDzfVZtw=="], + "@graphql-yoga/plugin-defer-stream": ["@graphql-yoga/plugin-defer-stream@3.22.0", "", { "dependencies": { "@graphql-tools/utils": "^11.2.0" }, "peerDependencies": { "graphql": "^15.2.0 || ^16.0.0 || ^17.0.0", "graphql-yoga": "^5.22.0" } }, "sha512-7F8/ADgCxRIUgGrG0mWEOI5HB0FYQeUj2YbH88+U05Fe72lrGufKpxOPN3O70jECPukh8jKFtyXfqjl5POPEzA=="], "@graphql-yoga/subscription": ["@graphql-yoga/subscription@5.0.5", "", { "dependencies": { "@graphql-yoga/typed-event-target": "^3.0.2", "@repeaterjs/repeater": "^3.0.4", "@whatwg-node/events": "^0.1.0", "tslib": "^2.8.1" } }, "sha512-oCMWOqFs6QV96/NZRt/ZhTQvzjkGB4YohBOpKM4jH/lDT4qb7Lex/aGCxpi/JD9njw3zBBtMqxbaC22+tFHVvw=="], "@graphql-yoga/typed-event-target": ["@graphql-yoga/typed-event-target@3.0.2", "", { "dependencies": { "@repeaterjs/repeater": "^3.0.4", "tslib": "^2.8.1" } }, "sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA=="], - "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.11.2", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.11.2" } }, "sha512-7fkpoXZWzyxaBkzRtlD0wRU5ckxbNT4j6BywzpSYh+SFPsGxEVmNR9KHaMwM2xkHB0pADQLl4Z8DSrztECJ7Ww=="], + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.11.6", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.11.6" } }, "sha512-ZQ47qUTeNbGhHkCGExJ1oZhruoxKRaxO44RgFETl3T4c1rRxIBlAOnM3SAH4XHu7Ue2owJXP+jOx1vOuKuxcSg=="], "@hono/node-server": ["@hono/node-server@1.19.17", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ=="], @@ -402,13 +431,13 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@langchain/core": ["@langchain/core@1.2.7", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "@standard-schema/spec": "^1.1.0", "js-tiktoken": "^1.0.12", "langsmith": ">=0.5.0 <1.0.0", "mustache": "^4.2.0", "p-queue": "^6.6.2", "zod": "^3.25.76 || ^4" } }, "sha512-NKEjQimC9IR7YKkfirH9Hq1BIFFKd4tjhWvbtjggS+5mE+bM4ZeFwTIUe9BDIz9zi3hzG4DLbRMi5E8k/PwVTA=="], + "@langchain/core": ["@langchain/core@1.2.9", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "@standard-schema/spec": "^1.1.0", "js-tiktoken": "^1.0.12", "langsmith": ">=0.5.0 <1.0.0", "mustache": "^4.2.0", "p-queue": "^6.6.2", "zod": "^3.25.76 || ^4" } }, "sha512-conzSEj9Zu1AyXJLXsSbgrtxtxinmI1yGqQ5CIJZSoV5rvv+yvQE/vgBnoySpBQ/bl3YPgj2FL/gbDjWykLSfg=="], - "@langchain/langgraph": ["@langchain/langgraph@1.4.9", "", { "dependencies": { "@langchain/langgraph-checkpoint": "^1.1.3", "@langchain/langgraph-sdk": "~1.9.28", "@langchain/protocol": "^0.0.18", "@standard-schema/spec": "1.1.0" }, "peerDependencies": { "@langchain/core": "^1.1.48", "zod": "^3.25.32 || ^4.2.0" } }, "sha512-EvD9rS66Cya09y6rbMgD3Ir8miAkJQFo7FyJOPRPO736Kz3y5TeyeBDOS8ctff/jRc788bPijHx2NVFM79Qqig=="], + "@langchain/langgraph": ["@langchain/langgraph@1.4.12", "", { "dependencies": { "@langchain/langgraph-checkpoint": "^1.1.5", "@langchain/langgraph-sdk": "~1.9.30", "@langchain/protocol": "^0.0.18", "@standard-schema/spec": "1.1.0" }, "peerDependencies": { "@langchain/core": "^1.1.48", "zod": "^3.25.32 || ^4.2.0" } }, "sha512-63iH/igH5Fh5fHqmWp09YYWaDKKB9v4RCmYNJBrnQ224rFRbjebgyYW6o5RCczN5FZxIhQj+xT51rrNmG0zi5A=="], - "@langchain/langgraph-checkpoint": ["@langchain/langgraph-checkpoint@1.1.3", "", { "peerDependencies": { "@langchain/core": "^1.1.48" } }, "sha512-wgzdQNeEsdw1e+4lvlj0tdq/RYR/k1vPin10g0ymGoehZDDgd9nvIllGXSXN4TFgF9sf5qQP/KTkOcLfeseIhA=="], + "@langchain/langgraph-checkpoint": ["@langchain/langgraph-checkpoint@1.1.5", "", { "peerDependencies": { "@langchain/core": "^1.1.48" } }, "sha512-BwDwl5VeTOh6CVuiIPgsUgfK51vTJDMSbFcSCUfjJWsl8/DPdK/mbv+ejxJstkSk/BlSPMP4JfXWcN6jD2ea2Q=="], - "@langchain/langgraph-sdk": ["@langchain/langgraph-sdk@1.9.29", "", { "dependencies": { "@langchain/protocol": "^0.0.18", "@types/json-schema": "^7.0.15", "p-queue": "^9.0.1", "p-retry": "^7.1.1" }, "peerDependencies": { "@langchain/core": "^1.1.48", "react": "^18 || ^19", "react-dom": "^18 || ^19", "svelte": "^4.0.0 || ^5.0.0", "vue": "^3.0.0" }, "optionalPeers": ["react", "react-dom", "svelte", "vue"] }, "sha512-W+ccugM5EzBHvaOieyYxlSbhAy6HWF8Tyn6b/BlTWuFd8xtcnQOz2WuFyofcAc9+aQHE+6yHJfIywGOvjxS+bA=="], + "@langchain/langgraph-sdk": ["@langchain/langgraph-sdk@1.9.30", "", { "dependencies": { "@langchain/protocol": "^0.0.18", "@types/json-schema": "^7.0.15", "p-queue": "^9.0.1", "p-retry": "^7.1.1" }, "peerDependencies": { "@langchain/core": "^1.1.48", "react": "^18 || ^19", "react-dom": "^18 || ^19", "svelte": "^4.0.0 || ^5.0.0", "vue": "^3.0.0" }, "optionalPeers": ["react", "react-dom", "svelte", "vue"] }, "sha512-hPzbhenvvFRn46PHnlkad3E+TuUIfRhNGpekR5cY12D1WKh3Viwfyz9HpzCbfVebChU9Kgk494V9RJTnh1jrPQ=="], "@langchain/protocol": ["@langchain/protocol@0.0.18", "", {}, "sha512-XW1egQtPfsGI41w2AMZNFZrUIwFSQHTjVMZs0OaTpCAvht/QLoaPN8FQcsysMVypOhupG28J29yOorrc70otBQ=="], @@ -424,7 +453,7 @@ "@lukeed/uuid": ["@lukeed/uuid@2.0.1", "", { "dependencies": { "@lukeed/csprng": "^1.1.0" } }, "sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w=="], - "@mermaid-js/parser": ["@mermaid-js/parser@1.2.0", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA=="], + "@mermaid-js/parser": ["@mermaid-js/parser@1.2.1", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw=="], "@microsoft/agents-activity": ["@microsoft/agents-activity@1.7.2", "", { "dependencies": { "zod": "3.25.75" } }, "sha512-aMPZFuyIkdAdKxscyEZ6o6m60rB+JbWE4n02HzP3QOmILWBfddRMxYlK6gFLd86Wpf1hYGvfJBdpx8Av5ZRBIA=="], @@ -448,6 +477,26 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.221.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.10.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ=="], + + "@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.221.0", "", { "dependencies": { "@opentelemetry/otlp-exporter-base": "0.221.0", "@opentelemetry/otlp-transformer": "0.221.0", "@opentelemetry/sdk-trace": "2.10.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Z9i2T7vgZbWe9rSLYxXVIbeW+XyzUq4rZanW3ZyVNwVDqCsh0EJKUgBWWQ0CZfeuUA+RQPzKgJQHMuWAUnKqXw=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.221.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/otlp-transformer": "0.221.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA=="], + + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.221.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.221.0", "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/sdk-logs": "0.221.0", "@opentelemetry/sdk-metrics": "2.10.0", "@opentelemetry/sdk-trace": "2.10.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.221.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.221.0", "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ=="], + + "@opentelemetry/sdk-trace": ["@opentelemetry/sdk-trace@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.10.0", "", { "dependencies": { "@opentelemetry/core": "2.10.0", "@opentelemetry/resources": "2.10.0", "@opentelemetry/sdk-trace": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ=="], + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], @@ -518,55 +567,55 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.4", "", { "os": "android", "cpu": "arm" }, "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.5", "", { "os": "android", "cpu": "arm" }, "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.4", "", { "os": "android", "cpu": "arm64" }, "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.5", "", { "os": "android", "cpu": "arm64" }, "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.4", "", { "os": "linux", "cpu": "arm" }, "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw=="], - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA=="], + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w=="], - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ=="], + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw=="], - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ=="], - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w=="], + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.4", "", { "os": "linux", "cpu": "none" }, "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.4", "", { "os": "linux", "cpu": "x64" }, "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw=="], - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ=="], + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw=="], - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.4", "", { "os": "none", "cpu": "arm64" }, "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ=="], + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.5", "", { "os": "none", "cpu": "arm64" }, "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA=="], - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw=="], + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.4", "", { "os": "win32", "cpu": "x64" }, "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg=="], "@scarf/scarf": ["@scarf/scarf@1.4.0", "", {}, "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ=="], @@ -608,6 +657,18 @@ "@slack/web-api": ["@slack/web-api@7.19.0", "", { "dependencies": { "@slack/logger": "^4.0.1", "@slack/types": "^2.21.0", "@types/node": ">=18", "@types/retry": "0.12.0", "axios": "^1.16.0", "eventemitter3": "^5.0.1", "form-data": "^4.0.4", "is-electron": "2.2.2", "is-stream": "^2", "p-queue": "^6", "p-retry": "^4", "retry": "^0.13.1" } }, "sha512-ItjyjEZml+LDH8CjcCLRLJHh7VZtevPKExrRN3l5KWyBliyDnGAeoO4Y+K+fFBmRpKLYVPgqWMX4THldv2HVtA=="], + "@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.7.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow=="], + + "@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@tabler/icons": ["@tabler/icons@3.46.0", "", {}, "sha512-f2RYFl3fzPwj5WO82x6en0dmkjefxEfOm16D1ByM6cj/McNiwOkL4VaPUoP9VVIrXAD9WnTSVFr70px703b//A=="], @@ -660,23 +721,23 @@ "@tanstack/react-query": ["@tanstack/react-query@5.101.4", "", { "dependencies": { "@tanstack/query-core": "5.101.4" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA=="], - "@tanstack/react-router": ["@tanstack/react-router@1.170.27", "", { "dependencies": { "@tanstack/history": "1.162.1", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.22", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Hxl49xzd8ffWd2ZMigqfXZmpySpixWGvjq5zfh2nK2DbzzDH6IGVh+iUuwarK9MJNcnhWPZ85tOoy6o/OmNlww=="], + "@tanstack/react-router": ["@tanstack/react-router@1.170.31", "", { "dependencies": { "@tanstack/history": "1.162.1", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.26", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-jqITLcf9Y+es9Wm7fD7XfXFGKk1Fujm+okGqHtr+UhISnQuyLQ8d3frsJlIN0Np1doYoeb5weozSkv72+EZ5bw=="], "@tanstack/react-store": ["@tanstack/react-store@0.11.1", "", { "dependencies": { "@tanstack/store": "0.11.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HaIGKI3YLmjBYIvy5DFDY23oNaYZIsTZfngey07Uh5iLVJgM3bIGCnZeOFOqzjFld9JHWcaHJnasD/bKoGKwJQ=="], - "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.9", "", { "dependencies": { "@tanstack/virtual-core": "3.17.7" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-qZyr0FZDP8rDC4WBhsryIZmAd9bveJvFGUJJtskWaew6/0dTRS6wZxnR6VQ5bY2KwL3LjerrHqQLk3a0GKcPXQ=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.10", "", { "dependencies": { "@tanstack/virtual-core": "3.17.8" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-SRyoUbdFMRHuYXMijV5H4ZarQWpXkj3iANq8OFre+pybeVap8ZJjZ3Nz9bVjx4d8PfobVUQUdKyyyHYk3E+djw=="], - "@tanstack/router-core": ["@tanstack/router-core@1.171.22", "", { "dependencies": { "@tanstack/history": "1.162.1", "cookie-es": "^3.0.0", "seroval": "^1.6.2", "seroval-plugins": "^1.6.2" } }, "sha512-sitsuRkz4qpTjIAV97S5zFCoeGv0OFd6+VWg3ZcIlQbi5R4NIXfz6ogg51n5w6rvTwSODz/lKWb5dl5tvh2bQw=="], + "@tanstack/router-core": ["@tanstack/router-core@1.171.26", "", { "dependencies": { "@tanstack/history": "1.162.1", "cookie-es": "^3.0.0", "seroval": "^1.6.2", "seroval-plugins": "^1.6.2" } }, "sha512-VymmPSs/93szHur/7PBygT6tRbmfcNO1xR46Wn/JVHeBqVduLPCuxBeJEgfVmVq8E4hSklPEh8i6qGXQd6WRqA=="], - "@tanstack/router-generator": ["@tanstack/router-generator@1.167.28", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.22", "@tanstack/router-utils": "1.162.2", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^4.4.3" } }, "sha512-AxvzdqQoBxrA8hoO1fHYg4cAUVug/xLqQhsGbNG1PelU3RbYRYEtDim7+HbYQCOqJaEuvV8tCvrTPTnT69iIwg=="], + "@tanstack/router-generator": ["@tanstack/router-generator@1.167.32", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.26", "@tanstack/router-utils": "1.162.2", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^4.4.3" } }, "sha512-KKPWUMUda7JYil+uDQPrX4ecyoXP9J3DesdpavOWpGCavsiAd6cxH6yq18krLGa1j20qmPJc10LJSLOOFia5tQ=="], - "@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.30", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.22", "@tanstack/router-generator": "1.167.28", "@tanstack/router-utils": "1.162.2", "chokidar": "^5.0.0", "unplugin": "^3.0.0", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.26", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-Z53FeZjSddyn3c+lmUGHOU3bhZPpaFabcynja8/s87CZeyMYS7oNgUMoaYZAhVLk9cAEC/z3fkqISXqktZadDA=="], + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.34", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.26", "@tanstack/router-generator": "1.167.32", "@tanstack/router-utils": "1.162.2", "chokidar": "^5.0.0", "unplugin": "^3.0.0", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.31", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-WsXLQU8tQmoTyoQRB1JI3lGg5wnwo6VB2R8D2RQbtCVlUi3bjreSNgZSJ7Ghw06ZoUyNMO+Csiqf679nNnzN+w=="], "@tanstack/router-utils": ["@tanstack/router-utils@1.162.2", "", { "dependencies": { "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ=="], "@tanstack/store": ["@tanstack/store@0.11.1", "", {}, "sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA=="], - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.7", "", {}, "sha512-bp+v10y65sp2H7WpWfIMyxTNfl8ZVfxFTLRjPIFRryi6FV/J33z4IS53WO4pTk36KlvJ4iLiQz+oaydDC1xbcA=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.8", "", {}, "sha512-BfEvehNpOT75r5Ksc5xW6NZuXujTfb7nlSEyVu4XHG3gdxNg1KqXruWbDewXOUaUYIo4oRbSfkjIajz4MAT8tA=="], "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="], @@ -684,7 +745,7 @@ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], - "@testing-library/user-event": ["@testing-library/user-event@14.6.4", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew=="], + "@testing-library/user-event": ["@testing-library/user-event@14.6.5", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-FhqjldLTpteueBaKflhNFlMT3+PM0O5fiBUivht6b9CZ1eesJyy7+g3Jr7XwJzt/Hip3ZG5hWwK1MX1FuDiE4w=="], "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], @@ -700,7 +761,7 @@ "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], - "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], @@ -856,7 +917,7 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ai": ["ai@6.0.253", "", { "dependencies": { "@ai-sdk/gateway": "3.0.172", "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.45", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NNwp47xpD4c47NB0ad+MP8HL66f8g6l0JwZ2UdHR6PpBPk9WACEq1k7md3r0GVR6skqAU/Slt5Hk9mJYQwJkvA=="], + "ai": ["ai@6.0.260", "", { "dependencies": { "@ai-sdk/gateway": "3.0.177", "@ai-sdk/provider": "3.0.15", "@ai-sdk/provider-utils": "4.0.46", "@opentelemetry/api": "^1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-vMaV6Mh3+b9+XaSrKA76zx1ku7Z/6kE0VeQ26AsZDoJXpsswreOa/TlVjQxkqwEQgdkRB1NBgrUTdIPq8xUSAQ=="], "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], @@ -866,7 +927,7 @@ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "ansis": ["ansis@4.3.1", "", {}, "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA=="], @@ -898,9 +959,9 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.13", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H/bNPUFHewJHyCTdjn1n3Pit5+2GmWT6mmeHImPX+8MA9NA6b67jO4gYmi4jTbCJb2otq34KMZnovndDPqJwhQ=="], - "better-auth": ["better-auth@1.6.27", "", { "dependencies": { "@better-auth/core": "1.6.27", "@better-auth/drizzle-adapter": "1.6.27", "@better-auth/kysely-adapter": "1.6.27", "@better-auth/memory-adapter": "1.6.27", "@better-auth/mongo-adapter": "1.6.27", "@better-auth/prisma-adapter": "1.6.27", "@better-auth/telemetry": "1.6.27", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.4.0", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-x3jyxpAiBSsMO/DaXMFSVwM8bTqnYZlCKsZxBV43zL3ZtsGa/CzeUuNKxPT2Lsz7ahTBY90Ku1tibN6nRpVEbw=="], + "better-auth": ["better-auth@1.7.1", "", { "dependencies": { "@better-auth/core": "1.7.1", "@better-auth/drizzle-adapter": "1.7.1", "@better-auth/kysely-adapter": "1.7.1", "@better-auth/memory-adapter": "1.7.1", "@better-auth/mongo-adapter": "1.7.1", "@better-auth/prisma-adapter": "1.7.1", "@better-auth/telemetry": "1.7.1", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.2.0", "@noble/hashes": "^2.2.0", "better-call": "1.4.0", "defu": "^6.1.4", "jose": "^6.2.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.3.0", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-g8WlTQijxXWJjPVZfFu1+EJg9cwwHrKDmIkcYMzx8CzYA+tDxl6NI7qQbKkbgw5UtHILsT5VH+RMzFzwnVJqAg=="], "better-call": ["better-call@1.4.0", "", { "dependencies": { "@better-auth/utils": "^0.5.0", "@better-fetch/fetch": "^1.3.1", "rou3": "^0.9.1", "set-cookie-parser": "^3.1.2" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA=="], @@ -910,6 +971,8 @@ "boring-avatars": ["boring-avatars@2.0.4", "", { "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-xhZO/w/6aFmRfkaWohcl2NfyIy87gK5SBbys8kctZeTGF1Apjpv/10pfUuv+YEfVPkESU/h2Y6tt/Dwp+bIZPw=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -924,7 +987,7 @@ "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -1092,11 +1155,11 @@ "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], - "dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="], + "dayjs": ["dayjs@1.11.23", "", {}, "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ=="], "debounce-fn": ["debounce-fn@4.0.0", "", { "dependencies": { "mimic-fn": "^3.0.0" } }, "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], @@ -1104,7 +1167,7 @@ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], + "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -1132,7 +1195,7 @@ "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - "dompurify": ["dompurify@3.4.13", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ=="], + "dompurify": ["dompurify@3.4.14", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg=="], "dot-prop": ["dot-prop@6.0.1", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="], @@ -1150,7 +1213,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.405", "", {}, "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew=="], + "electron-to-chromium": ["electron-to-chromium@1.5.411", "", {}, "sha512-gglkxzokjHfawpGxq75XdBV2/l3BAPzrsMs70qgaZdTW5rpV1tC4MdgJVP9fN126bODA4ZJQkn1wryEzJyQXIg=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -1176,7 +1239,7 @@ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "es-toolkit": ["es-toolkit@1.50.0", "", {}, "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w=="], + "es-toolkit": ["es-toolkit@1.51.0", "", {}, "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw=="], "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], @@ -1222,6 +1285,8 @@ "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], + "fastdom": ["fastdom@1.0.12", "", { "dependencies": { "strictdom": "^1.0.1" } }, "sha512-LB+xjSTEbjHE1cWsxu+tN2Xqr1kpi+V9aADI7sVM5ZMaXyYGPHULQMzpJMYqOTULK/73pUkWVzzObFRBkPr+hg=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -1236,7 +1301,7 @@ "find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], - "follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], @@ -1244,7 +1309,7 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "framer-motion": ["framer-motion@13.1.0", "", { "dependencies": { "motion-dom": "^13.0.0", "motion-utils": "^13.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-QSZrF0Id3QGuHJ+OL+9PSY9pk86C8ERFalwAGSchzTm65+ZoGH/RM26lmEARLljcHj2lqhv0jZOOks+EI3COOw=="], + "framer-motion": ["framer-motion@13.1.1", "", { "dependencies": { "motion-dom": "^13.1.1", "motion-utils": "^13.0.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-B/xn2TPS4f61cEBLFjiYlQFnBZUW1YVj/LM+C+N4OP8Rs95VLEI2ot/RlfBg111la/EiyECFaJJi/A3FWA8MUA=="], "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], @@ -1274,7 +1339,7 @@ "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], - "get-tsconfig": ["get-tsconfig@4.14.2", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-XpwZALwwl/BaKTAyC6+c5T8y6kCg2jk+XGqOVrKIQmW49pNypYLMRjCUXqa28tQgJlhS2RlzP7sc+Rx7W6qsfw=="], + "get-tsconfig": ["get-tsconfig@4.14.3", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA=="], "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1292,11 +1357,11 @@ "graphql-scalars": ["graphql-scalars@1.26.0", "", { "dependencies": { "tslib": "^2.5.0" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-o7BnU5yxAFUWTMKpC5yshB12DjzzoxLgEBO9OpOAcxodWrL3sPldD/fqVQoIfcqPH8OclyQilLtg3/7ZRqQUVQ=="], - "graphql-yoga": ["graphql-yoga@5.21.3", "", { "dependencies": { "@envelop/core": "^5.5.1", "@envelop/instrumentation": "^1.0.0", "@graphql-tools/executor": "^1.5.0", "@graphql-tools/schema": "^10.0.11", "@graphql-tools/utils": "^10.11.0", "@graphql-yoga/logger": "^2.0.1", "@graphql-yoga/subscription": "^5.0.5", "@whatwg-node/fetch": "^0.10.6", "@whatwg-node/promise-helpers": "^1.3.2", "@whatwg-node/server": "^0.11.0", "lru-cache": "^10.0.0", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^15.2.0 || ^16.0.0" } }, "sha512-1hJTKZd7k2V0Q2tFMb+rIUSbcovL6uIkkKnW2W9HcldtrC1udukdf5CswKi6SzaI5keG4scxKPEolyZ6U83i/g=="], + "graphql-yoga": ["graphql-yoga@5.22.0", "", { "dependencies": { "@envelop/core": "^5.6.0", "@envelop/instrumentation": "^1.0.0", "@graphql-tools/executor": "^2.0.0", "@graphql-tools/schema": "^10.0.11", "@graphql-tools/utils": "^11.2.0", "@graphql-yoga/logger": "^2.0.1", "@graphql-yoga/subscription": "^5.0.5", "@whatwg-node/fetch": "^0.10.6", "@whatwg-node/promise-helpers": "^1.3.2", "@whatwg-node/server": "^0.11.0", "lru-cache": "^10.0.0", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^15.2.0 || ^16.0.0 || ^17.0.0" } }, "sha512-RTMS9WfDJQdYT1VRwDQ34Q9zFvuD3PZXBEDedNdUuoeNsJ6jZBwPo5PhdExzkR+s5iwf8MoiGJOS5Byt/PNAZg=="], "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], - "happy-dom": ["happy-dom@20.11.2", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw=="], + "happy-dom": ["happy-dom@20.11.6", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-Hldbg8AdAa5a5oDcZpjqnGitp7JB0hqWmfv/8qr+kft4vzSD8BHsbdRfzYvL/0QcbKcURC/yyoygbeDQarPvYg=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -1338,7 +1403,7 @@ "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], - "hono": ["hono@4.13.2", "", {}, "sha512-JydRilDRkYBQMt9qR9U92mXxmbGqsqSn/IKOrh4e7/gEbn+0zSr8igTu0obwJoNGN4sez28DIql7FBHWydoJpA=="], + "hono": ["hono@4.13.3", "", {}, "sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw=="], "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], @@ -1366,7 +1431,7 @@ "inline-style-parser": ["inline-style-parser@0.1.1", "", {}, "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="], - "internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], "ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="], @@ -1422,7 +1487,7 @@ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "jose": ["jose@6.2.8", "", {}, "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ=="], + "jose": ["jose@6.2.9", "", {}, "sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA=="], "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], @@ -1464,13 +1529,13 @@ "kysely": ["kysely@0.29.5", "", {}, "sha512-ooa+eSbBNPTo3MycPEuW5jdrxQdQwdtB3LC3h43FiXQbIry5tR0C5lDG7eealK0E4D7XjrnOP5DIUg/LyjRMYQ=="], - "langchain": ["langchain@1.5.8", "", { "dependencies": { "@langchain/langgraph": "^1.4.8", "@langchain/langgraph-checkpoint": "^1.1.3", "langsmith": ">=0.5.0 <1.0.0", "zod": "^3.25.76 || ^4" }, "peerDependencies": { "@langchain/core": "^1.2.7" } }, "sha512-wW7T+E73w9Pu3OovLlSaF6Np50ivCECrNG8Y+BPZmYMUMhzA2Azd8OXCq7hA+kdMkGff39daM4HQZjhBfrHQKg=="], + "langchain": ["langchain@1.5.10", "", { "dependencies": { "@langchain/langgraph": "^1.4.10", "@langchain/langgraph-checkpoint": "^1.1.5", "langsmith": ">=0.5.0 <1.0.0", "zod": "^3.25.76 || ^4" }, "peerDependencies": { "@langchain/core": "^1.2.9" } }, "sha512-JaC12C1qyGn985vvjttr4hr8lfFzWhrXp2M1byZJGmNJ2RiIgqnhiYDuLlG/xHDxhKD3onJ5pCuUif/cbdqPhA=="], - "langsmith": ["langsmith@0.8.10", "", { "dependencies": { "p-queue": "6.6.2" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*", "ws": ">=7" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-proto", "@opentelemetry/sdk-trace-base", "openai", "ws"] }, "sha512-OG07MX2vyWcLyx/RhXPcydZVpuJSmCrcI1PxVmOR7TOoGWnGV68yUIaA85kc9v0gBkyD6cWa2VnBNBZcTN/qcQ=="], + "langsmith": ["langsmith@0.9.0", "", { "dependencies": { "p-queue": "6.6.2" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*", "ws": ">=7" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-proto", "@opentelemetry/sdk-trace-base", "openai", "ws"] }, "sha512-tlg/aG7qezAKY6G3fgADSX7PkRj+JKoF3z7QNkCMsAOvwvuzhiwP9Amn1Z+zAIxuKoWuXQdIjtFN0LVmUC1oUQ=="], "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], - "libphonenumber-js": ["libphonenumber-js@1.13.10", "", {}, "sha512-xJxrdqvbl2rtn2MaUJrUejz8J7/uZNC0V77oks2LxYrO/+ZtVpRmz+fEQMuu6VusnEB1fmpByiLS1WXecOnAnw=="], + "libphonenumber-js": ["libphonenumber-js@1.13.11", "", {}, "sha512-ETER2kMaIFTI/Nh1a8Gk03dUF/SL0VZqtI+CcVHZxp5WIHYwNS7S+uiYZDYCvLy3lOR4/DAD5jf0h5WkePPpqg=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -1594,7 +1659,7 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "mermaid": ["mermaid@11.16.1", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.2.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.20", "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", "katex": "^0.16.45", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g=="], + "mermaid": ["mermaid@11.17.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.2.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.34.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.21", "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", "fastdom": "1.0.12", "katex": "^0.16.47", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-Jo9N377Wb4MSnHFPTbLi2SxFpsQl4eVHoxnW5U1Md9EazvgMp3s+4ohDxr81YNTgbn5Kj7HJ3yslrSJ52kwpbA=="], "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], @@ -1678,9 +1743,9 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "motion": ["motion@13.1.0", "", { "dependencies": { "framer-motion": "^13.1.0", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-qtvscq59uCPdWnNW4SdSkrxR+BS/QYsa923bx7ocA+4p+ZGNbbVQwkSnG4aukB81QWjtl3AxX36plxNyZLmHCA=="], + "motion": ["motion@13.1.1", "", { "dependencies": { "framer-motion": "^13.1.1", "tslib": "^2.4.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-WNZoK6xiF+kkTqkZ5K7FDDh6A8BG4i5Hc7KXtW8gtTxkpJFds+hIOrDaQGKjQj/AE/i4hJqAaUHEqp/Qo02y6Q=="], - "motion-dom": ["motion-dom@13.0.0", "", { "dependencies": { "motion-utils": "^13.0.0" } }, "sha512-Xk+SJas70uMAUIApg+m3lZDShxI3LBFHq7mFGbBKoRXc2PVPDyAKmzN64Bbzt4CZdP/CItTiJxWtn4TA0v53Ng=="], + "motion-dom": ["motion-dom@13.1.1", "", { "dependencies": { "motion-utils": "^13.0.0" } }, "sha512-XSf8VYWSB6G/0IY3rWVbyLcxWXtAVHkN1PQE2agTaCv3u8RGvbwu56TyyR/MNzBqqNavEBTZzErcxI1TxBrjcA=="], "motion-utils": ["motion-utils@13.0.0", "", {}, "sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ=="], @@ -1692,7 +1757,7 @@ "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], - "nanostores": ["nanostores@1.4.2", "", {}, "sha512-Wxv8Roefr2nqtiRG0bnaFlpYqpIVtOEeJZHaH+4nGgOK1/7n6OHOuHCb/bhqrNQgZM8fyd0s1PqhdrJc9Ib44g=="], + "nanostores": ["nanostores@1.5.2", "", {}, "sha512-B0UbxzK1s0CN8Xht6r+7iT5+xV8PTaRERR1nATeplRv1Rw5YLWfVAid0hkqY3EceqpG4RjTk8GAwIxQY39Rnwg=="], "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], @@ -1722,9 +1787,9 @@ "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], - "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], + "open": ["open@11.0.1", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.2.0", "wsl-utils": "^1.0.0" } }, "sha512-NzwMUB6C1D0+Kd+9iMS/H4k+Ck3cTX6Ckyfr/gAGlmvSE1LUQZnEZvWBi4PYmMwH/S5SMeTXnE+9uAz8uF+pWw=="], - "openai": ["openai@7.4.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-+C9Muit5x8j9R8ej8ZzVgKcrVDtqFqTy9gxFdov0EItLgU68zrJtF9ZeT0cyqJQW9S3PCJkdFgADtRGquRBtew=="], + "openai": ["openai@7.5.0", "", { "peerDependencies": { "@aws-sdk/credential-provider-node": ">=3.972.0 <4", "@smithy/hash-node": ">=4.3.0 <5", "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@aws-sdk/credential-provider-node", "@smithy/hash-node", "@smithy/signature-v4", "ws", "zod"] }, "sha512-ZbDBz8FSB8Mv8fFYIUvzTFMdV5vl93/octp1MdtK2lfYepSpfv/ewmeugpKz/cwGtFSx+YuUM4NwpZ2P55YiPA=="], "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], @@ -1770,7 +1835,7 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "phoenix": ["phoenix@1.8.11", "", {}, "sha512-MbS8y0+8lYNjOYRIorek1GzMFS3nKvycetLlLxB/DZOFRu0yLI0PCvvm8wg+7aEORbmWuioe95+7uoyNKlErZg=="], + "phoenix": ["phoenix@1.8.12", "", {}, "sha512-svUniHb83aGh1XpYWHe4fkbfrGd6xa26TWO/3clkuNplXwgjxh7RI7GOWxf28VAgDWwQ2/yN8PxIa5dqjXOcAA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -1798,7 +1863,7 @@ "postgres": ["postgres@3.4.9", "", {}, "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="], - "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "powershell-utils": ["powershell-utils@0.2.0", "", {}, "sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw=="], "prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="], @@ -1810,7 +1875,7 @@ "process-warning": ["process-warning@5.1.0", "", {}, "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw=="], - "prompt-area": ["prompt-area@0.6.3", "", { "peerDependencies": { "clsx": ">=2", "react": ">=18", "react-dom": ">=18", "tailwind-merge": ">=2" } }, "sha512-/M70Qfui+yd/Z2YOsGloRt69dmMz1HXh+PF3CNZHzHemKHZNWjuPUEd3KAn/xIH+3y5aW+DCSuph39bHF7ACgg=="], + "prompt-area": ["prompt-area@0.6.7", "", { "peerDependencies": { "clsx": ">=2", "react": ">=18", "react-dom": ">=18", "tailwind-merge": ">=2" } }, "sha512-y8rL0ezYWmuWS/5n4wQyvwvL7u5gU78R5psc6/9jrNzdu/I97k9s7jJzmqgHHay9gUxiqajOYa+nIQu88HNTZQ=="], "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], @@ -1908,7 +1973,7 @@ "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], - "rollup": ["rollup@4.62.4", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.4", "@rollup/rollup-android-arm64": "4.62.4", "@rollup/rollup-darwin-arm64": "4.62.4", "@rollup/rollup-darwin-x64": "4.62.4", "@rollup/rollup-freebsd-arm64": "4.62.4", "@rollup/rollup-freebsd-x64": "4.62.4", "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", "@rollup/rollup-linux-arm-musleabihf": "4.62.4", "@rollup/rollup-linux-arm64-gnu": "4.62.4", "@rollup/rollup-linux-arm64-musl": "4.62.4", "@rollup/rollup-linux-loong64-gnu": "4.62.4", "@rollup/rollup-linux-loong64-musl": "4.62.4", "@rollup/rollup-linux-ppc64-gnu": "4.62.4", "@rollup/rollup-linux-ppc64-musl": "4.62.4", "@rollup/rollup-linux-riscv64-gnu": "4.62.4", "@rollup/rollup-linux-riscv64-musl": "4.62.4", "@rollup/rollup-linux-s390x-gnu": "4.62.4", "@rollup/rollup-linux-x64-gnu": "4.62.4", "@rollup/rollup-linux-x64-musl": "4.62.4", "@rollup/rollup-openbsd-x64": "4.62.4", "@rollup/rollup-openharmony-arm64": "4.62.4", "@rollup/rollup-win32-arm64-msvc": "4.62.4", "@rollup/rollup-win32-ia32-msvc": "4.62.4", "@rollup/rollup-win32-x64-gnu": "4.62.4", "@rollup/rollup-win32-x64-msvc": "4.62.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg=="], + "rollup": ["rollup@4.62.5", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.5", "@rollup/rollup-android-arm64": "4.62.5", "@rollup/rollup-darwin-arm64": "4.62.5", "@rollup/rollup-darwin-x64": "4.62.5", "@rollup/rollup-freebsd-arm64": "4.62.5", "@rollup/rollup-freebsd-x64": "4.62.5", "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", "@rollup/rollup-linux-arm-musleabihf": "4.62.5", "@rollup/rollup-linux-arm64-gnu": "4.62.5", "@rollup/rollup-linux-arm64-musl": "4.62.5", "@rollup/rollup-linux-loong64-gnu": "4.62.5", "@rollup/rollup-linux-loong64-musl": "4.62.5", "@rollup/rollup-linux-ppc64-gnu": "4.62.5", "@rollup/rollup-linux-ppc64-musl": "4.62.5", "@rollup/rollup-linux-riscv64-gnu": "4.62.5", "@rollup/rollup-linux-riscv64-musl": "4.62.5", "@rollup/rollup-linux-s390x-gnu": "4.62.5", "@rollup/rollup-linux-x64-gnu": "4.62.5", "@rollup/rollup-linux-x64-musl": "4.62.5", "@rollup/rollup-openbsd-x64": "4.62.5", "@rollup/rollup-openharmony-arm64": "4.62.5", "@rollup/rollup-win32-arm64-msvc": "4.62.5", "@rollup/rollup-win32-ia32-msvc": "4.62.5", "@rollup/rollup-win32-x64-gnu": "4.62.5", "@rollup/rollup-win32-x64-msvc": "4.62.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw=="], "rou3": ["rou3@0.9.2", "", {}, "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ=="], @@ -1952,7 +2017,7 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "shadcn": ["shadcn@4.17.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "kleur": "^4.1.5", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "socks": "^2.8.8", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "undici": "^7.27.2", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-kphndh72CD7mxLQ/thkbPVgfXpIjzPUllzvTWRJmU3sNGZc/WbOqzJ0FXbBGj6sqNiaGPiOLDQByy8FAtCuVUQ=="], + "shadcn": ["shadcn@4.18.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "kleur": "^4.1.5", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "socks": "^2.8.8", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "undici": "^7.27.2", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-tUFZgkYmfVNQVm3xX7lhSzOvDsp+O14ac5dwgXIr5mIsr79ISueb/Mu+ZtWMz0DH6v77u4eYyvbQ9TTMpSn3aw=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1994,6 +2059,8 @@ "streamdown": ["streamdown@2.5.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "mermaid": "^11.12.2", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.3.0", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA=="], + "strictdom": ["strictdom@1.0.1", "", {}, "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg=="], + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], @@ -2154,7 +2221,7 @@ "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], - "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + "wsl-utils": ["wsl-utils@1.0.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], @@ -2180,7 +2247,7 @@ "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.95", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.32" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-V0nIwhyv8f9rD+p6glm0uq30seid8y2CrvNvHCAbnuPm5bPpqVChEB63kixrIDNnogkgI+ZjSTtIbIL2q3QPvQ=="], - "@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.88", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.32" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-1EgXWI+9wKAvbTyNP2DLuiQ7KZM9bzSzLMXouzxfEC0rvLVSX0tN1emXiEurj8+9kXyElJO7Qm3xWSNhdyVEvA=="], + "@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.89", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.32" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zYCrUuiCxLk65iQ3C5AU9qLo0lsyCbYNAS983rDTg/lMVmdsWz8aqrL6K746y7/lRyv+gM1y1HoFCxVsKOSlSw=="], "@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], @@ -2190,7 +2257,7 @@ "@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.32", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-izgUo50kamJMwoYCXoFwVccuJeyYp0y1+twf0FfpEz7kdQBloZLZbgLYUOUpannLWrV7xYSJR48BQJIQFbFiAQ=="], - "@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "@ai-sdk/provider-utils/undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -2202,8 +2269,6 @@ "@copilotkit/react-core/streamdown": ["streamdown@1.6.11", "", { "dependencies": { "clsx": "^2.1.1", "hast": "^1.0.0", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "katex": "^0.16.22", "lucide-react": "^0.542.0", "marked": "^16.2.1", "mermaid": "^11.11.0", "rehype-harden": "^1.1.6", "rehype-katex": "^7.0.1", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-cjk-friendly": "^1.2.3", "remark-cjk-friendly-gfm-strikethrough": "^1.2.3", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.0.1", "shiki": "^3.12.2", "tailwind-merge": "^3.3.1", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-Y38fwRx5kCKTluwM+Gf27jbbi9q6Qy+WC9YrC1YbCpMkktT3PsRBJHMWiqYeF8y/JzLpB1IzDoeaB6qkQEDnAA=="], - "@copilotkit/runtime/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], - "@copilotkit/runtime/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@copilotkit/shared/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], @@ -2218,7 +2283,7 @@ "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - "@graphql-tools/executor/@graphql-tools/utils": ["@graphql-tools/utils@11.2.2", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-Do2A/t6ayqOma8m7PvpYMsCBswL+NB35BQSng4GWSBqafL2/BnfAZy/NSENPwV721Rm2fG0BHETFC0+FJJZmLw=="], + "@graphql-tools/executor/@graphql-tools/utils": ["@graphql-tools/utils@12.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-aMdIo/l+8j4lhamWAf+MWHr+lOW8zArSBKXspqtKHgt5I2JF9LS55KDLUe/L9AiJOV/O6qJJ60hgYydbnJHbxg=="], "@graphql-tools/merge/@graphql-tools/utils": ["@graphql-tools/utils@12.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-aMdIo/l+8j4lhamWAf+MWHr+lOW8zArSBKXspqtKHgt5I2JF9LS55KDLUe/L9AiJOV/O6qJJ60hgYydbnJHbxg=="], @@ -2232,8 +2297,6 @@ "@microsoft/agents-hosting/zod": ["zod@3.25.75", "", {}, "sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg=="], - "@modelcontextprotocol/sdk/@hono/node-server": ["@hono/node-server@2.1.0", "", { "peerDependencies": { "hono": "^4" } }, "sha512-XovyyCCnBzW+zKu+z/zq8hwNs4KOR5rEMAOxo2f40Q5xoOI37IMm6MIg2COOUtUApo0i6850MTBKH2u4QLGIqg=="], - "@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "@segment/analytics-node/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], @@ -2286,6 +2349,8 @@ "body-parser/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "conf/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], "conf/json-schema-typed": ["json-schema-typed@7.0.3", "", {}, "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A=="], @@ -2304,8 +2369,6 @@ "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "dagre-d3-es/lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], - "dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], @@ -2426,8 +2489,6 @@ "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], @@ -2504,6 +2565,8 @@ "wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], + "wsl-utils/powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "@ag-ui/mcp-middleware/@ag-ui/client/@ag-ui/core": ["@ag-ui/core@0.0.54", "", { "dependencies": { "zod": "^3.22.4" } }, "sha512-Ilx31OvRQaZfU7jSArGqz06JZKOsAt8zWiCPJljyp9zR6Tzl18oyfx8o6FsuGfAktGRe50GI9SCCxNXXysZwtA=="], "@ag-ui/mcp-middleware/@ag-ui/client/@ag-ui/encoder": ["@ag-ui/encoder@0.0.54", "", { "dependencies": { "@ag-ui/core": "0.0.54", "@ag-ui/proto": "0.0.54" } }, "sha512-0dPuE/eAeBRBDj/OOj5AW8SoP1r0dufmoOdrtKgmf+dlbVXKSNkDDHGrrvIWFPxwvPTWhHeN6wnsVUayWpUsGg=="], @@ -2598,8 +2661,6 @@ "@modelcontextprotocol/sdk/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - "@modelcontextprotocol/sdk/express/range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], - "@modelcontextprotocol/sdk/express/send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], "@modelcontextprotocol/sdk/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], @@ -2624,8 +2685,6 @@ "@slack/bolt/express/merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - "@slack/bolt/express/range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], - "@slack/bolt/express/send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], "@slack/bolt/express/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], @@ -2644,6 +2703,8 @@ "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], + "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], @@ -2770,19 +2831,19 @@ "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], - "@modelcontextprotocol/sdk/express/body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@modelcontextprotocol/sdk/express/body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "@modelcontextprotocol/sdk/express/type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@modelcontextprotocol/sdk/express/type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "@modelcontextprotocol/sdk/express/type-is/media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], - "@slack/bolt/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "@slack/bolt/express/accepts/negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], - "@slack/bolt/express/body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@slack/bolt/express/body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "@slack/bolt/express/type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "@slack/bolt/express/type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "@slack/bolt/express/type-is/media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], @@ -2804,6 +2865,10 @@ "react-markdown/remark-rehype/mdast-util-to-hast/unist-util-position": ["unist-util-position@4.0.4", "", { "dependencies": { "@types/unist": "^2.0.0" } }, "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg=="], + "@modelcontextprotocol/sdk/express/accepts/negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + + "@slack/bolt/express/accepts/negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "react-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-core-commonmark": ["micromark-core-commonmark@1.1.0", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-factory-destination": "^1.0.0", "micromark-factory-label": "^1.0.0", "micromark-factory-space": "^1.0.0", "micromark-factory-title": "^1.0.0", "micromark-factory-whitespace": "^1.0.0", "micromark-util-character": "^1.0.0", "micromark-util-chunked": "^1.0.0", "micromark-util-classify-character": "^1.0.0", "micromark-util-html-tag-name": "^1.0.0", "micromark-util-normalize-identifier": "^1.0.0", "micromark-util-resolve-all": "^1.0.0", "micromark-util-subtokenize": "^1.0.0", "micromark-util-symbol": "^1.0.0", "micromark-util-types": "^1.0.1", "uvu": "^0.5.0" } }, "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw=="], "react-markdown/remark-parse/mdast-util-from-markdown/micromark/micromark-factory-space": ["micromark-factory-space@1.1.0", "", { "dependencies": { "micromark-util-character": "^1.0.0", "micromark-util-types": "^1.0.0" } }, "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ=="], diff --git a/docker/s6/s6-rc.d/api/dependencies.d/computer b/docker/s6/s6-rc.d/api/dependencies.d/computer new file mode 100644 index 00000000..e69de29b diff --git a/docker/s6/s6-rc.d/api/dependencies.d/migrate b/docker/s6/s6-rc.d/api/dependencies.d/migrate new file mode 100644 index 00000000..e69de29b diff --git a/docker/s6/s6-rc.d/api/dependencies.d/postgres b/docker/s6/s6-rc.d/api/dependencies.d/postgres new file mode 100644 index 00000000..e69de29b diff --git a/docker/s6/s6-rc.d/api/run b/docker/s6/s6-rc.d/api/run new file mode 100755 index 00000000..231ef2e2 --- /dev/null +++ b/docker/s6/s6-rc.d/api/run @@ -0,0 +1,5 @@ +#!/command/with-contenv sh +# The API, and the app it serves. Started after the browser so a Bot's first action does not race a +# computer that is still coming up. +cd /app/server +exec s6-setuidgid pwuser /usr/local/bin/bun src/index.ts diff --git a/docker/s6/s6-rc.d/api/type b/docker/s6/s6-rc.d/api/type new file mode 100644 index 00000000..5883cff0 --- /dev/null +++ b/docker/s6/s6-rc.d/api/type @@ -0,0 +1 @@ +longrun diff --git a/docker/s6/s6-rc.d/computer-token/type b/docker/s6/s6-rc.d/computer-token/type new file mode 100644 index 00000000..bdd22a18 --- /dev/null +++ b/docker/s6/s6-rc.d/computer-token/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/s6/s6-rc.d/computer-token/up b/docker/s6/s6-rc.d/computer-token/up new file mode 100755 index 00000000..f977bf28 --- /dev/null +++ b/docker/s6/s6-rc.d/computer-token/up @@ -0,0 +1 @@ +/command/with-contenv /etc/s6-overlay/scripts/computer-token.sh diff --git a/docker/s6/s6-rc.d/computer/dependencies.d/computer-token b/docker/s6/s6-rc.d/computer/dependencies.d/computer-token new file mode 100644 index 00000000..e69de29b diff --git a/docker/s6/s6-rc.d/computer/run b/docker/s6/s6-rc.d/computer/run new file mode 100755 index 00000000..77989bda --- /dev/null +++ b/docker/s6/s6-rc.d/computer/run @@ -0,0 +1,9 @@ +#!/command/with-contenv sh +# The Bot's browser. Bound to loopback: its only caller is the API beside it. +# +# `with-contenv` is not decoration. Without it s6 starts a service with none of the container's +# environment, and the failure is a config error naming a variable that is plainly set. +cd /app/agent-computer +export PORT=4100 +export WORKSPACE_DIR=/workspace +exec s6-setuidgid pwuser /usr/local/bin/bun src/index.ts diff --git a/docker/s6/s6-rc.d/computer/type b/docker/s6/s6-rc.d/computer/type new file mode 100644 index 00000000..5883cff0 --- /dev/null +++ b/docker/s6/s6-rc.d/computer/type @@ -0,0 +1 @@ +longrun diff --git a/docker/s6/s6-rc.d/migrate/dependencies.d/postgres b/docker/s6/s6-rc.d/migrate/dependencies.d/postgres new file mode 100644 index 00000000..e69de29b diff --git a/docker/s6/s6-rc.d/migrate/type b/docker/s6/s6-rc.d/migrate/type new file mode 100644 index 00000000..bdd22a18 --- /dev/null +++ b/docker/s6/s6-rc.d/migrate/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/s6/s6-rc.d/migrate/up b/docker/s6/s6-rc.d/migrate/up new file mode 100644 index 00000000..e6776a21 --- /dev/null +++ b/docker/s6/s6-rc.d/migrate/up @@ -0,0 +1 @@ +/command/with-contenv /etc/s6-overlay/scripts/migrate.sh diff --git a/docker/s6/s6-rc.d/postgres-init/type b/docker/s6/s6-rc.d/postgres-init/type new file mode 100644 index 00000000..bdd22a18 --- /dev/null +++ b/docker/s6/s6-rc.d/postgres-init/type @@ -0,0 +1 @@ +oneshot diff --git a/docker/s6/s6-rc.d/postgres-init/up b/docker/s6/s6-rc.d/postgres-init/up new file mode 100644 index 00000000..61624548 --- /dev/null +++ b/docker/s6/s6-rc.d/postgres-init/up @@ -0,0 +1 @@ +/command/with-contenv /etc/s6-overlay/scripts/postgres-init.sh diff --git a/docker/s6/s6-rc.d/postgres/dependencies.d/postgres-init b/docker/s6/s6-rc.d/postgres/dependencies.d/postgres-init new file mode 100644 index 00000000..e69de29b diff --git a/docker/s6/s6-rc.d/postgres/run b/docker/s6/s6-rc.d/postgres/run new file mode 100755 index 00000000..a2ee4bdd --- /dev/null +++ b/docker/s6/s6-rc.d/postgres/run @@ -0,0 +1,13 @@ +#!/command/with-contenv sh +# The database, when this container is asked to be its own. +# +# `EMBEDDED_POSTGRES=on` turns it on. Off, this service exits 0 immediately and s6 leaves it alone, +# which is how one image serves both shapes without two Dockerfiles. +set -eu +if [ "${EMBEDDED_POSTGRES:-off}" != "on" ]; then + exec /bin/true +fi +exec s6-setuidgid postgres /usr/lib/postgresql/16/bin/postgres \ + -D /var/lib/postgresql/data \ + -c listen_addresses=127.0.0.1 \ + -c port=5432 diff --git a/docker/s6/s6-rc.d/postgres/type b/docker/s6/s6-rc.d/postgres/type new file mode 100644 index 00000000..5883cff0 --- /dev/null +++ b/docker/s6/s6-rc.d/postgres/type @@ -0,0 +1 @@ +longrun diff --git a/docker/s6/s6-rc.d/user/contents.d/api b/docker/s6/s6-rc.d/user/contents.d/api new file mode 100644 index 00000000..e69de29b diff --git a/docker/s6/s6-rc.d/user/contents.d/computer b/docker/s6/s6-rc.d/user/contents.d/computer new file mode 100644 index 00000000..e69de29b diff --git a/docker/s6/s6-rc.d/user/contents.d/computer-token b/docker/s6/s6-rc.d/user/contents.d/computer-token new file mode 100644 index 00000000..e69de29b diff --git a/docker/s6/s6-rc.d/user/contents.d/migrate b/docker/s6/s6-rc.d/user/contents.d/migrate new file mode 100644 index 00000000..e69de29b diff --git a/docker/s6/s6-rc.d/user/contents.d/postgres b/docker/s6/s6-rc.d/user/contents.d/postgres new file mode 100644 index 00000000..e69de29b diff --git a/docker/s6/s6-rc.d/user/contents.d/postgres-init b/docker/s6/s6-rc.d/user/contents.d/postgres-init new file mode 100644 index 00000000..e69de29b diff --git a/docker/s6/scripts/computer-token.sh b/docker/s6/scripts/computer-token.sh new file mode 100755 index 00000000..5cd436fa --- /dev/null +++ b/docker/s6/scripts/computer-token.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# The secret the API presents to the browser beside it. +# +# Both processes live in this container and the browser's port is not published, so nobody outside +# can present anything. What this defends is somebody publishing 4100 anyway, and the browser +# refuses to start without it regardless. +# +# Generated rather than required, because an operator should not have to invent a shared secret for +# two processes they cannot address separately. Set COMPUTER_TOKEN yourself and this leaves it be. +set -eu +if [ -z "${COMPUTER_TOKEN:-}" ]; then + head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' \ + > /run/s6/container_environment/COMPUTER_TOKEN +fi diff --git a/docker/s6/scripts/migrate.sh b/docker/s6/scripts/migrate.sh new file mode 100755 index 00000000..3a71d4df --- /dev/null +++ b/docker/s6/scripts/migrate.sh @@ -0,0 +1,11 @@ +#!/bin/sh +# Migrations, for the embedded database only. +# +# An external database is somebody else's release process: two replicas starting together would +# race, and a failed migration should stop a deploy rather than leave a half-migrated database +# serving. An embedded one has exactly one process and no deploy pipeline, so doing it here is the +# difference between the container working and the operator reading a runbook. +set -eu +[ "${EMBEDDED_POSTGRES:-off}" = "on" ] || exit 0 +cd /app/server +exec s6-setuidgid pwuser /usr/local/bin/bun x drizzle-kit migrate --config=drizzle.config.ts diff --git a/docker/s6/scripts/postgres-init.sh b/docker/s6/scripts/postgres-init.sh new file mode 100755 index 00000000..69bbeb14 --- /dev/null +++ b/docker/s6/scripts/postgres-init.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# Create the cluster the first time, and only the first time. +# +# Bound to loopback and trust-auth on purpose: the only client is the process beside it, inside this +# container, and a password would be a secret with nobody to keep it from. Publishing 5432 from this +# container would change that, which is why nothing here does. +set -eu +[ "${EMBEDDED_POSTGRES:-off}" = "on" ] || exit 0 + +DATA=/var/lib/postgresql/data +if [ ! -s "$DATA/PG_VERSION" ]; then + s6-setuidgid postgres /usr/lib/postgresql/16/bin/initdb -D "$DATA" -A trust -U openbot >/dev/null + s6-setuidgid postgres /usr/lib/postgresql/16/bin/pg_ctl -D "$DATA" -o "-c listen_addresses=127.0.0.1" -w start >/dev/null + s6-setuidgid postgres /usr/lib/postgresql/16/bin/createdb -U openbot openbot + s6-setuidgid postgres /usr/lib/postgresql/16/bin/psql -U openbot -d openbot -c 'CREATE EXTENSION IF NOT EXISTS vector' >/dev/null + s6-setuidgid postgres /usr/lib/postgresql/16/bin/pg_ctl -D "$DATA" -w stop >/dev/null +fi diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 00000000..47da5cfe --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,129 @@ +# Deployment + +OpenBot ships as one container. It carries the app, the API that serves it, and the browser the Bots +drive, and it can carry its own PostgreSQL as well. It does what it does on a laptop. + +```sh +docker build -t openbot . + +# A database you already run. +docker run -p 3001:3001 --env-file .env openbot + +# Or one inside the container. Nothing else to provision. +docker run -p 3001:3001 --env-file .env \ + -e EMBEDDED_POSTGRES=on -v openbot-data:/var/lib/postgresql/data openbot +``` + +## What is in the image, and what is not + +**In it:** the built app, the API, and Chromium. One port, 3001. The browser listens on 4100 inside +the container and is deliberately not published: it holds real logins and its only caller is the +process beside it. + +**PostgreSQL, if you ask for it.** `EMBEDDED_POSTGRES=on` starts one inside the container, creates +the database and the `vector` extension the first time, and runs the migrations on every start. It +listens on loopback only and is never published, so there is no password to manage. + +Give it a volume at `/var/lib/postgresql/data`. Without one, a redeploy takes the audit trail with +it, and the audit trail is the product. Platforms that offer no persistent volume are the ones to +point at a managed database instead: set `DATABASE_URL` and leave `EMBEDDED_POSTGRES` off. The +`vector` extension must be enabled there; RDS, Cloud SQL and Azure Database all support it, none +enable it for you. + +**Not in it:** + +**The supervisor.** It gives each Bot its own container, which needs a Docker socket, which no +serverless container platform permits. Without it every Bot shares the one browser, exactly as they +do on a laptop with no supervisor configured. A shared browser means shared logins, shared files and +shared session between Bots, which is fine for a deployment where one team trusts its own Bots and +is not fine as a boundary between tenants. + +## Minimum size + +Measured on the real image, one Bot, arm64. + +| | Measured | Minimum | Recommended | +| --- | --- | --- | --- | +| Memory | 409 MB idle, 498 MB after three page loads, 548 MB after a snapshot | **2 GB** | **4 GB** | +| vCPU | 3 to 6 percent at rest, bursty while a page renders | **1** | **2** | +| Disk | 5.3 GB image | **8 GB** | 10 GB with room for `/workspace` | + +**Why 2 GB when it measures at 550 MB.** That figure is one Bot with one page open. Every additional +concurrent page is roughly another 100 to 200 MB, and Playwright's own guidance is to allow about +1 GB per concurrent browser. 2 GB is the floor at which one person using it does not meet the OOM +killer; 4 GB is where a handful of Bots working at once stays comfortable. + +**Do not configure shared memory.** Chromium is launched with `--disable-dev-shm-usage`, so it writes +to `/tmp` rather than `/dev/shm` and the 64 MB default is irrelevant. This matters because **AWS +Fargate does not support `sharedMemorySize` at all**; without that flag Chromium would crash there +and the fix would not be available. + +## Required configuration + +| Variable | | +| --- | --- | +| `DATABASE_URL` | PostgreSQL with the `vector` extension. Not needed with `EMBEDDED_POSTGRES=on` | +| `EMBEDDED_POSTGRES` | `on` to run the database inside the container. Off by default | +| `KEY_ENCRYPTION_KEY` | base64 32 bytes. `openssl rand -base64 32`. The example key is refused in production | +| `INTELLIGENCE_API_URL`, `INTELLIGENCE_GATEWAY_WS_URL`, `INTELLIGENCE_API_KEY` | CopilotKit Intelligence. A free plan is available and it can be self-hosted | +| `COPILOTKIT_LICENSE_TOKEN` | from `npx copilotkit@latest license --write` | +| `MANAGED_AGENT_AG_UI_URL` | the AG-UI endpoint for the example remote Bot | +| a model key | `OPENAI_API_KEY`, or the provider you configured | + +`COMPUTER_TOKEN` is generated at start if you do not set one. Both processes that need it are inside +the container, so there is nothing to share it with. + +**Authentication is required.** `OPENBOT_DEV_NO_AUTH` is refused when `NODE_ENV=production`, which +the image sets. A deployment anybody can reach needs Google sign-in configured, or every visitor is +an administrator. + +**Put TLS in front of it.** Not only for the cookies. A page served from `http://
` is not a +secure context, which removes a set of browser APIs that are present on `http://localhost` and so +never missing on a laptop. The app no longer depends on any of them, but sign-in cookies still want +`Secure`, and every platform below terminates TLS for you. + +## Migrations + +With `EMBEDDED_POSTGRES=on` they run at start and there is nothing to do. There is exactly one +process and no deploy pipeline, so the alternative would be a runbook. + +With an external database they are a release step, not a start step. Two replicas starting together +would race, and a failed migration should stop a deploy rather than leave a half-migrated database +serving traffic. + +```sh +docker run --rm --env-file .env openbot \ + sh -c "cd /app/server && bun x drizzle-kit migrate --config=drizzle.config.ts" +``` + +## One replica, for now + +Run one. The gateway still caches the page snapshot a Bot resolves element references against in +process memory, so a second replica answers a click with a snapshot it never took. The symptom is an +element that cannot be found, intermittently, which reads as a flaky Bot rather than as a +configuration problem. Pin the platform's maximum instance count until that moves to the database. + +## Platform notes + +**Google Cloud Run.** Set memory to at least 2 GB and max instances to 1. Cloud Run runs every +container under gVisor, which Chromium is sensitive to; test a navigation before trusting it. +`gcloud run compose up` will also deploy the whole compose file if you want a throwaway database +alongside. + +**AWS.** ECS Express Mode provisions the cluster, load balancer, HTTPS and autoscaling from an image +in ECR, and is what AWS points App Runner users at now that App Runner takes no new customers. +Plain ECS on Fargate behind an ALB is the answer if you want task definitions and fine-grained IAM. +No shared-memory configuration is needed or possible. + +**Azure Container Apps.** Managed ingress with TLS and custom domains. Note the **240 second request +timeout**: the live screen holds a long connection, so expect it to reconnect. Concurrent WebSockets +are capped at 350 per instance on the basic tier. + +**Railway, Render, Fly.io.** All run this image directly and all provision PostgreSQL in a click, +which makes them the shortest path from nothing to a running deployment. + +## Known costs + +**The image is 5.3 GB**, most of it the Playwright base, which ships Firefox and WebKit alongside the +Chromium we use. Deleting them afterwards does not help, because the bytes still ship in the layer +below. Building Chromium-only onto a slim base would cut this substantially and is not done yet. diff --git a/package.json b/package.json index 72e93762..1b28ec55 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.3.8", - "@copilotkit/aimock": "^1.38.0", + "@copilotkit/aimock": "1.39.0", "@types/bun": "^1.3.3", "roughjs": "^4.6.6", "typescript": "^5.9.3", diff --git a/server/Dockerfile b/server/Dockerfile index 1a92fae9..be891a31 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,14 +1,72 @@ -FROM oven/bun:1.3.14 +# The API and the app it serves, in one image. +# +# ONE IMAGE ON PURPOSE. There is no CORS anywhere in the server, so the app must reach `/api` on its +# own origin. Shipping them together means a deployment needs one service and no path rules, and the +# two halves cannot end up disagreeing about which host they are on. +# +# Multi-stage so the build's dependencies do not ship. Non-root because nothing here needs to be. + +FROM oven/bun:1.3.14 AS deps WORKDIR /app +# Manifests before sources, so a source change does not reinstall the world. COPY package.json bun.lock ./ +# The shared tsconfig, which every package extends. Vite reads it while transforming and fails +# without it, in a stack trace that names esbuild rather than a missing file. +COPY tsconfig.base.json tsconfig.base.json +COPY bunfig.toml bunfig.toml COPY app/package.json app/package.json COPY server/package.json server/package.json COPY worker/package.json worker/package.json RUN bun install --frozen-lockfile + +FROM deps AS app-build + +# `prebuild` generates the app's config, so the app is built through its own script rather than by +# calling vite directly, or the generated file is missing and the build fails on an import. +COPY app app +COPY scripts scripts +COPY shared shared +# The server's source too: `generate-app-config` reads the tenant package through +# `server/src/tenant-package`, so the app cannot be built without it. +COPY server server +COPY examples examples +RUN bun run --cwd app build + + +FROM oven/bun:1.3.14 AS runtime + +# Not root. The API opens a socket and talks to Postgres; neither needs privileges, and a Chromium +# container is a separate image with its own reasons. +RUN groupadd --system --gid 1001 openbot \ + && useradd --system --uid 1001 --gid openbot openbot + +WORKDIR /app + +COPY --from=deps /app/node_modules node_modules +COPY --from=deps /app/package.json package.json +COPY --from=deps /app/bun.lock bun.lock +COPY --from=deps /app/server/node_modules server/node_modules + COPY server server +COPY shared shared COPY examples examples +# The built app, served by the process below. `APP_DIST_DIR` is what turns that on, so an image +# without this layer is still a working API. +COPY --from=app-build /app/app/dist app/dist +ENV APP_DIST_DIR=/app/app/dist + +USER openbot:openbot + +ENV NODE_ENV=production +ENV PORT=3001 +EXPOSE 3001 + WORKDIR /app/server + +# Migrations are a release step, not a start step: two replicas starting together would race, and a +# failed migration should stop a deploy rather than leave a half-migrated database serving. +CMD ["bun", "src/index.ts"] diff --git a/server/package.json b/server/package.json index e51e7847..43b1c354 100644 --- a/server/package.json +++ b/server/package.json @@ -14,8 +14,7 @@ "dependencies": { "@ag-ui/client": "0.0.57", "@better-auth/drizzle-adapter": "^1.6.27", - "@copilotkit/runtime": "1.67.1", - "@copilotkit/shared": "1.67.1", + "@copilotkit/runtime": "1.68.3", "@modelcontextprotocol/sdk": "^1.30.0", "better-auth": "^1.6.27", "cel-js": "^0.8.2", @@ -26,7 +25,7 @@ "zod": "^4.4.3" }, "devDependencies": { - "@copilotkit/aimock": "^1.38.0", + "@copilotkit/aimock": "1.39.0", "drizzle-kit": "^0.31.10", "eventsource": "3.0.7" } diff --git a/server/src/app.ts b/server/src/app.ts index 35ac76d3..7fef8fe7 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -19,7 +19,6 @@ import { createComponentRoutes } from "./components/routes"; import type { SandboxedStore } from "./components/sandboxed"; import { createSandboxedRoutes } from "./components/sandboxed-routes"; import type { ComponentStore } from "./components/store"; -import type { ComputerClient } from "./computer/client"; import type { ComputerGateway } from "./computer/gateway"; import type { PolicyStore } from "./computer/policy-store"; import { createComputerRoutes } from "./computer/routes"; @@ -27,6 +26,7 @@ import { authoriseAgentCall } from "./agents/callback-token"; import type { DeploymentConfig } from "./config"; import type { ConnectorAdminService } from "./connectors"; import type { CredentialAdminService, CredentialInput } from "./credentials"; +import { serveStatic } from "hono/bun"; import { createPluginRoutes } from "./plugins/routes"; import { REFUSAL_MARKER } from "./plugins/tools"; import type { PluginStore } from "./plugins/store"; @@ -48,9 +48,7 @@ export function createApp( * scope broke every server test that touches createApp even though none of them use CopilotKit. */ copilotHandler?: HonoApp, - /** Absent when no computer is configured, and the routes are then not mounted at all. */ - computerClient?: ComputerClient, - /** The only path to an acting call: policy decision, then audit row, then the action. */ + /** The single governed computer module: policy, audit trail, transport, and provider lifecycle. */ computerGateway?: ComputerGateway, /** What the gateway enforces, and what an administrator can change while running. */ computerPolicy?: PolicyStore, @@ -291,24 +289,19 @@ export function createApp( if (copilotHandler) { // Mounted at the ROOT with the handler carrying its own basePath. Mounting it at // "/api/copilotkit" as well double-prefixes it: Hono strips the prefix before the handler sees - // the path, so every route lands at /api/copilotkit/api/copilotkit/* and /info 404s. The client + // the path, so every route lands at /api/copilotkit/api/copilotkit/* and /info 404s. The browser // reports that as "Runtime info request failed with status 404" and every run fails before it // starts, with nothing at all in the server log. app.route("/", copilotHandler); } - // The Bot computer. Acting on a page needs the gateway and the policy it enforces, so all - // three arrive together or the routes are not mounted: a computer whose actions were ungoverned is - // not a reduced feature, it is the one shape of this feature that must not exist. - if (computerClient && computerGateway && computerPolicy) { + // The Bot computer. Acting on a page needs the gateway and the policy it enforces, so both arrive + // together or the routes are not mounted. An ungoverned computer is not a reduced feature. It is + // the one shape of this feature that must not exist. + if (computerGateway && computerPolicy) { app.route( "/api/computers", - createComputerRoutes( - computerClient, - computerGateway, - computerPolicy, - requireUser, - ), + createComputerRoutes(computerGateway, computerPolicy, requireUser), ); } @@ -426,6 +419,45 @@ export function createApp( app.route("/api/threads", createThreadRoutes(threadIdentity, requireUser)); } + /* + * The built app, served by the API that serves it. + * + * WHY THE SAME PROCESS. There is no CORS anywhere in this server, deliberately, so the app has to + * reach `/api` on its own origin. Two containers behind one ingress does that too, and costs a + * path rule on every deployment plus a way for the two to disagree about which host they are on. + * One process cannot disagree with itself. + * + * MOUNTED LAST, so every `/api` route above already claimed its path. The catch-all below would + * otherwise answer an unmatched `/api` call with the app's HTML, which is the failure that reads + * as "the API returned HTML" and takes an hour to place. + * + * Absent in development: Vite serves the app and proxies `/api` here, so `APP_DIST_DIR` is unset + * and none of this mounts. + */ + if (config.appDistDir) { + const root = config.appDistDir; + app.use("/*", serveStatic({ root })); + /* + * A single-page app owns its routing, so a path with no file behind it is not missing: it is a + * route the browser resolves once index.html has loaded. Without this, every deep link and every + * refresh away from `/` is a 404, which is the classic way this deployment shape breaks. + * + * Written out rather than a second `serveStatic`, whose `path` option is resolved relative to the + * working directory and silently matches nothing when handed the absolute root used above. + * + * `/api` is excluded so an unmatched API route still answers as one. Returning the app's HTML to + * a fetch that expected JSON is the failure that gets read as "the API returned HTML". + */ + app.get("*", async (context) => { + if (context.req.path.startsWith("/api")) return context.notFound(); + const index = Bun.file(`${root}/index.html`); + if (!(await index.exists())) return context.notFound(); + return new Response(index, { + headers: { "content-type": "text/html; charset=utf-8" }, + }); + }); + } + return app; } diff --git a/server/src/computer/client.ts b/server/src/computer/client.ts index 26bd83e8..8d0a83c6 100644 --- a/server/src/computer/client.ts +++ b/server/src/computer/client.ts @@ -1,60 +1,7 @@ -import type { - ActionResult, - ClickInput, - ComputerProfile, - ComputerStatus, - ControlState, - HumanInput, - HumanInputResult, - KeyInput, - ListFilesInput, - ListFilesResult, - NavigateResult, - ReadFileInput, - ReadFileResult, - ReadResult, - ScreenshotResult, - ScrollInput, - SecretRequest, - SecretResult, - SnapshotResult, - TypeInput, - WriteFileInput, - WriteFileResult, -} from "./schema"; -import { checkComputerAddress, checkNavigationTarget } from "./target"; - -/** - * How the server talks to a Bot's computer. - * - * The computer has no authentication of its own and trusts whatever reaches it, so this module is - * the boundary: it decides whether a navigation is permitted before the request leaves, and it is - * the only place that knows the computer's address. Nothing downstream of here should be handed a - * raw URL from a model. - */ - -export type ComputerClientOptions = { - /** - * The secret this deployment's computers require. Absent means every call is refused by them, which - * is the correct failure: a computer that answers an unauthenticated caller is the bug. - */ - token?: string; - /** Base URL of the Bot's computer, e.g. http://agent-computer:4100 */ - baseUrl: string; - /** - * Where this Bot's computer is, when each Bot has one of its own. - * - * A supervisor gives every Bot its own container, so the address stops being one fixed URL and becomes - * whatever the supervisor published for that Bot, which also changes when its computer is reset. - * Left unset, `baseUrl` answers for everyone as one shared computer. - */ - resolveBaseUrl?: (botId: string) => Promise; - /** True on a laptop, where browsing the deployment's own services is the point. */ - allowPrivateHosts?: boolean; - timeoutMs?: number; - fetchImpl?: typeof fetch; -}; +import type { NavigateResult } from "./schema"; +import { checkNavigationTarget } from "./target"; +/** The computer did not accept or answer a request. */ export class ComputerUnavailableError extends Error { constructor(reason: string) { super(reason); @@ -62,16 +9,7 @@ export class ComputerUnavailableError extends Error { } } -/** - * The Bot acted on something that is not on the page. - * - * Its own error because it is its own condition, and the one the Bot can fix by taking a fresh - * snapshot. - * - * The message a locator failure carries is a Playwright call log, several lines of `waiting for - * locator('aria-ref=e5')`, which is noise to a model and to a person. It is replaced with the thing - * to do next. - */ +/** The requested element is not on the current page. */ export class ElementNotFoundError extends Error { constructor(reason: string) { super(reason); @@ -79,6 +17,7 @@ export class ElementNotFoundError extends Error { } } +/** The navigation target is not permitted. */ export class NavigationRefusedError extends Error { constructor(reason: string) { super(reason); @@ -86,14 +25,7 @@ export class NavigationRefusedError extends Error { } } -/** - * The file request itself was refused by the computer: outside the workspace, missing, or too large. - * - * Distinct from a policy refusal, which happens in the gateway before the request is ever made. Both - * reach the browser as a 403 but they mean different things: this one says the path is not a thing a - * Bot may name at all, the other says this Bot may not touch an otherwise perfectly valid path. Only - * the second has a rule an administrator can go and edit. - */ +/** The computer refused access to a path outside its workspace. */ export class WorkspaceRefusedError extends Error { constructor(reason: string) { super(reason); @@ -101,14 +33,7 @@ export class WorkspaceRefusedError extends Error { } } -/** - * The request asked for something that is not there, or not usable: no such file, a folder where a - * file was wanted, a write that is too big. - * - * Not a refusal. Nothing declined to let the Bot do this; the thing it named does not fit the request. - * Kept separate from {@link WorkspaceRefusedError} because a Bot's next move differs completely: here - * it should look at what IS there and try again, whereas a refusal is final and should be reported. - */ +/** The workspace request names a path or value that cannot be used. */ export class WorkspaceRequestError extends Error { constructor(reason: string) { super(reason); @@ -116,12 +41,7 @@ export class WorkspaceRequestError extends Error { } } -/** - * The refs the caller is using were taken before the page changed. - * - * Its own type because it is the one failure here that the model can fix without a person: take a new - * snapshot and try again. Collapsed into a generic failure, the Bot apologises to the person instead. - */ +/** The page changed after the caller received its element references. */ export class StaleSnapshotError extends Error { constructor(reason: string) { super(reason); @@ -129,320 +49,159 @@ export class StaleSnapshotError extends Error { } } -export function createComputerClient(options: ComputerClientOptions) { - const doFetch = options.fetchImpl ?? fetch; - /* - * The secret the computer demands. Without it this process is just another caller, which is the - * point: a computer that answers without this token bypasses the policy gateway, audit trail, and - * sign-in boundary. - */ - const token = options.token; - const timeoutMs = options.timeoutMs ?? 45_000; - const base = options.baseUrl.replace(/\/$/, ""); - - /** - * A view of the computer as one Bot. - * - * Which Bot is asking has to reach the computer, or nothing on the far side can be per-Bot: its - * profile, its logins, the proxy its traffic leaves through and who holds its wheel all key off this - * one string. If the id is omitted, every Bot resolves the same fixed default and per-Bot settings - * such as `EGRESS_PROXY_` cannot apply. - * - * A bound view rather than a parameter on twenty methods: the gateway already knows the Bot at the - * point it acts, and threading it through every signature would put the same argument in every call - * site for a value that never changes within a request. - */ - function build(botId?: string) { - async function call( - path: string, - init?: RequestInit, - caller?: AbortSignal, - ): Promise { - // Resolved per call rather than held, because a computer that was reset comes back on a - // different port and a cached address would point at nothing. - // - // Outside the try below on purpose: that catch reports "the computer is not running", which is - // true of a computer that will not answer and misleading about a supervisor that could not be - // reached or refused. Those are different operator-facing problems. - let target: string; - if (botId && options.resolveBaseUrl) { - const located = (await options.resolveBaseUrl(botId)).replace( - /\/$/, - "", - ); - // Checked because this address is not necessarily ours. A hosted provider answers from its - // own API, and whatever comes back is about to be called with this deployment's computer - // token. Our own supervisor answers with a private address, which is fine and why this is - // not the navigation check. - const verdict = checkComputerAddress(located); - if (!verdict.allowed) { - throw new ComputerUnavailableError(verdict.reason); - } - target = located; - } else { - target = base; - } - - // Already stopped before this left: do not dispatch at all. Relying on fetch to reject an - // aborted signal makes "did the click happen" depend on how quickly the runtime notices, and - // the answer to "the person pressed Stop first" should never be a race. - if (caller?.aborted) { - throw new ComputerUnavailableError("The action was stopped."); - } +/** + * Transport options used inside the computer gateway. + * + * This is an internal seam. Application code uses ComputerGateway and does not + * use this interface directly. + */ +export type ComputerTransportOptions = { + token?: string; + allowPrivateHosts?: boolean; + timeoutMs?: number; + fetchImpl?: typeof fetch; +}; - let response: Response; - try { - response = await doFetch(`${target}${path}`, { - ...init, - // The Bot's identity, as a header rather than in the path, so the computer's published routes - // are unchanged and a caller that does not know which Bot it is still works. - headers: { - ...(init?.headers as Record | undefined), - ...(botId ? { "x-openbot-bot-id": botId } : {}), - ...(token ? { "x-openbot-computer-token": token } : {}), - }, - /* - * Both reasons to give up. The timeout protects the server from a computer that - * has stopped answering; `caller` is the person pressing Stop, and it has to reach the - * browser or the click they were stopping still lands. Combined rather than chosen between: - * whichever fires first ends the request. - */ - signal: caller - ? AbortSignal.any([caller, AbortSignal.timeout(timeoutMs)]) - : AbortSignal.timeout(timeoutMs), - }); - } catch (error) { - // Distinguished from a failed page load on purpose: this one means the computer itself is not - // there, which is an operator problem, not something the person asking can fix by rephrasing. - throw new ComputerUnavailableError( - error instanceof Error && error.name === "TimeoutError" - ? "The assistant's computer did not respond in time." - : "The assistant's computer is not running.", - ); - } +/** Internal HTTP interface used only by ComputerGateway. */ +export interface ComputerTransport { + call( + baseUrl: string, + botId: string, + path: string, + init?: RequestInit, + caller?: AbortSignal, + ): Promise; + post( + baseUrl: string, + botId: string, + path: string, + payload: unknown, + caller?: AbortSignal, + ): Promise; + navigate( + baseUrl: string, + botId: string, + url: string, + ): Promise; +} - const body = (await response.json().catch(() => null)) as Record< - string, - unknown - > | null; +/** + * Send authenticated HTTP requests to one located agent-computer process. + * + * Lifecycle and location are deliberately absent. ComputerGateway owns those + * operations through ComputerProvider. + */ +export function createComputerTransport( + options: ComputerTransportOptions, +): ComputerTransport { + const doFetch = options.fetchImpl ?? fetch; + const timeoutMs = options.timeoutMs ?? 45_000; - if (!response.ok) { - const detail = - typeof body?.error === "string" - ? body.error - : `HTTP ${response.status}`; - // A stale ref is fixed by taking a new snapshot, so it is not reported as the computer being - // unavailable. - if (response.status === 409) { - throw new StaleSnapshotError(detail); - } - // These two must not be collapsed: path confinement and ordinary bad requests lead to - // different next actions. - // 403 is the path confinement: a boundary, and the answer will never change. - if (response.status === 403) { - throw new WorkspaceRefusedError(detail); - } - // 400 is an ordinary bad request: no such file, a folder where a file was wanted, too large. A - // different request would succeed, which is exactly what the Bot needs to understand. - if (response.status === 400) { - throw new WorkspaceRequestError(detail); - } - /* - * A locator that never resolved is not an outage. Playwright reports it as a timeout whose - * message is a call log naming the selector, which is how "that button is not there" ended up - * indistinguishable from "the computer is down". - */ - if (/waiting for locator|Timeout .* exceeded/i.test(detail)) { - const ref = detail.match(/aria-ref=([A-Za-z0-9_-]+)/)?.[1]; - throw new ElementNotFoundError( - `${ref ? `Element ${ref} is` : "That element is"} not on the page any more. Take a fresh snapshot and use the refs from it.`, - ); - } - throw new ComputerUnavailableError(detail); - } - return body; + async function call( + baseUrl: string, + botId: string, + path: string, + init?: RequestInit, + caller?: AbortSignal, + ): Promise { + if (caller?.aborted) { + throw new ComputerUnavailableError("The action was stopped."); } - async function post( - path: string, - payload: unknown, - caller?: AbortSignal, - ): Promise { - return call( - path, - { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(payload), + const target = baseUrl.replace(/\/$/, ""); + let response: Response; + try { + response = await doFetch(`${target}${path}`, { + ...init, + headers: { + ...(init?.headers as Record | undefined), + "x-openbot-bot-id": botId, + ...(options.token + ? { "x-openbot-computer-token": options.token } + : {}), }, - caller, + signal: caller + ? AbortSignal.any([caller, AbortSignal.timeout(timeoutMs)]) + : AbortSignal.timeout(timeoutMs), + }); + } catch (error) { + throw new ComputerUnavailableError( + error instanceof Error && error.name === "TimeoutError" + ? "The assistant's computer did not respond in time." + : "The assistant's computer is not running.", ); } - return { - async status(botId: string): Promise { - try { - await call("/health"); - return { botId, state: "ready" }; - } catch (error) { - return { - botId, - state: "unreachable", - reason: error instanceof Error ? error.message : "Unknown failure.", - }; - } - }, - - /** Open a page. Refuses before the request leaves if the target is not permitted. */ - async navigate(url: string): Promise { - const verdict = checkNavigationTarget(url, { - allowPrivateHosts: options.allowPrivateHosts, - }); - if (!verdict.allowed) { - throw new NavigationRefusedError(verdict.reason); - } - - return (await call("/navigate", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ url: verdict.url }), - })) as NavigateResult; - }, - - async screenshot(): Promise { - return (await call("/screenshot")) as ScreenshotResult; - }, - - /** The current page as text. No navigation, so no target check applies. */ - async read(): Promise { - return (await call("/read")) as ReadResult; - }, - - async snapshot(): Promise { - return (await call("/snapshot", { method: "POST" })) as SnapshotResult; - }, - - /** - * The acting calls. - * - * Deliberately unguarded here. Unlike `navigate`, which checks its target in this module, these - * carry no policy of their own: the gateway in front of them is the only thing that knows which - * Bot is asking and what the deployment allows, and putting a second half-check here would create - * two places to keep in agreement. Never call these directly from a route. - */ - async click( - input: ClickInput, - caller?: AbortSignal, - ): Promise { - return (await post("/click", input, caller)) as ActionResult; - }, - - async type( - input: TypeInput, - caller?: AbortSignal, - ): Promise { - return (await post("/type", input, caller)) as ActionResult; - }, - - async key(input: KeyInput, caller?: AbortSignal): Promise { - return (await post("/key", input, caller)) as ActionResult; - }, - - async scroll( - input: ScrollInput, - caller?: AbortSignal, - ): Promise { - return (await post("/scroll", input, caller)) as ActionResult; - }, - - /** - * The workspace files. Also unguarded here: the computer confines the path to the workspace, and - * the gateway decides whether this Bot may touch it. Two questions, neither answered in this file. - */ - async readFile(input: ReadFileInput): Promise { - return (await post("/files/read", input)) as ReadFileResult; - }, - - async writeFile(input: WriteFileInput): Promise { - return (await post("/files/write", input)) as WriteFileResult; - }, - - async listFiles(input: ListFilesInput): Promise { - return (await post("/files/list", input)) as ListFilesResult; - }, - - /** Who has the wheel, and whether the Bot is waiting for a person. */ - async control(): Promise { - return (await call("/control")) as ControlState; - }, - - async requestControl(reason: string): Promise { - return (await post("/control/request", { reason })) as ControlState; - }, - - async takeControl(): Promise { - return (await post("/control/take", {})) as ControlState; - }, - - async releaseControl(): Promise { - return (await post("/control/release", {})) as ControlState; - }, - - /** - * A person's own mouse and keyboard, straight through. - * - * Deliberately NOT governed by the policy gateway. The policy exists to constrain what a BOT may - * do; a person taking the wheel is the escape hatch that makes a governed Bot usable at all, and - * a rule that could lock somebody out of their own browser mid-login would be a worse failure - * than anything it prevented. The takeover itself is audited as an event; the keystrokes are not. - */ - /** Ask for a secret. Carries the label and the field, never a value. */ - async requestSecret(input: SecretRequest): Promise { - return (await post("/control/secret", input)) as ControlState; - }, - - /** - * Supply one. The value passes through this call and is kept nowhere: not returned upward, not - * logged here, and not written to the audit trail by the gateway. - */ - /** The computers this process holds, running or not. */ - async computers(): Promise<{ computers: ComputerProfile[] }> { - return (await call("/computers")) as { computers: ComputerProfile[] }; - }, - - /** Stop the browser and keep what it knows. */ - async stopComputer(): Promise<{ stopped: boolean; wasRunning: boolean }> { - return (await post("/computers/stop", {})) as { - stopped: boolean; - wasRunning: boolean; - }; - }, - - /** Delete the profile. Every login the Bot had goes with it. */ - async resetComputer(): Promise<{ reset: boolean; botId: string }> { - return (await post("/computers/reset", {})) as { - reset: boolean; - botId: string; - }; - }, - - async supplySecret(text: string): Promise { - return (await post("/human/secret", { text })) as SecretResult; - }, + const body = (await response.json().catch(() => null)) as Record< + string, + unknown + > | null; + if (!response.ok) { + throwMappedError(response.status, body); + } + return body as T; + } - async humanInput(input: HumanInput): Promise { - const { kind, ...rest } = input; - return (await post(`/human/${kind}`, rest)) as HumanInputResult; - }, + function post( + baseUrl: string, + botId: string, + path: string, + payload: unknown, + caller?: AbortSignal, + ): Promise { + return call( + baseUrl, + botId, + path, + { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(payload), + }, + caller, + ); + } - /** The same computer, addressed as a particular Bot. */ - forBot(id: string) { - return build(id); - }, - }; + async function navigate( + baseUrl: string, + botId: string, + url: string, + ): Promise { + const verdict = checkNavigationTarget(url, { + allowPrivateHosts: options.allowPrivateHosts, + }); + if (!verdict.allowed) { + throw new NavigationRefusedError(verdict.reason); + } + return post(baseUrl, botId, "/navigate", { + url: verdict.url, + }); } - return build(); + return { call, post, navigate }; } -export type ComputerClient = ReturnType; +/** Map agent-computer responses to errors that a caller can act on. */ +function throwMappedError( + status: number, + body: Record | null, +): never { + const detail = + typeof body?.error === "string" ? body.error : `HTTP ${status}`; + if (status === 409) { + throw new StaleSnapshotError(detail); + } + if (status === 403) { + throw new WorkspaceRefusedError(detail); + } + if (status === 400) { + throw new WorkspaceRequestError(detail); + } + if (/waiting for locator|Timeout .* exceeded/i.test(detail)) { + const ref = detail.match(/aria-ref=([A-Za-z0-9_-]+)/)?.[1]; + throw new ElementNotFoundError( + `${ref ? `Element ${ref} is` : "That element is"} not on the page any more. Take a fresh snapshot and use the refs from it.`, + ); + } + throw new ComputerUnavailableError(detail); +} diff --git a/server/src/computer/gateway.ts b/server/src/computer/gateway.ts index b2337be9..c7d3e32b 100644 --- a/server/src/computer/gateway.ts +++ b/server/src/computer/gateway.ts @@ -18,25 +18,48 @@ * The refs are opaque to the caller precisely so that the server holds the mapping. */ import { type AuditStore, recordAuditEvent } from "../audit"; -import type { ComputerClient } from "./client"; +import { ComputerUnavailableError, createComputerTransport } from "./client"; +import { checkComputerAddress } from "./target"; +export { + ComputerUnavailableError, + ElementNotFoundError, + NavigationRefusedError, + StaleSnapshotError, + WorkspaceRefusedError, + WorkspaceRequestError, +} from "./client"; import { type ActionPolicy, evaluateActionPolicy, type PolicyContext, type PolicyDecision, } from "./policy"; +import type { ComputerProvider } from "./provider"; import type { + ActionResult, ClickInput, + ComputerStatus, + ControlState, + HumanInput, + HumanInputResult, KeyInput, ListFilesInput, + ListFilesResult, + NavigateResult, ReadFileInput, + ReadFileResult, ReadResult, + ScreenshotResult, ScrollInput, SecretRequest, + SecretResult, SnapshotElement, SnapshotResult, TypeInput, + RunCommandInput, + RunCommandResult, WriteFileInput, + WriteFileResult, } from "./schema"; export class ActionRefusedError extends Error { @@ -59,25 +82,112 @@ export type ActionActor = { }; export type ComputerGatewayOptions = { - /** - * The container supervisor, when each Bot has a computer of its own. - * - * Stop and reset prefer it: a computer that is wedged cannot be asked to stop itself, and that is - * exactly the state where a person reaches for the button. Without a supervisor these stay profile - * operations performed by the computer itself, which fits the single-computer deployment where - * nothing else holds the Docker socket. - */ - supervisor?: { - stop(botId: string): Promise; - reset(botId: string): Promise; - list?(): Promise<{ botId: string; status: string; startedAt?: string }[]>; - }; - client: ComputerClient; + provider: ComputerProvider; auditStore: AuditStore; /** Absent denies everything. See evaluateActionPolicy. */ policy: () => ActionPolicy | undefined; + /** True on a laptop, where browsing private network addresses is required. */ + allowPrivateHosts?: boolean; + /** The secret that agent-computer requires on each request. */ + token?: string; + /** An injectable fetch implementation for focused gateway tests. */ + fetchImpl?: typeof fetch; }; +export interface ComputerGateway { + readonly provider: ComputerProvider; + locate(botId: string): Promise; + status(botId: string): Promise; + screenshot(botId: string): Promise; + snapshot(botId: string): Promise; + read(botId: string): Promise; + navigate( + botId: string, + actor: ActionActor, + url: string, + ): Promise; + click( + botId: string, + actor: ActionActor, + input: ClickInput, + signal?: AbortSignal, + ): Promise; + type( + botId: string, + actor: ActionActor, + input: TypeInput, + signal?: AbortSignal, + ): Promise; + key( + botId: string, + actor: ActionActor, + input: KeyInput, + signal?: AbortSignal, + ): Promise; + scroll( + botId: string, + actor: ActionActor, + input: ScrollInput, + ): Promise; + readFile( + botId: string, + actor: ActionActor, + input: ReadFileInput, + ): Promise; + listFiles( + botId: string, + actor: ActionActor, + input: ListFilesInput, + ): Promise; + runCommand( + botId: string, + actor: ActionActor, + input: RunCommandInput, + signal?: AbortSignal, + ): Promise; + writeFile( + botId: string, + actor: ActionActor, + input: WriteFileInput, + ): Promise; + control(botId: string): Promise; + requestHelp( + botId: string, + actor: ActionActor, + reason: string, + ): Promise; + takeControl(botId: string, actor: ActionActor): Promise; + releaseControl(botId: string, actor: ActionActor): Promise; + requestSecret( + botId: string, + actor: ActionActor, + input: SecretRequest, + ): Promise; + supplySecret( + botId: string, + actor: ActionActor, + text: string, + ): Promise; + humanInput(botId: string, input: HumanInput): Promise; + computers(): Promise<{ + isolation: "per-bot" | "shared"; + computers: { + botId: string; + running: boolean; + startedAt: string | null; + egress?: string | null; + }[]; + }>; + stopComputer( + botId: string, + actor: ActionActor, + ): Promise<{ wasRunning: boolean }>; + resetComputer( + botId: string, + actor: ActionActor, + ): Promise<{ cleared: boolean }>; +} + /** * The last snapshot the server took, per computer. * @@ -92,23 +202,75 @@ type CachedSnapshot = { url: string; }; -export function createComputerGateway(options: ComputerGatewayOptions) { - const { client, auditStore, supervisor } = options; +export function createComputerGateway( + options: ComputerGatewayOptions, +): ComputerGateway { + const { provider, auditStore } = options; + const transport = createComputerTransport({ + ...(options.token ? { token: options.token } : {}), + ...(options.allowPrivateHosts !== undefined + ? { allowPrivateHosts: options.allowPrivateHosts } + : {}), + ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}), + }); const snapshots = new Map(); /** - * The computer, addressed as the Bot that is asking. + * Where this Bot's computer is, checked before anything is sent to it. + * + * The provider decides the address, and a provider is a plug: it can be this deployment's own + * supervisor on loopback or a backend somewhere else answering over its own API. Either way the + * address goes straight into `fetch` carrying this deployment's computer token, so it is worth + * confirming it is an address we speak to rather than whatever came back. * - * Every call goes through this. The Bot's browser, its logins and the proxy its traffic leaves - * through are all keyed on this id at the far end, so a call that forgets it lands on the wrong - * computer, because there is always a computer to answer. + * Not the navigation check. That one refuses private hosts, which is the right answer for where a + * Bot may browse and the wrong one here, where loopback is the normal case. */ - const as = (botId: string) => client.forBot(botId); + async function locate(botId: string): Promise { + const address = await provider.locate(botId); + const verdict = checkComputerAddress(address); + if (!verdict.allowed) { + throw new ComputerUnavailableError(verdict.reason); + } + return verdict.url; + } + + async function get( + botId: string, + path: string, + signal?: AbortSignal, + ): Promise { + return transport.call( + await locate(botId), + botId, + path, + undefined, + signal, + ); + } + + async function post( + botId: string, + path: string, + payload: unknown, + signal?: AbortSignal, + ): Promise { + return transport.post(await locate(botId), botId, path, payload, signal); + } /** Read-only, so it passes straight through. Nothing has changed and there is nothing to decide. */ - async function snapshot(computerId: string): Promise { - const result = await as(computerId).snapshot(); - snapshots.set(computerId, { + async function screenshot(botId: string): Promise { + return get(botId, "/screenshot"); + } + + async function snapshot(botId: string): Promise { + const result = await transport.call( + await locate(botId), + botId, + "/snapshot", + { method: "POST" }, + ); + snapshots.set(botId, { snapshotId: result.snapshotId, url: result.url, elements: new Map( @@ -119,7 +281,7 @@ export function createComputerGateway(options: ComputerGatewayOptions) { } async function read(botId: string): Promise { - return as(botId).read(); + return get(botId, "/read"); } /** @@ -130,11 +292,11 @@ export function createComputerGateway(options: ComputerGatewayOptions) { * A deny rule written against a page a Bot has not snapshotted should still refuse it. */ function resolve( - computerId: string, + botId: string, ref: string | undefined, ): SnapshotElement | undefined { if (!ref) return undefined; - return snapshots.get(computerId)?.elements.get(ref); + return snapshots.get(botId)?.elements.get(ref); } /** @@ -145,7 +307,6 @@ export function createComputerGateway(options: ComputerGatewayOptions) { * show that sequence. */ async function govern( - computerId: string, toolName: string, botId: string, actor: ActionActor, @@ -154,14 +315,16 @@ export function createComputerGateway(options: ComputerGatewayOptions) { filePath?: string; targetUrl?: string; key?: string; + /** The command a shell call is about to run, so a rule can be written against it. */ + command?: string; /** The person's Stop, on its way to the browser. See the acting methods below. */ signal?: AbortSignal; }, run: () => Promise, ): Promise { const { ref, filePath } = subject; - const element = resolve(computerId, ref); - const cached = snapshots.get(computerId); + const element = resolve(botId, ref); + const cached = snapshots.get(botId); // For a navigation the relevant page is the one being opened, not the one already loaded. Using // the cached URL would mean `page.host == "..."` could never match the destination, which is the // only thing a rule about navigation would ever want to say. @@ -187,6 +350,7 @@ export function createComputerGateway(options: ComputerGatewayOptions) { } : {}), ...(filePath ? { file: describeFile(filePath) } : {}), + ...(subject.command ? { command: subject.command } : {}), }; const decision = evaluateActionPolicy(options.policy(), context); @@ -194,15 +358,14 @@ export function createComputerGateway(options: ComputerGatewayOptions) { toolName, botId, actor, - computerId, element, ref, ...(subject.key ? { key: subject.key } : {}), + ...(subject.command ? { command: subject.command } : {}), filePath, pageUrl, decision, }); - if (!decision.forward) { throw new ActionRefusedError(decision.reason, decision.matched); } @@ -225,7 +388,6 @@ export function createComputerGateway(options: ComputerGatewayOptions) { toolName, botId, actor, - computerId, element, ref, filePath, @@ -244,9 +406,16 @@ export function createComputerGateway(options: ComputerGatewayOptions) { } return { + provider, + locate, + screenshot, snapshot, read, + status(botId: string): Promise { + return provider.status(botId); + }, + /** * Handovers, recorded but not policy-gated. * @@ -256,28 +425,23 @@ export function createComputerGateway(options: ComputerGatewayOptions) { * row and do not ask. What IS recorded is the period: who, when, and why the Bot asked, the fact * an investigator wants is that a human drove this browser between two times. */ - async requestHelp( - computerId: string, - botId: string, - actor: ActionActor, - reason: string, - ) { - const state = await as(botId).requestControl(reason); + async requestHelp(botId: string, actor: ActionActor, reason: string) { + const state = await post(botId, "/control/request", { + reason, + }); await writeControlEvent(auditStore, "computer.help_requested", { botId, actor, - computerId, reason, }); return state; }, - async takeControl(computerId: string, botId: string, actor: ActionActor) { - const state = await as(botId).takeControl(); + async takeControl(botId: string, actor: ActionActor) { + const state = await post(botId, "/control/take", {}); await writeControlEvent(auditStore, "computer.control_taken", { botId, actor, - computerId, // Carried onto the row so the trail says what the person was handed, not merely that they // took over. reason: state.reason, @@ -285,49 +449,31 @@ export function createComputerGateway(options: ComputerGatewayOptions) { return state; }, - async releaseControl( - computerId: string, - botId: string, - actor: ActionActor, - ) { - const state = await as(botId).releaseControl(); + async releaseControl(botId: string, actor: ActionActor) { + const state = await post(botId, "/control/release", {}); await writeControlEvent(auditStore, "computer.control_released", { botId, actor, - computerId, }); return state; }, - control(botId: string) { - return as(botId).control(); + control(botId: string): Promise { + return get(botId, "/control"); }, - /** - * The computers, for the admin surface. A read, so no audit row. - * - * With a supervisor the list is the containers, because that is what a computer is: one per - * Bot, each with its own storage, and the page's Stop and Reset act on those. Asking a single - * computer for its profiles would answer for the one shared browser instead, which is the older - * arrangement and no longer what an administrator is looking at. - */ + /** Return every computer that the configured provider owns. */ async computers() { - if (supervisor?.list) { - const running = await supervisor.list(); - return { - // Said, not inferred. Without a supervisor every Bot shares one browser, which looks - // identical on every screen to each having its own, same cards, same trail, same - // screenshots. A reader has to be told which deployment they are looking at. - isolation: "per-bot" as const, - computers: running.map((computer) => ({ - botId: computer.botId, - running: computer.status === "running", - startedAt: computer.startedAt ?? null, - egress: null, - })), - }; - } - return { isolation: "shared" as const, ...(await client.computers()) }; + const computers = await provider.list(); + return { + isolation: provider.isolation, + computers: computers.map((computer) => ({ + botId: computer.botId, + running: computer.status === "running", + startedAt: computer.startedAt ?? null, + egress: computer.egress, + })), + }; }, /** @@ -338,25 +484,14 @@ export function createComputerGateway(options: ComputerGatewayOptions) { * fact worth having, and a trail that only records effective actions cannot tell you what somebody * tried. */ - async stopComputer(computerId: string, botId: string, actor: ActionActor) { - if (supervisor) { - await supervisor.stop(botId); - await writeControlEvent(auditStore, "computer.stopped", { - botId, - actor, - computerId, - reason: "the container was stopped", - }); - return { wasRunning: true }; - } - const result = await as(botId).stopComputer(); + async stopComputer(botId: string, actor: ActionActor) { + const result = await provider.stop(botId); await writeControlEvent(auditStore, "computer.stopped", { botId, actor, - computerId, reason: result.wasRunning - ? "browser was running" - : "no browser was running", + ? "the computer was stopped" + : "the computer was already stopped", }); return result; }, @@ -367,23 +502,15 @@ export function createComputerGateway(options: ComputerGatewayOptions) { * The most destructive button we have. Every login the Bot had is gone and no undo exists, so the * row is written whatever happens next. */ - async resetComputer(computerId: string, botId: string, actor: ActionActor) { - if (supervisor) { - await supervisor.reset(botId); - await writeControlEvent(auditStore, "computer.reset", { - botId, - actor, - computerId, - reason: "the container and its profile were deleted", - }); - return { cleared: true }; - } - const result = await as(botId).resetComputer(); + async resetComputer(botId: string, actor: ActionActor) { + const result = await provider.reset(botId); + snapshots.delete(botId); await writeControlEvent(auditStore, "computer.reset", { botId, actor, - computerId, - reason: "every saved login on this computer was deleted", + reason: result.cleared + ? "the computer and its saved state were deleted" + : "no saved state was present to delete", }); return result; }, @@ -397,129 +524,104 @@ export function createComputerGateway(options: ComputerGatewayOptions) { * keyboard to the page, and is not on this one. */ async requestSecret( - computerId: string, botId: string, actor: ActionActor, input: SecretRequest, ) { - const state = await as(botId).requestSecret(input); + const state = await post(botId, "/control/secret", input); await writeControlEvent(auditStore, "computer.secret_requested", { botId, actor, - computerId, reason: `${input.label} (into ${input.ref})`, }); return state; }, - async supplySecret( - computerId: string, - botId: string, - actor: ActionActor, - text: string, - ) { - const result = await as(botId).supplySecret(text); + async supplySecret(botId: string, actor: ActionActor, text: string) { + const result = await post(botId, "/human/secret", { text }); await writeControlEvent(auditStore, "computer.secret_supplied", { botId, actor, - computerId, // Length, never content. Enough to show something real was entered. reason: `${result.characters} characters`, }); return result; }, - humanInput( + async humanInput( botId: string, - input: Parameters[0], - ) { - return as(botId).humanInput(input); + input: HumanInput, + ): Promise { + const { kind, ...payload } = input; + return post(botId, `/human/${kind}`, payload); }, /** * Opening a page, through the gateway so it lands in the audit trail. * - * The client still applies its target guard, which is the floor that holds under every policy, - * including one that permits everything. This adds the record and the per-Bot decision on top: a - * refusal by either produces a row, so navigation denials are visible in the audit trail. + * The transport applies its target guard before it sends a request. This is + * the minimum rule that applies even when the action policy permits the URL. */ - navigate( - computerId: string, - botId: string, - actor: ActionActor, - url: string, - ) { + navigate(botId: string, actor: ActionActor, url: string) { return govern( - computerId, "computer_navigate", botId, actor, { targetUrl: url }, - () => as(botId).navigate(url), + async () => transport.navigate(await locate(botId), botId, url), ); }, click( - computerId: string, botId: string, actor: ActionActor, input: ClickInput, signal?: AbortSignal, ) { return govern( - computerId, "computer_click", botId, actor, { ref: input.ref, ...(signal ? { signal } : {}) }, - () => as(botId).click(input, signal), + () => post(botId, "/click", input, signal), ); }, type( - computerId: string, botId: string, actor: ActionActor, input: TypeInput, signal?: AbortSignal, ) { return govern( - computerId, "computer_type", botId, actor, { ref: input.ref, ...(signal ? { signal } : {}) }, - () => as(botId).type(input, signal), + () => post(botId, "/type", input, signal), ); }, key( - computerId: string, botId: string, actor: ActionActor, input: KeyInput, signal?: AbortSignal, ) { return govern( - computerId, "computer_key", botId, actor, // The key is part of the subject, so a rule can tell Enter from a letter. Form submission can // happen through a keypress as well as a click, so the policy context carries the key. { ref: input.ref, key: input.key, ...(signal ? { signal } : {}) }, - () => as(botId).key(input, signal), + () => post(botId, "/key", input, signal), ); }, - scroll( - computerId: string, - botId: string, - actor: ActionActor, - input: ScrollInput, - ) { - return govern(computerId, "computer_scroll", botId, actor, {}, () => - as(botId).scroll(input), + scroll(botId: string, actor: ActionActor, input: ScrollInput) { + return govern("computer_scroll", botId, actor, {}, () => + post(botId, "/scroll", input), ); }, @@ -530,19 +632,13 @@ export function createComputerGateway(options: ComputerGatewayOptions) { * workspace accumulates whatever a Bot has saved across every task it has ever run, so which of * those files it may read back is a real question for a deployment to be able to answer. */ - readFile( - computerId: string, - botId: string, - actor: ActionActor, - input: ReadFileInput, - ) { + readFile(botId: string, actor: ActionActor, input: ReadFileInput) { return govern( - computerId, "computer_read_file", botId, actor, { filePath: input.path }, - () => as(botId).readFile(input), + () => post(botId, "/files/read", input), ); }, @@ -551,35 +647,45 @@ export function createComputerGateway(options: ComputerGatewayOptions) { * every task it has run is worth being able to restrict. A rule denying a folder hides it from the * listing as well as from reads, which is the consistent answer. */ - listFiles( - computerId: string, - botId: string, - actor: ActionActor, - input: ListFilesInput, - ) { + listFiles(botId: string, actor: ActionActor, input: ListFilesInput) { return govern( - computerId, "computer_list_files", botId, actor, { filePath: input.path ?? "." }, - () => as(botId).listFiles(input), + () => post(botId, "/files/list", input), ); }, - writeFile( - computerId: string, + /** + * A command, judged before it runs. + * + * The same four steps as a click: resolve, decide, record, act. The policy sees the command + * text, so a deployment can refuse a shell outright with `intent == "run_command"` or refuse + * particular commands, and either way the attempt is a row in the trail whether or not it ran. + */ + runCommand( botId: string, actor: ActionActor, - input: WriteFileInput, + input: RunCommandInput, + caller?: AbortSignal, ) { return govern( - computerId, + "computer_run_command", + botId, + actor, + { command: input.command, ...(caller ? { signal: caller } : {}) }, + () => post(botId, "/exec", input, caller), + ); + }, + + writeFile(botId: string, actor: ActionActor, input: WriteFileInput) { + return govern( "computer_write_file", botId, actor, { filePath: input.path }, - () => as(botId).writeFile(input), + () => post(botId, "/files/write", input), ); }, }; @@ -607,8 +713,6 @@ function describeFile(path: string): { }; } -export type ComputerGateway = ReturnType; - /** * One audit row for one decision. * @@ -652,6 +756,8 @@ function intentOf( return "read_file"; case "computer_write_file": return "write_file"; + case "computer_run_command": + return "run_command"; case "computer_list_files": return "list_files"; default: @@ -665,7 +771,6 @@ async function write( toolName: string; botId: string; actor: ActionActor; - computerId: string; element: SnapshotElement | undefined; ref: string | undefined; /** Which key, for a keypress. Recorded because a keypress can act without naming a button. */ @@ -673,6 +778,8 @@ async function write( filePath: string | undefined; pageUrl: string; decision: PolicyDecision; + /** The command a shell call ran, so the trail says what was run and not merely that something was. */ + command?: string; /** Set only when a permitted action was attempted and did not succeed. */ failure?: string; }, @@ -686,7 +793,7 @@ async function write( ? "computer.action_allowed" : "computer.action_refused", targetType: "computer", - targetId: entry.computerId, + targetId: entry.botId, // Only ever a real users row. The audit table has a foreign key to it, so writing the local // development actor's id here makes every action fail on a constraint violation instead of being // recorded. Who it was is in the payload either way. @@ -706,15 +813,23 @@ async function write( // The path, never the contents. A Bot writes down what it was told, so a file body is exactly as // sensitive as text typed into a form field, and for the same reason it is not put here. ...(entry.filePath ? { file: entry.filePath } : {}), + /* + * The command, in full, and its output never. + * + * The opposite call from the file body above, deliberately. A command IS the action, so a + * trail recording that a Bot "ran something" answers nothing anyone would ask it. Its output + * is the file body of this pair, and stays out. + */ + ...(entry.command ? { command: entry.command } : {}), element: entry.element ? { role: entry.element.role, name: entry.element.name, ...(entry.element.type ? { type: entry.element.type } : {}), } - : entry.filePath - ? // A file action has no element and never will. File rows leave the element field absent - // rather than describing a browser snapshot. + : entry.filePath || entry.command + ? // A file or command action has no element and never will. Those rows leave the element + // field absent rather than describing a browser snapshot. undefined : // An action on an element the server cannot identify is worth recording plainly, rather // than as an absent field that reads like a logging gap. @@ -762,14 +877,13 @@ async function writeControlEvent( entry: { botId: string; actor: ActionActor; - computerId: string; reason?: string; }, ) { await recordAuditEvent(auditStore, { eventType, targetType: "computer", - targetId: entry.computerId, + targetId: entry.botId, ...(entry.actor.userId ? { actorUserId: entry.actor.userId } : {}), payload: { bot: entry.botId, diff --git a/server/src/computer/policy.ts b/server/src/computer/policy.ts index a5b96d04..299e7f22 100644 --- a/server/src/computer/policy.ts +++ b/server/src/computer/policy.ts @@ -96,7 +96,8 @@ export type PolicyContext = { // intents: an operator thinks "nothing may change anything in Jira", not "nothing may call // editJiraIssue, transitionJiraIssue, addCommentToJiraIssue and the six others". | "read_tool" - | "write_tool"; + | "write_tool" + | "run_command"; /** * The file a `computer_read_file` or `computer_write_file` call is aimed at. * @@ -131,6 +132,17 @@ export type PolicyContext = { tool: string; effect: "read" | "write"; }; + /** + * The command a Bot is about to run on its computer, verbatim. + * + * Verbatim because a rule about a shell can only be written against what was actually typed. This + * is the field for `deny: contains(command, "rm -rf")`, and for the blunter and more useful + * `deny: intent == "run_command"`, which is how a deployment says its Bots do not get a shell. + * + * Matching on command text is a filter, not a boundary: a command can be written a hundred ways + * and no list catches them all. The boundary is the container the command runs in. + */ + command?: string; }; export type PolicyDecision = { @@ -272,6 +284,15 @@ function describeRefusal(context: PolicyContext, expression: string): string { // tests below true of a tool call and all of them wrong about it: without this branch a refused // Jira call reads "the file is blocked", naming a workspace it never touched and a path that is // not there. Checked first because it is the only one of these that is ever certain. + // A command is described by the command. Falling through to the page branch below would produce + // "a run_command action on " with an empty host, because a shell call has no page. + if (context.command) { + return ( + `This deployment's policy does not allow that: the command \`${context.command}\` ` + + `is blocked by the rule \`${expression}\`.` + ); + } + if (context.mcp) { return ( `This deployment's policy does not allow that: ${context.mcp.tool} on ` + diff --git a/server/src/computer/provider.ts b/server/src/computer/provider.ts new file mode 100644 index 00000000..950e9fe7 --- /dev/null +++ b/server/src/computer/provider.ts @@ -0,0 +1,227 @@ +import type { ComputerConfig } from "../config"; +import { + createDockerSupervisorProvider, + type SupervisorOptions, +} from "./supervisor"; + +import type { ComputerStatus } from "./schema"; + +/** The address and lifecycle details for one Bot's computer. */ +export type ComputerLocation = { + botId: string; + status: "running" | "stopped"; + url?: string; + startedAt?: string; + egress?: string | null; +}; + +/** A description of how a provider separates one Bot's computer from another. */ +export type IsolationDescription = { + isolation: "off" | "one computer per Bot" | "one shared computer"; + note: string; + warning?: string; +}; + +/** An error from a computer provider. */ +export class ProviderError extends Error { + constructor(message: string) { + super(message); + this.name = "ProviderError"; + } +} + +/** Describe the isolation that this provider (or lack of provider) gives to Bots. */ +export function describeComputerIsolation( + provider?: ComputerProvider, +): IsolationDescription { + if (!provider) { + return { + isolation: "off", + note: "The computer feature is off. No computer provider is configured.", + }; + } + + if (provider.isolation === "per-bot") { + return { + isolation: "one computer per Bot", + note: "Each Bot gets its own isolated computer with its own /workspace and browser profile.", + }; + } + + return { + isolation: "one shared computer", + note: "No supervisor is configured, so every Bot uses the same browser. Sessions, files and logins are shared between them. Set COMPUTER_SUPERVISOR_URL to give each Bot its own.", + warning: + "Every Bot shares one browser. Set COMPUTER_SUPERVISOR_URL for a computer each.", + }; +} + +/** + * A backend that gives Bots access to a computer. + * + * Implementations can use a computer for each Bot or one computer for all Bots. + * Callers use this interface and do not need to know which backend is active. + */ +export interface ComputerProvider { + /** The provider name for logs and status output. */ + readonly name: string; + /** How the provider separates computers between Bots. */ + readonly isolation: "per-bot" | "shared"; + /** Return the base address of the computer for this Bot. */ + locate(botId: string): Promise; + /** Return the lifecycle state of the computer for this Bot. */ + status(botId: string): Promise; + /** Stop the computer for this Bot if it exists. */ + stop(botId: string): Promise<{ wasRunning: boolean }>; + /** Remove the computer state for this Bot if it exists. */ + reset(botId: string): Promise<{ cleared: boolean }>; + /** List the computers that this provider owns. */ + list(): Promise; + /** Prepare provider resources before the first computer request. */ + warm?(): Promise; +} + +export type SharedComputerProviderOptions = { + baseUrl: string; + token?: string; + fetchImpl?: typeof fetch; + timeoutMs?: number; +}; +type SharedComputerEntry = { + botId: string; + running?: boolean; + status?: string; + url?: string; + startedAt?: string | null; + egress?: string | null; +}; +/** + * Give every Bot the same computer. + * + * This adapter keeps shared deployments behind the same provider seam as the + * Docker supervisor, and is the seam a remote backend plugs into. + */ +export function createSharedComputerProvider( + options: SharedComputerProviderOptions, +): ComputerProvider { + const base = options.baseUrl.replace(/\/$/, ""); + const fetchImpl = options.fetchImpl ?? fetch; + const timeoutMs = options.timeoutMs ?? 45_000; + + function headers(botId?: string): Record { + return { + ...(botId ? { "x-openbot-bot-id": botId } : {}), + ...(options.token ? { "x-openbot-computer-token": options.token } : {}), + }; + } + + async function call( + path: string, + method: "GET" | "POST", + botId?: string, + ): Promise { + let response: Response; + try { + response = await fetchImpl(`${base}${path}`, { + method, + headers: headers(botId), + signal: AbortSignal.timeout(timeoutMs), + }); + } catch (error) { + throw new ProviderError( + `The shared computer at ${base} could not be reached (${error instanceof Error ? error.message : String(error)}).`, + ); + } + + const body = (await response.json().catch(() => null)) as { + error?: string; + } | null; + if (!response.ok) { + throw new ProviderError( + body?.error ?? `The shared computer answered ${response.status}.`, + ); + } + return body; + } + + return { + name: "shared", + isolation: "shared", + + async locate(_botId: string): Promise { + return options.baseUrl; + }, + + async status(botId: string): Promise { + try { + await call("/health", "GET", botId); + return { botId, state: "ready" }; + } catch (error) { + return { + botId, + state: "unreachable", + reason: + error instanceof Error && error.message.length > 0 + ? error.message + : "Unknown failure.", + }; + } + }, + + async stop(botId: string): Promise<{ wasRunning: boolean }> { + const body = (await call("/computers/stop", "POST", botId)) as { + wasRunning?: boolean; + stopped?: boolean; + } | null; + return { + wasRunning: body?.wasRunning ?? body?.stopped ?? false, + }; + }, + + async reset(botId: string): Promise<{ cleared: boolean }> { + const body = (await call("/computers/reset", "POST", botId)) as { + cleared?: boolean; + reset?: boolean; + } | null; + return { + cleared: body?.cleared ?? body?.reset ?? false, + }; + }, + + async list(): Promise { + const body = (await call("/computers", "GET")) as { + computers?: SharedComputerEntry[]; + }; + return (body?.computers ?? []).map((computer) => ({ + botId: computer.botId, + status: + computer.status === "running" || computer.running === true + ? "running" + : "stopped", + url: computer.url ?? base, + ...(computer.startedAt ? { startedAt: computer.startedAt } : {}), + ...(computer.egress !== undefined ? { egress: computer.egress } : {}), + })); + }, + }; +} + +/** Build the one computer provider selected by deployment configuration. */ +export function createComputerProvider( + config: ComputerConfig, +): ComputerProvider { + switch (config.provider) { + case "docker": { + const options: SupervisorOptions = { + baseUrl: config.baseUrl, + ...(config.supervisorToken ? { token: config.supervisorToken } : {}), + }; + return createDockerSupervisorProvider(options); + } + case "shared": + return createSharedComputerProvider({ + baseUrl: config.baseUrl, + ...(config.token ? { token: config.token } : {}), + }); + } +} diff --git a/server/src/computer/routes.ts b/server/src/computer/routes.ts index 3ce1c285..599ddf75 100644 --- a/server/src/computer/routes.ts +++ b/server/src/computer/routes.ts @@ -3,18 +3,15 @@ import { Hono } from "hono"; import type { AppVariables } from "../auth/guards"; import { requireAdmin } from "../auth/guards"; import { - type ComputerClient, + type ActionActor, + ActionRefusedError, + type ComputerGateway, ComputerUnavailableError, ElementNotFoundError, NavigationRefusedError, StaleSnapshotError, WorkspaceRefusedError, WorkspaceRequestError, -} from "./client"; -import { - type ActionActor, - ActionRefusedError, - type ComputerGateway, } from "./gateway"; import { type PolicyStore, parseActionPolicy } from "./policy-store"; @@ -25,27 +22,24 @@ import { type PolicyStore, parseActionPolicy } from "./policy-store"; * session guard because `COMPUTER_TOKEN` proves the caller is an internal service, not which user is * asking to drive the browser. * - * Read-only calls go to the client; acting calls go to the gateway. That split is the governance - * boundary: every acting route in this file passes through a policy decision and audit row before it - * reaches the computer. + * Every computer call goes through the gateway. That is the governance seam: each acting route in + * this file passes through a policy decision and audit row before it reaches the computer. */ export function createComputerRoutes( - client: ComputerClient, gateway: ComputerGateway, policyStore: PolicyStore, requireUser: MiddlewareHandler<{ Variables: AppVariables }>, ) { const routes = new Hono<{ Variables: AppVariables }>(); - routes.get("/:botId/status", requireUser, async (context) => - context.json(await client.status(context.req.param("botId"))), - ); + routes.get("/:botId/status", requireUser, async (context) => { + const botId = context.req.param("botId"); + return context.json(await gateway.status(botId)); + }); routes.get("/:botId/screenshot", requireUser, async (context) => { try { - return context.json( - await client.forBot(context.req.param("botId")).screenshot(), - ); + return context.json(await gateway.screenshot(context.req.param("botId"))); } catch (error) { return context.json({ error: describe(error) }, statusFor(error)); } @@ -70,7 +64,6 @@ export function createComputerRoutes( try { return context.json( await gateway.navigate( - context.req.param("botId") ?? "default", context.req.param("botId") ?? "default", { id: context.var.actor.id, @@ -106,14 +99,14 @@ export function createComputerRoutes( /** * The acting routes. * - * Each one hands the gateway the computer id, the Bot, the actor and the input, and does no checking + * Each one hands the gateway the Bot, the actor and the input, and does no checking * of its own beyond the shape of the request. Where a decision gets made is a single place. */ routes.post("/:botId/click", requireUser, (context) => act(context, (botId, actor, body, signal) => { const ref = asRef(body); if (!ref) return badRef; - return gateway.click(botId, botId, actor, ref, signal); + return gateway.click(botId, actor, ref, signal); }), ); @@ -125,7 +118,6 @@ export function createComputerRoutes( return { error: "The text to enter is required." }; } return gateway.type( - botId, botId, actor, { @@ -145,7 +137,6 @@ export function createComputerRoutes( } const ref = asRef(body); return gateway.key( - botId, botId, actor, { @@ -159,7 +150,7 @@ export function createComputerRoutes( routes.post("/:botId/scroll", requireUser, (context) => act(context, (botId, actor, body) => - gateway.scroll(botId, botId, actor, { + gateway.scroll(botId, actor, { ...(typeof body?.deltaY === "number" ? { deltaY: body.deltaY } : {}), }), ), @@ -180,7 +171,6 @@ export function createComputerRoutes( routes.post("/:botId/control/request", requireUser, (context) => act(context, (botId, actor, body) => gateway.requestHelp( - botId, botId, actor, typeof body?.reason === "string" && body.reason.trim() @@ -207,20 +197,20 @@ export function createComputerRoutes( /** Stop the browser, keep the logins. */ routes.post("/:botId/computers/stop", requireUser, (context) => - act(context, (botId, actor) => gateway.stopComputer(botId, botId, actor)), + act(context, (botId, actor) => gateway.stopComputer(botId, actor)), ); /** Delete the profile. Every login goes with it, which is the point and also the danger. */ routes.post("/:botId/computers/reset", requireUser, (context) => - act(context, (botId, actor) => gateway.resetComputer(botId, botId, actor)), + act(context, (botId, actor) => gateway.resetComputer(botId, actor)), ); routes.post("/:botId/control/take", requireUser, (context) => - act(context, (botId, actor) => gateway.takeControl(botId, botId, actor)), + act(context, (botId, actor) => gateway.takeControl(botId, actor)), ); routes.post("/:botId/control/release", requireUser, (context) => - act(context, (botId, actor) => gateway.releaseControl(botId, botId, actor)), + act(context, (botId, actor) => gateway.releaseControl(botId, actor)), ); /** The Bot asking for a value it must not be told. */ @@ -235,7 +225,7 @@ export function createComputerRoutes( if (typeof body?.snapshotId !== "number") { return { error: "The snapshotId the ref came from is required." }; } - return gateway.requestSecret(botId, botId, actor, { + return gateway.requestSecret(botId, actor, { label: typeof body?.label === "string" && body.label.trim() ? body.label.trim() @@ -258,17 +248,16 @@ export function createComputerRoutes( if (typeof body?.text !== "string" || !body.text) { return { error: "A value is required." }; } - return gateway.supplySecret(botId, botId, actor, body.text); + return gateway.supplySecret(botId, actor, body.text); }), ); /** * A person's own mouse and keyboard. * - * Not through the policy gateway, and not audited per keystroke, see the note on `humanInput` in - * client.ts. The takeover is the audited event; what the person typed during it is deliberately - * unrecorded, because the reason a takeover exists is to let them enter the thing nothing else - * should keep. + * Not through the policy decision, and not audited per keystroke. See `ComputerGateway.humanInput`. + * The takeover is the audited event; what the person typed during it is deliberately unrecorded, + * because the reason a takeover exists is to let them enter the thing nothing else should keep. */ routes.post("/:botId/human/:kind", requireUser, async (context) => { const kind = context.req.param("kind"); @@ -299,7 +288,7 @@ export function createComputerRoutes( /** The Bot's files. Through the gateway, like every other acting call. */ routes.post("/:botId/files/list", requireUser, (context) => act(context, (botId, actor, body) => - gateway.listFiles(botId, botId, actor, { + gateway.listFiles(botId, actor, { ...(typeof body?.path === "string" && body.path.trim() ? { path: body.path.trim() } : {}), @@ -312,7 +301,28 @@ export function createComputerRoutes( if (typeof body?.path !== "string" || !body.path.trim()) { return { error: "A file path is required." }; } - return gateway.readFile(botId, botId, actor, { path: body.path.trim() }); + return gateway.readFile(botId, actor, { path: body.path.trim() }); + }), + ); + + /* + * A command on the Bot's computer. + * + * Same shape as every other acting route: the gateway decides and records, this only shapes the + * request. `timeoutMs` is passed through and capped by the computer rather than here, so one place + * owns the limit. + */ + routes.post("/:botId/exec", requireUser, (context) => + act(context, (botId, actor, body) => { + if (typeof body?.command !== "string" || !body.command.trim()) { + return { error: "A command is required." }; + } + return gateway.runCommand(botId, actor, { + command: body.command, + ...(typeof body.timeoutMs === "number" + ? { timeoutMs: body.timeoutMs } + : {}), + }); }), ); @@ -324,7 +334,7 @@ export function createComputerRoutes( if (typeof body?.contents !== "string") { return { error: "The contents to write are required." }; } - return gateway.writeFile(botId, botId, actor, { + return gateway.writeFile(botId, actor, { path: body.path.trim(), contents: body.contents, append: body.append === true, diff --git a/server/src/computer/schema.ts b/server/src/computer/schema.ts index 659e8d7a..70e88ac9 100644 --- a/server/src/computer/schema.ts +++ b/server/src/computer/schema.ts @@ -209,6 +209,35 @@ export type ReadFileResult = { bytes: number; }; +/** + * A command for the Bot's computer to run. + * + * The whole command as one string, because a shell's usefulness is pipes, redirection and `&&`, and + * a shape that took a binary plus arguments would be a worse shell wearing the name. + */ +export type RunCommandInput = { + command: string; + /** Capped by the computer. Absent takes its default. */ + timeoutMs?: number; +}; + +/** + * What running one reports back. + * + * `truncated` and `timedOut` are separate from the exit code because they are different facts about + * the same run: a command can succeed and still have had its output cut, and one that was killed on + * the clock never produced an exit code of its own. + */ +export type RunCommandResult = { + command: string; + exitCode: number; + stdout: string; + stderr: string; + truncated: boolean; + timedOut: boolean; + elapsedMs: number; +}; + export type WriteFileInput = { path: string; contents: string; diff --git a/server/src/computer/supervisor.ts b/server/src/computer/supervisor.ts index 6212973d..2af756c7 100644 --- a/server/src/computer/supervisor.ts +++ b/server/src/computer/supervisor.ts @@ -13,9 +13,12 @@ * honest about being one shared computer. */ -export type ComputerLocation = { +import type { ComputerStatus } from "./schema"; +import type { ComputerLocation, ComputerProvider } from "./provider"; + +type SupervisorComputerLocation = { botId: string; - container: string; + container?: string; status: string; port?: number; /** Where to reach it, decided by the supervisor rather than assembled here. */ @@ -39,7 +42,9 @@ export class SupervisorError extends Error { } } -export function createSupervisorClient(options: SupervisorOptions) { +export function createDockerSupervisorProvider( + options: SupervisorOptions, +): ComputerProvider { const doFetch = options.fetchImpl ?? fetch; const base = options.baseUrl.replace(/\/$/, ""); const timeoutMs = options.timeoutMs ?? 120_000; @@ -64,6 +69,9 @@ export function createSupervisorClient(options: SupervisorOptions) { const body = (await response.json().catch(() => null)) as { error?: string; + stopped?: boolean; + reset?: boolean; + computers?: SupervisorComputerLocation[]; } | null; if (!response.ok) { throw new SupervisorError( @@ -73,7 +81,64 @@ export function createSupervisorClient(options: SupervisorOptions) { return body; } + async function listRaw(): Promise { + const body = (await call("/computers", "GET")) as { + computers?: SupervisorComputerLocation[]; + } | null; + return body?.computers ?? []; + } + + async function list(): Promise { + const computers = await listRaw(); + return computers.map((computer) => ({ + botId: computer.botId, + status: + computer.status.toLowerCase() === "running" ? "running" : "stopped", + ...(computer.url + ? { url: computer.url } + : computer.port + ? { url: hostForPort(computer.port) } + : {}), + ...(computer.startedAt ? { startedAt: computer.startedAt } : {}), + })); + } + + function statusFromLocation( + botId: string, + location: SupervisorComputerLocation | undefined, + ): ComputerStatus { + if (!location) return { botId, state: "absent" }; + + const rawStatus = location.status.toLowerCase(); + switch (rawStatus) { + case "running": + return { botId, state: "ready" }; + case "created": + case "restarting": + return { botId, state: "starting" }; + case "paused": + case "removing": + case "exited": + return { botId, state: "absent" }; + case "dead": + return { + botId, + state: "unreachable", + reason: `The computer reported state "${location.status}".`, + }; + default: + return { + botId, + state: "unreachable", + reason: `The computer reported unknown state "${location.status}".`, + }; + } + } + return { + name: "Docker supervisor", + isolation: "per-bot", + /** * The URL of this Bot's computer, starting it if it is not already up. * @@ -84,7 +149,7 @@ export function createSupervisorClient(options: SupervisorOptions) { async locate(botId: string): Promise { const state = (await call( `/computers/${encodeURIComponent(botId)}/ensure`, - )) as ComputerLocation; + )) as SupervisorComputerLocation; // The supervisor says where it is, because only it knows whether these computers sit on a // shared network or answer on a published port. if (state?.url) return state.url; @@ -94,21 +159,39 @@ export function createSupervisorClient(options: SupervisorOptions) { ); }, - async stop(botId: string): Promise { - await call(`/computers/${encodeURIComponent(botId)}/stop`); + async status(botId: string): Promise { + try { + const computers = await listRaw(); + return statusFromLocation( + botId, + computers.find((computer) => computer.botId === botId), + ); + } catch (error) { + return { + botId, + state: "unreachable", + reason: + error instanceof Error && error.message.length > 0 + ? error.message + : "Unknown failure.", + }; + } }, - async reset(botId: string): Promise { - await call(`/computers/${encodeURIComponent(botId)}/reset`); + async stop(botId: string): Promise<{ wasRunning: boolean }> { + const result = (await call( + `/computers/${encodeURIComponent(botId)}/stop`, + )) as { stopped?: boolean } | null; + return { wasRunning: result?.stopped === true }; }, - async list(): Promise { - const body = (await call("/computers", "GET")) as { - computers?: ComputerLocation[]; - }; - return body?.computers ?? []; + async reset(botId: string): Promise<{ cleared: boolean }> { + const result = (await call( + `/computers/${encodeURIComponent(botId)}/reset`, + )) as { reset?: boolean } | null; + return { cleared: result?.reset === true }; }, + + list, }; } - -export type SupervisorClient = ReturnType; diff --git a/server/src/config.ts b/server/src/config.ts index a85e8f01..96f45cea 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -21,6 +21,25 @@ export type IntelligenceSettings = { licenseToken: string; }; +export type DockerComputerConfig = { + provider: "docker"; + baseUrl: string; + supervisorToken?: string; + token?: string; + allowPrivateHosts: boolean; + policy?: ActionPolicy; +}; + +export type SharedComputerConfig = { + provider: "shared"; + baseUrl: string; + token?: string; + allowPrivateHosts: boolean; + policy?: ActionPolicy; +}; + +export type ComputerConfig = DockerComputerConfig | SharedComputerConfig; + export type DeploymentConfig = { databaseUrl: string; keyEncryptionKey: string; @@ -58,31 +77,20 @@ export type DeploymentConfig = { * See auth/dev-actor.ts for the two locks that stop this reaching a deployment. */ devNoAuth: boolean; + /** Names OpenBot on the analytics the runtime already sends. Off with OPENBOT_ACCESSIBILITY_DISABLED. */ + accessibility: boolean; + /** + * Where the built app is, when this process serves it. + * + * Set in a container image that carries both. Unset in development, where Vite serves the app and + * proxies the API here, so the server stays an API and nothing shadows a route. + */ + appDistDir?: string; /** * The Bot computer. Absent means the feature is off and its routes are not mounted, rather than * mounted and failing: a capability that is not configured should be missing, not broken. */ - computer?: { - baseUrl: string; - /** The secret every computer requires of its caller. */ - token?: string; - /** - * The container supervisor, when each Bot is to get a computer of its own. Absent means one - * shared computer at `baseUrl`, which is what a laptop wants and is honest about being one - * machine. - */ - supervisor?: { baseUrl: string; token?: string }; - /** True on a laptop, where browsing the deployment's own services is the point. */ - allowPrivateHosts: boolean; - /** - * What Bots may do on their computers. Absent means the built-in default applies. - * - * A whole policy in one variable rather than a variable per rule, because the rules are an - * ordered pair of lists and splitting them across `AGENT_COMPUTER_DENY_1`-style names makes their - * precedence, which is the only subtle thing about them, impossible to see. - */ - policy?: ActionPolicy; - }; + computer?: ComputerConfig; /** * The secret a Bot presents when it calls a tool back through this server. * @@ -274,36 +282,48 @@ function runtimeCapabilities(environment: Environment): RuntimeCapabilities { }; } -function computerConfig( - environment: Environment, -): DeploymentConfig["computer"] { - const baseUrl = url(environment, "AGENT_COMPUTER_URL"); - if (!baseUrl) { +function computerConfig(environment: Environment): ComputerConfig | undefined { + const supervisorAddress = optional(environment, "COMPUTER_SUPERVISOR_URL"); + const sharedAddress = optional(environment, "AGENT_COMPUTER_URL"); + if (!supervisorAddress && !sharedAddress) { return undefined; } - const policy = actionPolicy(environment); + /* * The secret the computers require. Without it every call to a computer is refused, and that is the * intended failure: `agent-computer` drives a browser holding real logins and must not answer * unauthenticated callers that can reach its port. */ const computerToken = optional(environment, "COMPUTER_TOKEN"); + + const allowPrivateHosts = + optional(environment, "AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS") === "true"; + const policy = actionPolicy(environment); + const supervisorUrl = url(environment, "COMPUTER_SUPERVISOR_URL"); - const supervisorToken = optional(environment, "SUPERVISOR_TOKEN"); + if (supervisorUrl) { + const supervisorToken = optional(environment, "SUPERVISOR_TOKEN"); + return { + provider: "docker", + baseUrl: supervisorUrl, + allowPrivateHosts, + ...(supervisorToken ? { supervisorToken } : {}), + ...(computerToken ? { token: computerToken } : {}), + ...(policy ? { policy } : {}), + }; + } + + const baseUrl = url(environment, "AGENT_COMPUTER_URL"); + if (!baseUrl) { + return undefined; + } + return { + provider: "shared", baseUrl, - allowPrivateHosts: - optional(environment, "AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS") === "true", - ...(policy ? { policy } : {}), + allowPrivateHosts, ...(computerToken ? { token: computerToken } : {}), - ...(supervisorUrl - ? { - supervisor: { - baseUrl: supervisorUrl, - ...(supervisorToken ? { token: supervisorToken } : {}), - }, - } - : {}), + ...(policy ? { policy } : {}), }; } @@ -345,6 +365,11 @@ function actionPolicy(environment: Environment): ActionPolicy | undefined { * * Zero is a legitimate value and means off. It is not the same as a malformed one. */ +function accessibilityEnabled(environment: Environment): boolean { + const off = optional(environment, "OPENBOT_ACCESSIBILITY_DISABLED"); + return off !== "true" && off !== "1"; +} + function agentStallTimeoutMs(environment: Environment): number { const raw = optional(environment, "AGENT_STALL_TIMEOUT_MS"); if (!raw) { @@ -380,6 +405,10 @@ export function loadConfig( oauth: { google }, auth: authConfig(environment, google), devNoAuth: devAuthEnabled(environment), + accessibility: accessibilityEnabled(environment), + ...(optional(environment, "APP_DIST_DIR") + ? { appDistDir: optional(environment, "APP_DIST_DIR") as string } + : {}), computer: computerConfig(environment), ...(optional(environment, "AGENT_TOOL_TOKEN") ? { agentToolToken: optional(environment, "AGENT_TOOL_TOKEN") as string } diff --git a/server/src/copilot.ts b/server/src/copilot.ts index 7a44a239..02297684 100644 --- a/server/src/copilot.ts +++ b/server/src/copilot.ts @@ -498,6 +498,11 @@ export function mountCopilotRuntime( apiKey: intelligence.apiKey, }), licenseToken: intelligence.licenseToken, + // Carried on the events the runtime already sends, so OpenBot's traffic is separable from any + // other deployment's. Adds no events of its own. + ...(config.accessibility + ? { telemetryProperties: { accessibility_title: "OpenBot" } } + : {}), // `identifyUser` is the Intelligence projection of the same person `identifyActor` returns: // one resolver decides both whose threads these are and whose coworkers exist. agents: createRequestAgents( diff --git a/server/src/index.ts b/server/src/index.ts index 9831c3aa..7d0b7e15 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -18,13 +18,15 @@ import { createThreadIdentity } from "./channels/thread-identity"; import { websocket as channelSocket } from "./channels/socket"; import { createSandboxedStore } from "./components/sandboxed"; import { createComponentStore } from "./components/store"; -import { createComputerClient } from "./computer/client"; import { createComputerGateway } from "./computer/gateway"; import { createPolicyStore, DEFAULT_ACTION_POLICY, } from "./computer/policy-store"; -import { createSupervisorClient } from "./computer/supervisor"; +import { + createComputerProvider, + describeComputerIsolation, +} from "./computer/provider"; import { loadConfig } from "./config"; import { createConnectorAdminService } from "./connectors"; import { @@ -152,21 +154,13 @@ const roleRepository = createRoleRepository(database); const loadAgentsForActor = createRuntimeAgentLoader(database, agentVault); await synchronizeTenantPackage(database, tenantPackage); const auth = config.auth ? createAuth(config, database) : undefined; -// One computer each, when a supervisor is configured to give them out. Without one every Bot shares -// the computer at `baseUrl`, which is what a laptop wants and is honest about being one machine. -const supervisor = config.computer?.supervisor - ? createSupervisorClient(config.computer.supervisor) - : undefined; -const computerClient = config.computer - ? createComputerClient({ - baseUrl: config.computer.baseUrl, - allowPrivateHosts: config.computer.allowPrivateHosts, - ...(config.computer.token ? { token: config.computer.token } : {}), - ...(supervisor - ? { resolveBaseUrl: (botId: string) => supervisor.locate(botId) } - : {}), - }) +const computerProvider = config.computer + ? createComputerProvider(config.computer) : undefined; + +if (computerProvider?.warm) { + void computerProvider.warm(); +} // What Bots may do on their computers. Configuration supplies the deployment's default; an // administrator can change it while running, and a restart returns to the configured one. const policyStore = createPolicyStore( @@ -187,6 +181,15 @@ const policySource = await policyStore.load(); * unavailable, and the row is a note for a reader rather than something the server depends on. */ const bootAuditStore = createAuditStore(database); +const computerGateway = computerProvider + ? createComputerGateway({ + provider: computerProvider, + auditStore: bootAuditStore, + policy: () => policyStore.get(), + allowPrivateHosts: config.computer?.allowPrivateHosts, + token: config.computer?.token, + }) + : undefined; /** * What a Bot can reach beyond its own computer. @@ -227,33 +230,26 @@ void recordAuditEvent(bootAuditStore, { /* * Record whether each Bot has a computer of its own. * - * Without a supervisor every Bot shares the browser at `AGENT_COMPUTER_URL`. That is a fine way to - * run on a laptop, but the shared isolation state must be visible rather than inferred. + * A shared provider is a fine way to run on a laptop, but the shared isolation state must be visible + * rather than inferred. */ +const isolation = describeComputerIsolation(computerProvider); + void recordAuditEvent(bootAuditStore, { eventType: "computer.isolation_loaded", targetType: "computer", - payload: supervisor - ? { - isolation: "one computer per Bot", - note: "Each Bot gets its own container, its own /workspace and its own browser profile.", - } - : { - isolation: "one shared computer", - note: "No supervisor is configured, so every Bot uses the same browser. Sessions, files and logins are shared between them. Set COMPUTER_SUPERVISOR_URL to give each Bot its own.", - }, + payload: { + isolation: isolation.isolation, + note: isolation.note, + }, }).catch(() => undefined); console.info( JSON.stringify({ type: "computer-isolation", - isolation: supervisor ? "one computer per Bot" : "one shared computer", - ...(supervisor - ? {} - : { - warning: - "Every Bot shares one browser. Set COMPUTER_SUPERVISOR_URL for a computer each.", - }), + provider: computerProvider ? computerProvider.name : "none", + isolation: isolation.isolation, + ...(isolation.warning ? { warning: isolation.warning } : {}), }), ); /** @@ -350,19 +346,8 @@ const app = createApp( (actorId) => (botId, runId) => mintRunAssertion({ botId, actorId, runId }, config.keyEncryptionKey), ), - computerClient, // The only path to an acting call. - computerClient - ? createComputerGateway({ - client: computerClient, - auditStore: bootAuditStore, - // Read on every decision rather than captured once, so a rule an administrator adds while the - // server is running applies to the very next action instead of after a restart. - policy: () => policyStore.get(), - // Stop, reset and the listing act on containers when there are containers to act on. - ...(supervisor ? { supervisor } : {}), - }) - : undefined, + computerGateway, policyStore, // Bots as durable objects, and the channels they run in. agentProfileStore, @@ -445,15 +430,18 @@ serve({ if (!actor) { return new Response("Sign in first.", { status: 401 }); } - // Located per Bot when there is a supervisor, and the one shared computer when there is not. + // Located through the configured provider so every stream follows the same isolation rules. let upstream: string; try { - upstream = toStreamUrl( - supervisor - ? await supervisor.locate(streamBotId) - : config.computer.baseUrl, - streamBotId, - ); + const streamBase = computerProvider + ? await computerProvider.locate(streamBotId) + : undefined; + if (!streamBase) { + return new Response("No computer address is configured.", { + status: 503, + }); + } + upstream = toStreamUrl(streamBase, streamBotId); } catch (error) { // Said out loud rather than falling back to another Bot's computer, which is the failure this // whole path exists to prevent. diff --git a/server/tests/agent-routes.test.ts b/server/tests/agent-routes.test.ts index b05774ea..68ff108e 100644 --- a/server/tests/agent-routes.test.ts +++ b/server/tests/agent-routes.test.ts @@ -551,9 +551,8 @@ describe("agent route composition", () => { api: { getSession: async () => session }, }, { rolesForUser: async () => ["user"] }, - // Positions 4-11: auditReader, credentialService, packageStatusReader, connectorService, - // copilotHandler, computerClient, computerGateway, computerPolicy. - undefined, + // Positions 4-10: auditReader, credentialService, packageStatusReader, connectorService, + // copilotHandler, computerGateway, computerPolicy. undefined, undefined, undefined, diff --git a/server/tests/channel-routes.test.ts b/server/tests/channel-routes.test.ts index baaf7cf1..58120272 100644 --- a/server/tests/channel-routes.test.ts +++ b/server/tests/channel-routes.test.ts @@ -305,9 +305,8 @@ describe("channel route composition", () => { api: { getSession: async () => session }, }, { rolesForUser: async () => ["user"] }, - // Positions 4-12, ending at agentProfileStore. the computer gateway and policy store were added - // ahead of these, so the run of placeholders grew with them. - undefined, + // Positions 4-11, ending at agentProfileStore. The computer gateway and policy store come + // before these placeholders. undefined, undefined, undefined, diff --git a/server/tests/computer-client.test.ts b/server/tests/computer-client.test.ts index 462a7b5c..aa2ae90a 100644 --- a/server/tests/computer-client.test.ts +++ b/server/tests/computer-client.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { - createComputerClient, + createComputerTransport, ElementNotFoundError, NavigationRefusedError, } from "../src/computer/client"; @@ -9,12 +9,19 @@ function clientWith( handler: (url: string, init?: RequestInit) => Promise | Response, allowPrivateHosts = false, ) { - return createComputerClient({ - baseUrl: "http://agent-computer:4100", + const transport = createComputerTransport({ allowPrivateHosts, fetchImpl: ((url: string, init?: RequestInit) => Promise.resolve(handler(url, init))) as unknown as typeof fetch, }); + const baseUrl = "http://agent-computer:4100"; + const botId = "bot-1"; + return { + navigate: (url: string) => transport.navigate(baseUrl, botId, url), + screenshot: () => transport.call(baseUrl, botId, "/screenshot"), + click: (input: unknown, signal?: AbortSignal) => + transport.post(baseUrl, botId, "/click", input, signal), + }; } const ok = (body: unknown) => @@ -102,6 +109,15 @@ describe("computer client", () => { "The assistant's computer is not running.", ); + const timedOut = clientWith(() => { + const error = new Error("timed out"); + error.name = "TimeoutError"; + throw error; + }); + await expect(timedOut.navigate("https://example.com")).rejects.toThrow( + "The assistant's computer did not respond in time.", + ); + const badPage = clientWith( () => new Response(JSON.stringify({ error: "net::ERR_NAME_NOT_RESOLVED" }), { @@ -114,18 +130,6 @@ describe("computer client", () => { ); }); - test("status reports unreachable rather than throwing", async () => { - const client = clientWith(() => { - throw new Error("down"); - }); - - await expect(client.status("bot-1")).resolves.toEqual({ - botId: "bot-1", - state: "unreachable", - reason: "The assistant's computer is not running.", - }); - }); - test("screenshot returns the png a transcript can render", async () => { const client = clientWith(() => ok({ @@ -141,19 +145,6 @@ describe("computer client", () => { width: 1280, }); }); - - test("surfaces a timeout as the computer not responding", async () => { - const client = clientWith(() => { - const error = new Error("timed out"); - error.name = "TimeoutError"; - throw error; - }); - - await expect(client.status("bot-1")).resolves.toMatchObject({ - state: "unreachable", - reason: "The assistant's computer did not respond in time.", - }); - }); }); /** diff --git a/server/tests/computer-gateway.test.ts b/server/tests/computer-gateway.test.ts index eff68c07..2213dd4d 100644 --- a/server/tests/computer-gateway.test.ts +++ b/server/tests/computer-gateway.test.ts @@ -1,12 +1,16 @@ import { describe, expect, test } from "bun:test"; import type { AuditEventInput, AuditStore } from "../src/audit"; -import type { ComputerClient } from "../src/computer/client"; import { ActionRefusedError, createComputerGateway, + WorkspaceRefusedError, } from "../src/computer/gateway"; import type { ActionPolicy } from "../src/computer/policy"; import type { SnapshotResult } from "../src/computer/schema"; +import type { + ComputerLocation, + ComputerProvider, +} from "../src/computer/provider"; /** * What the gateway must guarantee, tested as properties rather than as call sequences. @@ -29,69 +33,145 @@ const SNAPSHOT: SnapshotResult = { ], }; -/** A client that records what reached it, so "did not reach the computer" is checkable. */ -function fakeClient() { +/** A computer that records which HTTP actions reached it. */ +function fakeComputer(options?: { + stopResult?: { wasRunning: boolean }; + resetResult?: { cleared: boolean }; + locations?: ComputerLocation[]; + routes?: Record Response | Promise>; +}) { const calls: string[] = []; - /** Which Bot the gateway addressed the computer as, per call. */ const addressedAs: string[] = []; + const requests: Array<{ url: string; init?: RequestInit }> = []; + const stopResult = options?.stopResult ?? { wasRunning: true }; + const resetResult = options?.resetResult ?? { cleared: true }; + const locations = options?.locations ?? []; const result = (action: string) => ({ action, url: SNAPSHOT.url, elapsedMs: 1, }); - const client = { - snapshot: async () => SNAPSHOT, - read: async () => ({ - url: SNAPSHOT.url, - title: "Order", - text: "", - truncated: false, - }), - click: async () => { - calls.push("click"); - return result("click") as never; - }, - type: async () => { - calls.push("type"); - return result("type") as never; - }, - key: async () => { - calls.push("key"); - return result("key") as never; - }, - scroll: async () => { - calls.push("scroll"); - return result("scroll") as never; - }, - readFile: async () => { - calls.push("readFile"); - return { - path: "notes.md", - text: "kept", - truncated: false, - bytes: 4, - } as never; - }, - writeFile: async () => { - calls.push("writeFile"); - return { path: "notes.md", bytes: 4, appended: false } as never; + const provider: ComputerProvider = { + name: "test", + isolation: "per-bot", + locate: async (botId) => { + addressedAs.push(botId); + return "http://agent-computer:4100"; }, - status: async () => ({ botId: "b", state: "ready" as const }), - navigate: async () => { - calls.push("navigate"); - return { url: "https://example.com/", title: "Example" } as never; + status: async (botId) => ({ botId, state: "ready" }), + stop: async (botId) => { + calls.push(`stop:${botId}`); + return stopResult; }, - screenshot: async () => ({}) as never, - /** - * The per-Bot view. Recorded rather than ignored, so a test can prove the gateway told the - * computer which Bot is asking, the thing every per-Bot behaviour on the far side keys off. - */ - forBot(botId: string) { - addressedAs.push(botId); - return client; + reset: async (botId) => { + calls.push(`reset:${botId}`); + return resetResult; }, - } as unknown as ComputerClient; - return { client, calls, addressedAs }; + list: async () => locations, + }; + const fetchImpl = (async (url: string, init?: RequestInit) => { + requests.push({ url, init }); + const path = new URL(url).pathname; + if (options?.routes && path in options.routes) { + return options.routes[path](init); + } + switch (path) { + case "/snapshot": + return Response.json(SNAPSHOT); + case "/read": + return Response.json({ + url: SNAPSHOT.url, + title: "Order", + text: "", + truncated: false, + }); + case "/screenshot": + calls.push("screenshot"); + return Response.json({ + image: "aGVsbG8=", + mimeType: "image/png", + }); + case "/files/read": + calls.push("readFile"); + return Response.json({ + path: "notes.md", + text: "kept", + truncated: false, + bytes: 4, + }); + case "/files/write": + calls.push("writeFile"); + return Response.json({ + path: "notes.md", + bytes: 4, + appended: false, + }); + case "/files/list": + calls.push("listFiles"); + return Response.json({ + path: "notes", + entries: [], + }); + case "/exec": + calls.push("runCommand"); + return Response.json({ + command: "cat secrets.txt", + exitCode: 0, + output: "the customer's card number is 4111-1111-1111-1111", + truncated: false, + timedOut: false, + }); + case "/navigate": + calls.push("navigate"); + return Response.json({ + url: "https://example.com/", + title: "Example", + elapsedMs: 1, + }); + case "/click": + calls.push("click"); + return Response.json(result("click")); + case "/type": + calls.push("type"); + return Response.json(result("type")); + case "/key": + calls.push("key"); + return Response.json(result("key")); + case "/scroll": + calls.push("scroll"); + return Response.json(result("scroll")); + case "/control": + calls.push("control"); + return Response.json({ mode: "bot" }); + case "/control/request": + calls.push("requestHelp"); + return Response.json({ mode: "human", reason: "Sign in" }); + case "/control/take": + calls.push("takeControl"); + return Response.json({ mode: "human" }); + case "/control/release": + calls.push("releaseControl"); + return Response.json({ mode: "bot" }); + case "/control/secret": + calls.push("requestSecret"); + return Response.json({ mode: "secret", ref: "e1" }); + case "/human/secret": + calls.push("supplySecret"); + return Response.json({ supplied: true }); + case "/human/click": + case "/human/move": + case "/human/button": + case "/human/wheel": + calls.push(path.slice(1)); + return Response.json({ ok: true }); + default: + return Response.json( + { error: `Unknown endpoint: ${path}` }, + { status: 404 }, + ); + } + }) as unknown as typeof fetch; + return { provider, fetchImpl, calls, addressedAs, requests }; } function fakeAudit() { @@ -103,23 +183,34 @@ function fakeAudit() { const ACTOR = { id: "dev-local-user" }; const PERMISSIVE: ActionPolicy = { mode: "enforce", deny: [], allow: ["true"] }; -async function gatewayWith(policy: ActionPolicy | undefined) { - const { client, calls } = fakeClient(); +async function gatewayWith( + policy: ActionPolicy | undefined, + options?: { + stopResult?: { wasRunning: boolean }; + resetResult?: { cleared: boolean }; + locations?: ComputerLocation[]; + token?: string; + }, +) { + const { provider, fetchImpl, calls, addressedAs, requests } = + fakeComputer(options); const { store, rows } = fakeAudit(); const gateway = createComputerGateway({ - client, + provider, + fetchImpl, auditStore: store, policy: () => policy, + token: options?.token, }); // Every test acts on refs, so the server must hold a snapshot first, exactly as the real flow does. - await gateway.snapshot("default"); - return { gateway, calls, rows }; + await gateway.snapshot("bot-1"); + return { gateway, calls, rows, addressedAs, requests, provider }; } describe("the computer gateway", () => { test("carries out an allowed action and records it", async () => { const { gateway, calls, rows } = await gatewayWith(PERMISSIVE); - await gateway.click("default", "bot-1", ACTOR, { + await gateway.click("bot-1", ACTOR, { ref: "e9", snapshotId: 7, }); @@ -127,6 +218,7 @@ describe("the computer gateway", () => { expect(calls).toEqual(["click"]); expect(rows).toHaveLength(1); expect(rows[0]?.eventType).toBe("computer.action_allowed"); + expect(rows[0]?.targetId).toBe("bot-1"); }); test("a refused action never reaches the computer", async () => { @@ -136,13 +228,14 @@ describe("the computer gateway", () => { }); await expect( - gateway.click("default", "bot-1", ACTOR, { ref: "e9", snapshotId: 7 }), + gateway.click("bot-1", ACTOR, { ref: "e9", snapshotId: 7 }), ).rejects.toThrow(ActionRefusedError); // The decision happens before the effect. expect(calls).toEqual([]); expect(rows).toHaveLength(1); expect(rows[0]?.eventType).toBe("computer.action_refused"); + expect(rows[0]?.targetId).toBe("bot-1"); }); test("the refusal names the rule, so an operator can find it", async () => { @@ -152,7 +245,7 @@ describe("the computer gateway", () => { }); const error = await gateway - .click("default", "bot-1", ACTOR, { ref: "e9", snapshotId: 7 }) + .click("bot-1", ACTOR, { ref: "e9", snapshotId: 7 }) .catch((caught: unknown) => caught); expect(error).toBeInstanceOf(ActionRefusedError); @@ -170,7 +263,7 @@ describe("the computer gateway", () => { }); await expect( - gateway.click("default", "bot-1", ACTOR, { + gateway.click("bot-1", ACTOR, { ref: "e9", snapshotId: 7, // Not part of the input contract, and must not influence anything even when supplied. @@ -182,7 +275,7 @@ describe("the computer gateway", () => { test("an allowed action reports the element's label for the transcript", async () => { const { gateway } = await gatewayWith(PERMISSIVE); - const result = await gateway.type("default", "bot-1", ACTOR, { + const result = await gateway.type("bot-1", ACTOR, { ref: "e1", snapshotId: 7, text: "Grace Hopper", @@ -192,7 +285,7 @@ describe("the computer gateway", () => { test("the typed text never enters the audit payload", async () => { const { gateway, rows } = await gatewayWith(PERMISSIVE); - await gateway.type("default", "bot-1", ACTOR, { + await gateway.type("bot-1", ACTOR, { ref: "e1", snapshotId: 7, text: "hunter2-not-a-real-password", @@ -213,7 +306,7 @@ describe("the computer gateway", () => { test("an absent policy refuses every action", async () => { const { gateway, calls, rows } = await gatewayWith(undefined); await expect( - gateway.click("default", "bot-1", ACTOR, { ref: "e9", snapshotId: 7 }), + gateway.click("bot-1", ACTOR, { ref: "e9", snapshotId: 7 }), ).rejects.toThrow(ActionRefusedError); expect(calls).toEqual([]); expect(rows[0]?.eventType).toBe("computer.action_refused"); @@ -226,7 +319,7 @@ describe("the computer gateway", () => { allow: ["true"], }); - await gateway.click("default", "bot-1", ACTOR, { + await gateway.click("bot-1", ACTOR, { ref: "e9", snapshotId: 7, }); @@ -242,7 +335,7 @@ describe("the computer gateway", () => { // Writing an id with no `users` row fails the constraint and loses the row entirely, so who it // was is carried in the payload instead. The route decides this; the gateway must honour it. const { gateway, rows } = await gatewayWith(PERMISSIVE); - await gateway.click("default", "bot-1", ACTOR, { + await gateway.click("bot-1", ACTOR, { ref: "e9", snapshotId: 7, }); @@ -257,7 +350,7 @@ describe("the computer gateway", () => { }); await expect( - gateway.readFile("default", "bot-1", ACTOR, { + gateway.readFile("bot-1", ACTOR, { path: "credentials/aws.txt", }), ).rejects.toThrow(ActionRefusedError); @@ -273,10 +366,10 @@ describe("the computer gateway", () => { }); await expect( - gateway.readFile("default", "bot-1", ACTOR, { path: "config/prod.env" }), + gateway.readFile("bot-1", ACTOR, { path: "config/prod.env" }), ).rejects.toThrow(ActionRefusedError); // A different extension in the same folder is untouched by that rule. - await gateway.readFile("default", "bot-1", ACTOR, { + await gateway.readFile("bot-1", ACTOR, { path: "config/prod.json", }); expect(calls).toEqual(["readFile"]); @@ -284,7 +377,7 @@ describe("the computer gateway", () => { test("a permitted write happens and is recorded by path, never by contents", async () => { const { gateway, calls, rows } = await gatewayWith(PERMISSIVE); - await gateway.writeFile("default", "bot-1", ACTOR, { + await gateway.writeFile("bot-1", ACTOR, { path: "notes.md", contents: "the customer's card number is 4111-1111-1111-1111", }); @@ -297,19 +390,51 @@ describe("the computer gateway", () => { expect(JSON.stringify(rows[0]?.payload)).not.toContain("4111"); }); + test("a permitted command runs and is recorded by command, never by output", async () => { + const { gateway, calls, rows } = await gatewayWith(PERMISSIVE); + await gateway.runCommand("bot-1", ACTOR, { command: "cat secrets.txt" }); + + expect(calls).toEqual(["runCommand"]); + expect(rows[0]?.eventType).toBe("computer.action_allowed"); + expect(rows[0]?.payload.command).toBe("cat secrets.txt"); + // The command IS the action, so it is recorded in full. Its output is the file body of this + // pair: whatever the command read off a page or out of a file, and never in the row. + expect(JSON.stringify(rows[0]?.payload)).not.toContain("4111"); + // A command has no page element, so the row says nothing about a snapshot it was never part of. + expect(rows[0]?.payload.element).toBeUndefined(); + }); + + test("a command the policy refuses is recorded and never reaches the computer", async () => { + const { gateway, calls, rows } = await gatewayWith({ + ...PERMISSIVE, + deny: ['intent == "run_command"'], + }); + + await expect( + gateway.runCommand("bot-1", ACTOR, { command: "cat secrets.txt" }), + ).rejects.toThrow(ActionRefusedError); + expect(calls).toEqual([]); + expect(rows[0]?.eventType).toBe("computer.action_refused"); + expect(rows[0]?.payload.command).toBe("cat secrets.txt"); + }); + test("the computer is told WHICH Bot is asking", async () => { // Every per-Bot behaviour on the computer keys off this id: the profile it opens, the logins it // has, the proxy its traffic leaves through, and who holds its wheel. - const { client, addressedAs } = fakeClient(); + const { provider, fetchImpl, addressedAs } = fakeComputer(); const { store } = fakeAudit(); const gateway = createComputerGateway({ - client, + provider, + fetchImpl, auditStore: store, policy: () => PERMISSIVE, }); await gateway.snapshot("sales-bot"); - await gateway.click("sales-bot", "sales-bot", ACTOR, "e1"); + await gateway.click("sales-bot", ACTOR, { + ref: "e1", + snapshotId: 7, + }); await gateway.read("research-bot"); expect(addressedAs).toContain("sales-bot"); @@ -323,25 +448,28 @@ describe("the computer gateway", () => { test("a permitted action that FAILS gets its own row, not an allowed one", async () => { // A permitted read that later fails path confinement records both the decision and the failed // outcome, so the trail does not imply the Bot received the file. - const { client, calls } = fakeClient(); - const { store, rows } = fakeAudit(); - const failing: ComputerClient = { - ...client, - readFile: async () => { - calls.push("readFile"); - throw new Error("that path is outside your workspace"); + const { provider, fetchImpl, calls } = fakeComputer({ + routes: { + "/files/read": () => { + calls.push("readFile"); + return Response.json( + { error: "that path is outside your workspace" }, + { status: 403 }, + ); + }, }, - forBot: () => failing, - } as unknown as ComputerClient; + }); + const { store, rows } = fakeAudit(); const gateway = createComputerGateway({ - client: failing, + provider, + fetchImpl, auditStore: store, policy: () => PERMISSIVE, }); await expect( - gateway.readFile("default", "bot-1", ACTOR, { path: "../../etc/passwd" }), - ).rejects.toThrow(); + gateway.readFile("bot-1", ACTOR, { path: "../../etc/passwd" }), + ).rejects.toThrow(WorkspaceRefusedError); expect(rows).toHaveLength(2); // The decision, then the outcome. Both are needed: the first says it was permitted, the second @@ -352,11 +480,9 @@ describe("the computer gateway", () => { }); test("opening a page is recorded, with the address it was opening", async () => { - // The target guard refuses a forbidden address inside the - // client and nothing was written, so an attempt on the cloud metadata endpoint left no row while - // ticking a radio button left one. + // The target guard refuses forbidden addresses before the request leaves. const { gateway, calls, rows } = await gatewayWith(PERMISSIVE); - await gateway.navigate("default", "bot-1", ACTOR, "https://example.com/"); + await gateway.navigate("bot-1", ACTOR, "https://example.com/"); expect(calls).toEqual(["navigate"]); expect(rows[0]?.eventType).toBe("computer.action_allowed"); @@ -373,22 +499,17 @@ describe("the computer gateway", () => { }); await expect( - gateway.navigate( - "default", - "bot-1", - ACTOR, - "https://intranet.example.com/hr", - ), + gateway.navigate("bot-1", ACTOR, "https://intranet.example.com/hr"), ).rejects.toThrow(ActionRefusedError); expect(calls).toEqual([]); - await gateway.navigate("default", "bot-1", ACTOR, "https://example.com/"); + await gateway.navigate("bot-1", ACTOR, "https://example.com/"); expect(calls).toEqual(["navigate"]); }); test("an action on an unresolvable ref is still decided and still recorded", async () => { const { gateway, rows } = await gatewayWith(PERMISSIVE); - await gateway.click("default", "bot-1", ACTOR, { + await gateway.click("bot-1", ACTOR, { ref: "e404", snapshotId: 7, }); @@ -396,4 +517,166 @@ describe("the computer gateway", () => { // could not identify what was touched, rather than omitting the field. expect(rows[0]?.payload.element).toBe("not in the current snapshot"); }); + + test("stopComputer returns true when the computer was running and audits with the bot id as target", async () => { + const { gateway, calls, rows } = await gatewayWith(PERMISSIVE, { + stopResult: { wasRunning: true }, + }); + + const result = await gateway.stopComputer("bot-1", ACTOR); + + expect(result).toEqual({ wasRunning: true }); + expect(calls).toContain("stop:bot-1"); + expect(rows).toHaveLength(1); + expect(rows[0]?.eventType).toBe("computer.stopped"); + expect(rows[0]?.targetId).toBe("bot-1"); + expect(rows[0]?.targetType).toBe("computer"); + }); + + test("stopComputer preserves wasRunning=false when the computer was already stopped", async () => { + const { gateway, calls, rows } = await gatewayWith(PERMISSIVE, { + stopResult: { wasRunning: false }, + }); + + const result = await gateway.stopComputer("bot-2", ACTOR); + + expect(result).toEqual({ wasRunning: false }); + expect(calls).toContain("stop:bot-2"); + expect(rows).toHaveLength(1); + expect(rows[0]?.eventType).toBe("computer.stopped"); + expect(rows[0]?.targetId).toBe("bot-2"); + }); + + test("resetComputer returns true when state was cleared and audits with the bot id as target", async () => { + const { gateway, calls, rows } = await gatewayWith(PERMISSIVE, { + resetResult: { cleared: true }, + }); + + const result = await gateway.resetComputer("bot-1", ACTOR); + + expect(result).toEqual({ cleared: true }); + expect(calls).toContain("reset:bot-1"); + expect(rows).toHaveLength(1); + expect(rows[0]?.eventType).toBe("computer.reset"); + expect(rows[0]?.targetId).toBe("bot-1"); + expect(rows[0]?.targetType).toBe("computer"); + }); + + test("resetComputer preserves cleared=false when provider could not clear state and audits the bot id", async () => { + const { gateway, calls, rows } = await gatewayWith(PERMISSIVE, { + resetResult: { cleared: false }, + }); + + const result = await gateway.resetComputer("bot-2", ACTOR); + + expect(result).toEqual({ cleared: false }); + expect(calls).toContain("reset:bot-2"); + expect(rows).toHaveLength(1); + expect(rows[0]?.eventType).toBe("computer.reset"); + expect(rows[0]?.targetId).toBe("bot-2"); + }); + + test("computers maps provider status 'running' and 'stopped' directly and preserves egress distinctions", async () => { + const locations: ComputerLocation[] = [ + { + botId: "bot-proxied", + status: "running", + startedAt: "2026-08-20T12:00:00.000Z", + egress: "198.51.100.42", + }, + { + botId: "bot-direct", + status: "stopped", + startedAt: "2026-08-20T11:00:00.000Z", + egress: null, + }, + { + botId: "bot-unknown-egress", + status: "stopped", + startedAt: "2026-08-20T10:00:00.000Z", + egress: undefined, + }, + ]; + const { gateway } = await gatewayWith(PERMISSIVE, { locations }); + + const result = await gateway.computers(); + + expect(result).toEqual({ + isolation: "per-bot", + computers: [ + { + botId: "bot-proxied", + running: true, + startedAt: "2026-08-20T12:00:00.000Z", + egress: "198.51.100.42", + }, + { + botId: "bot-direct", + running: false, + startedAt: "2026-08-20T11:00:00.000Z", + egress: null, + }, + { + botId: "bot-unknown-egress", + running: false, + startedAt: "2026-08-20T10:00:00.000Z", + egress: undefined, + }, + ], + }); + }); + + test("takes a screenshot through the located computer with its identity and token", async () => { + const { gateway, requests } = await gatewayWith(PERMISSIVE, { + token: "computer-secret", + }); + + const result = await gateway.screenshot("bot-1"); + + expect(result).toEqual({ + image: "aGVsbG8=", + mimeType: "image/png", + }); + const screenshotReq = requests.find((r) => r.url.endsWith("/screenshot")); + expect(screenshotReq).toBeDefined(); + expect(screenshotReq?.url).toBe("http://agent-computer:4100/screenshot"); + expect(screenshotReq?.init?.headers).toMatchObject({ + "x-openbot-bot-id": "bot-1", + "x-openbot-computer-token": "computer-secret", + }); + }); + + test("routes acting, control, file, secret, and human input calls to the correct endpoint paths", async () => { + const { gateway, requests } = await gatewayWith(PERMISSIVE); + + await gateway.key("bot-1", ACTOR, { key: "Tab" }); + await gateway.scroll("bot-1", ACTOR, { deltaY: 400 }); + await gateway.listFiles("bot-1", ACTOR, { path: "notes" }); + await gateway.control("bot-1"); + await gateway.requestHelp("bot-1", ACTOR, "Sign in"); + await gateway.takeControl("bot-1", ACTOR); + await gateway.releaseControl("bot-1", ACTOR); + await gateway.requestSecret("bot-1", ACTOR, { + label: "Password", + ref: "e1", + snapshotId: 7, + }); + await gateway.supplySecret("bot-1", ACTOR, "secret"); + await gateway.humanInput("bot-1", { kind: "click", x: 10, y: 20 }); + + const paths = requests.map(({ url }) => new URL(url).pathname); + expect(paths).toEqual([ + "/snapshot", + "/key", + "/scroll", + "/files/list", + "/control", + "/control/request", + "/control/take", + "/control/release", + "/control/secret", + "/human/secret", + "/human/click", + ]); + }); }); diff --git a/server/tests/computer-policy.test.ts b/server/tests/computer-policy.test.ts index 94c2b7df..b4168fd4 100644 --- a/server/tests/computer-policy.test.ts +++ b/server/tests/computer-policy.test.ts @@ -407,3 +407,73 @@ describe("describing a refusal", () => { expect(decision.reason).toContain("the file /workspace/secrets.env"); }); }); + +/** + * A shell command, judged like any other action. + * + * The blunt rule matters more than the clever one here. A deployment that does not want its Bots + * running commands says so once with `intent`, and does not have to imagine every command it would + * have wanted to refuse. + */ +describe("commands", () => { + const runCommand = (command: string): PolicyContext => ({ + tool: { name: "computer_run_command" }, + bot: { id: "general-assistant" }, + actor: { id: "dev-local-user" }, + page: { url: "", host: "" }, + intent: "run_command", + command, + }); + + test("a deployment can refuse the shell outright", () => { + const decision = evaluateActionPolicy( + { mode: "enforce", deny: ['intent == "run_command"'], allow: ["true"] }, + runCommand("apt-get install -y jq"), + ); + expect(decision.allowed).toBe(false); + expect(decision.matched).toBe('intent == "run_command"'); + }); + + test("a rule can name what the command says", () => { + const policy = { + mode: "enforce" as const, + deny: ['contains(command, "rm -rf")'], + allow: ["true"], + }; + expect(evaluateActionPolicy(policy, runCommand("rm -rf /")).allowed).toBe( + false, + ); + expect(evaluateActionPolicy(policy, runCommand("ls -la")).allowed).toBe( + true, + ); + }); + + test("commands are allowed when nothing refuses them", () => { + const decision = evaluateActionPolicy( + { mode: "enforce", deny: [], allow: ["true"] }, + runCommand("echo hello"), + ); + expect(decision.allowed).toBe(true); + }); + + /* + * A rule written about the browser must not catch a command. The neutral empty fields make + * `page.host` and the element fields evaluate to false rather than being unevaluable, which is + * what keeps the shipped deny preset from refusing every command a Bot ever runs. + */ + test("a browser rule does not refuse a command", () => { + const decision = evaluateActionPolicy( + { + mode: "enforce", + deny: ['contains(element.name, "submit") || key == "Enter"'], + allow: ["true"], + }, + { + ...runCommand("echo hello"), + element: { ref: "", role: "", name: "", type: "" }, + key: "", + }, + ); + expect(decision.allowed).toBe(true); + }); +}); diff --git a/server/tests/computer-provider.test.ts b/server/tests/computer-provider.test.ts new file mode 100644 index 00000000..3285c844 --- /dev/null +++ b/server/tests/computer-provider.test.ts @@ -0,0 +1,304 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import type { ComputerConfig } from "../src/config"; +import { + createComputerProvider, + createSharedComputerProvider, + describeComputerIsolation, + ProviderError, +} from "../src/computer/provider"; + +const servers: { stop(closeActiveConnections?: boolean): void }[] = []; + +afterEach(() => { + for (const server of servers.splice(0)) server.stop(true); +}); + +function serve(handler: (request: Request) => Response | Promise) { + const server = Bun.serve({ port: 0, fetch: handler }); + servers.push(server); + return `http://127.0.0.1:${server.port}`; +} + +type FakeAgentComputerHandler = { + health?: (request: Request) => Response | Promise; + computers?: (request: Request) => Response | Promise; + stop?: (request: Request) => Response | Promise; + reset?: (request: Request) => Response | Promise; +}; + +function serveAgentComputer( + handlers: FakeAgentComputerHandler = {}, + options?: { token?: string }, +) { + return serve(async (request) => { + const url = new URL(request.url); + const token = request.headers.get("x-openbot-computer-token"); + + if ( + options?.token && + url.pathname !== "/health" && + token !== options.token + ) { + return Response.json({ error: "Not authorised." }, { status: 401 }); + } + + if (url.pathname === "/health" && request.method === "GET") { + if (handlers.health) return handlers.health(request); + return Response.json({ status: "ok", browser: true }); + } + + if (url.pathname === "/computers" && request.method === "GET") { + if (handlers.computers) return handlers.computers(request); + return Response.json({ computers: [] }); + } + + if (url.pathname === "/computers/stop" && request.method === "POST") { + if (handlers.stop) return handlers.stop(request); + return Response.json({ stopped: true, wasRunning: true }); + } + + if (url.pathname === "/computers/reset" && request.method === "POST") { + if (handlers.reset) return handlers.reset(request); + const botId = request.headers.get("x-openbot-bot-id") ?? "shared"; + return Response.json({ reset: true, botId }); + } + + return Response.json({ error: "Not found." }, { status: 404 }); + }); +} + +describe("computer isolation description", () => { + test("describes the computer feature as off when no provider is configured", () => { + const description = describeComputerIsolation(undefined); + expect(description.isolation).toBe("off"); + expect(description.note.toLowerCase()).toContain("off"); + expect(description.note.toLowerCase()).not.toContain("shared"); + expect(description.note.toLowerCase()).not.toContain("browser"); + }); + + test("describes provider machine isolation when configured", () => { + const provider = createSharedComputerProvider({ + baseUrl: "http://computer:4100/", + }); + + expect(provider.name).toBe("shared"); + expect(provider.isolation).toBe("shared"); + expect(describeComputerIsolation(provider).isolation).toBe( + "one shared computer", + ); + }); +}); + +describe("shared computer provider", () => { + test("locates the shared computer address", async () => { + const provider = createSharedComputerProvider({ + baseUrl: "http://computer:4100/", + }); + expect(await provider.locate("sales")).toBe("http://computer:4100/"); + }); + + test("reports a healthy shared computer as ready", async () => { + const paths: string[] = []; + const baseUrl = serveAgentComputer({ + health: (request) => { + paths.push(new URL(request.url).pathname); + return Response.json({ status: "ok" }); + }, + }); + const provider = createSharedComputerProvider({ baseUrl }); + + expect(await provider.status("sales")).toEqual({ + botId: "sales", + state: "ready", + }); + expect(paths).toEqual(["/health"]); + }); + + test("reports the HTTP failure when the shared computer is not healthy", async () => { + const baseUrl = serveAgentComputer({ + health: () => new Response("not ready", { status: 503 }), + }); + const provider = createSharedComputerProvider({ baseUrl }); + + expect(await provider.status("sales")).toEqual({ + botId: "sales", + state: "unreachable", + reason: "The shared computer answered 503.", + }); + }); + + test("posts /computers/stop with identity and token and returns wasRunning", async () => { + const requests: { + path: string; + method: string; + botId: string | null; + token: string | null; + }[] = []; + const baseUrl = serveAgentComputer( + { + stop: (request) => { + const botId = request.headers.get("x-openbot-bot-id"); + requests.push({ + path: new URL(request.url).pathname, + method: request.method, + botId, + token: request.headers.get("x-openbot-computer-token"), + }); + const wasRunning = botId === "running-bot"; + return Response.json({ stopped: true, wasRunning }); + }, + }, + { token: "computer-secret" }, + ); + const provider = createSharedComputerProvider({ + baseUrl, + token: "computer-secret", + }); + + const runningResult = await provider.stop("running-bot"); + expect(runningResult).toEqual({ wasRunning: true }); + + const idleResult = await provider.stop("idle-bot"); + expect(idleResult).toEqual({ wasRunning: false }); + + expect(requests).toEqual([ + { + path: "/computers/stop", + method: "POST", + botId: "running-bot", + token: "computer-secret", + }, + { + path: "/computers/stop", + method: "POST", + botId: "idle-bot", + token: "computer-secret", + }, + ]); + }); + + test("posts /computers/reset with identity and token and returns cleared", async () => { + const requests: { + path: string; + method: string; + botId: string | null; + token: string | null; + }[] = []; + const baseUrl = serveAgentComputer( + { + reset: (request) => { + const botId = request.headers.get("x-openbot-bot-id"); + requests.push({ + path: new URL(request.url).pathname, + method: request.method, + botId, + token: request.headers.get("x-openbot-computer-token"), + }); + return Response.json({ reset: true, botId }); + }, + }, + { token: "computer-secret" }, + ); + const provider = createSharedComputerProvider({ + baseUrl, + token: "computer-secret", + }); + + const resetResult = await provider.reset("sales"); + expect(resetResult).toEqual({ cleared: true }); + + expect(requests).toEqual([ + { + path: "/computers/reset", + method: "POST", + botId: "sales", + token: "computer-secret", + }, + ]); + }); + + test("maps the shared computer inventory to provider locations preserving egress and status", async () => { + const baseUrl = serveAgentComputer({ + computers: () => + Response.json({ + computers: [ + { + botId: "sales", + running: true, + startedAt: "2026-08-20T12:00:00.000Z", + egress: null, + }, + { + botId: "support", + running: false, + startedAt: null, + egress: "us-east-egress", + }, + { + botId: "analytics", + status: "running", + egress: null, + }, + ], + }), + }); + const provider = createSharedComputerProvider({ baseUrl }); + + expect(await provider.list()).toEqual([ + { + botId: "sales", + status: "running", + url: baseUrl, + startedAt: "2026-08-20T12:00:00.000Z", + egress: null, + }, + { + botId: "support", + status: "stopped", + url: baseUrl, + egress: "us-east-egress", + }, + { + botId: "analytics", + status: "running", + url: baseUrl, + egress: null, + }, + ]); + }); + + test("aborts fetch that never settles with configurable timeoutMs and throws ProviderError", async () => { + const baseUrl = serve(() => new Promise(() => {})); + const provider = createSharedComputerProvider({ + baseUrl, + timeoutMs: 25, + }); + + await expect(provider.stop("sales")).rejects.toThrow(ProviderError); + }); +}); + +describe("computer provider factory", () => { + test("selects the Docker supervisor adapter", () => { + const config: ComputerConfig = { + provider: "docker", + baseUrl: "http://supervisor:4300", + supervisorToken: "supervisor-secret", + token: "computer-secret", + allowPrivateHosts: false, + }; + + expect(createComputerProvider(config).name).toBe("Docker supervisor"); + }); + + test("selects the shared computer adapter", () => { + const config: ComputerConfig = { + provider: "shared", + baseUrl: "http://computer:4100", + token: "computer-secret", + allowPrivateHosts: false, + }; + + expect(createComputerProvider(config).name).toBe("shared"); + }); +}); diff --git a/server/tests/computer-routes.test.ts b/server/tests/computer-routes.test.ts new file mode 100644 index 00000000..641abe87 --- /dev/null +++ b/server/tests/computer-routes.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, test } from "bun:test"; +import type { MiddlewareHandler } from "hono"; +import type { AppVariables } from "../src/auth/guards"; +import type { ComputerGateway } from "../src/computer/gateway"; +import type { PolicyStore } from "../src/computer/policy-store"; +import { createComputerRoutes } from "../src/computer/routes"; + +describe("computer routes", () => { + test("gets a screenshot through the governed computer gateway", async () => { + const requestedBotIds: string[] = []; + const gateway = { + screenshot: async (botId: string) => { + requestedBotIds.push(botId); + return { image: "aGVsbG8=", mimeType: "image/png" as const }; + }, + } as unknown as ComputerGateway; + const policyStore = {} as PolicyStore; + const requireUser: MiddlewareHandler<{ Variables: AppVariables }> = async ( + _context, + next, + ) => next(); + const routes = createComputerRoutes(gateway, policyStore, requireUser); + + const response = await routes.request( + "http://openbot.test/bot-17/screenshot", + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + image: "aGVsbG8=", + mimeType: "image/png", + }); + expect(requestedBotIds).toEqual(["bot-17"]); + }); +}); diff --git a/server/tests/computer-supervisor.test.ts b/server/tests/computer-supervisor.test.ts index f591a479..919d4cc3 100644 --- a/server/tests/computer-supervisor.test.ts +++ b/server/tests/computer-supervisor.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; +import type { ComputerProvider } from "../src/computer/provider"; import { - createSupervisorClient, + createDockerSupervisorProvider, SupervisorError, } from "../src/computer/supervisor"; @@ -13,8 +14,8 @@ import { * to prevent, and it would look like it was working. */ -function clientWith(handler: (path: string) => Response) { - return createSupervisorClient({ +function clientWith(handler: (path: string) => Response): ComputerProvider { + return createDockerSupervisorProvider({ baseUrl: "http://supervisor:4300", token: "t", fetchImpl: (async (url: string | URL | Request) => @@ -76,7 +77,7 @@ describe("locating a Bot's computer", () => { test("an unreachable supervisor says so, rather than looking like a broken computer", async () => { // These are different problems for whoever has to fix them: one is the supervisor, the other is // the Bot's own container. - const client = createSupervisorClient({ + const client = createDockerSupervisorProvider({ baseUrl: "http://supervisor:4300", fetchImpl: (async () => { throw new Error("connection refused"); @@ -87,7 +88,7 @@ describe("locating a Bot's computer", () => { test("the bot id is escaped into the path", async () => { let seen = ""; - const client = createSupervisorClient({ + const client = createDockerSupervisorProvider({ baseUrl: "http://supervisor:4300", fetchImpl: (async (url: string | URL | Request) => { seen = new URL(String(url)).pathname; @@ -98,3 +99,140 @@ describe("locating a Bot's computer", () => { expect(seen).toBe("/computers/a%2Fb/ensure"); }); }); + +describe("Docker supervisor provider", () => { + test("describes one container and browser profile for each Bot", () => { + const provider = createDockerSupervisorProvider({ + baseUrl: "http://supervisor:4300", + fetchImpl: (async () => + Response.json({ computers: [] })) as unknown as typeof fetch, + }); + + expect(provider.name).toBe("Docker supervisor"); + expect(provider.isolation).toBe("per-bot"); + }); + + test.each([ + ["created", { botId: "bot", state: "starting" }], + ["running", { botId: "bot", state: "ready" }], + ["paused", { botId: "bot", state: "absent" }], + ["restarting", { botId: "bot", state: "starting" }], + ["removing", { botId: "bot", state: "absent" }], + ["exited", { botId: "bot", state: "absent" }], + ["dead", { botId: "bot", state: "unreachable" }], + ] as const)( + "maps Docker container status %s to lifecycle state", + async (dockerStatus, expected) => { + const provider = createDockerSupervisorProvider({ + baseUrl: "http://supervisor:4300", + fetchImpl: (async () => + Response.json({ + computers: [ + { + botId: "bot", + container: "openbot-computer-bot", + status: dockerStatus, + url: "http://openbot-computer-bot:4100", + }, + ], + })) as unknown as typeof fetch, + }); + + const result = await provider.status("bot"); + expect(result).toMatchObject(expected); + }, + ); + + test("reports missing bot as absent", async () => { + const provider = createDockerSupervisorProvider({ + baseUrl: "http://supervisor:4300", + fetchImpl: (async () => + Response.json({ computers: [] })) as unknown as typeof fetch, + }); + + expect(await provider.status("missing-bot")).toEqual({ + botId: "missing-bot", + state: "absent", + }); + }); + + test("lists only the provider location fields with mapped status", async () => { + const provider = createDockerSupervisorProvider({ + baseUrl: "http://supervisor:4300", + fetchImpl: (async () => + Response.json({ + computers: [ + { + botId: "sales", + container: "computer-sales", + status: "running", + port: 49152, + url: "http://computer-sales:4100", + startedAt: "2026-08-20T12:00:00.000Z", + }, + { + botId: "support", + container: "computer-support", + status: "exited", + port: 49153, + url: "http://computer-support:4100", + }, + ], + })) as unknown as typeof fetch, + }); + + expect(await provider.list()).toEqual([ + { + botId: "sales", + status: "running", + url: "http://computer-sales:4100", + startedAt: "2026-08-20T12:00:00.000Z", + }, + { + botId: "support", + status: "stopped", + url: "http://computer-support:4100", + }, + ]); + }); + + test("stop reports whether the container was running", async () => { + const provider = createDockerSupervisorProvider({ + baseUrl: "http://supervisor:4300", + fetchImpl: (async () => + Response.json({ stopped: true })) as unknown as typeof fetch, + }); + + expect(await provider.stop("bot")).toEqual({ wasRunning: true }); + }); + + test("stop reports false when container was not running", async () => { + const provider = createDockerSupervisorProvider({ + baseUrl: "http://supervisor:4300", + fetchImpl: (async () => + Response.json({ stopped: false })) as unknown as typeof fetch, + }); + + expect(await provider.stop("bot")).toEqual({ wasRunning: false }); + }); + + test("reset reports whether container state was cleared", async () => { + const provider = createDockerSupervisorProvider({ + baseUrl: "http://supervisor:4300", + fetchImpl: (async () => + Response.json({ reset: true })) as unknown as typeof fetch, + }); + + expect(await provider.reset("bot")).toEqual({ cleared: true }); + }); + + test("reset reports false when container was not present to clear", async () => { + const provider = createDockerSupervisorProvider({ + baseUrl: "http://supervisor:4300", + fetchImpl: (async () => + Response.json({ reset: false })) as unknown as typeof fetch, + }); + + expect(await provider.reset("bot")).toEqual({ cleared: false }); + }); +}); diff --git a/server/tests/computer-target.test.ts b/server/tests/computer-target.test.ts index c3acadf6..60bbd112 100644 --- a/server/tests/computer-target.test.ts +++ b/server/tests/computer-target.test.ts @@ -98,7 +98,7 @@ describe("checkComputerAddress", () => { }); test("allows a hosted provider's public address", () => { - const verdict = checkComputerAddress("https://sandbox-abc123.daytona.app"); + const verdict = checkComputerAddress("https://sandbox-abc123.example.net"); expect(verdict.allowed).toBe(true); }); diff --git a/server/tests/config.test.ts b/server/tests/config.test.ts index 326ad5cb..5ce2c1ee 100644 --- a/server/tests/config.test.ts +++ b/server/tests/config.test.ts @@ -186,4 +186,87 @@ describe("deployment configuration", () => { expect(attempt).toThrow("AGENT_STALL_TIMEOUT_MS"); }, ); + + test("configures Docker as the per-Bot computer provider", () => { + const config = loadConfig({ + ...baseEnvironment, + COMPUTER_SUPERVISOR_URL: "http://localhost:4000", + SUPERVISOR_TOKEN: "supervisor-token", + COMPUTER_TOKEN: "computer-token", + }); + + expect(config.computer?.provider).toBe("docker"); + expect(config.computer).toEqual({ + provider: "docker", + baseUrl: "http://localhost:4000", + supervisorToken: "supervisor-token", + token: "computer-token", + allowPrivateHosts: false, + }); + }); + + test("configures one shared computer", () => { + const config = loadConfig({ + ...baseEnvironment, + AGENT_COMPUTER_URL: "http://localhost:4100", + COMPUTER_TOKEN: "computer-token", + }); + + expect(config.computer?.provider).toBe("shared"); + expect(config.computer).toEqual({ + provider: "shared", + baseUrl: "http://localhost:4100", + token: "computer-token", + allowPrivateHosts: false, + }); + }); + + test("leaves computers off when no provider address is configured", () => { + expect(loadConfig(baseEnvironment).computer).toBeUndefined(); + }); + + test.each([ + ["Docker", "COMPUTER_SUPERVISOR_URL"], + ["shared", "AGENT_COMPUTER_URL"], + ] as const)("refuses an invalid %s computer provider URL", (_, urlName) => { + expect(() => + loadConfig({ + ...baseEnvironment, + [urlName]: "not a URL", + }), + ).toThrow(`${urlName} must be a valid URL`); + }); +}); + +describe("accessibility", () => { + test("is on when nothing is set", () => { + expect(loadConfig(baseEnvironment).accessibility).toBe(true); + }); + + test.each(["true", "1"])( + "is off on OPENBOT_ACCESSIBILITY_DISABLED=%p", + (value) => { + expect( + loadConfig({ + ...baseEnvironment, + OPENBOT_ACCESSIBILITY_DISABLED: value, + }).accessibility, + ).toBe(false); + }, + ); + + // Anything else is not a way of saying off. A deployment that typed something + // else has not opted out, and silently treating it as opt-out would be a + // setting that appears to work and does not. + test.each(["false", "no", "", "yes"])( + "stays on for OPENBOT_ACCESSIBILITY_DISABLED=%p", + (value) => { + expect( + loadConfig({ + ...baseEnvironment, + OPENBOT_ACCESSIBILITY_DISABLED: value, + }).accessibility, + ).toBe(true); + }, + ); });